Files
claude-skills-reference/engineering-team/playwright-pro/skills/review/anti-patterns.md
Alireza Rezvani d33d03da50 feat: add playwright-pro plugin — production-grade Playwright testing toolkit (#254)
Complete Claude Code plugin with:
- 9 skills (/pw:init, generate, review, fix, migrate, coverage, testrail, browserstack, report)
- 3 specialized agents (test-architect, test-debugger, migration-planner)
- 55 test case templates across 11 categories (auth, CRUD, checkout, search, forms, dashboard, settings, onboarding, notifications, API, accessibility)
- TestRail MCP server (TypeScript) — 8 tools for bidirectional sync
- BrowserStack MCP server (TypeScript) — 7 tools for cross-browser testing
- Smart hooks (auto-validate tests, auto-detect Playwright projects)
- 6 curated reference docs (golden rules, locators, assertions, fixtures, pitfalls, flaky tests)
- Leverages Claude Code built-ins (/batch, /debug, Explore subagent)
- Zero-config for core features; TestRail/BrowserStack via env vars
- Both TypeScript and JavaScript support throughout

Co-authored-by: Leo <leo@openclaw.ai>
2026-03-05 13:50:05 +01:00

4.2 KiB

Playwright Anti-Patterns Reference

1. Using waitForTimeout()

Bad:

await page.click('.submit');
await page.waitForTimeout(3000);
await expect(page.locator('.result')).toBeVisible();

Good:

await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByTestId('result')).toBeVisible();

Why: Arbitrary waits slow tests and cause flakiness. Web-first assertions auto-retry.

2. Non-Web-First Assertions

Bad:

const text = await page.textContent('.message');
expect(text).toBe('Success');

Good:

await expect(page.getByText('Success')).toBeVisible();

Why: expect(locator) auto-retries until timeout. expect(value) checks once and fails.

3. Hardcoded URLs

Bad:

await page.goto('http://localhost:3000/login');

Good:

await page.goto('/login');

Why: baseURL in config handles the host. Tests break across environments with hardcoded URLs.

4. CSS/XPath When Role-Based Exists

Bad:

await page.click('#submit-btn');
await page.locator('.nav-link.active').click();

Good:

await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('link', { name: 'Dashboard' }).click();

Why: Role-based locators survive CSS renames, class refactors, and component library changes.

5. Missing await

Bad:

page.goto('/dashboard');
expect(page.getByText('Welcome')).toBeVisible();

Good:

await page.goto('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();

Why: Missing await causes race conditions. Tests pass sometimes, fail others.

6. Shared Mutable State

Bad:

let userId: string;

test('create user', async ({ page }) => {
  // ... creates user, sets userId
  userId = '123';
});

test('edit user', async ({ page }) => {
  await page.goto(`/users/${userId}`); // depends on previous test
});

Good:

test('edit user', async ({ page }) => {
  // Create user via API in this test's setup
  const userId = await createUserViaAPI();
  await page.goto(`/users/${userId}`);
});

Why: Tests must be independent. Shared state causes order-dependent failures.

7. Execution Order Dependencies

Bad:

test('step 1: fill form', async ({ page }) => { ... });
test('step 2: submit form', async ({ page }) => { ... });
test('step 3: verify result', async ({ page }) => { ... });

Good:

test('should fill and submit form successfully', async ({ page }) => {
  // All steps in one test
});

Why: Playwright runs tests in parallel by default. Order-dependent tests fail randomly.

8. Tests Over 50 Lines

Split into focused tests. Each test should verify one behavior.

9. Magic Strings

Bad:

await page.getByLabel('Email').fill('admin@test.com');

Good:

const TEST_USER = { email: 'admin@test.com', password: 'Test123!' };
await page.getByLabel('Email').fill(TEST_USER.email);

10. Missing Error Cases

If you test the happy path, also test:

  • Invalid input
  • Empty state
  • Network error
  • Permission denied
  • Timeout/loading state

11. Using page.evaluate() Unnecessarily

Bad:

const text = await page.evaluate(() => document.querySelector('.title')?.textContent);

Good:

await expect(page.getByRole('heading')).toHaveText('Expected Title');

12. Deep Nesting

Keep test.describe() to max 2 levels. More makes tests hard to find and maintain.

13. Generic Test Names

Bad: test('test 1'), test('should work'), test('login test')

Good: test('should show error when email is invalid'), test('should redirect to dashboard after successful login')

14-20. Style Issues

  • No page objects for complex pages → create them
  • Inline data → use factories or fixtures
  • Missing a11y assertions → add toHaveAttribute('role', ...)
  • No visual regression → add toHaveScreenshot() for key pages
  • Not checking console errors → add page.on('console', ...)
  • Using networkidle → use specific assertions instead
  • No test.describe() → group related tests