QA How-To
Playwright Cheat Sheet: Locators, Actions, and Assertions
Use this Playwright cheat sheet for locators, actions, assertions, fixtures, network waits, configuration, CLI commands, debugging, and interview review.
15 min read | 2,216 words
TL;DR
Use user-facing locators, rely on actionability, and assert observable states with retrying expect(locator) matchers. Keep event waits race-free, data isolated, and CI evidence enabled.
Key Takeaways
- Prefer roles and labels, then scope and filter locators until one meaningful target remains.
- Await every Playwright action, assertion, event promise, and fixture operation.
- Replace fixed sleeps with web-first assertions or a specific event, response, or URL.
- Register popup, download, and response waits before the action that triggers them.
- Use fixtures to give test data one owner and reliable teardown in parallel runs.
- Preserve traces and focused failure evidence before increasing retries or timeouts.
This playwright cheat sheet is a practical TypeScript reference for selecting elements, performing user actions, making retrying assertions, handling frames and popups, debugging failures, and running tests from the command line. The examples use Playwright Test and favor user-visible semantics, automatic waiting, and isolated test data.
Copy individual patterns into a suite, but keep the intent behind them. A locator is a reusable query, an action waits for actionability, and a web-first assertion retries until its condition is satisfied. Those three ideas remove most manual waits and make failures easier to explain.
TL;DR
| Task | Preferred pattern |
|---|---|
| Find a button | page.getByRole('button', { name: 'Save' }) |
| Find a form field | page.getByLabel('Email') |
| Click and verify | await button.click(); await expect(status).toHaveText('Saved') |
| Wait for UI state | Use expect(locator) rather than waitForTimeout() |
| Target a list row | Locate the row, filter it, then locate inside it |
| Debug locally | npx playwright test --debug |
| Preserve failure evidence | Enable trace, screenshot, and video policies in config |
1. Playwright cheat sheet setup and test anatomy
Install Playwright Test in a Node project, download the configured browsers, and create a config. Package versions should be pinned by the lockfile.
npm init playwright@latest
npx playwright install
npx playwright test
A test receives isolated fixtures such as page, context, and request. Hooks prepare state, while assertions report the observable result.
import { test, expect } from '@playwright/test';
test.describe('profile', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/profile');
});
test('updates the display name', async ({ page }) => {
await page.getByLabel('Display name').fill('Morgan QA');
await page.getByRole('button', { name: 'Save profile' }).click();
await expect(page.getByRole('status')).toHaveText('Profile saved');
});
});
Every promise from Playwright must be awaited. Omitting await can let the test finish before an action or assertion completes. Keep tests independent: a test should not rely on another test creating a record, retaining a login, or running first.
Useful annotations and control methods include:
test.skip(process.platform === 'win32', 'Not supported on Windows');
test.fixme(Boolean(process.env.KNOWN_BUG), 'Tracked product defect');
test.slow();
Use skip for an intentional non-execution condition, fixme for known broken coverage, and slow only when the behavior is legitimately slower. Do not use them to hide flaky tests. Tags and annotations can also be supplied through test details and filtered in CI, but keep naming conventions documented.
2. Locator priority and accessible queries
Locators are strict by default for operations that imply one target. If click() matches two buttons, Playwright fails instead of guessing. Choose a locator that expresses what a user perceives and narrows to one element.
| Priority | Locator | Typical use |
|---|---|---|
| 1 | getByRole(role, { name }) |
Buttons, links, headings, dialogs, tables |
| 2 | getByLabel(text) |
Inputs associated with visible or accessible labels |
| 3 | getByPlaceholder(text) |
Inputs when placeholder is the intended product contract |
| 4 | getByText(text) |
Non-interactive visible text |
| 5 | getByAltText(text) |
Images and image inputs with alternative text |
| 6 | getByTitle(text) |
Elements whose title is a genuine user contract |
| 7 | getByTestId(id) |
Explicit automation contract when semantic options do not fit |
| Last | locator(css) |
Structural targeting that cannot be expressed more meaningfully |
const save = page.getByRole('button', { name: 'Save' });
const email = page.getByLabel('Work email');
const search = page.getByPlaceholder('Search projects');
const notice = page.getByText('Changes published', { exact: true });
const avatar = page.getByAltText('Profile photo');
const help = page.getByTitle('Keyboard shortcuts');
const chart = page.getByTestId('usage-chart');
Role locators validate assumptions about semantics and accessible names. Read the complete Playwright getByRole guide when a role query unexpectedly matches nothing. A visible label is usually better than a placeholder because placeholders can disappear as users type and should not replace labels in accessible forms.
Text options accept strings or regular expressions:
page.getByRole('heading', { name: /^Order #\d+$/ });
page.getByText(/payment (approved|declined)/i);
Use regular expressions to represent real variation, not to compensate for not understanding the page. A broad expression can match an unintended element and weaken strictness.
3. Chaining, filtering, and selecting list items
Build complex targets from readable pieces. First locate the ownership boundary, then filter it by content or descendants, and finally locate the control inside it.
const row = page
.getByRole('row')
.filter({ has: page.getByRole('cell', { name: 'INV-1042' }) });
await row.getByRole('button', { name: 'Open' }).click();
Common composition patterns:
const activeCard = page.getByTestId('project-card').filter({
hasText: 'Active'
});
const cardWithoutError = page.getByTestId('project-card').filter({
hasNotText: 'Sync failed'
});
const namedDialog = page.getByRole('dialog').filter({
has: page.getByRole('heading', { name: 'Invite member' })
});
const submit = page
.getByRole('button', { name: 'Submit' })
.and(page.locator('.primary'));
const cancelOrClose = page
.getByRole('button', { name: 'Cancel' })
.or(page.getByRole('button', { name: 'Close' }));
locator('selector') called on another locator searches descendants. filter({ has }) keeps outer elements containing the inner locator. The inner locator is evaluated relative to each candidate, so make sure its query begins inside the candidate rather than at a separate page region.
Use .first(), .last(), or .nth(index) only when order is the intended contract. If the third button happens to be Submit today, nth(2) hides a missing semantic locator and can click the wrong control after layout changes. For repeated data, select by a unique row label or test ID instead.
To inspect collections, use allTextContents() or allInnerTexts() after the list has reached an expected state. Prefer toHaveCount() and toHaveText([...]) for assertions because they retry:
const items = page.getByRole('listitem');
await expect(items).toHaveCount(3);
await expect(items).toHaveText(['Queued', 'Running', 'Complete']);
4. Actions, keyboard input, files, and selection
Playwright actions wait for relevant actionability checks, such as visibility, stability, event reception, and enabled state. Use force only when the test intentionally bypasses a real user constraint and can explain why.
await page.getByRole('button', { name: 'Create' }).click();
await page.getByText('Advanced').dblclick();
await page.getByLabel('Email').fill('qa@example.com');
await page.getByLabel('Search').clear();
await page.getByLabel('Remember me').check();
await page.getByLabel('Remember me').uncheck();
await page.getByLabel('Country').selectOption({ label: 'Canada' });
await page.getByRole('link', { name: 'Account' }).hover();
await page.getByLabel('Name').focus();
await page.getByLabel('Name').blur();
fill() sets the field value efficiently and is right for most forms. Use pressSequentially() when the application must receive individual keyboard events, such as an autocomplete or masked field.
await page.getByLabel('Search').pressSequentially('playwright', {
delay: 40
});
await page.getByLabel('Search').press('Enter');
await page.keyboard.press('ControlOrMeta+A');
Uploads accept a path or files. A buffer keeps a small test self-contained:
await page.getByLabel('Upload report').setInputFiles({
name: 'report.txt',
mimeType: 'text/plain',
buffer: Buffer.from('release status: ready')
});
Clear a file input with an empty array:
await page.getByLabel('Upload report').setInputFiles([]);
For drag and drop:
await page.getByText('Task A').dragTo(page.getByTestId('done-column'));
Prefer high-level actions over dispatchEvent() because they represent user interaction and include actionability. Low-level events are appropriate only when testing an event-specific integration that cannot be triggered normally.
5. Web-first assertions reference
Assertions on locators retry until they pass or reach the assertion timeout. Generic assertions such as expect(value).toBe(...) evaluate immediately. Assert on the UI locator when the value may still be changing.
const dialog = page.getByRole('dialog', { name: 'Checkout' });
await expect(dialog).toBeVisible();
await expect(dialog).toBeAttached();
await expect(dialog).toContainText('Order total');
await expect(dialog).toHaveAttribute('aria-modal', 'true');
await expect(dialog).toHaveClass(/checkout-dialog/);
await expect(dialog).toHaveCSS('position', 'fixed');
Frequently used state assertions:
await expect(page.getByRole('button', { name: 'Pay' })).toBeEnabled();
await expect(page.getByLabel('Terms')).toBeChecked();
await expect(page.getByLabel('Promo code')).toBeEditable();
await expect(page.getByLabel('Promo code')).toBeEmpty();
await expect(page.getByLabel('Email')).toHaveValue('qa@example.com');
await expect(page.getByRole('alert')).toBeHidden();
await expect(page.getByRole('heading', { name: 'Receipt' })).toBeInViewport();
Page and response assertions:
await expect(page).toHaveTitle(/Dashboard/);
await expect(page).toHaveURL(/\/projects\/\d+$/);
const response = await page.request.get('/api/health');
await expect(response).toBeOK();
Positive visibility is generally more meaningful than checking element existence. toBeAttached() is useful when attachment itself is the contract, but an attached hidden control may still be unusable.
Use soft assertions when the test should collect several related mismatches before stopping:
await expect.soft(page.getByTestId('subtotal')).toHaveText('$40.00');
await expect.soft(page.getByTestId('tax')).toHaveText('$2.00');
await expect.soft(page.getByTestId('total')).toHaveText('$42.00');
expect(test.info().errors).toHaveLength(0);
Do not make every assertion soft. Continuing after a failed prerequisite can produce misleading actions and extra noise.
6. Waiting, navigation, network, and downloads
Never use waitForTimeout() as a synchronization strategy. A fixed sleep is both too long on a fast run and too short on a slow run. Wait for the user-visible state, a specific response, or a URL that proves readiness.
await page.getByRole('button', { name: 'Refresh' }).click();
await expect(page.getByRole('status')).toHaveText('Data updated');
Start waits before the action when the event could occur immediately:
const responsePromise = page.waitForResponse(response =>
response.url().endsWith('/api/orders') && response.status() === 201
);
await page.getByRole('button', { name: 'Place order' }).click();
const response = await responsePromise;
expect((await response.json()).status).toBe('created');
The same pattern applies to downloads and popups:
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export CSV' }).click();
const download = await downloadPromise;
await download.saveAs(test.info().outputPath(download.suggestedFilename()));
const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Open preview' }).click();
const popup = await popupPromise;
await expect(popup).toHaveTitle(/Preview/);
For navigation, page.goto() waits according to its options, while web assertions can verify the resulting URL and content. Avoid networkidle as a universal readiness signal. Applications may keep analytics, polling, or streaming connections active. A heading, status, or API response tied to the feature is usually clearer.
When timeouts occur, identify which action or expectation is waiting and inspect its locator. The Playwright timeout troubleshooting guide provides a systematic diagnosis path.
7. Frames, dialogs, multiple pages, and shadow DOM
Use frameLocator() to target content inside an iframe without manually retrieving a frame object:
const payment = page.frameLocator('iframe[title="Secure payment"]');
await payment.getByLabel('Card number').fill('4242424242424242');
await payment.getByRole('button', { name: 'Pay' }).click();
For a frame selected by its owner element:
const frame = page.locator('iframe[name="editor"]').contentFrame();
await frame.getByRole('textbox').fill('Release notes');
Register JavaScript dialog handling before the action:
page.once('dialog', async dialog => {
expect(dialog.type()).toBe('confirm');
expect(dialog.message()).toBe('Delete project?');
await dialog.accept();
});
await page.getByRole('button', { name: 'Delete' }).click();
Playwright locators pierce open shadow DOM by default, except XPath does not pierce shadow roots and closed roots remain inaccessible. Prefer user-facing locators without encoding the shadow structure:
await page.getByRole('button', { name: 'Add to cart' }).click();
For multiple tabs, keep references explicit. A browser context owns pages and shared session storage such as cookies. A new context creates isolation:
const adminContext = await browser.newContext();
const adminPage = await adminContext.newPage();
await adminPage.goto('/admin');
await adminContext.close();
In a standard test, prefer built-in context and page fixtures. Create additional contexts only when the scenario needs separate users or clean sessions, and always close contexts you create.
8. Fixtures, authentication, API setup, and test data
Fixtures package setup, dependencies, and teardown. Extend the base test when many cases need a domain object. Keep fixture scope as narrow as practical and avoid one mutable account shared by parallel workers.
import { test as base, expect } from '@playwright/test';
type Fixtures = {
projectId: string;
};
export const test = base.extend<Fixtures>({
projectId: async ({ request }, use) => {
const created = await request.post('/api/projects', {
data: { name: `e2e-${crypto.randomUUID()}` }
});
expect(created.ok()).toBeTruthy();
const project = await created.json() as { id: string };
await use(project.id);
await request.delete(`/api/projects/${project.id}`);
}
});
export { expect };
The use() call divides setup from teardown. Teardown still runs after the test fails. Cleanup should be idempotent when possible, and it should not erase evidence belonging to another worker.
Authentication state can be prepared in a setup project and loaded with storageState. The state file can contain sensitive cookies or tokens, so keep it out of version control and regenerate it in trusted environments. Use separate accounts when tests modify server-side state, because separate browser contexts do not isolate one shared backend user.
API setup is often faster and clearer than navigating through unrelated screens. Create the order through request, then test the UI behavior that matters. Do not bypass the UI for the behavior under test itself. A checkout test still needs to exercise checkout, even if its catalog data arrived through an API fixture.
For asynchronous fixture code and test callbacks, follow the patterns in async and await for test automation.
9. Routing, mocking, clock, and advanced assertions
Intercept network requests with page.route() or browserContext.route(). Match narrowly and remove routes when a test installs them outside its isolated context.
await page.route('**/api/recommendations', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [{ id: 'p1', name: 'QA Handbook' }] })
});
});
await page.goto('/store');
await expect(page.getByText('QA Handbook')).toBeVisible();
To modify a real response:
await page.route('**/api/profile', async route => {
const response = await route.fetch();
const json = await response.json();
await route.fulfill({ response, json: { ...json, plan: 'enterprise' } });
});
Control browser time with page.clock when timers or Date drive behavior:
await page.clock.install({ time: new Date('2026-07-13T09:00:00Z') });
await page.goto('/session');
await page.clock.runFor(30_000);
await expect(page.getByRole('alert')).toHaveText('Session expires soon');
expect.poll() repeatedly evaluates an arbitrary async value:
await expect.poll(async () => {
const response = await page.request.get('/api/jobs/42');
return (await response.json()).status;
}).toBe('complete');
Use expect().toPass() to retry a block when multiple observations must converge:
await expect(async () => {
const response = await page.request.get('/api/inventory/SKU-1');
expect(response.status()).toBe(200);
expect((await response.json()).available).toBe(true);
}).toPass();
Choose a retry interval and timeout appropriate to the system rather than turning polling into an unbounded wait.
10. Playwright cheat sheet for config, CLI, and debugging
A practical configuration names projects, sets evidence policies, and keeps assertions explicit:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [['html', { open: 'never' }], ['list']],
use: {
baseURL: 'http://127.0.0.1:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
expect: { timeout: 5_000 },
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } }
],
webServer: {
command: 'npm run dev',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI
}
});
CLI reference:
npx playwright test
npx playwright test tests/checkout.spec.ts
npx playwright test -g "guest checkout"
npx playwright test --project=chromium
npx playwright test --headed
npx playwright test --debug
npx playwright test --ui
npx playwright test --workers=1
npx playwright test --repeat-each=10
npx playwright show-report
npx playwright show-trace test-results/path-to-trace.zip
Use --workers=1 for diagnosis, not as the permanent fix for shared-state bugs. Use repeat mode to measure reproducibility while preserving failure evidence. The Inspector and UI Mode help examine locators, steps, and DOM state. Trace Viewer adds action snapshots, network, console, and source context.
For CI, install dependencies deterministically, install Playwright browsers as documented for the environment, and shard only isolated tests. Container guidance is available in Docker for Playwright testing.
Treat configuration as executable policy. Review changes to retries, workers, ignored paths, and evidence retention with the same care as test code, because a convenient config edit can silently reduce coverage. Keep developer and CI behavior close enough that a failure can be reproduced locally. When environments require differences, express them with a small documented conditional and print non-sensitive target context in the report. A command that exits successfully after discovering zero intended tests is also dangerous, so include a launch or discovery check when suite selection is generated dynamically.
Interview Questions and Answers
Q: Why are Playwright locators preferred over storing element handles?
Locators represent a query that is resolved when an action or assertion runs. They work with auto-waiting and retryability, so they tolerate normal re-rendering. An element handle points to a particular DOM node and can become stale relative to application behavior.
Q: What is the difference between fill() and pressSequentially()?
fill() sets an input value efficiently and is appropriate for most forms. pressSequentially() sends individual key input with optional delay, which is useful when the application logic depends on per-key events, autocomplete, or masking.
Q: Why should a response wait be created before clicking?
The request and response may complete quickly. Registering waitForResponse() first prevents the test from missing the event, then awaiting the promise after the action associates the response with the triggering behavior.
Q: When would you use getByTestId()?
I use it when the element has no reliable user-facing semantic locator or when the test needs an explicit stable contract below visible copy. I still prefer roles and labels for interactive elements because they align the test with accessibility and user behavior.
Q: How do Playwright assertions reduce flakiness?
Locator assertions retry the observation until it matches or the assertion timeout expires. They synchronize on a meaningful condition instead of sampling once after a fixed delay. They cannot fix shared data, wrong locators, or an application that never reaches the intended state.
Q: What is strictness in Playwright?
An operation that expects one element fails when its locator resolves to multiple elements. Strictness prevents Playwright from guessing. The fix is normally a more meaningful unique locator, not .first() unless first position is truly the requirement.
Q: How would you debug a test that passes locally but fails in CI?
I would inspect the trace and first meaningful error, compare browser, configuration, data, and feature flags, and reproduce the smallest test using CI-like settings. I would also check parallel isolation and network dependencies before increasing timeouts.
Q: What belongs in a Playwright fixture?
A fixture should own reusable setup, a typed value supplied to tests, and reliable teardown. Good examples include a unique project, authenticated page, domain client, or feature configuration. It should not hide the behavior the test is supposed to exercise.
Common Mistakes
- Forgetting
awaiton actions, assertions, fixture operations, or event promises. - Using long CSS or XPath chains when roles and labels express intent.
- Calling
.first()to suppress a strictness error without understanding duplicates. - Waiting with
waitForTimeout()instead of an observable UI or network condition. - Starting
waitForEvent()after the event-triggering click. - Sharing one mutable user or record across parallel workers.
- Raising global timeouts before inspecting the waiting locator and application state.
- Using
force: trueto click a covered or disabled control without validating the real behavior. - Over-mocking the exact workflow being tested.
- Keeping authentication state files with live credentials in version control.
- Catching assertion errors and letting the test pass.
- Disabling retries, traces, or projects inconsistently without documenting the coverage impact.
Conclusion
The most useful playwright cheat sheet is built around semantics, actionability, and retrying assertions. Choose the smallest user-facing locator, perform an action that resembles real use, and assert the state that proves the behavior. Add fixtures for ownership and cleanup, event promises for races, and traces for evidence.
Keep this reference near code review, then turn its patterns into team-specific conventions. Start by replacing one fixed sleep, one brittle selector, and one shared test record. Those changes usually improve both execution reliability and the quality of the failure report.
Interview Questions and Answers
Why are locators preferred over element handles?
A locator is a reusable query that resolves when an action or assertion runs. It participates in auto-waiting and can follow normal rerendering. An element handle refers to a particular node and is a lower-level choice for cases that truly need it.
What is strictness in Playwright?
An operation that implies one target fails if its locator matches multiple elements. Strictness prevents Playwright from guessing. I fix it with meaningful scope or a unique name instead of using first() unless order is the requirement.
Why create waitForResponse before clicking?
The network event may happen immediately after the action. Registering the wait first closes the race, then awaiting it after the click ties the response to the behavior. I also match method, URL, and status narrowly enough to avoid unrelated traffic.
What is a web-first assertion?
It is an assertion that repeatedly evaluates a page or locator state until it passes or reaches its timeout. Examples include toHaveText() and toBeVisible(). It synchronizes on evidence rather than sampling once after a sleep.
How do you isolate Playwright tests?
I use the built-in browser context isolation and give each test unique mutable backend data. Fixtures create and clean records that they own. Separate contexts are not enough when tests still edit the same account or database entity.
When would you use expect.poll?
I use it for an eventually consistent arbitrary value, often a read-only API status. The producer is retried with bounded timeout and deliberate intervals, then a normal matcher checks the returned value. I avoid polling side effects that could create duplicates.
What should Playwright CI preserve on failure?
It should preserve the first error plus useful trace, screenshot, video, console, and network evidence according to the project's retention policy. Reports should include browser and non-sensitive environment context. Secrets and personal data must be redacted.
How do you choose between page.route and API setup?
I use page.route when the test targets client behavior under a controlled response. I use the request fixture to create real prerequisites when downstream UI behavior needs server state. I keep separate contract coverage for the actual service boundary.
Frequently Asked Questions
What locator should I use first in Playwright?
Start with getByRole() and an accessible name for interactive elements and landmarks. Use getByLabel() for form fields, then other user-facing locators or an explicit test ID when semantics do not fit.
How does Playwright wait for elements?
Actions perform relevant actionability checks, and web-first locator assertions retry their observation. You normally wait for the state that proves behavior instead of sleeping for a fixed duration.
How do I run one Playwright test?
Pass its file path to npx playwright test or use -g with a focused title pattern. Add --project to select a browser project and --debug to open an interactive debugging session.
What is the difference between fill and pressSequentially?
fill() sets the input value efficiently and fits most fields. pressSequentially() sends individual key input and is appropriate when autocomplete, masks, or key-by-key handlers are part of the behavior.
How do I handle a new tab in Playwright?
Create page.waitForEvent('popup') before clicking the link or button, then await the returned page. This order avoids missing a popup that opens immediately.
When should I use getByTestId?
Use it when no reliable user-facing semantic locator represents the element or when the team wants a stable automation contract. Keep roles and labels for controls when possible because they also validate accessibility assumptions.
How can I debug a flaky Playwright test?
Inspect the first error and trace, then compare data, browser, configuration, and parallel ownership. Reproduce the smallest case under CI-like conditions before adding timeouts or retries.
Related Guides
- Selenium Cheat Sheet: Locators, Waits, and Commands
- Playwright drag and drop: Examples and Best Practices
- Playwright mock date and time: Examples and Best Practices
- Playwright popup and new tab handling: Examples and Best Practices
- Playwright tag and grep filters: Examples and Best Practices
- How to Build a BDD framework with Cucumber and Playwright (2026)