Home / Blog / Playwright cheat sheet
Playwright cheat sheet (1.62)
This is a Playwright cheat sheet for version 1.62, checked against the documentation on 1 September 2026. It is a quick reference arranged by the question you have at the keyboard: which locator, which assertion, which action, which command. The last section lists the forms that still run and that Playwright now tells you not to use, which is what makes an old cheat sheet dangerous and not merely stale.
The expressions in the cells below come out of the three spec files further down this page,
and those files ran green on @playwright/test 1.62.1 before it was published.
Every table links into playwright.dev.
Which Playwright locator do I use?
Walk this locators cheat sheet from the top and stop at the first row that describes your
element and only your element. Each step down swaps a property a user can perceive for a
property of the markup, and the markup changes every time somebody refactors a component. The
order below is not the order docs/locators happens to present, and it has an
argument behind it that our guide to picking a locator
makes at length.
| Locator | What it matches | Reach for it when |
|---|---|---|
getByRole('button', { name: 'Save' }) | ARIA role plus accessible name | Almost always, and always for something a user clicks, types into or reads as a heading |
getByLabel('Full name') | A form control through its label | Inputs, selects and textareas with an associated label |
getByPlaceholder('Order number') | The placeholder attribute | An input with a placeholder and no label worth using |
getByText('Pending') | Text inside one element | Non-interactive content: a status, a paragraph, a cell |
getByAltText('Shipping status') | The alt attribute | Images and image maps |
getByTitle('Updated hourly') | The title attribute | The rare element whose tooltip is the only thing naming it |
getByTestId('row-1802') | data-testid, or whatever testIdAttribute is set to | Nothing above identifies the element and you can edit the source |
locator('table tr') | Any CSS selector | A structural hook the application promises to keep |
locator('xpath=//table//tr[1]') | An XPath expression | A legacy suite you are converting a file at a time |
Playwright locators are strict, so one that matches two elements throws instead of taking the
first, and most of the work on this page is narrowing. filter() takes
hasText, hasNotText, has, hasNot and
visible. and() demands both descriptions and or()
accepts either. first(), last() and nth() pin a
position in document order, which is a property of markup nobody is maintaining on your
behalf. The full set is on
playwright.dev/docs/locators, and the two
fallback rows are on
docs/other-locators.
The second test here has three Refund buttons, one of them hidden, and narrows down to the row that owns each one.
import { test, expect } from '@playwright/test';
const ORDERS = `
<h1>Orders</h1>
<label for="q">Search orders</label>
<input id="q" placeholder="Order number">
<img src="data:," alt="Shipping status" title="Updated hourly">
<table>
<tr data-testid="row-1801"><td>1801</td><td>Shipped</td><td><button>Refund</button></td></tr>
<tr data-testid="row-1802"><td>1802</td><td>Pending</td><td><button>Refund</button></td></tr>
</table>
<p hidden><button>Refund</button></p>`;
test('walk the ranking from the top', async ({ page }) => {
await page.setContent(ORDERS);
await expect(page.getByRole('heading', { name: 'Orders' })).toBeVisible();
await expect(page.getByLabel('Search orders')).toBeEditable();
await expect(page.getByPlaceholder('Order number')).toBeEditable();
await expect(page.getByText('Pending')).toBeVisible();
await expect(page.getByAltText('Shipping status')).toBeVisible();
await expect(page.getByTitle('Updated hourly')).toBeVisible();
await expect(page.getByTestId('row-1802')).toContainText('1802');
await expect(page.locator('table tr')).toHaveCount(2);
await expect(page.locator('xpath=//table//tr[1]')).toContainText('1801');
});
test('three Refund buttons, one row', async ({ page }) => {
await page.setContent(ORDERS);
const rows = page.getByRole('row');
await expect(rows.filter({ hasText: 'Pending' }).getByRole('button')).toHaveCount(1);
await expect(rows.filter({ hasNotText: 'Pending' })).toHaveCount(1);
await expect(rows.filter({ has: page.getByText('1801') })).toHaveCount(1);
await expect(rows.filter({ hasNot: page.getByText('1801') })).toHaveCount(1);
await expect(page.locator('button')).toHaveCount(3);
await expect(page.locator('button').filter({ visible: true })).toHaveCount(2);
await rows.filter({ hasText: '1801' }).getByRole('button', { name: 'Refund' }).click();
});
Playwright 1.62 · TypeScript · run on @playwright/test 1.62.1, Chromium
Which Playwright assertion do I use?
One line governs this whole assertions cheat sheet. An expect() wrapped around a
Playwright object retries until it passes or the assertion times out; an expect()
wrapped around a value you have already computed does not.
docs/test-assertions splits its own
tables along that line, under the headings Auto-retrying assertions and
Non-retrying assertions.
A reference documents both halves of a confusable pair correctly and never chooses between them, which leaves the choosing to an expect cheat sheet. Each row below is a pair people mix up, the question that separates them, and which one to write.
| The pair | The question that separates them | Write |
|---|---|---|
toHaveText / toContainText | Is the whole string the claim, or a substring of it? | toHaveText when a stray badge or a second line should fail the test |
toBeVisible / toBeAttached / toBeInViewport | In the DOM, painted, or on screen right now? | toBeVisible unless you mean scroll position, which is toBeInViewport |
toBeHidden / not.toBeVisible | Neither of them separates gone from never rendered | Either, with a positive assertion above it so the check cannot run early |
toHaveCount / await locator.count() | Does the number need re-reading until it settles? | toHaveCount in an assertion; count() only to branch in test code |
toHaveClass / toContainClass | The whole class attribute, or one class inside it? | toContainClass unless a fourth class arriving should fail the test |
toHaveValue / toHaveText on an input | An input's value is not its text content | toHaveValue; toHaveText('') passes on a filled input |
Why the retrying half exists, and what a check that should have retried costs you, is the
subject of the article on choosing assertions. The
spec below proves the last row of that table:
#coupon holds SPRING25 and toHaveText('') passes on
it.
import { test, expect } from '@playwright/test';
const BASKET = `
<p id="status" class="badge pending">Pending review</p>
<input id="coupon" value="SPRING25">
<ul id="items"><li>Keyboard</li></ul>
<div id="offscreen" style="position:absolute; top:4000px">Footer note</div>
<script>
setTimeout(() => {
document.querySelector('#status').textContent = 'Shipped';
document.querySelector('#items').insertAdjacentHTML('beforeend', '<li>Mouse</li>');
}, 400);
</script>`;
test('the same check, retried and not retried', async ({ page }) => {
await page.setContent(BASKET);
const items = page.locator('#items li');
// A number that was true once.
expect(await items.count()).toBe(1);
// The same question, re-asked until it passes or the assertion times out.
await expect(items).toHaveCount(2);
await expect(page.locator('#status')).toHaveText('Shipped');
});
test('the pairs that get confused', async ({ page }) => {
await page.setContent(BASKET);
await expect(page.locator('#status')).toContainText('Pending');
await expect(page.locator('#status')).toHaveText('Pending review');
await expect(page.locator('#offscreen')).toBeAttached();
await expect(page.locator('#offscreen')).toBeVisible();
await expect(page.locator('#offscreen')).not.toBeInViewport();
await expect(page.locator('#missing')).toBeHidden();
await expect(page.locator('#missing')).not.toBeVisible();
await expect(page.locator('#status')).toHaveClass('badge pending');
await expect(page.locator('#status')).toContainClass('pending');
await expect(page.locator('#coupon')).toHaveValue('SPRING25');
await expect(page.locator('#coupon')).toHaveText('');
});
Playwright 1.62 · TypeScript · run on @playwright/test 1.62.1, Chromium
How do I do the thing I do every day?
The names here are guessable and the ambiguity sits in one row. fill() sets a
value in one operation. pressSequentially() emits a keydown, keypress and keyup
per character, which matters only where the page reacts to them. type() was the
old name for the second, and Playwright now marks it deprecated and points at
fill() for most cases.
| What you want | The call |
|---|---|
| Put text in a field | locator.fill('Dana Whitfield') |
| Type it key by key, because the page listens for keystrokes | locator.pressSequentially('Dana W.') |
| Tick or untick a checkbox or radio | locator.check(), locator.uncheck() |
Choose from a <select> | locator.selectOption('pro') |
| Click, with the actionability checks in front of it | locator.click() |
| Double-click | locator.dblclick() |
| Hover | locator.hover() |
| Send a key or a shortcut | locator.press('Control+a') |
| Attach a file without touching the file dialog | locator.setInputFiles({ name, mimeType, buffer }) |
| Drag one element onto another | source.dragTo(target) |
| Focus without clicking | locator.focus() |
| Scroll something into view | locator.scrollIntoViewIfNeeded() |
| Click anyway, on an element Playwright will not click | locator.click({ force: true }) — this bypasses the actionability checks, so read what it costs below before copying the row |
import { test, expect } from '@playwright/test';
const FORM = `
<form>
<label for="name">Full name</label><input id="name">
<label for="terms">Accept terms</label><input id="terms" type="checkbox">
<label for="plan">Plan</label>
<select id="plan"><option value="free">Free</option><option value="pro">Pro</option></select>
<label for="avatar">Avatar</label><input id="avatar" type="file">
<button type="button" id="save">Save</button>
<div id="drag" draggable="true">Card</div>
<div id="drop">Drop here</div>
<div id="far" style="margin-top:4000px">Bottom of the form</div>
</form>
<p id="log"></p>
<script>
const $ = (s) => document.querySelector(s);
const log = (m) => $('#log').textContent += m + ' ';
$('#save').addEventListener('click', () => log('click'));
$('#save').addEventListener('dblclick', () => log('dblclick'));
$('#save').addEventListener('mouseover', () => log('hover'));
$('#drop').addEventListener('dragover', (e) => e.preventDefault());
$('#drop').addEventListener('drop', () => log('drop'));
</script>`;
test('the actions a form needs', async ({ page }) => {
await page.setContent(FORM);
await page.getByLabel('Full name').fill('Dana Whitfield');
await page.getByLabel('Accept terms').check();
await page.getByLabel('Plan').selectOption('pro');
await page.getByLabel('Avatar').setInputFiles({
name: 'avatar.png', mimeType: 'image/png', buffer: Buffer.from('not really a png'),
});
const save = page.getByRole('button', { name: 'Save' });
await save.hover();
await save.click();
await save.dblclick();
await page.getByLabel('Full name').press('Control+a');
await page.getByLabel('Full name').pressSequentially('Dana W.');
await page.getByLabel('Full name').focus();
await page.locator('#drag').dragTo(page.locator('#drop'));
await page.locator('#far').scrollIntoViewIfNeeded();
await expect(page.getByLabel('Full name')).toHaveValue('Dana W.');
await expect(page.getByLabel('Accept terms')).toBeChecked();
await expect(page.getByLabel('Plan')).toHaveValue('pro');
// dblclick() is two clicks and then the dblclick event, which is what a browser does.
await expect(page.locator('#log')).toContainText('hover click click click dblclick');
await expect(page.locator('#log')).toContainText('drop');
await expect(page.locator('#far')).toBeInViewport();
});
test('force skips the checks, not the overlay', async ({ page }) => {
await page.setContent(`
<button id="save">Save</button>
<div id="cookie-banner" style="position:fixed; inset:0">We use cookies</div>
<p id="log"></p>
<script>
save.addEventListener('click', () => log.textContent += 'button ');
document.querySelector('#cookie-banner')
.addEventListener('click', () => log.textContent += 'banner ');
</script>`);
await page.getByRole('button', { name: 'Save' }).click({ force: true });
await expect(page.locator('#log')).toHaveText('banner ');
});
Playwright 1.62 · TypeScript · run on @playwright/test 1.62.1, Chromium
The log assertion in that test looks wrong until you count the events.
dblclick() fires two click events and then the
dblclick event, so a handler counting clicks sees three.
Which command do I run?
The complete list of Playwright CLI commands and flags is on docs/test-cli, it is one page, and copying it here would help nobody. This table is indexed by the situation instead, and every row was run against the specs above.
| The situation | The command |
|---|---|
| Forty specs failed and you want only those again | npx playwright test --last-failed |
| Stop at the first failure | npx playwright test -x |
| One file, one browser, and you want to watch it | npx playwright test tests/checkout.spec.ts --project=chromium --headed |
| Run one test by name | npx playwright test -g "adds a coupon" |
| See what would run without running it | npx playwright test --list |
| Only the specs touched on this branch | npx playwright test --only-changed |
| Prove a fix by running one spec twenty times | npx playwright test --repeat-each=20 |
| Open the report you just made | npx playwright show-report |
| Read the trace attached to a bug report | npx playwright show-trace trace.zip |
| Record what you click as a starting spec | npx playwright codegen https://example.com |
What is your old cheat sheet getting wrong?
Playwright's reference carries two markers and a sheet written before you learned the
difference will have flattened them into one. Deprecated sits at method
level on the type entries and on page.waitForNavigation.
Discouraged sits on the long run of
page.* action and query methods, each carrying the same note pointing at the
locator-based method instead. Every entry in the table below still runs in 1.62.
| The form you have | The form for 1.62 | How 1.62 marks it | Still runs? |
|---|---|---|---|
page.click(selector) | page.locator(selector).click() | Discouraged | Yes |
page.dblclick(selector), page.tap(selector) | The same names on a locator | Discouraged | Yes |
page.fill(selector, value) | page.locator(selector).fill(value) | Discouraged | Yes |
page.press(sel, key), page.hover(sel), page.focus(sel) | The same names on a locator | Discouraged | Yes |
page.check(sel), page.uncheck(sel), page.setChecked(sel, on) | The same names on a locator | Discouraged | Yes |
page.selectOption(sel, v), page.setInputFiles(sel, f) | The same names on a locator | Discouraged | Yes |
page.getAttribute, page.innerText, page.innerHTML, page.textContent | The same names on a locator, or a retrying assertion | Discouraged | Yes |
page.inputValue(selector) | await expect(page.locator(sel)).toHaveValue(v) | Discouraged | Yes |
page.isVisible, isHidden, isChecked, isEnabled, isDisabled, isEditable | The matching retrying assertion, which waits where these six return immediately | Discouraged | Yes |
page.$(selector), page.$$(selector) | page.locator(selector) | Discouraged | Yes |
page.$eval(sel, fn), page.$$eval(sel, fn) | locator.evaluate(fn), locator.evaluateAll(fn) | Discouraged | Yes |
page.waitForSelector(selector) | page.locator(selector).waitFor(), or an assertion on the thing you are waiting for | Discouraged | Yes |
locator.elementHandle(), locator.elementHandles() | The locator itself. A handle points at one node and goes stale; a locator is re-resolved on every use | Discouraged | Yes |
locator.type(text), page.type(sel, text) | locator.fill(text), or locator.pressSequentially(text) where the page has special keyboard handling | Deprecated | Yes |
Four more forms belong on that list and did not fit the table.
page.waitForNavigation() is deprecated in favour of
page.waitForURL(), and its note gives the reason: the older call is racy by
construction. The layout CSS pseudo-classes — :right-of(),
:left-of() and their neighbours — carry a warning on
docs/other-locators reading "Layout
selectors are deprecated and may be removed in the future". The legacy text locator syntax has
a section on the same page that recommends the modern text locator in its place. And
page.waitForTimeout() is marked Discouraged, in a note telling you never to wait
for a timeout in production code.
Nothing on your machine warns you about any of them. A deprecation in this framework is a badge on a reference page you open once you already suspect the method, plus a line in release notes for a minor you skipped. A suite built entirely from the left-hand column throws nothing, prints nothing at the terminal, and goes green. That is the answer to page.click vs locator.click: both work, one of them is the API Microsoft is still developing, and no tool will tell you which one you used.
An old sheet goes wrong by continuing to work. It teaches a new engineer an API the vendor has moved off, and the bill arrives eighteen months later, when four hundred specs written against raw selectors have to be brought up to something that survives a redesign.
Where a one-line answer is not enough
A count inside an assertion. locator.count() returns a number
that was true at the moment it was read; toHaveCount re-reads until the page
agrees. A row printing expect(await rows.count()).toBe(3) has quietly turned a
retrying check into a race, and it passes on your laptop and fails on the CI machine that was
busy. The first assertions test above runs both against the same list: the count reads one,
the assertion settles at two.
Visible, attached and in the viewport are three different questions. In the
second assertions test #offscreen sits four thousand pixels down the page.
toBeAttached() passes. toBeVisible() passes as well, because the
element is painted and merely below the fold. not.toBeInViewport() passes too. If
what you meant was "the user can see this without scrolling", only the third one says it.
force: true buys a green step and no coverage. The
documentation is plain that the flag bypasses the actionability checks. The second actions
test puts a fixed cookie banner over the Save button and clicks it with
force: true: the call succeeds, the test moves on, and the log shows the banner
received the click while the button's own handler never ran. A user faced with that page could
not have finished the step either, and the suite now says they can.
A discouraged method that still runs has no error message. There is no failing test, no warning at the terminal, no lint rule in the box. The only place the drift surfaces is a human reading a diff, which is where the habits we look for in a suite start.
The sheet itself, one minor later. Every row here was checked against 1.62 on the date printed at the top. The values most likely to move without a word of the prose around them changing are the numeric defaults, so this page prints none and the article on what Playwright waits for gives each one its source. When you upgrade, the sheet is part of the upgrade.
What should your team pin to the wall?
One page, in the repository the tests live in, rather than a wiki page nobody owns. It states
the version it was written against, and the pull request that bumps
@playwright/test is the pull request that updates it. A stamp raised without a
re-read is worse than a stale one, because it looks checked.
Then one question in code review: is this line one the documentation still recommends, or one that merely still runs? It is answerable in thirty seconds against the reference, and it catches every row of the table above.
A document like that separates a suite somebody can inherit from one that leaves with the engineer who wrote it. It is why a runbook for the team that inherits the work is one of the three things a suite build from us hands over, alongside the specs themselves and the pipeline that runs them.
Questions
Is there an official Playwright cheat sheet?
There is no page at the obvious slug. playwright.dev/docs/cheat-sheet returned 404 on 1 September 2026 while every other documentation page fetched for this article answered normally at the URL typed. The nearest official thing is a Cheat Sheet section inside the Protractor migration guide, and it maps Protractor idioms onto Playwright calls rather than summarising Playwright. What the documentation does have is its guide pages, which explain a feature better than any sheet and are organised by feature.
Which Playwright methods are deprecated?
In the 1.62 source, the Deprecated marker sits at method level on locator.type, page.type, elementHandle.type and page.waitForNavigation. The much longer run of page action and query methods carries a different marker, Discouraged, which points each one at its locator equivalent: page.click, page.fill, page.getAttribute, page.$, page.$eval and their neighbours. The table above prints both, with the marker each entry carries.
Does page.click() still work?
Yes, and that is the problem. It runs, the test passes, and nothing at the terminal mentions it. Playwright's reference marks page.click Discouraged and points at locator.click instead, in a note on a reference page you open only if you already suspect the method. A suite full of page.click still works while drifting away from the API Microsoft maintains, and the only place that shows up is code review.
What version is this Playwright cheat sheet for?
Playwright 1.62, checked against the documentation on 1 September 2026, with the spec files run on @playwright/test 1.62.1. To find what your own project is on, read the @playwright/test entry in your package.json, or the resolved version in your lockfile if that entry is a range. If yours is older or newer than 1.62, treat every row here as a claim about 1.62 and confirm it against the documentation for your version.
Four hundred specs, five sets of conventions
Print this page and it still only governs the line somebody writes tomorrow. The specs already in the repository were written by whoever was on the team that quarter, in whatever style each of them had learned, and nobody has read all of them end to end since. If you want somebody to, the useful things to hand over are the repository, the last week of CI runs, and the name of the one flow the business cannot afford to have broken.