Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

In a Playwright test, locate the control as a user would see it, click it, and assert the resulting state:

await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Welcome, John!')).toBeVisible();

getByRole() uses the button’s accessible role and name. The locator is resolved when the action runs, so it can follow ordinary DOM re-renders. Playwright then waits for the target to be actionable instead of sending an input to an element that is hidden, moving, covered, or disabled. See the official locator guide and auto-waiting documentation.

Start with a user-facing locator

A click starts with a locator. Prefer a locator that describes the control’s meaning rather than its position in the DOM.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Role and accessible name: the default

For a semantic button, use the ARIA role and the name a user or assistive technology would perceive:

import { test, expect } from '@playwright/test';

test('signs in', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByText('Welcome, John!')).toBeVisible();
});

The name can come from visible text or an accessible-labeling mechanism. Use an exact string when similarly named controls must not be confused. A regular expression is appropriate only when matching variants is intentional:

await page.getByRole('button', { name: /^Save/ }).click();
await page.getByRole('button', { name: 'Save', exact: true }).click();

Text locators

When visible text is the clearest stable identifier, a text locator can work:

await page.getByText('Continue').click();

Check that the text identifies the control itself, not a heading, paragraph, or hidden duplicate. If the text appears in several places, scope the search to the relevant region or use a role locator.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Test IDs

A test ID is an explicit testing contract. It is useful when the product has no suitable user-facing name or when wording changes independently of behavior:

await page.getByTestId('checkout-submit').click();

Playwright lets you configure which attribute supplies test IDs; keep that convention consistent across the application.

CSS, XPath, and positional locators

CSS and XPath are available for unusual cases, but long selectors tied to generated markup or layout are fragile. A selector such as div:nth-child(3) > button can break after an innocent DOM change. first(), last(), and nth() also work, yet a repeated list can reorder and send the click to a different item. Prefer a distinguishing name, attribute, or container whenever one exists. The best-practices guide recommends user-facing locators over implementation details.

Make a strict locator when several buttons exist

Locator actions are strict: a click must resolve to exactly one element. If two buttons match, Playwright reports a strictness violation instead of guessing.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Scope to a component

const dialog = page.getByRole('dialog', { name: 'Delete project' });
await dialog.getByRole('button', { name: 'Delete', exact: true }).click();

Filter a repeated region

const row = page.getByRole('row').filter({ hasText: 'Quarterly report' });
await row.getByRole('button', { name: 'Download' }).click();

Use a positional method only when the position is the behavior under test, not as a shortcut for an ambiguous locator.

Understand Playwright’s automatic waiting

Before locator.click() sends input, Playwright checks that the locator still has one match and that the element is visible, stable, able to receive events, and enabled. It waits while those conditions become true; if they do not become true before the applicable timeout, the action raises a timeout error. These checks are described in the official actionability table.

What each check means

  • Exactly one: the locator is not ambiguous.
  • Visible: the control is rendered and not hidden.
  • Stable: an animation or layout movement is not in progress.
  • Receives events: another element, such as an overlay, is not intercepting the pointer.
  • Enabled: the control is not disabled.

Locators are evaluated against the current DOM when the action executes. That is why retaining a locator is generally safer than capturing an element handle before a framework re-renders the page.

Navigation and asynchronous UI

If the click starts a navigation, the click waits for that navigation to succeed or fail by default. For an in-place update, keep the click and the assertion together; the assertion retries until its condition is met or its timeout expires:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.getByRole('button', { name: 'Add to cart' }).click();
await expect(page.getByRole('status')).toHaveText('Added to cart');

Click options: change only what the interaction requires

A normal button needs no options. The Locator API documents these controls:

Option Use Caution
button Choose left, right, or middle mouse input. Use only when the product defines a non-left-button action.
clickCount Send a double- or multi-click. Do not use it to compensate for a missing assertion.
delay Pause between mouse-down and mouse-up. Realistic timing can expose an interaction requirement, but adds test time.
modifiers Hold keys such as Control or Shift. Use the modifier the application actually handles.
position Click a coordinate within the element. Coordinates are more sensitive to layout than a normal click.
timeout Set the maximum wait for this action. A longer timeout does not fix a wrong locator or a permanently blocked control.
force Skip non-essential actionability checks. It can click through an overlay and hide a real user-facing defect.
trial: true Run actionability checks without carrying out the click. Use it to diagnose readiness, then perform the real action separately.

