The core of every E2E test is finding elements on the page and interacting with them. Playwright provides a powerful locator system that prioritizes accessibility and resilience over brittle CSS selectors.
Locator Strategies
Playwright recommends locators that reflect how users and assistive technology see the page. These locators are resilient to implementation changes — they survive refactors that change class names or DOM structure.
getByRole — The Preferred Approach
The getByRole locator finds elements by their ARIA role, which is how screen readers identify elements:
// Find a button with the text "Submit"
page.getByRole('button', { name: 'Submit' });
// Find a text input labeled "Email"
page.getByRole('textbox', { name: 'Email' });
// Find a navigation landmark
page.getByRole('navigation');
// Find a heading
page.getByRole('heading', { name: 'Welcome', level: 1 });getByText — Match Visible Text
When role-based selection is not specific enough, match by visible text:
// Exact match
page.getByText('Sign up for free', { exact: true });
// Partial match (default)
page.getByText('Sign up');
// Regex match
page.getByText(/sign up/i);getByTestId — The Escape Hatch
When semantic selectors do not work, use data-testid attributes. Add them to your markup specifically for testing:
<div data-testid="user-profile-card">...</div>page.getByTestId('user-profile-card');This is a stable contract between your code and your tests. Unlike class names, test IDs do not change during styling updates.
Other Locators
Playwright offers several more locators for specific scenarios:
// By label text (for form fields)
page.getByLabel('Password');
// By placeholder text
page.getByPlaceholder('Search...');
// By alt text (for images)
page.getByAltText('Company logo');
// CSS selector (use sparingly)
page.locator('.sidebar >> .menu-item');Performing Actions
Once you have a locator, you can interact with the element. Playwright auto-waits for elements to be visible and actionable before performing any action.
Click
await page.getByRole('button', { name: 'Save' }).click();
// Double click
await page.getByText('Edit title').dblclick();
// Right click
await page.getByText('File').click({ button: 'right' });Type and Fill
// fill() clears the field first, then types
await page.getByLabel('Email').fill('user@example.com');
// type() simulates key-by-key input (useful for autocomplete)
await page.getByLabel('Search').pressSequentially('playwright', { delay: 50 });Select Dropdowns
// By value
await page.getByLabel('Country').selectOption('us');
// By visible text
await page.getByLabel('Country').selectOption({ label: 'United States' });Checkboxes and Radio Buttons
await page.getByLabel('Accept terms').check();
await page.getByLabel('Accept terms').uncheck();
// Verify state
await expect(page.getByLabel('Accept terms')).toBeChecked();Auto-Waiting
Playwright automatically waits for elements before acting on them. When you call click(), Playwright waits until the element is:
- Attached to the DOM
- Visible
- Stable (not animating)
- Enabled (not disabled)
- Not obscured by another element
This eliminates the need for manual sleep() or waitFor() calls that plague Selenium tests. If the element does not become actionable within the timeout, the test fails with a clear error message.
Chaining Locators
You can scope locators to a specific section of the page:
const sidebar = page.locator('.sidebar');
await sidebar.getByRole('link', { name: 'Settings' }).click();This is useful when the same text or role appears multiple times on the page. By narrowing the scope first, you avoid ambiguity.
Key Takeaways
- Prefer
getByRolefor resilient, accessible selectors. - Use
getByTestIdas a stable escape hatch when semantic selectors fail. - Playwright auto-waits — do not add manual sleeps.
- Use
fill()for form inputs andclick()for buttons and links. - Chain locators to narrow scope when elements are ambiguous.