agentleFS
pekralpekral/cursor-rules/skills/e2e-testing/SKILL.md

e2e-testing

Skill10 starsChanged 3 months agoMITView on GitHub

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.

  • Reads credentials

We split this skill into the rules it gives an agent

8 rules. 0 are common, 3 are distinctive, and 5 are very specific to this project.

  • 0

    Common

    Most of the 38 edited versions we read keep these.

  • 3

    Distinctive

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    No arbitrary waitForTimeout.

    line 13
  2. Distinctive:

    Check whether Playwright is actually present:

    line 21
  3. Very specific:

    If neither a playwright.config.* file nor @playwright/test in package.json exists, STOP.

    line 28
  4. Distinctive:

    Only when Playwright IS present, continue with the patterns below.

    line 31
  5. Very specific:

    Use a dedicated testing database/env (.env.testing) so E2E runs never touch development data, and migrate/seed it before the suite.

    line 147
  6. Very specific:

    Use auto-waiting locators (locator.click()), never page.click(selector) on a maybe-not-ready node.

    line 200
  7. Very specific:

    Wait for the element state before acting on animated UI: await locator.waitFor({ state: 'visible' }).

    line 202
  8. Distinctive:

    No arbitrary sleeps; all waits are condition-based.

    line 211

“Common” is measured: kept by at least 6 in 10 of the 38 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Constraints

  • Apply @rules/code-testing/general.mdc for 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-testid over 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/test in package.json exists, STOP. Do not install Playwright. Defer to:
    • @skills/test-like-human/SKILL.md for 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:

  1. getByRole(...) with an accessible name.
  2. getByTestId(...) — add data-testid to the relevant Blade/Livewire markup.
  3. getByLabel / getByText for forms and copy.
  4. 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()), never page.click(selector) on a maybe-not-ready node.
  • Replace await page.waitForTimeout(...) with waitForResponse, waitForURL, or expect(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.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.