microsoft/playwright · error · Error

"${engine.name}" is a predefined selector engine

Error message

"${engine.name}" is a predefined selector engine

What it means

Thrown by Selectors.register when the engine name collides with a predefined/builtin engine. The check covers everything in _builtinEngines (css, xpath, text, role, internal:*, data-testid variants, etc.) plus the reserved names 'zs' and 'zs:light' kept for future use. Registering over a builtin would break core selector resolution, so it is rejected outright.

Source

Thrown at packages/playwright-core/src/server/selectors.ts:63

      'role', 'internal:attr', 'internal:label', 'internal:text',
      'internal:role', 'internal:testid', 'internal:describe',
      'aria-ref'
    ]);
    this._builtinEnginesInMainWorld = new Set([
      '_react', '_vue',
    ]);
    this._engines = new Map();
    this._testIdAttributeName = testIdAttributeName ?? 'data-testid';
    for (const engine of engines)
      this.register(engine);
  }

  register(engine: channels.SelectorEngine) {
    if (!engine.name.match(/^[a-zA-Z_0-9-]+$/))
      throw new Error('Selector engine name may only contain [a-zA-Z0-9_] characters');
    // Note: we keep 'zs' for future use.
    if (this._builtinEngines.has(engine.name) || engine.name === 'zs' || engine.name === 'zs:light')
      throw new Error(`"${engine.name}" is a predefined selector engine`);
    if (this._engines.has(engine.name))
      throw new Error(`"${engine.name}" selector engine has been already registered`);
    this._engines.set(engine.name, engine);
  }

  testIdAttributeName(): string {
    return this._testIdAttributeName;
  }

  setTestIdAttributeName(testIdAttributeName: string) {
    this._testIdAttributeName = testIdAttributeName;
  }

  parseSelector(selector: string | ParsedSelector, strict: boolean) {
    const parsed = typeof selector === 'string' ? parseSelector(selector) : selector;
    let needsMainWorld = false;
    visitAllSelectorParts(parsed, part => {
      const name = part.name;

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Choose a non-builtin, non-reserved name for your custom engine.
  2. If you intended to extend a builtin, register a new engine with a distinct name and use that engine prefix in your selectors.
  3. Check the _builtinEngines list in the source before naming your engine.

Example fix

// before
selectors.register({ name: 'text', query: () => {...} });

// after
selectors.register({ name: 'my-text', query: () => {...} });
Defensive patterns

Strategy: validation

Validate before calling

const BUILTIN_ENGINES = new Set([
  'css','css:light','xpath','xpath:light','_react','_vue',
  'text','text:light','id','id:light','role','nth','visible',
  'internal:control','internal:has','internal:has-not','internal:has-text',
  'internal:has-not-text','internal:and','internal:or','internal:chain',
  'internal:attr','internal:label','internal:text','internal:role',
  'internal:testid','internal:describe','aria-ref','zs','zs:light'
]);

if (BUILTIN_ENGINES.has(name)) throw new Error(`${name} is a builtin engine`);

Type guard

function isBuiltinEngine(name: string): boolean {
  const builtins = ['css','xpath','text','role','nth','visible','id','aria-ref','zs','zs:light'];
  return builtins.includes(name) || name.startsWith('internal:') || name.endsWith(':light');
}

Prevention

When it happens

Trigger: Calling selectors.register with a name like 'css', 'xpath', 'text', 'role', 'internal:has', 'data-testid', or the reserved 'zs'/'zs:light'.

Common situations: Unaware that a name is builtin; dynamically generating engine names without checking the builtin set; trying to override the text/role engine behavior.

Related errors


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/037c990b439acb9f. Report an issue: GitHub.