Reference catalog of the eight flake patterns - async/timing, test ordering, shared parallel state, resource leaks, network, locator drift, environment variance, randomness - with detection heuristics, remediation per pattern, and the concrete code-level fixes: replacing fixed sleeps with framework auto-waits, isolating state in beforeEach fixtures, per-worker DB schemas via workerIndex, try/finally teardown, mocking network + clock at the boundary, stable role-based locators, TZ pinning, and RNG seeding. Use when triaging an unknown flake to identify the category before bisecting, or when a classified flake needs the specific code change to apply.
98
91%
Does it follow best practices?
Impact
99%
1.07xAverage score across 10 eval scenarios
Passed
No findings from the security scan
Deep reference for flake-pattern-reference SKILL.md. The Pattern 5
(network / external service) and Pattern 6 (locator drift) code fixes,
split out of the main guide so the four core-pattern fixes stay in front.
Root cause: the test reaches a real network endpoint that is slow, rate-limited, or unavailable in CI.
page.route(urlPattern, handler) intercepts every request matching the
pattern and stalls it until you call fulfill, continue, or abort
(pw-network):
await page.route('**/api/users', route =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Alice' }]),
})
);
await page.goto('/users');
await expect(page.getByRole('listitem')).toHaveCount(1);Use browserContext.route() instead of page.route() when the request
originates from a popup or a new page (pw-api).
Block non-essential traffic (images, analytics) to speed up tests:
await page.route('**/*.{png,jpg,jpeg,gif,webp}', route => route.abort());Mock Service Worker intercepts fetch and XHR at the Node.js level for unit and integration tests (msw-start):
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('https://api.example.com/user', () =>
HttpResponse.json({ id: 'abc-123', name: 'Alice' })
)
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers()); // clean per-test overrides
afterAll(() => server.close());Isolate them in a separate Playwright project or Jest project with a
--testPathPattern that CI runs outside the main gate. The main merge
gate only runs mocked suites.
Root cause: selectors matched by CSS class, position, or text that shifts with unrelated UI changes.
Playwright recommends getByRole() as the primary locator strategy
because it reflects how users and assistive technology perceive the
page (pw-best-practices):
// Before - CSS class breaks on a design-system update
await page.locator('button.btn-primary.checkout-btn').click();
// After - survives CSS changes; tied to accessible role + name
await page.getByRole('button', { name: 'Checkout' }).click();Fallback order: getByRole > getByTestId > getByLabel / getByText
CSS/XPath (last resort).
<div class="card" data-testid="product-card-42">...</div>await page.getByTestId('product-card-42').click();Playwright locators are strict by default: if a locator matches more than one element, the action throws rather than silently acting on the first match (pw-locators):
// Throws immediately if two buttons match - forces you to be more specific
await page.getByRole('button', { name: 'Delete' }).click();Narrow an ambiguous locator with .filter():
await page
.getByRole('listitem')
.filter({ hasText: 'Product 42' })
.getByRole('button', { name: 'Delete' })
.click();