microsoft/playwright · error · Error

"${engine.name}" selector engine has been already registered

Error message

"${engine.name}" selector engine has been already registered

What it means

Thrown by Selectors.register when the engine name was already added to the _engines map by a prior successful register() call. Each custom engine name must be unique within a Selectors instance; re-registering is treated as a programming error rather than an update.

Source

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

      '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;
      const custom = this._engines.get(name);
      if (!custom && !this._builtinEngines.has(name))

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Guard registration: only register if the engine is not already present (check before calling register).
  2. Centralize engine registration in a single module imported once at startup.
  3. Use a Set/flag to make registration idempotent across HMR reloads.

Example fix

// before
selectors.register({ name: 'my-engine', query: () => {...} });
// imported again elsewhere → throws

// after — idempotent guard
if (!selectors._engines.has('my-engine'))
  selectors.register({ name: 'my-engine', query: () => {...} });
Defensive patterns

Strategy: validation

Validate before calling

// Guard against double registration (e.g. under HMR)
function registerOnce(selectors: any, engine: any) {
  if (!selectors._engines.has(engine.name))
    selectors.register(engine);
}

registerOnce(selectors, { name: 'my-engine', query: () => {/*...*/} });

Type guard

function isEngineRegistered(selectors: any, name: string): boolean {
  return selectors._engines.has(name);
}

Try / catch

try {
  selectors.register(engine);
} catch (e) {
  if (/has been already registered/.test(e.message)) return; // idempotent
  throw e;
}

Prevention

When it happens

Trigger: Calling selectors.register(engine) twice with the same name; a setup module that runs register() being imported more than once (e.g. HMR reload, duplicate imports) without guarding.

Common situations: Hot-module reloading re-running a registration module; a shared helper that registers the same engine in multiple test files that all load; copy-paste registering 'foo' in two places.

Related errors


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