Copy-paste Claude Code prompts for Playwright — writing stable role-based selectors, killing flaky tests, network-mocking, and visual-regression baselines.
Playwright tests are flaky for three reasons: brittle selectors, hardcoded waits, and unstable network. The prompts below force Claude to write tests that hold up under CI variance.
Write a Playwright test at tests/e2e/projects-create.spec.ts that:
1. Logs in as the seeded test user (use the existing 'authenticatedUser' fixture
from tests/fixtures/auth.ts — read it first).
2. Navigates to /projects/new.
3. Fills name + description, picks visibility=private, submits.
4. Asserts the URL is /projects/.
5. Asserts the project name appears in the page heading.
6. Cleans up by deleting via API (use the apiClient fixture).
Constraints:
- getByRole / getByLabel only. NEVER CSS selectors.
- NO page.waitForTimeout. Use auto-wait via web-first assertions.
- await expect(page.getByRole('heading', { name: 'My project' })).toBeVisible()
is the only correct pattern.
- Network: do not mock; this is an integration test against the local server.
- Place the test in the 'authenticated' project group in playwright.config.ts.
tests/e2e/checkout.spec.ts has flake rate ~12% on CI. Read it.
Identify each potential flake source:
- page.waitForTimeout(...) calls
- selectors that depend on element index (.nth(2), .first()) where order
could vary
- assertions on non-deterministic text (timestamps, counts)
- network state that depends on prior test leakage
- click followed by assertion on the same element (race with rerender)
For each issue, propose the smallest fix:
- Replace waitForTimeout with the actual condition (toBeVisible, toHaveURL).
- Replace .nth() with a getByRole that filters by accessible name.
- Stabilise timestamps via fixed clock: await page.clock.install({ time: ... }).
- Reset DB state in beforeEach via the seedDatabase fixture.
Output as a unified diff. Run the test 20× in your head and confirm each path
is deterministic now.
The /signup test occasionally fails because it calls the real Stripe API in
test mode and the Stripe sandbox is slow. Mock Stripe.
Constraints:
- Use page.route('**/api.stripe.com/**', async route => route.fulfill({
status: 200, json: }))
- Fixtures live in tests/fixtures/stripe/. Read the existing ones — match
filenames like checkout-session.created.json.
- Mock at the test fixture level (define a 'stripeMocked' fixture in
tests/fixtures/index.ts) so the same setup is reusable.
- For tests that DO need real Stripe (the contract test in
tests/contracts/), exclude them from the mock fixture.
- Confirm the mock fires by asserting route.request().url() includes 'stripe'
in a sentinel test.
## Playwright policy
- Selectors: getByRole / getByLabel / getByPlaceholder / getByTestId.
Never CSS, never XPath.
- No page.waitForTimeout. Use web-first assertions (toBeVisible, toHaveText,
toHaveURL) which auto-wait up to expect timeout.
- Tests are independent. Each beforeEach reseeds. No relying on test order.
- Network: mock external APIs (Stripe, Postmark) via page.route. Local API
is real.
- One assertion per logical step. No assertion-free clicks.
Related: testing prompts.