P0 · Chapter 02 / 15

Playwright core

The 60-second cheat sheet#

Playwright Test is a runner with worker processes, projects, fixtures, assertions and reporters. It creates isolated contexts/pages per test by default; this does not isolate backend accounts. Locators describe queries, actions wait for their actionability conditions, and web-first assertions retry the expected observation.

Use role/label/test-ID locators, register listeners before triggering events, verify business outcomes after transport responses, and inspect traces when CI disagrees with local runs.

Runner architecture and fixtures#

API / resource Purpose
test, test.describe Test registration and logical grouping; avoid dependent ordering
{ browser } Browser process shared within a worker for efficiency
{ context, page } Test-scoped browser isolation and default page
{ request } Test-scoped APIRequestContext for direct HTTP
expect Generic assertions, locator assertions, polling and response helpers
Tags / annotations Selection and reporting; tags such as @smoke, annotations for issue context
typescript
// tests/ui/settings.spec.ts; illustrative UI, config must set baseURL.
import { test, expect } from '@playwright/test';
test.describe('account settings', () => {
  test('saves preferences', { tag: '@smoke' }, async ({ page }) => {
    await test.step('save and observe confirmation', async () => {
      await page.goto('/settings');
      await page.getByRole('button', { name: 'Save', exact: true }).click();
      await expect(page.getByTestId('save-status')).toHaveText('Saved');
    });
  });
});

Locator choice and strictness#

Locator Use Trap
getByRole('button', {name:'Verify'}) User-facing semantic interaction Accessible name may differ from visible text
getByLabel('Email') Properly associated form label Placeholder is not a label
getByText('Verified', {exact:true}) User-visible content Multiple matching ancestors/items
getByPlaceholder('Search users') Placeholder is intended stable contract Localization/text changes
getByTestId('verification-status') Explicit testing contract Must still assert meaningful state
locator('#verification-form') Stable unique CSS ID Long DOM chains/nth-child are brittle
.filter({hasText: 'qa-1'}) Scope a row before finding its button Substring may match multiple rows

Strict operations requiring one element fail on ambiguity. Do not silence this by adding .first() without proving order is the requirement. A Locator re-resolves when used and handles DOM replacement better than a cached element handle.

typescript
// Scoped test body: stable row scope, then semantic action.
const row = page.getByRole('row').filter({ hasText: 'qa-unique-123' });
await expect(row).toHaveCount(1);
await row.getByRole('button', { name: 'Review', exact: true }).click();

Actionability and readiness#

For click, Playwright waits for a unique target that is visible, stable, receives events and is enabled. Different actions have different checks (fill also needs editability). force bypasses some checks and can hide usability defects. page.goto() normally waits for load; neither load nor a successful click guarantees a later API/Kafka projection has completed. Prefer an application-specific readiness assertion over network-idle heuristics or fixed sleeps.

Retrying assertions and timeout scopes#

typescript
// Scoped test body: assertion uses the live locator repeatedly.
await expect(page.getByTestId('verification-status')).toHaveText('VERIFIED');
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled();
await expect(page.getByRole('alert')).toBeVisible();
await expect(page.getByRole('row')).toHaveCount(3);
await expect(page).toHaveURL(/\/dashboard(?:\?|$)/);
// WRONG for eventual state: expect(await locator.innerText()).toBe('VERIFIED');
// The generic assertion receives a single string snapshot and cannot reread it.
Timeout Scope / decision
Test Budget for test execution and relevant setup; teardown has runner-managed budgeting
Assertion Each retrying assertion's deadline; override for a measured slow projection
Action Limit for actions such as click/fill, bounded by enclosing test
Navigation Navigation-specific waiting; does not cover business processing
Poll/toPass Explicit retry budget for custom observations/blocks

Set explicit budgets in config rather than memorizing defaults that may vary by version. A 60-second assertion cannot finish inside an exhausted 30-second test budget. Increasing timeouts without a latency hypothesis only delays evidence.

waitForResponse without races#

typescript
// Scoped test body; illustrative endpoint. Match method and exact path.
const savedPromise = page.waitForResponse(response =>
  new URL(response.url()).pathname === '/api/settings' &&
  response.request().method() === 'PUT'
);
await page.getByRole('button', { name: 'Save', exact: true }).click();
const saved = await savedPromise;
expect(saved.status()).toBe(200);
await expect(page.getByTestId('save-status')).toHaveText('Saved');

Registering after click can miss a fast response. A status condition in the predicate can be useful when several responses are expected, but filtering only for 200 may turn an immediate 500 into a confusing timeout. Match request identity and assert status afterward when diagnosing errors. Include a correlation/body predicate when identical requests can overlap.

Browser mocks versus real APIs#

