Home / Blog / Playwright cheat sheet

Playwright cheat sheet (1.62)

Quick answer

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.

LocatorWhat it matchesReach for it when
getByRole('button', { name: 'Save' })ARIA role plus accessible nameAlmost always, and always for something a user clicks, types into or reads as a heading
getByLabel('Full name')A form control through its labelInputs, selects and textareas with an associated label
getByPlaceholder('Order number')The placeholder attributeAn input with a placeholder and no label worth using
getByText('Pending')Text inside one elementNon-interactive content: a status, a paragraph, a cell
getByAltText('Shipping status')The alt attributeImages and image maps
getByTitle('Updated hourly')The title attributeThe rare element whose tooltip is the only thing naming it
getByTestId('row-1802')data-testid, or whatever testIdAttribute is set toNothing above identifies the element and you can edit the source
locator('table tr')Any CSS selectorA structural hook the application promises to keep
locator('xpath=//table//tr[1]')An XPath expressionA legacy suite you are converting a file at a time
Seven built-in methods and two fallbacks, from playwright.dev/docs/locators and docs/other-locators, read 1 September 2026 · Playwright 1.62

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 pairThe question that separates themWrite
toHaveText / toContainTextIs 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 / toBeInViewportIn the DOM, painted, or on screen right now?toBeVisible unless you mean scroll position, which is toBeInViewport
toBeHidden / not.toBeVisibleNeither of them separates gone from never renderedEither, 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 / toContainClassThe whole class attribute, or one class inside it?toContainClass unless a fourth class arriving should fail the test
toHaveValue / toHaveText on an inputAn input's value is not its text contenttoHaveValue; toHaveText('') passes on a filled input
Descriptions read from docs/api/class-locatorassertions and docs/test-assertions, 1 September 2026 · Playwright 1.62

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 wantThe call
Put text in a fieldlocator.fill('Dana Whitfield')
Type it key by key, because the page listens for keystrokeslocator.pressSequentially('Dana W.')
Tick or untick a checkbox or radiolocator.check(), locator.uncheck()
Choose from a <select>locator.selectOption('pro')
Click, with the actionability checks in front of itlocator.click()
Double-clicklocator.dblclick()
Hoverlocator.hover()
Send a key or a shortcutlocator.press('Control+a')
Attach a file without touching the file dialoglocator.setInputFiles({ name, mimeType, buffer })
Drag one element onto anothersource.dragTo(target)
Focus without clickinglocator.focus()
Scroll something into viewlocator.scrollIntoViewIfNeeded()
Click anyway, on an element Playwright will not clicklocator.click({ force: true }) — this bypasses the actionability checks, so read what it costs below before copying the row
From playwright.dev/docs/input, read 1 September 2026 · Playwright 1.62
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 situationThe command
Forty specs failed and you want only those againnpx playwright test --last-failed
Stop at the first failurenpx playwright test -x
One file, one browser, and you want to watch itnpx playwright test tests/checkout.spec.ts --project=chromium --headed
Run one test by namenpx playwright test -g "adds a coupon"
See what would run without running itnpx playwright test --list
Only the specs touched on this branchnpx playwright test --only-changed
Prove a fix by running one spec twenty timesnpx playwright test --repeat-each=20
Open the report you just madenpx playwright show-report
Read the trace attached to a bug reportnpx playwright show-trace trace.zip
Record what you click as a starting specnpx playwright codegen https://example.com
Flags confirmed on docs/test-cli and against npx playwright test --help on 1.62.1, 1 September 2026

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 haveThe form for 1.62How 1.62 marks itStill runs?
page.click(selector)page.locator(selector).click()DiscouragedYes
page.dblclick(selector), page.tap(selector)The same names on a locatorDiscouragedYes
page.fill(selector, value)page.locator(selector).fill(value)DiscouragedYes
page.press(sel, key), page.hover(sel), page.focus(sel)The same names on a locatorDiscouragedYes
page.check(sel), page.uncheck(sel), page.setChecked(sel, on)The same names on a locatorDiscouragedYes
page.selectOption(sel, v), page.setInputFiles(sel, f)The same names on a locatorDiscouragedYes
page.getAttribute, page.innerText, page.innerHTML, page.textContentThe same names on a locator, or a retrying assertionDiscouragedYes
page.inputValue(selector)await expect(page.locator(sel)).toHaveValue(v)DiscouragedYes
page.isVisible, isHidden, isChecked, isEnabled, isDisabled, isEditableThe matching retrying assertion, which waits where these six return immediatelyDiscouragedYes
page.$(selector), page.$$(selector)page.locator(selector)DiscouragedYes
page.$eval(sel, fn), page.$$eval(sel, fn)locator.evaluate(fn), locator.evaluateAll(fn)DiscouragedYes
page.waitForSelector(selector)page.locator(selector).waitFor(), or an assertion on the thing you are waiting forDiscouragedYes
locator.elementHandle(), locator.elementHandles()The locator itself. A handle points at one node and goes stale; a locator is re-resolved on every useDiscouragedYes
locator.type(text), page.type(sel, text)locator.fill(text), or locator.pressSequentially(text) where the page has special keyboard handlingDeprecatedYes
Markers read from docs/src/api/class-page.md, class-locator.md and class-elementhandle.md at the v1.62.0 tag, whole files, and confirmed on docs/api/class-page. Every left-hand form re-run on 1.62.1 on 1 September 2026

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.