microsoft/playwright · error · InvalidSelectorError

Only one of the selectors can capture using * modifier

Error message

Only one of the selectors can capture using * modifier

What it means

parseSelectorString throws when more than one selector part is marked with the '*' capture modifier. The capture marker designates the single element Playwright should resolve out of a compound selector; multiple captures are ambiguous and therefore rejected.

Source

Thrown at packages/isomorphic/selectorParser.ts:217

    } else if (/^\(*\/\//.test(part) || part.startsWith('..')) {
      // If selector starts with '//' or '//' prefixed with multiple opening
      // parenthesis, consider xpath. @see https://github.com/microsoft/playwright/issues/817
      // If selector starts with '..', consider xpath as well.
      name = 'xpath';
      body = part;
    } else {
      name = 'css';
      body = part;
    }
    let capture = false;
    if (name[0] === '*') {
      capture = true;
      name = name.substring(1);
    }
    result.parts.push({ name, body });
    if (capture) {
      if (result.capture !== undefined)
        throw new InvalidSelectorError(`Only one of the selectors can capture using * modifier`);
      result.capture = result.parts.length - 1;
    }
  };

  if (!selector.includes('>>')) {
    index = selector.length;
    append();
    return result;
  }

  const shouldIgnoreTextSelectorQuote = () => {
    const prefix = selector.substring(start, index);
    const match = prefix.match(/^\s*text\s*=(.*)$/);
    // Must be a text selector with some text before the quote.
    return !!match && !!match[1];
  };

  while (index < selector.length) {

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Use exactly one '*' — on the part whose element should be the resolved result.
  2. If you need elements from multiple parts, run separate locators and resolve each independently.
  3. Audit dynamically built selectors to ensure '*' is inserted at most once.

Example fix

// before
await page.locator('*div >> *span').click();

// after
await page.locator('div >> *span').click(); // capture only the span
Defensive patterns

Strategy: validation

Validate before calling

function atMostOneCapture(sel: string): boolean {
  const matches = sel.match(/(^|>>)\s*\*/g);
  return !matches || matches.length <= 1;
}

Try / catch

try { await page.locator(sel).click(); }
catch (e) { if (isInvalidSelectorError(e) && /Only one of the selectors can capture/.test(e.message)) { /* remove extra '*' */ } else throw e; }

Prevention

When it happens

Trigger: Authoring a selector with two '*' prefixes, e.g. '*div >> *span' or '*css=div >> *internal:attr=[id="x"]'. Each '*' tries to register a capture; the second one trips the check.

Common situations: Misunderstanding that '*' designates the result element (only one allowed); building selectors dynamically and prepending '*' to multiple fragments; copy-paste errors duplicating the modifier.

Related errors


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