typescript
// UI rendering test: no real verification backend is covered.
await page.route('**/api/verification/qa-1', route => route.fulfill({
  status: 200, contentType: 'application/json',
  body: JSON.stringify({ id: 'qa-1', status: 'REJECTED', reason: 'Document expired' }),
}));
await page.goto('/verification/qa-1');
await expect(page.getByRole('alert')).toContainText('Document expired');

route.fulfill supplies a response, route.continue lets a request proceed (possibly modified), and route.abort simulates transport failure. Register routes before navigation. Service workers can affect interception; control them in a dedicated test project if needed. Page routes do not intercept direct request.get() calls. Mocking proves rendering/interaction for a contract assumption; a separate real integration test must prove the service contract and state transitions.

Popups, downloads and custom retry#

typescript
// Scoped test body: event promises before actions.
const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Audit details' }).click();
const popup = await popupPromise;
await expect(popup).toHaveURL(/\/audit\//);
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.csv$/);
await download.saveAs(test.info().outputPath('audit.csv'));

expect.poll retries a returned value; use it for bounded read-only status checks. expect(async () => {...}).toPass({timeout: ...}) retries an entire assertion block. Repeating that block repeats its side effects: never place an unprotected payment creation inside it. Explicitly set the toPass budget; do not assume it inherits ordinary expect timeout configuration. Full polling example: Kafka chapter.

Eight wrong-versus-right patterns#

Wrong Right Why
expect(locator).toBeTruthy() await expect(locator).toBeVisible() Locator existence as an object proves nothing
expect(await status.innerText()).toBe(...) await expect(status).toHaveText(...) Snapshot vs retry
click then waitForResponse Create promise then click Prevent missed event
waitForTimeout(5000) Assert observable completion Latency varies
goto proves backend readiness Assert status/ready control Load is browser lifecycle only
.nth(3) after strictness failure Scope by stable row identity Match intended entity
page.route mocks request fixture Separate UI mock and real API tests Different network paths
Missing await on assertion Await retrying assertions Runner must observe rejection
typescript
// Wrong and corrected patterns, scoped to a test with { page }.
const status = page.getByTestId('save-status');
// WRONG: expect(await status.innerText()).toBe('Saved');
await expect(status).toHaveText('Saved');
// WRONG: await page.waitForTimeout(5000);
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled();
// WRONG: expect(status).toBeTruthy();
await expect(status).toBeVisible();

Eight executable correction patterns#

Each fragment is scoped inside a test with page/request and imported expect. Wrong lines are comments so copied code remains syntactically valid. Hypothetical UI/API contracts apply.

typescript
// 1. WRONG: expect(page.getByRole('alert')).toBeTruthy();
await expect(page.getByRole('alert')).toBeVisible();
typescript
// 2. WRONG: expect(await page.getByTestId('status').innerText()).toBe('VERIFIED');
await expect(page.getByTestId('status')).toHaveText('VERIFIED');
typescript
// 3. WRONG: click first, then create waitForResponse.
const saved = page.waitForResponse(r => new URL(r.url()).pathname === '/api/settings');
await page.getByRole('button', { name: 'Save' }).click();
expect((await saved).status()).toBe(200);
typescript
// 4. WRONG: await page.waitForTimeout(5000);
await expect(page.getByTestId('ready')).toHaveText('Ready', { timeout: 10_000 });
typescript
// 5. WRONG: goto alone proves the asynchronous page data is ready.
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await expect(page.getByTestId('account-state')).toHaveText('Active');
typescript
// 6. WRONG: page.getByRole('button', {name:'Review'}).first() ignores ambiguity.
const ownedRow = page.getByRole('row').filter({ hasText: 'qa-unique-user' });
await ownedRow.getByRole('button', { name: 'Review', exact: true }).click();
typescript
// 7. WRONG: assume browser route interception changes this direct API call.
const actual = await request.get('/api/health');
expect(actual.status()).toBe(200); // this is a real HTTP response
typescript
// 8. WRONG: expect(page.getByTestId('status')).toHaveText('VERIFIED');
await expect(page.getByTestId('status')).toHaveText('VERIFIED');

Advanced practice: prove what the test observes#

15-minute coding/review exercise with ideal reasoning

Write a test that opens a user's verification page, submits it, receives 202, and later renders VERIFIED. Include a duplicate “Verify” button in an unrelated row. The correct design scopes the target row, starts the response listener before click, checks method/path and returned 202, then uses a locator assertion or public API poll for the final state. The test should fail if the UI remains PENDING or the API returns 500. It should not pass because a “Verification” heading exists.

