The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Use a Playwright Locator with a CSS ID selector: page.locator('#save-button'). Playwright also has an explicit ID selector engine, page.locator('id=save-button'). Keep the Locator and use it for actions and assertions so Playwright can auto-wait and retry when the page changes.
The two correct ID selectors
Given this element:
<button id='save-button'>Save</button>
Both of these expressions locate it:
const saveButton = page.locator('#save-button');
await saveButton.click();
const sameButton = page.locator('id=save-button');
await sameButton.click();
The first form uses standard CSS ID syntax and is usually the most readable. The second uses Playwright’s explicit id selector engine, which makes the intended engine clear. Playwright documents page.locator() as the API for creating a Locator, and Locators provide auto-waiting and retry-ability during actions and assertions (Locator API; other locators).
Run a complete Playwright example
Install the test runner
In a new Node.js project, install Playwright Test and its browsers:
npm init playwright@latest
Choose TypeScript or JavaScript when prompted. The following TypeScript test assumes a page containing an input with id='search' and a button with id='save-button'.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errors#1 Best Overall
Locate, act, and assert
import { test, expect } from '@playwright/test';
test('uses HTML IDs with locators', async ({ page }) => {
await page.goto('https://example.com/form');
const searchInput = page.locator('#search');
await searchInput.fill('playwright');
await expect(searchInput).toHaveValue('playwright');
const saveButton = page.locator('id=save-button');
await saveButton.click();
await expect(page.locator('#status')).toHaveText('Saved');
});
Replace the URL and expected text with values from your application. A Locator is not a one-time snapshot of the DOM. Playwright resolves it when an operation runs, waits for the element to be actionable, and retries supported assertions. That is why retaining searchInput or saveButton is preferable to taking an element handle and trying to manage stale references yourself.
Choosing between #id and id=value
| Form | What it means | Best use |
|---|---|---|
page.locator('#save-button') |
CSS selector whose ID is save-button |
Concise, familiar selectors when the ID is a simple CSS identifier |
page.locator('id=save-button') |
Playwright’s explicit ID selector engine | Making the selector engine obvious or avoiding CSS escaping concerns |
Both target the element’s HTML id attribute. They do not search for visible text, an ARIA role, or a data-testid attribute.
Use the Locator for every operation
Actions
const email = page.locator('#email');
await email.fill('user@example.com');
const submit = page.locator('#submit');
await submit.click();
Playwright waits for actionability before clicking or filling. If the element is not yet attached, visible, enabled, or otherwise ready for the requested action, the action waits until its timeout rather than requiring a manual sleep.
Assertions
const searchInput = page.locator('#search');
await expect(searchInput).toHaveValue('playwright');
const result = page.locator('#result');
await expect(result).toBeVisible();
await expect(result).toContainText('Playwright');
Web-first assertions retry until the condition is met or the assertion timeout expires. This is safer than reading a value once and comparing it immediately.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteCounting and inspecting matches
const matches = page.locator('#save-button');
console.log(await matches.count());
console.log(await matches.first().getAttribute('aria-label'));
An HTML ID is intended to be unique. If the count is greater than one, fix the markup or narrow the locator deliberately; do not silently rely on first() unless the test really allows multiple matches.
Rank #2
HTML ID is not the same as a Playwright test ID
For this markup:
<button id='save-button'>Save</button>
<button data-testid='save'>Save</button>
Use the HTML ID with page.locator('#save-button') or page.locator('id=save-button'). Use the test ID with:
await page.getByTestId('save').click();
By default, getByTestId() looks for the data-testid attribute. A project can configure another attribute, such as data-pw. It does not treat an ordinary HTML id as a test ID unless you intentionally configure the test-ID attribute that way. See the Page API documentation for the test-ID option.
When an ID is the right locator—and when it is not
| Locator | Use it when | What the test expresses |
|---|---|---|
#id or id=value |
The ID is stable and is the contract you want to test | A specific DOM element identified by its HTML ID |
getByRole() |
The element has a meaningful accessible role and name | How a user perceives and operates the control |
getByLabel() |
A form control has an associated label | The field’s user-facing label |
getByText() |
Visible text is the intentional contract | Content a user can read |
getByTestId() |
Your team maintains an explicit test-only attribute | A deliberate automation contract |
For example, these controls may all refer to the same button:
await page.locator('#save-button').click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByTestId('save').click();
Choose the one that best represents the behavior under test. The Playwright locator guide recommends user-facing locators when they are available and an explicit test-ID contract when that is what your team intends (Locators guide). CSS and XPath selectors can be coupled to implementation details, so an ID that is regenerated during every build is a poor long-term contract even though the syntax is valid.
Handling difficult IDs
IDs containing CSS-special characters
Some valid HTML IDs contain characters that have a special meaning in CSS. If a CSS selector becomes difficult to escape, use Playwright’s explicit engine or an attribute selector:
Rank #3
const item = page.locator('id=order:123');
const sameItem = page.locator('[id="order:123"]');
The explicit id= form avoids making the value part of a CSS expression. Keep the selector in one place if the ID is generated, and prefer a stable application or test attribute when you control the markup.
IDs that change between runs
If the value contains a random token, timestamp, or database key, an exact ID locator will be brittle. Look for a stable role, label, visible name, or dedicated data-testid. If only a predictable part is stable, use a narrowly scoped attribute selector rather than a long chain of parent and descendant selectors:
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →const row = page.locator('[id^="invoice-"]');
Use this pattern only when the prefix is an intentional contract and the resulting locator still identifies one element.
Elements inside an iframe
A page-level locator cannot cross into a separate frame. Select the frame first, then locate the ID inside it:
const paymentFrame = page.frameLocator('iframe[title="Payment"]');
await paymentFrame.locator('#card-number').fill('4242424242424242');
The frame selector must identify the iframe element, and #card-number is resolved in that frame’s document.
Elements in a shadow tree
For open shadow DOM, Playwright’s locators can generally pierce the shadow boundary. Keep the locator anchored to a stable component or user-facing attribute rather than depending on internal nesting. Closed shadow roots cannot be queried from page content; expose a testable contract from the component instead.
Free tools Windows power users keep installed
One-click scans. No signup required.
Common mistakes and fixes
- Using
getByTestId()for an HTML ID: changepage.getByTestId('save-button')topage.locator('#save-button'), unless your project deliberately configured the test-ID attribute to beid. - Forgetting the hash:
page.locator('save-button')is not a CSS ID selector. Use#save-buttonorid=save-button. - Creating an XPath only because an ID exists:
page.locator('#save-button')is clearer than a long XPath for the same element. - Taking a one-time element handle: retain a Locator so Playwright can re-resolve it and apply its waiting behavior.
- Ignoring duplicate IDs: inspect
await page.locator('#save-button').count(). Correct invalid markup or scope the locator to the component that is supposed to own the control. - Clicking before the page is ready: use the Locator action directly and let Playwright wait; avoid arbitrary
waitForTimeout()calls that merely slow tests and can still race. - Testing the wrong document: if the count is zero, check whether the element is inside an iframe, whether navigation finished, and whether the application renders it only after an action.
Troubleshoot a locator that fails
| Symptom | Likely cause | What to do |
|---|---|---|
locator.click: Timeout exceeded |
No matching element, an obstructing overlay, or an element that never becomes actionable | Verify the exact ID, inspect count(), wait for the application state with a web-first assertion, and check for overlays or disabled controls |
strict mode violation |
More than one element matched | Make the ID unique or add a meaningful scope; use first() only when multiple matches are expected by design |
| Zero matches after navigation | The selector ran in the wrong frame or before client rendering | Use frameLocator() for an iframe and wait for a visible or attached state instead of sleeping |
| Value assertion fails intermittently | The field is updated asynchronously or the assertion reads it too early | Use expect(locator).toHaveValue() or another retrying assertion |
getByTestId finds nothing |
The markup has id, not the configured test-ID attribute |
Use the ID locator or add the project’s agreed test attribute |
During diagnosis, temporarily log the URL and match count:
console.log('URL:', page.url());
console.log('save matches:', await page.locator('#save-button').count());
Once the cause is understood, keep the assertion or locator that expresses the required state rather than leaving diagnostic sleeps in the test.
Reliability, readability, and maintenance
- Make uniqueness intentional: one stable ID per control gives a short, readable locator and avoids ambiguity.
- Prefer behavior over implementation: use role, label, or visible text when the test is specifically about what a user can perceive.
- Use test IDs as contracts: when visual wording changes frequently, a maintained
data-testidcan make intent explicit. - Keep locators near the test’s purpose: a descriptive variable such as
saveButtonmakes failures easier to understand than repeating a selector string everywhere. - Let assertions wait: Playwright’s retrying assertions reduce races caused by asynchronous rendering without hard-coded delays.
Or skip the browser setup
If your goal is a rendered image or PDF rather than an interactive test, ScreenshotNeo can capture a URL with one HTTP request. Its consent step accepts cookie banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
See the ScreenshotNeo API documentation for all options, including viewport and device settings, full-page capture, CSS selectors, waits, custom headers and cookies, PDF output, caching, signed links, asynchronous jobs, and bulk capture.
import requests
r = requests.get('https://api.screenshotneo.com/v1/shot', params={'access_key': 'YOUR_API_KEY', 'url': 'https://example.com'}, timeout=90)
open('shot.webp', 'wb').write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account to get started.
FAQ
What is the Playwright equivalent of document.getElementById()?
Use page.locator('#your-id') for the concise CSS form or page.locator('id=your-id') for Playwright’s explicit ID engine. The result is a Locator, not a one-time DOM node.
Can an ID locator select an element that is not visible?
It can identify a matching DOM element, but actions such as click() require the element to become actionable. For visibility itself, assert with await expect(locator).toBeVisible() and investigate overlays, rendering state, or disabled controls when that assertion fails.
Should every element in a test have an ID?
No. Add stable IDs where they represent a useful contract, but use role, label, text, or an intentional test ID when those better describe the user behavior being tested.
Frequently Asked Questions
What is the Playwright equivalent of document.getElementById()?
Use page.locator(‘#your-id’) or page.locator(‘id=your-id’); both return a Playwright Locator.
Can an ID locator select an element that is not visible?
It can match the DOM element, but actions require it to become actionable. Use a visibility assertion when that state matters.
Should every element in a test have an ID?
No. Choose role, label, text, test ID, or HTML ID according to the contract your test is intended to express.
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.

