E2e testing
PHP and Laravel Cursor rules — coding standards, testing, and conventions for the Cursor editor. Install via Composer.
npx -y skills add pekral/cursor-rules --skill e2e-testingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when writing or stabilizing Playwright end-to-end browser tests against a Laravel app — but only when the project already has Playwright; otherwise defer to manual testing or Pest/Dusk.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
7.3 KB, as published. Nobody here has run it
Constraints
- Apply
@rules/code-testing/general.mdcfor the deterministic-test contract: tests must be reliable, isolated, and free of arbitrary sleeps. - GATED skill: proceed only when the consuming project already ships Playwright. Never install Playwright unprompted.
- Stable selectors over brittle ones: prefer role and
data-testidover CSS/structure. - No arbitrary
waitForTimeout. Rely on auto-waiting locators and explicit conditions.
Use when
- Writing Playwright E2E browser tests for a Laravel app.
- Stabilizing flaky Playwright tests.
- Wiring Playwright into CI against the app under test.
Preconditions (run first, do not skip)
Check whether Playwright is actually present:
ls playwright.config.* 2>/dev/null
grep -q '@playwright/test' package.json 2>/dev/null && echo "playwright present"
- If neither a
playwright.config.*file nor@playwright/testinpackage.jsonexists, STOP. Do not install Playwright. Defer to:@skills/test-like-human/SKILL.mdfor manual, scenario-based testing, or- Pest feature tests / Laravel Dusk for browser coverage inside the PHP stack.
- Only when Playwright IS present, continue with the patterns below.
File organization
tests/
e2e/
auth/login.spec.ts
features/search.spec.ts
pages/
ItemsPage.ts
fixtures/
auth.ts
playwright.config.ts
Page Object Model
Encapsulate page structure so specs read as behavior, not selectors.
import { Page, Locator } from '@playwright/test'
export class ItemsPage {
readonly page: Page
readonly searchInput: Locator
readonly itemCards: Locator
readonly createButton: Locator
constructor(page: Page) {
this.page = page
// Prefer role/test-id selectors. data-testid is stable across restyles.
this.searchInput = page.getByTestId('search-input')
this.itemCards = page.getByTestId('item-card')
this.createButton = page.getByRole('button', { name: 'Create' })
}
async goto() {
await this.page.goto('/items')
}
async search(query: string) {
await this.searchInput.fill(query)
// Wait for the real response, not a fixed delay.
await this.page.waitForResponse(r => r.url().includes('/items') && r.ok())
}
itemCount() {
return this.itemCards.count()
}
}
Stable selectors
Order of preference:
getByRole(...)with an accessible name.getByTestId(...)— adddata-testidto the relevant Blade/Livewire markup.getByLabel/getByTextfor forms and copy.- Raw CSS only as a last resort; it breaks on Tailwind restyles.
Test structure
import { test, expect } from '@playwright/test'
import { ItemsPage } from '../pages/ItemsPage'
test.describe('Item search', () => {
let items: ItemsPage
test.beforeEach(async ({ page }) => {
items = new ItemsPage(page)
await items.goto()
})
test('searches by keyword', async () => {
await items.search('test')
expect(await items.itemCount()).toBeGreaterThan(0)
await expect(items.itemCards.first()).toContainText(/test/i)
})
test('shows empty state with no results', async ({ page }) => {
await items.search('zzz-nonexistent')
await expect(page.getByTestId('no-results')).toBeVisible()
expect(await items.itemCount()).toBe(0)
})
})
Running against the Laravel app
Point Playwright at the running app and let it boot php artisan serve (or use an already-running instance / CI app URL).
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: [['html', { outputFolder: 'playwright-report' }], ['junit', { outputFile: 'playwright-results.xml' }]],
use: {
baseURL: process.env.APP_URL ?? 'http://127.0.0.1:8000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
],
webServer: {
command: 'php artisan serve --port=8000',
url: 'http://127.0.0.1:8000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
})
Use a dedicated testing database/env (.env.testing) so E2E runs never touch development data, and migrate/seed it before the suite.
Auth and network state reuse
Log in once, save storage state, and reuse it so specs skip the login flow.
// global-setup.ts — sign in once, persist cookies/session
import { chromium } from '@playwright/test'
export default async () => {
const browser = await chromium.launch()
const page = await browser.newPage()
await page.goto(`${process.env.APP_URL ?? 'http://127.0.0.1:8000'}/login`)
await page.getByLabel('Email').fill(process.env.E2E_EMAIL!)
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!)
await page.getByRole('button', { name: 'Log in' }).click()
await page.waitForURL('**/dashboard')
await page.context().storageState({ path: 'tests/e2e/.auth/user.json' })
await browser.close()
}
Wire it via globalSetup and use: { storageState: 'tests/e2e/.auth/user.json' }. Keep .auth/ out of version control. You can also stub backend responses with page.route(...) to isolate the frontend from flaky external services.
CI wiring
name: E2E
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with: { php-version: '8.3' }
- run: composer install --no-interaction --prefer-dist
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: php artisan migrate --seed --env=testing
- run: npx playwright test
env:
APP_URL: http://127.0.0.1:8000
- uses: actions/upload-artifact@v4
if: always()
with: { name: playwright-report, path: playwright-report/, retention-days: 30 }
Flaky-test avoidance
Per @rules/code-testing/general.mdc, tests must be deterministic.
- Use auto-waiting locators (
locator.click()), neverpage.click(selector)on a maybe-not-ready node. - Replace
await page.waitForTimeout(...)withwaitForResponse,waitForURL, orexpect(locator).toBeVisible(). - Wait for the element state before acting on animated UI:
await locator.waitFor({ state: 'visible' }). - Quarantine genuinely flaky tests with
test.fixme(true, 'flaky — issue #NNN')and link an issue rather than leaving them red. - Reproduce flakiness with
npx playwright test path.spec.ts --repeat-each=10.
Done when
- Preconditions confirmed Playwright is present (otherwise deferred to manual/Pest/Dusk).
- Specs use POM and role/test-id selectors.
- The suite runs against the Laravel app on a dedicated testing env/database.
- Auth/network state is reused where it speeds the suite.
- No arbitrary sleeps; all waits are condition-based.
- CI runs the suite and uploads the report.