Now change the scenario: page.route returns VERIFIED but the deployed API returns REJECTED. Explain why the mocked UI check passes and which separate integration check detects the mismatch. Add a popup audit trail or download assertion using listener-before-trigger ordering. Review how test, action, navigation and assertion budgets interact. Finally, run with two independent users in parallel and inspect a trace for a deliberately wrong locator. The purpose is a causal explanation of every wait, not typing speed.

Diagnostics and interview response#

Use test.step to name business operations. Enable traces on first retry, screenshots on failure and video only when it adds evidence. npx playwright test --debug opens debugging tools; npx playwright show-report opens the HTML report; npx playwright show-trace test-results/path/trace.zip inspects a local trace. Redact traces containing session data before sharing.

30 seconds: “I choose locators by user semantics or a stable test contract. Actions wait for actionability; assertions retry the expected state. I register response listeners before clicks, then check the exact HTTP contract and user-visible result. I use isolated data and trace evidence to diagnose timing rather than adding sleeps.”

90-second senior answer: reliable UI synchronization

I separate browser readiness, transport completion and business completion. A page can load before its verification state arrives. A click can succeed while a later service call fails. I scope a locator to the right user and use a retrying assertion for the final status. If the transport is relevant, I register waitForResponse before the trigger and match method/path or correlation identity. A 202 requires subsequent polling of the final outcome. A mocked route only proves the frontend handles that response; I retain real API/integration coverage for the backend. On failure I inspect trace actions, DOM, network and service correlation logs, then fix the observed cause and verify it across parallel CI runs.

Memory hook: query → actionability → response → business state; each is a different contract.

Quick practice · reveal answers#

Is getByRole itself a wait?
  • It creates a Locator.
  • Actions perform their checks; web-first assertions retry expected state.

Pitfall: Treating locator construction as success evidence.

Full explanation →

What does click wait for?
  • Unique target and applicable actionability checks such as visible, stable, enabled and receiving events.
  • Business completion still needs assertion.

Pitfall: A successful click proves a saved record.

Full explanation →

Why prefer toHaveText over innerText plus toBe?
  • toHaveText rereads and retries a live locator.
  • innerText returns a single snapshot before a generic comparison.

Pitfall: Thinking any expect automatically retries browser state.

Full explanation →

Why register waitForResponse before clicking?
  • A fast response may arrive during the click.
  • Create the promise first, trigger action, await response and assert status.

Pitfall: Listening after the event already occurred.

Full explanation →

How should a response predicate match?
  • Use method, exact path and relevant identity/correlation.
  • Assert status afterward when failures need immediate evidence.

Pitfall: Broad substring matches an unrelated request.

Full explanation →

Does goto prove the app is ready?
  • It normally waits for load.
  • Asynchronous application and backend state need explicit readiness checks.

Pitfall: Using page load as a Kafka completion signal.

Full explanation →

Does page.route mock request.get?
  • No.
  • It intercepts matching browser requests, not direct APIRequestContext calls.

Pitfall: Calling a mocked UI test real backend coverage.

Full explanation →

When use a test ID over role?
  • Use role/label for stable user semantics, test ID for an explicit testing contract where semantics are ambiguous.
  • Stable CSS IDs can also be appropriate.

Pitfall: Blanket bans on every CSS selector.

Full explanation →

What does strict locator failure mean?
  • An operation needing one element matched multiple.
  • Scope to the intended entity or accessible name.

Pitfall: Blindly adding first() and clicking a wrong record.

Full explanation →

How do you handle popup/download races?
  • Register waitForEvent first, trigger action, then await the event.
  • Assert popup target or download metadata/content as required.

Pitfall: Waiting only after clicking.

Full explanation →

expect.poll versus expect.toPass?
  • poll retries a returned observation; toPass retries a whole assertion block.
  • Use explicit budgets and keep repeated code side-effect safe.

Pitfall: Putting non-idempotent writes inside the retried block.

Full explanation →

What timeout should you increase?
  • Only the operation whose legitimate latency is demonstrated.
  • Fit assertion/action/navigation budgets within test budget.

Pitfall: Globally increasing everything to mask shared-state errors.

Full explanation →

What does a browser mock prove?
  • Frontend behavior for the supplied contract response.
  • Real service correctness needs separate integration evidence.

Pitfall: Passing a mocked VERIFIED response as proof of backend verification.

Full explanation →

What does Playwright Test add beyond a browser library?
  • Runner, fixtures, projects, workers, assertions, retries and reporters.
  • Browser contexts/pages are test-scoped by default.

Pitfall: Writing an unmanaged script and assuming runner isolation/reporting.

Full explanation →

Official references and related topics#

Optional online further reading; all core study material is included above.

Back to library index

Prepared for both Senior automation roles. Hypothetical sandbox contracts; no employer systems or credentials. Content: 16 October 2026.