Skip to main content
The Selectors API allows you to register custom selector engines that can be used throughout Playwright to locate elements.

Overview

Playwright supports multiple built-in selector engines (CSS, XPath, text, etc.). You can extend this with custom selector engines to match elements based on your own logic.
import { selectors } from '@playwright/test';

// Register a custom selector engine
await selectors.register('tag', {
  query(root, selector) {
    return root.querySelector(selector.toUpperCase());
  },
  queryAll(root, selector) {
    return Array.from(root.querySelectorAll(selector.toUpperCase()));
  },
});

// Use the custom selector
await page.locator('tag=button').click();

Methods

register(name, script, options)

Register a custom selector engine.
name
string
required
Name of the selector engine. Once registered, you can use it with the syntax name=selector.
script
string | Function | { path?: string, content?: string }
required
Script that evaluates to a selector engine instance. Can be:
  • A function that returns a selector engine
  • A string containing JavaScript code
  • An object with path to a JavaScript file or content with the code
options.contentScript
boolean
default:"false"
Whether to run the selector engine script as a content script in the page context. Only applies to Chromium.
Returns: Promise<void>

Selector Engine Interface

Your selector engine must implement:
interface SelectorEngine {
  // Find the first matching element
  query(root: Element, selector: string): Element | null;
  
  // Find all matching elements
  queryAll(root: Element, selector: string): Element[];
}

Example: Tag Name Selector

await selectors.register('tag', () => {
  return {
    query(root, selector) {
      return root.querySelector(selector.toUpperCase());
    },
    queryAll(root, selector) {
      return Array.from(root.querySelectorAll(selector.toUpperCase()));
    },
  };
});

// Usage
await page.click('tag=button');

Example: Data Attribute Selector

await selectors.register('data', () => {
  return {
    query(root, selector) {
      return root.querySelector(`[data-test="${selector}"]`);
    },
    queryAll(root, selector) {
      return Array.from(root.querySelectorAll(`[data-test="${selector}"]`));
    },
  };
});

// Usage
await page.locator('data=submit-button').click();

Example: Loading from File

await selectors.register('custom', {
  path: './my-selector-engine.js',
});

setTestIdAttribute(attributeName)

Set the attribute name to use for getByTestId() locators.
attributeName
string
required
Attribute name to use for test ID selectors
Returns: void By default, Playwright uses data-testid attribute. Change it to match your project’s convention:
import { selectors } from '@playwright/test';

// Use 'data-test' instead of 'data-testid'
selectors.setTestIdAttribute('data-test');

// Now this will look for data-test="my-button"
await page.getByTestId('my-button').click();
You can also configure this in playwright.config.ts:
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    testIdAttribute: 'data-test',
  },
});

Complete Example

import { test, selectors } from '@playwright/test';

// Register selector engine before tests run
test.beforeAll(async () => {
  // Register a custom selector for aria-label
  await selectors.register('label', () => {
    return {
      query(root, selector) {
        return root.querySelector(`[aria-label="${selector}"]`);
      },
      queryAll(root, selector) {
        return Array.from(
          root.querySelectorAll(`[aria-label="${selector}"]`)
        );
      },
    };
  });

  // Change test ID attribute
  selectors.setTestIdAttribute('data-qa');
});

test('use custom selectors', async ({ page }) => {
  await page.goto('https://example.com');
  
  // Use custom 'label' selector
  await page.click('label=Submit');
  
  // Use modified test ID attribute
  await page.getByTestId('login-button').click(); // Looks for data-qa
});

Best Practices

Selector Engine Naming

  • Use descriptive names that indicate what the selector matches
  • Avoid names that conflict with built-in engines (css, xpath, text, etc.)
  • Use lowercase names for consistency

Error Handling

await selectors.register('safe', () => {
  return {
    query(root, selector) {
      try {
        return root.querySelector(selector);
      } catch (e) {
        console.error('Selector error:', e);
        return null;
      }
    },
    queryAll(root, selector) {
      try {
        return Array.from(root.querySelectorAll(selector));
      } catch (e) {
        console.error('Selector error:', e);
        return [];
      }
    },
  };
});

Performance

  • Keep selector logic simple and fast
  • Avoid expensive computations in queryAll
  • Cache results when appropriate

Limitations

  • Custom selectors cannot be registered after tests have started
  • Each selector engine name can only be registered once
  • Content script mode only works in Chromium

Build docs developers (and LLMs) love