Introduction
Playwright selectors are powerful tools for
locating elements on web pages during automated testing. They provide a robust
and flexible way to identify UI elements reliably across different browsers and
scenarios. This guide covers everything from basic selector usage to advanced
techniques for building resilient test automation.
What are
Selectors?
Selectors are strings that describe how to
find elements in the DOM (Document Object Model). Playwright extends
traditional CSS selectors with additional capabilities like text-based
selection, role-based selection, and layout-based selection.
Why
Playwright Selectors?
- Cross-browser
compatibility: Work consistently across Chromium,
Firefox, and Safari
- Resilient:
Built-in waiting and retry mechanisms
- Expressive: Rich
set of selector engines beyond just CSS
- Readable:
Human-friendly syntax that makes tests more maintainable
Selector
Decision Matrix
Choosing the right selector depends on your
specific situation. Here's a quick reference:
Scenario |
Recommended Selector |
Why |
Element
has stable test ID |
[data-testid="element"] |
Most
reliable, test-specific |
Interactive
element (button, input) |
role=button[name="Submit"] |
Semantic,
accessible |
User-visible
text |
text=Submit |
User-centric,
intuitive |
Form
field with label |
input:right-of(label:has-text("Email")) |
Visual
relationship |
Element
in container |
.container .specific-element |
Scoped
selection |
Dynamic
content |
Multiple
fallback selectors |
Robust
against changes |
Table
cell operations |
tr:has(td:has-text("John")) |
Content-based
row selection |
A/B
testing scenarios |
Environment-based
selectors |
Handles
variations |
Element
Stability Ranking
From most stable (preferred) to least stable:
- Data
attributes (data-testid, data-*) - Designed for testing
- ARIA
roles/labels (role=button[name="Submit"]) -
Semantic meaning
- User-visible
text (text=Submit) -
What users see
- Stable
CSS classes (.primary-button) -
Intentional styling
- IDs (#submit-btn) - Can change with refactoring
- Specific
XPath (//form[@id="login"]//button[text()="Submit"]) -
Powerful but complex
- Generic
CSS (.btn.btn-primary) -
Tied to styling
- Tag
names (button) -
Too generic
- Position-based
XPath (//div[3]/button[1]) -
Fragile, position-dependent
Basic
Selectors
CSS
Selectors
Purpose: Leverage familiar CSS
syntax to select elements based on their structure, styling, and attributes.
When to use:
- When
you need precise control over element selection
- For
elements with stable CSS classes or IDs
- When
working with existing CSS knowledge
- For
selecting multiple related elements
Best for: Static elements, styled
components, structural navigation
javascript
// Select by ID - Use when
element has a unique, stable identifier
await page.locator('#submit-button').click();
// Select by class - Use for
styled components that won't change frequently
await page.locator('.btn-primary').click();
// Select by tag - Use for
generic element types when context is clear
await page.locator('input').fill('Hello World');
// Select by attribute - Use
for elements with stable data attributes
await page.locator('[data-testid="login-form"]').click();
// Descendant selector - Use
to find nested elements within specific containers
await page.locator('form input[type="email"]').fill('user@example.com');
// Child selector - Use for
direct parent-child relationships
await page.locator('nav > ul > li').first().click();
Text-based
Selectors
Purpose: Select elements based on
their visible text content, mimicking how users identify elements.
When to use:
- For
user-facing elements like buttons, links, and labels
- When
the visual text is more stable than underlying structure
- For
internationalization testing (though consider i18n keys)
- When
implementing user-centric test scenarios
Best for: Buttons, links, navigation
items, form labels, content verification
javascript
// Exact text match - Use
when text is precise and unlikely to change
await page.locator('text=Submit').click();
// Partial text match - Use
when only part of the text is stable
await page.locator('text=Sub').click();
// Case-insensitive text
match - Use for flexible text matching
await page.locator('text=SUBMIT').click();
// Text with quotes - Use
when text contains special characters or spaces
await page.locator('text="Submit Form"').click();
// Regular expression - Use
for dynamic text patterns (e.g., dates, numbers)
await page.locator('text=/sub.*mit/i').click();
Has-text
Selector
Purpose: Find container elements
that contain specific text anywhere within their content, including nested
elements.
When to use:
- For
selecting containers based on their content
- When
the target element doesn't directly contain text but its children do
- For
cards, articles, or sections identified by their content
- When
you need to select parent elements based on child text
Best for: Cards, list items, table
rows, content sections, complex containers
javascript
// Find div containing
specific text - Use for content containers
await page.locator('div:has-text("Welcome")').click();
// Case-insensitive has-text
- Use for flexible content matching
await page.locator('article:has-text("important")').hover();
// Regular expression with
has-text - Use for pattern-based content
await page.locator('section:has-text(/error|warning/i)').screenshot();
Advanced
Selectors
Role-based
Selectors
Purpose: Select elements based on
their semantic meaning and accessibility properties, following ARIA standards.
When to use:
- For
accessible, semantic element selection
- When
building tests that reflect user interactions with assistive technologies
- For
elements where role is more stable than visual styling
- When
ensuring your app works with screen readers and other accessibility tools
Best for: Form controls, interactive
elements, navigation, semantic content structure
Advantages: Future-proof,
accessibility-focused, semantic meaning, cross-browser consistency
javascript
// Select by role - Use for
semantic element types
await page.locator('role=button').click(); // Any
button element
await page.locator('role=textbox').fill('username'); // Input fields
await page.locator('role=checkbox').check(); //
Checkbox controls
// Role with name - Use when
multiple elements share the same role
await page.locator('role=button[name="Submit"]').click();
await page.locator('role=link[name="Learn More"]').click();
// Role with options - Use
for flexible matching with case-insensitivity or regex
await page.locator('role=textbox[name="Email" i]').fill('test@example.com');
await page.locator('role=button[name=/submit/i]').click();
Attribute
Selectors
Purpose: Target elements using
various attribute matching strategies for precise selection based on element
properties.
When to use:
- When
elements have stable, meaningful attributes
- For
selecting elements with specific data attributes or states
- When
you need partial attribute matching (starts with, ends with, contains)
- For
elements identified by custom attributes or properties
Best for: Data attributes,
state-based selection, API-generated content, component identification
Target elements using various attribute
matching strategies.
javascript
// Exact attribute value -
Use when attribute value is stable and unique
await page.locator('[data-testid="submit-btn"]').click();
// Attribute starts with -
Use for prefixed naming patterns
await page.locator('[class^="btn-"]').click();
// Attribute ends with - Use
for suffixed naming patterns
await page.locator('[id$="-submit"]').click();
// Attribute contains - Use
when attribute contains consistent keywords
await page.locator('[class*="primary"]').click();
// Multiple attributes - Use
for precise targeting with multiple criteria
await page.locator('[type="text"][required]').fill('data');
XPath
Selectors
Purpose: Use XML Path Language to
navigate and select elements through powerful path expressions that can
traverse the DOM in any direction.
When to use:
- When
CSS selectors cannot express the required selection logic
- For
complex DOM traversal that involves multiple axes (parent, ancestor,
sibling)
- When
you need powerful text manipulation and string functions
- For
selecting elements based on their position relative to other elements
- When
dealing with complex XML-like structures or namespaced elements
- As a
last resort when other selector strategies fail
Best for: Complex DOM navigation,
legacy applications, XML documents, advanced text matching
Advantages: Extremely powerful,
bidirectional navigation, rich function library, precise positioning Disadvantages:
Less readable, more brittle, performance overhead, harder to maintain
javascript
// Basic XPath element
selection
await page.locator('xpath=//button').click();
await page.locator('xpath=//input[@type="text"]').fill('data');
// XPath with text content -
Use for precise text matching
await page.locator('xpath=//button[text()="Submit"]').click();
await page.locator('xpath=//span[contains(text(), "Welcome")]').isVisible();
// XPath attribute matching
- Use for complex attribute conditions
await page.locator('xpath=//div[@class="card" and
@data-id="123"]').hover();
await page.locator('xpath=//input[@type="text" and @required]').fill('required data');
// Parent/ancestor
navigation - Use when CSS parent selectors aren't sufficient
await page.locator('xpath=//td[text()="John"]/parent::tr').click();
await page.locator('xpath=//input[@name="email"]/ancestor::form').screenshot();
// Sibling navigation - Use
for complex sibling relationships
await page.locator('xpath=//label[text()="Email"]/following-sibling::input').fill('test@example.com');
await page.locator('xpath=//button[text()="Save"]/preceding-sibling::button').click();
// Position-based selection
- Use when element position is the only stable identifier
await page.locator('xpath=//tr[3]/td[2]').textContent(); // 3rd row, 2nd column
await page.locator('xpath=(//button)[last()]').click(); // Last button on page
// Complex conditions - Use
for advanced logical conditions
await page.locator('xpath=//div[@class="item" and position() >
2 and position() < 6]').count();
await page.locator('xpath=//li[contains(@class, "active") or
contains(@class, "selected")]').click();
// String functions - Use
for advanced text manipulation
await page.locator('xpath=//button[starts-with(text(), "Save")]').click();
await page.locator('xpath=//span[string-length(text()) > 10]').count();
await page.locator('xpath=//div[normalize-space(text())="Clean
Text"]').isVisible();
// Multiple axes combination
- Use for complex DOM relationships
await page.locator('xpath=//table//tr[td[text()="Active"]]/following-sibling::tr[1]').click();
XPath vs
CSS Selector Comparison
Scenario |
XPath |
CSS Equivalent |
Recommendation |
Select by
class |
//div[@class="item"] |
.item |
Use CSS -
simpler |
Parent
selection |
//input/parent::div |
Not
possible directly |
Use XPath
when needed |
Text
content |
//button[text()="Save"] |
Not
possible directly |
Use XPath
or text= |
Following
sibling |
//label/following-sibling::input |
label + input (adjacent only) |
XPath for
non-adjacent |
Nth
element |
(//button)[3] |
button:nth-child(3) |
CSS
usually better |
Contains
text |
//span[contains(text(), "error")] |
:has-text("error") |
Use
Playwright's has-text |
When XPath
is the Best Choice
javascript
// 1. Complex
parent/ancestor relationships
// Find the form containing
a specific error message
const errorForm = page.locator('xpath=//span[@class="error"][text()="Invalid
email"]/ancestor::form');
// 2. Multiple conditions
with AND/OR logic
// Find buttons that are
either "Save" or "Submit" and not disabled
const actionButtons = page.locator('xpath=//button[(text()="Save" or
text()="Submit") and not(@disabled)]');
// 3. Complex sibling
relationships
// Find the input that comes
after a label containing "Password"
const passwordInput = page.locator('xpath=//label[contains(text(),
"Password")]/following-sibling::*[1][self::input]');
// 4. Position-based complex
selection
// Select every 3rd item in
a list, starting from the 2nd
const specificItems = page.locator('xpath=//ul[@class="items"]/li[position() mod 3
= 2]');
// 5. Advanced text matching
// Find elements with
specific text patterns
const phoneNumbers = page.locator('xpath=//span[matches(text(),
"\\d{3}-\\d{3}-\\d{4}")]');
// 6. Multiple level
navigation
// Find a table cell's value
based on header and row identifier
const cellValue = page.locator('xpath=//table//tr[td[1][text()="John"]]/td[count(//table//th[text()="Email"]/preceding-sibling::th)
+ 1]');
XPath Best
Practices in Playwright
javascript
// DO: Use specific,
readable XPath expressions
await page.locator('xpath=//form[@id="login"]//button[text()="Submit"]').click();
// DON'T: Use overly
complex, brittle paths
await page.locator('xpath=/html/body/div[3]/div[2]/form/div[4]/button[1]').click();
// DO: Combine with other
Playwright features
const form =
page.locator('xpath=//form[contains(@class,
"login")]');
await form.locator('role=button[name="Submit"]').click();
// DO: Use XPath functions
for better reliability
await page.locator('xpath=//button[normalize-space(text())="Save
Changes"]').click();
// DON'T: Use position-based
selection unless necessary
await page.locator('xpath=//div[1]/div[2]/button[3]').click(); // Fragile
// DO: Use descriptive
variables for complex XPath
const userRowXPath = '//tr[td[contains(@class,
"username")][text()="john_doe"]]';
const editButton = page.locator(`xpath=${userRowXPath}//button[text()="Edit"]`);
await editButton.click();
Purpose: Select elements based on
their position within their parent container or their current state/condition.
When to use:
- When
targeting elements by their position in lists, tables, or containers
- For
selecting alternating elements (odd/even patterns)
- When
you need to exclude certain elements from selection
- For
state-based selection (visible, hidden, enabled, disabled)
- When
working with dynamically generated content where position is predictable
Best for: Table rows, list items,
alternating patterns, state-based filtering, positional targeting
Select elements based on their position or
state.
javascript
// First child - Use for
selecting the first item in lists or containers
await page.locator('li:first-child').click();
// Last child - Use for
selecting the final item in sequences
await page.locator('li:last-child').click();
// Nth child (1-indexed) -
Use when you know the exact position
await page.locator('li:nth-child(3)').click();
// Even/odd children - Use
for alternating patterns or zebra striping
await page.locator('tr:nth-child(even)').count();
await page.locator('tr:nth-child(odd)').count();
// Not selector - Use to
exclude specific elements from selection
await page.locator('button:not(.disabled)').click();
// Visible elements only -
Use for elements that are currently displayed
await page.locator('div:visible').count();
Nth-child
and Pseudo Selectors
Purpose: Select elements based on
their position within their parent container or their current state/condition.
When to use:
- When
targeting elements by their position in lists, tables, or containers
- For
selecting alternating elements (odd/even patterns)
- When
you need to exclude certain elements from selection
- For
state-based selection (visible, hidden, enabled, disabled)
- When
working with dynamically generated content where position is predictable
Best for: Table rows, list items,
alternating patterns, state-based filtering, positional targeting
Select elements based on their position or
state.
javascript
// First child - Use for
selecting the first item in lists or containers
await page.locator('li:first-child').click();
// Last child - Use for
selecting the final item in sequences
await page.locator('li:last-child').click();
// Nth child (1-indexed) -
Use when you know the exact position
await page.locator('li:nth-child(3)').click();
// Even/odd children - Use
for alternating patterns or zebra striping
await page.locator('tr:nth-child(even)').count();
await page.locator('tr:nth-child(odd)').count();
// Not selector - Use to
exclude specific elements from selection
await page.locator('button:not(.disabled)').click();
// Visible elements only -
Use for elements that are currently displayed
await page.locator('div:visible').count();
Different
Techniques
Chaining
Selectors
Purpose: Combine multiple selector
strategies to create precise, context-aware element targeting.
When to use:
- When a
single selector isn't specific enough
- For
scoping searches within specific containers
- When
combining different selector engines for optimal targeting
- For
improving selector readability and maintainability
Best for: Complex nested structures,
scoped searches, multi-criteria selection
javascript
// Chain CSS with text - Use
when combining structure with content
await page.locator('.form-group').locator('text=Submit').click();
// Chain role with CSS - Use
when combining semantic meaning with styling
await page.locator('role=main').locator('.content-area').click();
// Multiple chaining - Use
for highly specific targeting
await page.locator('[data-testid="user-panel"]')
.locator('role=button')
.locator('text=Edit')
.click();
Filtering
Purpose: Narrow down element
selection by applying additional criteria to a broader set of elements.
When to use:
- When
you have multiple similar elements and need to select specific ones
- For
conditional selection based on content or state
- When
working with lists, tables, or repeated components
- For
excluding elements based on certain criteria
Best for: List filtering, table
operations, conditional element selection, content-based targeting
javascript
// Filter by text - Use to
find specific items in lists or repeated elements
await page.locator('li').filter({ hasText: 'Apple' }).click();
// Filter by another locator
- Use for complex conditional selection
await page.locator('tr').filter({
has:
page.locator('td.status:has-text("Active")')
}).count();
// Filter by not having text
- Use to exclude specific elements
await page.locator('button').filter({ hasNotText: 'Cancel' }).first().click();
Multiple
Selection Strategies
Purpose: Demonstrate different
approaches to selecting the same element, providing flexibility and
alternatives for robust test automation.
When to use:
- When
evaluating which selector strategy works best for your specific case
- For
backup strategies when your primary selector fails
- When
different team members prefer different approaches
- For
understanding the trade-offs between different selector methods
- When
elements can be identified through multiple stable characteristics
Best for: Selector evaluation, team
standardization, fallback planning, educational purposes
Use different approaches for the same element.
javascript
// Method 1: CSS selector -
Use when element has stable ID or class
const submitBtn1 = page.locator('#submit-form-btn');
// Method 2: Role-based -
Use for semantic, accessible selection
const submitBtn2 = page.locator('role=button[name="Submit Form"]');
// Method 3: Text-based -
Use when button text is stable and unique
const submitBtn3 = page.locator('text=Submit Form');
// Method 4: XPath - Use for
complex conditions or when other methods fail
const submitBtn4 = page.locator('xpath=//form[@id="contact"]//button[text()="Submit
Form"]');
// Method 5: Combined
approach - Use for maximum precision and context
const submitBtn5 = page.locator('form').locator('role=button').filter({ hasText: 'Submit' });
Building
Selectors Using Relatives
Purpose: Navigate and select
elements based on their hierarchical relationships in the DOM tree, enabling
selection through parent-child and sibling connections.
When to use:
- When
target elements lack unique identifiers but have stable relationships
- For
navigating from known elements to related unknown elements
- When
working with dynamic content where structure is more stable than
attributes
- For
complex DOM traversal scenarios
- When
you need to select parent containers based on child content
Best for: DOM navigation, form field
associations, table operations, component boundary detection
Advantages: Structure-based selection,
dynamic content handling, relationship-driven targeting
Parent-Child
Relationships
Navigate between related elements in the DOM
hierarchy.
javascript
// Select parent element -
Use when you need the container of a known element
const childElement = page.locator('[data-testid="user-card"]');
const parentElement = childElement.locator('..');
// Select specific ancestor
- Use for finding containing elements up the hierarchy
const tableCell = page.locator('td:has-text("John Doe")');
const tableRow = tableCell.locator('xpath=ancestor::tr');
// Select direct children -
Use for immediate child elements only
const navigation = page.locator('nav');
const directChildren = navigation.locator('> ul > li');
// Select all descendants -
Use for any nested elements within a container
const form =
page.locator('form');
const allInputs = form.locator('input');
Sibling
Relationships
Purpose: Select elements that are
at the same hierarchical level in the DOM, enabling navigation between elements
that share the same parent.
When to use:
- For
form field and label associations
- When
error messages are positioned near form inputs
- For
navigation between related elements (next/previous)
- When
working with consistently structured content
- For
carousel or slider navigation
Best for: Form validation,
navigation controls, related content selection
Work with elements at the same level in the
DOM.
javascript
// Next sibling - Use to
find the immediately following element
const label =
page.locator('label:has-text("Email")');
const emailInput = label.locator('xpath=following-sibling::input[1]');
// Previous sibling - Use to
find the immediately preceding element
const errorMessage = page.locator('.error-message');
const associatedInput = errorMessage.locator('xpath=preceding-sibling::input[1]');
// All following siblings -
Use to find all subsequent elements at the same level
const firstItem = page.locator('li').first();
const followingSiblings = firstItem.locator('xpath=following-sibling::li');
Has
Relationships
Purpose: Select container elements
based on the presence of specific child elements, enabling content-driven
container selection.
When to use:
- For
selecting containers based on their content
- When
you need parent elements that contain specific child elements
- For
table row selection based on cell content
- When
targeting cards, articles, or sections by their internal content
- For
conditional selection based on nested element presence
Best for: Container selection, table
operations, card/article targeting, conditional logic
Use the :has()
pseudo-class to select elements containing other elements.
javascript
// Select div that contains
a button - Use for containers with specific interactive elements
await page.locator('div:has(button)').hover();
// Select form that has an
error message - Use for conditional form state detection
await page.locator('form:has(.error-message)').screenshot();
// Select row that contains
specific text in any cell - Use for table row identification
await page.locator('tr:has(td:has-text("Active"))').click();
// Complex has relationships
- Use for multi-criteria container selection
await page.locator('article:has(h2:has-text("News")):has(.date)').count();
Building
Selectors Using Layout
Purpose: Select elements based on
their visual position relative to other elements, enabling intuitive
spatial-based selection.
When to use:
- When
elements lack stable identifiers but have consistent spatial relationships
- For
testing visual layouts and responsive designs
- When
form labels and inputs are positioned relationally
- For
dynamic content where position is more stable than structure
- When
mimicking how users visually navigate interfaces
Best for: Form field associations,
toolbar buttons, spatial navigation, layout testing
Advantages: Intuitive, mimics user
behavior, works with dynamic content, visual relationship-based
Spatial
Relationships
javascript
// Element to the right of
another - Use for horizontally aligned form fields
await page.locator('input:right-of(:text("Username"))').fill('john_doe');
// Element to the left of
another - Use for buttons positioned before other elements
await page.locator('button:left-of(:text("Cancel"))').click();
// Element above another -
Use for labels positioned above form fields
await page.locator('label:above([type="password"])').textContent();
// Element below another -
Use for error messages positioned under inputs
await page.locator('.error:below([type="email"])').isVisible();
// Element near another -
Use for closely grouped elements (within 50px by default)
await page.locator('button:near(:text("Submit"))').click();
Advanced
Layout Selectors
javascript
// Combine layout with other
selectors
await page.locator('role=button:right-of(:text("Email"))').click();
// Chain layout selectors
await page.locator('input:below(:text("Login")):above(:text("Password"))').fill('username');
// Layout with distance
specification
await page.locator('button:near(:text("Submit"), 100)').click(); // Within 100px
// Multiple layout
constraints
await page.locator('div:right-of(.sidebar):below(.header)').click();
Practical
Layout Examples
javascript
// Form field labeling
const emailField = page.locator('input:right-of(label:has-text("Email"))');
const passwordField = page.locator('input:below(input:right-of(label:has-text("Email")))');
// Table navigation
const editButton = page.locator('button:has-text("Edit"):right-of(:text("John
Doe"))');
const statusCell = page.locator('td:right-of(:text("john.doe@email.com"))');
// Card layouts
const cardAction = page.locator('button:below(:text("Product
Name")):near(.card)');
Building
Custom Selectors
Purpose: Create reusable,
domain-specific selector patterns that encapsulate common selection logic for
your application.
When to use:
- When
you have consistent patterns across your application
- For
creating a domain-specific testing language
- When
you want to abstract complex selector logic
- For
maintaining consistent selector strategies across teams
- When
working with component libraries or design systems
Best for: Component libraries,
enterprise applications, team standardization, complex domain logic
Advantages: Reusability,
maintainability, team consistency, abstraction of complexity
Registering
Custom Selector Engines
Create reusable custom selector strategies.
javascript
// Register a custom
selector engine
await page.addSelectorEngine('tag', ({ root,
selector }) =>
{
return root.getElementsByTagName(selector);
});
// Use the custom selector
await page.locator('tag=button').click();
Data-Attribute
Based Selectors
Create helper functions for common patterns.
javascript
// Helper function for test
IDs
function testId(id) {
return `[data-testid="${id}"]`;
}
// Usage
await page.locator(testId('submit-button')).click();
await page.locator(testId('user-form')).fill('data');
// More complex data
attributes
function dataAttr(attribute,
value) {
return `[data-${attribute}="${value}"]`;
}
await page.locator(dataAttr('component', 'modal')).isVisible();
await page.locator(dataAttr('state', 'loading')).waitFor({ state: 'hidden' });
Reusable
Selector Functions
javascript
// Create selector utilities
class SelectorUtils
{
static byRole(role, name) {
return
name ? `role=${role}[name="${name}"]` : `role=${role}`;
}
static byTestId(id) {
return `[data-testid="${id}"]`;
}
static byText(text, exact = true) {
return
exact ? `text="${text}"` : `text=${text}`;
}
static inContainer(container,
selector) {
return `${container} ${selector}`;
}
}
// Usage
await page.locator(SelectorUtils.byRole('button', 'Submit')).click();
await page.locator(SelectorUtils.byTestId('user-panel')).isVisible();
await page.locator(SelectorUtils.inContainer('.modal', SelectorUtils.byRole('button'))).click();
Building
Fallback Selectors
Purpose: Create resilient selector
strategies that gracefully handle element changes, different environments, and
dynamic content.
When to use:
- When
element attributes or structure change frequently
- For
cross-environment testing (dev, staging, production)
- When
dealing with A/B tests or feature flags
- For
legacy applications with inconsistent markup
- When
elements may load at different times or have different states
Best for: Robust test automation,
cross-environment testing, legacy applications, dynamic content
Advantages: Reliability, flexibility,
reduced test maintenance, environment adaptability
Multiple
Selector Strategy
Implement fallback mechanisms for robust
element selection.
javascript
// Function to try multiple
selectors
async function
clickWithFallback(page, selectors) {
for (const selector of
selectors) {
try {
const
element = page.locator(selector);
await
element.waitFor({ timeout: 2000 });
await
element.click();
return
true;
} catch (error) {
console.log(`Selector failed: ${selector}`);
continue;
}
}
throw new
Error('All selectors failed');
}
// Usage
await clickWithFallback(page, [
'[data-testid="submit-btn"]', // Most stable
'role=button[name="Submit"]', // Semantic
'text=Submit', // User-visible
'.btn-submit', // CSS styling
'input[type="submit"]',
// HTML semantics
'xpath=//button[contains(text(),
"Submit")]' // XPath fallback
]);
Conditional
Selectors
javascript
// Check for element
existence and use alternatives
async function
smartSelect(page, primarySelector,
fallbackSelector) {
const primary = page.locator(primarySelector);
const fallback = page.locator(fallbackSelector);
try {
await
primary.waitFor({ timeout: 3000 });
return
primary;
} catch
{
console.log('Primary selector
failed, using fallback');
return
fallback;
}
}
// Usage
const submitButton = await smartSelect(
page,
'[data-testid="submit"]',
'role=button:has-text("Submit")'
);
await submitButton.click();
Environment-based
Fallbacks
javascript
// Different selectors for
different environments
function getSelector(environment,
elementType) {
const selectors = {
production: {
submitButton: '[data-prod-id="submit"]',
loginForm: '#prod-login-form'
},
staging: {
submitButton: '[data-staging-id="submit"]',
loginForm: '#staging-login-form'
},
development: {
submitButton: '[data-testid="submit"]',
loginForm: '[data-testid="login-form"]'
}
};
return selectors[environment]?.[elementType] || `[data-testid="${elementType}"]`;
}
// Usage
const env =
process.env.NODE_ENV || 'development';
await page.locator(getSelector(env, 'submitButton')).click();
Best
Practices
Selector
Hierarchy (Most to Least Preferred)
- Data
Test IDs: [data-testid="element"]
- Most
reliable and specific to testing
- Unlikely
to change with UI updates
- ARIA
Roles and Labels: role=button[name="Submit"]
- Semantic
and accessible
- Represents
user interaction patterns
- User-Visible
Text: text=Submit
- Represents
what users actually see
- Good
for user-centric testing
- CSS
Selectors: .btn-primary
- Widely
understood
- Can
be brittle if styles change
- XPath: //button[@class='submit']
- Powerful
but complex
- Use
sparingly and as last resort
Writing
Resilient Selectors
javascript
// Good: Specific and stable
await page.locator('[data-testid="user-profile-edit-button"]').click();
// Better: Combines multiple
strategies
await page.locator('[data-testid="user-profile"]')
.locator('role=button[name="Edit"]').click();
// Best: Readable and
maintainable
const userProfile = page.locator('[data-testid="user-profile"]');
const editButton = userProfile.locator('role=button[name="Edit"]');
await editButton.click();
Selector
Trade-offs and Considerations
Understanding the trade-offs helps you make
informed decisions:
Data Test IDs ([data-testid="element"])
- ✅ Pros:
Stable, test-specific, clear intent
- ❌ Cons:
Requires developer cooperation, additional markup
- 🎯 Use
when: Building new features, have control over markup
Role-based Selectors (role=button[name="Submit"])
- ✅ Pros:
Semantic, accessible, future-proof
- ❌ Cons:
Requires proper ARIA implementation
- 🎯 Use
when: Accessibility is important, semantic markup exists
Text-based Selectors (text=Submit)
- ✅ Pros:
User-centric, intuitive, visual
- ❌ Cons:
Breaks with text changes, i18n challenges
- 🎯 Use
when: Text is stable, user perspective is priority
Layout Selectors (:right-of, :below)
- ✅ Pros:
Visual relationships, works with dynamic content
- ❌ Cons:
Can break with responsive design changes
- 🎯 Use
when: Spatial relationships are stable
CSS Selectors (.btn-primary, #submit)
- ✅ Pros:
Familiar, precise, widely supported
- ❌ Cons:
Tied to styling, can break with design changes
- 🎯 Use
when: Styling is stable, need precise control
XPath Selectors (//form//button[text()="Submit"])
- ✅ Pros:
Extremely powerful, bidirectional navigation, complex logic
- ❌ Cons:
Less readable, more brittle, performance overhead
- 🎯 Use
when: CSS can't express the logic, complex DOM traversal needed
Performance
Considerations
javascript
// Avoid: Re-querying
elements
for (let i = 0; i < 5; i++) {
await page.locator('.item').nth(i).click();
}
// Better: Store locator
reference
const items =
page.locator('.item');
for (let i = 0; i < 5; i++) {
await items.nth(i).click();
}
// Best: Batch operations
when possible
const items =
page.locator('.item');
const count =
await items.count();
for (let i = 0; i < count; i++) {
await items.nth(i).click();
}
Common
Patterns and Examples
Form
Interactions
javascript
// Login form
const loginForm = page.locator('[data-testid="login-form"]');
await loginForm.locator('role=textbox[name="Email"]').fill('user@example.com');
await loginForm.locator('role=textbox[name="Password"]').fill('password123');
await loginForm.locator('role=button[name="Login"]').click();
// Dynamic form validation
const emailField = page.locator('[data-testid="email-input"]');
await emailField.fill('invalid-email');
await emailField.blur();
const errorMessage = page.locator('.error:below([data-testid="email-input"])');
await expect(errorMessage).toBeVisible();
Table
Operations
javascript
// Find row by content and
perform action
const userRow = page.locator('tr:has(td:has-text("john.doe@email.com"))');
const editButton = userRow.locator('role=button[name="Edit"]');
await editButton.click();
// Sort table by column
const nameHeader = page.locator('th:has-text("Name")');
await nameHeader.click();
// Filter table
const statusFilter = page.locator('select:right-of(label:has-text("Status"))');
await statusFilter.selectOption('Active');
Modal and
Dialog Handling
javascript
// Open modal
await page.locator('role=button[name="Add User"]').click();
// Work within modal
const modal =
page.locator('role=dialog');
await modal.locator('role=textbox[name="Name"]').fill('John Doe');
await modal.locator('role=textbox[name="Email"]').fill('john@example.com');
await modal.locator('role=button[name="Save"]').click();
// Wait for modal to close
await modal.waitFor({ state: 'hidden' });
Navigation
and Menus
javascript
// Navigate through dropdown
menu
const menuButton = page.locator('role=button[name="User Menu"]');
await menuButton.click();
const menuPopover = page.locator('role=menu');
await menuPopover.locator('role=menuitem[name="Profile"]').click();
// Breadcrumb navigation
const breadcrumb = page.locator('nav[aria-label="Breadcrumb"]');
await breadcrumb.locator('role=link[name="Dashboard"]').click();
Error
Handling and Validation
javascript
// Check for error states
const submitButton = page.locator('role=button[name="Submit"]');
await submitButton.click();
const errorContainer = page.locator('[data-testid="form-errors"]');
if (await errorContainer.isVisible()) {
const errors =
await errorContainer.locator('.error-message').allTextContents();
console.log('Form errors:',
errors);
}
// Retry logic for dynamic
content
async function
waitForElementWithRetry(page, selector, maxRetries = 3) {
for (let i = 0; i < maxRetries;
i++) {
try {
await
page.locator(selector).waitFor({ timeout: 5000 });
return
true;
} catch (error) {
if (i ===
maxRetries - 1) throw error;
await
page.waitForTimeout(1000);
}
}
}
Advanced
Patterns
javascript
// Component-based selectors
class ComponentSelectors
{
constructor(page) {
this.page = page;
}
userCard(username) {
return this.page.locator(`[data-testid="user-card"]:has-text("${username}")`);
}
formField(label) {
return this.page.locator(`role=textbox[name="${label}" i]`);
}
buttonInContainer(container,
buttonText) {
return this.page.locator(container).locator(`role=button:has-text("${buttonText}")`);
}
}
// Usage
const components = new ComponentSelectors(page);
await components.userCard('John Doe').hover();
await components.formField('Email Address').fill('test@example.com');
await components.buttonInContainer('.modal', 'Save Changes').click();
Conclusion
Playwright selectors provide a powerful and
flexible way to interact with web elements. By understanding and combining
different selector strategies—from basic CSS selectors to advanced layout-based
selection—you can create robust, maintainable test automation that works
reliably across different browsers and scenarios.
Remember to:
- Prefer
stable selectors like data-testids and ARIA roles
- Use
multiple strategies for fallback scenarios
- Write
readable and maintainable selector code
- Consider
the user's perspective when choosing selectors
- Test
your selectors across different environments
With these techniques and patterns, you'll be
able to handle even the most complex web applications with confidence.
Element-to-Selector
Strategy Recommendations
This comprehensive table provides quick
guidance on which selector strategies work best for different types of UI
elements:
Element Type |
Primary Strategy |
Secondary Strategy |
Fallback Strategy |
Notes & Considerations |
Buttons |
role=button[name="Text"] |
[data-testid="button-id"] |
text="Button Text" |
Role-based
is most semantic; use name attribute for identification |
Form
Inputs |
role=textbox[name="Label"] |
[data-testid="input-id"] |
input:right-of(label:has-text("Label")) |
Consider
label associations; role covers input, textarea, etc. |
Checkboxes |
role=checkbox[name="Label"] |
[data-testid="checkbox-id"] |
input[type="checkbox"]:right-of(:text("Label")) |
Always
verify checked state with assertions |
Radio
Buttons |
role=radio[name="Option"] |
[data-testid="radio-id"] |
input[type="radio"][value="option"] |
Group by
name attribute for related options |
Dropdowns/Selects |
role=combobox[name="Label"] |
[data-testid="select-id"] |
select:right-of(label:has-text("Label")) |
Different
roles for native vs custom dropdowns |
Links |
role=link[name="Link Text"] |
[data-testid="link-id"] |
text="Link Text" |
Consider
both internal and external links |
Navigation
Menus |
role=navigation >> role=link |
nav [data-testid="nav-item"] |
nav a:has-text("Menu Item") |
Use ARIA
landmarks for main navigation |
Tables |
role=table >> role=row |
[data-testid="table"] tr |
table tr:has(td:has-text("Content")) |
Target
rows by content, then find cells within |
Table
Headers |
role=columnheader[name="Header"] |
th:has-text("Header Text") |
thead th:nth-child(n) |
Use for
sorting and column identification |
Table
Cells |
role=cell:has-text("Content") |
td:has-text("Content") |
tr:has-text("Row ID") td:nth-child(n) |
Find by
content or position within identified row |
Modals/Dialogs |
role=dialog |
[data-testid="modal"] |
.modal:visible |
Always
check visibility state |
Modal
Buttons |
role=dialog >> role=button[name="Action"] |
[data-testid="modal"] button |
.modal button:has-text("Action") |
Scope
within dialog container |
Breadcrumbs |
role=navigation[name="Breadcrumb"] |
[data-testid="breadcrumb"] |
nav.breadcrumb a |
Use ARIA
navigation landmarks |
Tabs |
role=tab[name="Tab Name"] |
[data-testid="tab"] |
.tab:has-text("Tab Name") |
Consider
both tab and tabpanel roles |
Tab
Panels |
role=tabpanel[name="Panel Name"] |
[data-testid="tab-panel"] |
[aria-labelledby="tab-id"] |
Link to
corresponding tab |
Search
Inputs |
role=searchbox[name="Search"] |
[data-testid="search"] |
input[type="search"] |
Specific
role for search functionality |
File
Uploads |
input[type="file"] |
[data-testid="file-upload"] |
role=button:has-text("Upload") |
May be
hidden behind custom UI |
Progress
Bars |
role=progressbar |
[data-testid="progress"] |
.progress-bar |
Check
aria-valuenow for progress value |
Alerts/Notifications |
role=alert |
[data-testid="alert"] |
.alert:visible |
Often
appear/disappear dynamically |
Form
Validation Errors |
.error:below(input) |
[data-testid="error"] |
xpath=//input[@name="email"]/following-sibling::*[contains(@class,
"error")] |
Use
layout selectors for field association; XPath for complex relationships |
Complex
Table Operations |
xpath=//tr[td[text()="John"]]/td[count(//th[text()="Email"]/preceding-sibling::th)+1] |
tr:has(td:has-text("John")) td:nth-child(n) |
[data-testid="user-row"]
[data-testid="email-cell"] |
XPath
excels at complex table navigation |
Multi-level
Navigation |
xpath=//nav//ul[contains(@class,
"submenu")]//a[text()="Settings"] |
nav [data-testid="submenu"]
role=link[name="Settings"] |
nav ul.submenu a:has-text("Settings") |
XPath for
complex nested navigation |
Conditional
Parent Selection |
xpath=//div[.//span[@class="error"]]/ancestor::form |
form:has(.error) |
[data-testid="form"]:has(.error) |
XPath for
ancestor navigation when CSS :has() isn't sufficient |
Toolbars |
role=toolbar |
[data-testid="toolbar"] |
.toolbar |
Group
related action buttons |
Toolbar
Buttons |
role=toolbar >> role=button |
[data-testid="toolbar"] button |
.toolbar button:has-text("Action") |
Scope
within toolbar container |
Cards |
[data-testid="card"] |
.card:has-text("Identifier") |
article:has-text("Title") |
Identify
by unique content within card |
Card
Actions |
[data-testid="card"] role=button |
.card button:has-text("Action") |
button:near(:text("Card Title")) |
Scope
within card or use spatial relationship |
List
Items |
role=listitem:has-text("Item") |
li:has-text("Item Text") |
[data-testid="list"] li:nth-child(n) |
Identify
by content or position |
Accordion
Headers |
role=button[expanded] |
[data-testid="accordion-header"] |
.accordion-header:has-text("Section") |
Check
expanded state |
Accordion
Content |
role=region |
[data-testid="accordion-content"] |
.accordion-content:visible |
Content
visibility tied to header state |
Pagination |
role=navigation[name="Pagination"] |
[data-testid="pagination"] |
.pagination |
Use
navigation landmark |
Page
Numbers |
role=navigation >> role=link[name="Page X"] |
[data-testid="page-link"] |
.pagination a:has-text("X") |
Identify
by page number |
Loading
Spinners |
role=status |
[data-testid="loading"] |
.loading:visible |
Check
visibility for loading state |
Tooltips |
role=tooltip |
[data-testid="tooltip"] |
[aria-describedby] |
Often
appear on hover |
Date
Pickers |
role=textbox[name*="date"] |
[data-testid="date-picker"] |
input[type="date"] |
May be
custom components |
Rich Text
Editors |
role=textbox[multiline=true] |
[data-testid="editor"] |
[contenteditable="true"] |
Different
implementation approaches |
Image
Elements |
role=img[name="Alt text"] |
[data-testid="image"] |
img[alt="Description"] |
Use alt
text for identification |
Video
Players |
role=application |
[data-testid="video-player"] |
video |
Custom
players may use application role |
Sliders/Range |
role=slider |
[data-testid="slider"] |
input[type="range"] |
Check
aria-valuenow for current value |
Toggle
Switches |
role=switch |
[data-testid="toggle"] |
input[type="checkbox"][role="switch"] |
Different
from regular checkboxes |
Step
Indicators |
role=progressbar |
[data-testid="stepper"] |
.step.active |
Track
current step in multi-step processes |
Floating
Action Buttons |
role=button[name="Action"] |
[data-testid="fab"] |
.fab:has-text("Action") |
Often
positioned absolutely |
Context
Menus |
role=menu |
[data-testid="context-menu"] |
.context-menu:visible |
Appear on
right-click or trigger |
Menu
Items |
role=menuitem[name="Action"] |
[data-testid="menu-item"] |
.menu-item:has-text("Action") |
Scope
within menu container |
Usage Notes
for the Recommendations Table:
Primary Strategy: The most
robust and recommended approach Secondary Strategy: Good alternative
when primary isn't available Fallback Strategy: Last resort option for
difficult cases
Key Principles:
- Always
prefer semantic approaches (role-based) when available
- Use
data-testid attributes for custom components and complex
scenarios
- Layout
selectors work well for form field associations
and spatial relationships
- Text-based
selectors are intuitive but less stable for
frequently changing content
- Combine
strategies using chaining for more precise
targeting
Remember: The best selector is one
that is stable, readable, and reflects how users interact with your
application!
Complete
Selector Type Summary
This documentation covered all major
Playwright selector strategies:
Basic
Selectors
- CSS
Selectors: #id, .class, [attribute] -
Familiar and precise
- Text
Selectors: text=Submit, text=/pattern/ - User-centric selection
- Has-text
Selectors: :has-text("content") -
Container selection by content
Advanced
Selectors
- Role-based: role=button[name="Submit"] -
Semantic, accessible
- Attribute
Matching: [data-testid="element"], [class^="btn-"] - Precise targeting
- Pseudo
Selectors: :first-child, :nth-child(3), :not(.disabled) -
Position and state
- XPath: xpath=//button[text()="Submit"] -
Complex DOM navigation
Advanced
Techniques
- Chaining: .container >> role=button -
Precise scoping
- Filtering: .filter({ hasText: 'Submit' }) - Conditional selection
- Relative
Navigation: Parent-child, sibling relationships
- Layout-based: :right-of(), :below(), :near() - Spatial relationships
- Custom
Selectors: Reusable patterns and utilities
- Fallback
Strategies: Multiple approaches for resilience
Each approach has its strengths - choose based
on your specific use case, element stability, and team preferences!
Advanced
Playwright Selector Features
Frame and
iframe Handling
Purpose: Select elements within
frames and iframes, which exist in separate document contexts.
When to use: When your application uses
embedded content, third-party widgets, or iframe-based components.
javascript
// Basic frame selection
const frame =
page.frame('frame-name');
await frame.locator('role=button[name="Submit"]').click();
// Frame by URL
const frame =
page.frame({ url: /.*checkout.*/
});
await frame.locator('[data-testid="payment-form"]').fill('data');
// Nested frames
const parentFrame = page.frame('parent-frame');
const childFrame = parentFrame.childFrames()[0];
await childFrame.locator('input[type="text"]').fill('nested content');
// Frame selector chaining
await page.frameLocator('#payment-iframe').locator('role=button[name="Pay Now"]').click();
// Multiple frame strategy
const frames =
page.frames();
for (const frame of
frames) {
const element = frame.locator('[data-testid="target"]');
if (await element.isVisible()) {
await
element.click();
break;
}
}
Shadow DOM
Selectors
Purpose: Penetrate shadow DOM
boundaries to select elements within web components.
When to use: With modern web
components, custom elements, or component libraries that use shadow DOM.
javascript
// Pierce shadow DOM
boundaries
await page.locator('custom-component').locator('button').click();
// Deep piercing for nested
shadow DOM
await page.locator('outer-component >> inner-component >>
button').click();
// Combine with other
selectors
await page.locator('my-widget').locator('role=button[name="Save"]').click();
// Text-based selection in
shadow DOM
await page.locator('shadow-button').locator('text=Submit').click();
Waiting
Strategies and Timeouts
Purpose: Control how Playwright
waits for elements and handles timing in selector operations.
When to use: For dynamic content,
slow-loading elements, or when default timeouts aren't sufficient.
javascript
// Custom timeout for
selector
await page.locator('[data-testid="slow-button"]').click({ timeout: 10000 });
// Wait for element to be
visible before interacting
await page.locator('[data-testid="dynamic-content"]').waitFor({ state: 'visible' });
// Wait for element to be
attached to DOM
await page.locator('[data-testid="ajax-content"]').waitFor({ state: 'attached' });
// Wait for element to be
hidden
await page.locator('[data-testid="loading-spinner"]').waitFor({ state: 'hidden' });
// Wait for element count
await page.locator('.item').waitFor({ count: 5 });
// Conditional waiting
const element = page.locator('[data-testid="conditional-element"]');
try {
await element.waitFor({ timeout: 5000 });
await element.click();
} catch
(error) {
console.log('Element not found, using fallback');
await page.locator('[data-testid="fallback-button"]').click();
}
Strict Mode
and Error Handling
Purpose: Handle cases where
selectors match multiple elements or no elements.
When to use: For robust error handling
and preventing flaky tests.
javascript
// Strict mode - throws
error if multiple elements match
await page.locator('button').click(); // May throw if multiple buttons exist
// Non-strict - get first
element
await page.locator('button').first().click();
// Count-based selection
const buttonCount = await page.locator('button').count();
if (buttonCount
> 1) {
await page.locator('button').nth(0).click(); // Click first button
} else
if (buttonCount
=== 1) {
await page.locator('button').click();
} else
{
throw new
Error('No buttons found');
}
// Safe element checking
async function
safeClick(page, selector,
fallbackSelector = null) {
const element = page.locator(selector);
const count =
await element.count();
if (count
=== 1) {
await
element.click();
} else
if (count
> 1) {
console.warn(`Multiple elements
found for ${selector}, clicking first`);
await
element.first().click();
} else
if (fallbackSelector) {
console.log(`Element not found
for ${selector}, trying fallback`);
await
page.locator(fallbackSelector).click();
} else
{
throw new Error(`No elements found
for selector: ${selector}`);
}
}
Selector
Debugging and Troubleshooting
Debug
Techniques
javascript
// Log element count
const elements = page.locator('.item');
console.log(`Found ${await elements.count()} elements`);
// Get all text content for
debugging
const texts =
await page.locator('.item').allTextContents();
console.log('Element texts:', texts);
// Screenshot element for
visual debugging
await page.locator('[data-testid="problematic-element"]').screenshot({ path: 'debug.png' });
// Highlight element for
debugging
await page.locator('[data-testid="target"]').highlight();
// Get element attributes
for debugging
const element = page.locator('[data-testid="debug-me"]');
const attributes = await element.evaluate(el => {
const attrs =
{};
for (let attr of el.attributes) {
attrs[attr.name] = attr.value;
}
return attrs;
});
console.log('Element attributes:', attributes);
Common
Issues and Solutions
javascript
// Issue: Element is covered
by another element
// Solution: Use force
option or wait for overlay to disappear
await page.locator('[data-testid="covered-button"]').click({ force: true });
// Or wait for overlay to
disappear
await page.locator('.loading-overlay').waitFor({ state: 'hidden' });
// Issue: Element is outside
viewport
// Solution: Scroll into
view
await page.locator('[data-testid="bottom-element"]').scrollIntoViewIfNeeded();
// Issue: Element loads
dynamically
// Solution: Wait for
network or specific condition
await page.waitForLoadState('networkidle');
await page.locator('[data-testid="dynamic-content"]').waitFor();
// Issue: Flaky selector due
to timing
// Solution: Add explicit
waits and retries
async function
robustClick(page, selector,
maxRetries = 3) {
for (let i = 0; i < maxRetries;
i++) {
try {
await
page.locator(selector).waitFor({ state: 'visible', timeout: 5000 });
await
page.locator(selector).click({ timeout: 5000 });
return;
} catch (error) {
if (i ===
maxRetries - 1) throw error;
await
page.waitForTimeout(1000);
}
}
}
Specialized
Use Cases
Single Page
Applications (SPAs)
javascript
// Wait for navigation in
SPAs
await page.locator('role=link[name="Dashboard"]').click();
await page.waitForURL(/.*dashboard.*/);
// Handle dynamic routing
await page.locator(`role=link[name="${routeName}"]`).click();
await page.waitForFunction(()
=> window.location.pathname.includes('expected-path'));
// Wait for AJAX content to
load
await page.locator('[data-testid="data-table"]').waitFor();
await page.waitForSelector('[data-testid="data-row"]'); // Wait for at
least one row
Internationalization
(i18n)
javascript
// Locale-aware selectors
const locale =
'en-US'; // or get from config
const submitText = locale === 'en-US' ? 'Submit' : 'Enviar';
await page.locator(`text=${submitText}`).click();
// Use data attributes
instead of text for i18n
await page.locator('[data-i18n-key="submit-button"]').click();
// Role-based selectors work
across languages
await page.locator('role=button[name=/submit|enviar|soumettre/i]').click();
Mobile and
Touch Considerations
javascript
// Touch-friendly
interactions
await page.locator('[data-testid="mobile-menu"]').tap();
// Handle mobile-specific
elements
if (await page.locator('[data-testid="mobile-nav"]').isVisible()) {
await page.locator('[data-testid="mobile-menu-toggle"]').click();
}
// Viewport-aware selection
const isMobile = page.viewportSize().width
< 768;
const menuSelector = isMobile ? '[data-testid="mobile-menu"]' : '[data-testid="desktop-menu"]';
await page.locator(menuSelector).click();
Performance
Optimization
Efficient
Selector Patterns
javascript
// GOOD: Store locator
references
const userForm = page.locator('[data-testid="user-form"]');
await userForm.locator('role=textbox[name="Name"]').fill('John');
await userForm.locator('role=textbox[name="Email"]').fill('john@example.com');
await userForm.locator('role=button[name="Save"]').click();
// BAD: Re-query each time
await page.locator('[data-testid="user-form"]').locator('role=textbox[name="Name"]').fill('John');
await page.locator('[data-testid="user-form"]').locator('role=textbox[name="Email"]').fill('john@example.com');
await page.locator('[data-testid="user-form"]').locator('role=button[name="Save"]').click();
// GOOD: Batch operations
const items =
page.locator('.list-item');
const itemCount = await items.count();
const itemTexts = await items.allTextContents();
// BAD: Individual queries
for (let i = 0; i < await page.locator('.list-item').count(); i++) {
const text =
await page.locator('.list-item').nth(i).textContent();
console.log(text);
}
Selector
Performance Tips
- Prefer
specific selectors over broad ones
- Use
data-testid for frequently accessed elements
- Avoid
deep XPath expressions when possible
- Cache
locator objects for repeated use
- Use
waitFor() strategies to reduce polling
- Combine operations when working with multiple elements
No comments :
Post a Comment