The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →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.
Recommended Free Tools
Role and accessible name: the default
For a semantic button, use the ARIA role and the name a user or assistive technology would perceive:
#1 Best Overall
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.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →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.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesScope 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.
Rank #3
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:
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:
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.
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.
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.
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.
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.
Quick Recap
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.

