Facebook

Course Name Start Date Time Duration Registration Link
No Training Programs Scheduled ClickHere to Contact
Please mail To sudhakar@qtpsudhakar.com to Register for any training

Tuesday, July 22, 2025

Complete Guide to Playwright Selectors

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:

  1. Data attributes (data-testid, data-*) - Designed for testing
  2. ARIA roles/labels (role=button[name="Submit"]) - Semantic meaning
  3. User-visible text (text=Submit) - What users see
  4. Stable CSS classes (.primary-button) - Intentional styling
  5. IDs (#submit-btn) - Can change with refactoring
  6. Specific XPath (//form[@id="login"]//button[text()="Submit"]) - Powerful but complex
  7. Generic CSS (.btn.btn-primary) - Tied to styling
  8. Tag names (button) - Too generic
  9. 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)

  1. Data Test IDs: [data-testid="element"]
    • Most reliable and specific to testing
    • Unlikely to change with UI updates
  2. ARIA Roles and Labels: role=button[name="Submit"]
    • Semantic and accessible
    • Represents user interaction patterns
  3. User-Visible Text: text=Submit
    • Represents what users actually see
    • Good for user-centric testing
  4. CSS Selectors: .btn-primary
    • Widely understood
    • Can be brittle if styles change
  5. 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:

  1. Always prefer semantic approaches (role-based) when available
  2. Use data-testid attributes for custom components and complex scenarios
  3. Layout selectors work well for form field associations and spatial relationships
  4. Text-based selectors are intuitive but less stable for frequently changing content
  5. 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

  1. Prefer specific selectors over broad ones
  2. Use data-testid for frequently accessed elements
  3. Avoid deep XPath expressions when possible
  4. Cache locator objects for repeated use
  5. Use waitFor() strategies to reduce polling
  6. Combine operations when working with multiple elements

No comments :

Post a Comment