For example, a deliberate context-menu click is explicit:

await page.getByRole('button', { name: 'More actions' }).click({ button: 'right' });

Do not make force: true your routine timeout remedy. If a normal user could not click the control, the test should usually expose that condition.

Assert the outcome, not just the input

A click by itself proves only that Playwright issued an input. Pair it with an observable result:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Confirmation or status

await page.getByRole('button', { name: 'Save changes' }).click();
await expect(page.getByRole('status')).toContainText('Saved');

Dialog state

await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByRole('dialog', { name: 'Confirm deletion' })).toBeVisible();

Destination

await page.getByRole('link', { name: 'Account' }).click();
await expect(page).toHaveURL(//account/);

Assertions retry, so an application that updates asynchronously can settle without arbitrary sleeps.

Troubleshoot a click that waits or fails

“Strict mode violation”

Cause: the locator matches multiple buttons. Fix: inspect the page’s accessible names, then add an exact name, scope to a dialog or row, or filter by identifying text. Do not blindly append first().

Timeout while the button is visible

Possible causes: an animation is still running, a transparent overlay intercepts events, the button is outside the current viewport, or the locator’s name is not the name Playwright computes. Fix: verify the role and accessible name, wait for the UI state that removes the overlay, and use trial: true to check readiness without clicking.

The button is disabled

Cause: required form data or an asynchronous operation has not completed. Fix: perform the prerequisite action and assert the enabled state or resulting UI before clicking. Raising the timeout will not enable a control.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The click hits the wrong control

Cause: a broad text locator or positional selector matched incidental text or a reordered list item. Fix: switch to role plus exact name, add a component scope, or use the application’s test ID.

The click succeeds but the test fails afterward

Cause: the test has no reliable assertion, asserts too early on a changing region, or expects navigation when the app updates in place. Fix: assert the actual confirmation, dialog, URL, or status element produced by that interaction.

Force appears to “fix” it

Cause: force bypassed checks such as whether the target receives pointer events. Fix: remove it and correct the overlay, animation, disabled state, or locator. Keep force for a consciously tested edge case, not as a default.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

A complete example with a scoped modal

import { test, expect } from '@playwright/test';

test('deletes the selected project', async ({ page }) => {
  await page.goto('https://example.com/projects');

  const project = page.getByRole('row').filter({ hasText: 'Quarterly report' });
  await project.getByRole('button', { name: 'More actions' }).click();

  const menu = page.getByRole('menu');
  await menu.getByRole('menuitem', { name: 'Delete' }).click();

  const confirm = page.getByRole('dialog', { name: 'Delete project' });
  await expect(confirm).toBeVisible();
  await confirm.getByRole('button', { name: 'Delete', exact: true }).click();

  await expect(page.getByRole('row').filter({ hasText: 'Quarterly report' })).toHaveCount(0);
});

Every action identifies its context, and the final assertion checks the business result rather than the mere fact that a pointer event was sent.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Or skip the browser setup

If your goal is a clean image or PDF of a page rather than an interactive test, ScreenshotNeo provides a single HTTP request. Before capture it accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; each response reports its result in X-Page-Verdict and X-Billed headers. Its MCP server also lets Claude, Cursor, or another MCP client call take_screenshot, get_page_info, and capture_pdf.

Use the ScreenshotNeo API documentation for all parameters. A minimal cURL request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same request in Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

And in Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const body = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', body);

Beyond screenshots, it supports full-page captures with lazy images, CSS-selector element captures, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper and margin controls, custom CSS and JavaScript, clicks before capture, selector or network-idle waits, request and resource blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify a migration.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to try it.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Practical checklist

  • Use getByRole('button', { name: ... }) when a useful accessible name exists.
  • Make the locator unique with exact naming, component scope, or a filter.
  • Let Playwright wait for visibility, stability, event reception, and enabled state.
  • Use click options only for a defined interaction requirement.
  • Assert the confirmation, state change, dialog, or destination produced by the click.
  • Treat a timeout as evidence to debug the page or locator, not as a reason to force the event.

Frequently Asked Questions

How do I configure a different attribute for test IDs?

Set Playwright’s test-ID attribute in the test configuration, then use the same application-wide convention with `getByTestId()`. This keeps selectors independent of layout while making the contract explicit.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.