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
- Use exactly one '*' — on the part whose element should be the resolved result.
- If you need elements from multiple parts, run separate locators and resolve each independently.
- 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
- Use at most one '*' per selector, on the part whose element you want resolved.
- Run separate locators if you need elements from multiple parts.
- Audit dynamic selector builders to insert '*' at most once.
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
- Can not capture the selector before diving into the frame. O
- Can not *-capture inside a frame-piercing selector, while pa
- Unsupported token "${unsupportedToken.toSource()}" while par
- Error while parsing css selector "${selector}". Did you mean
- Malformed selector: ${part.name}=${part.body}
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/e8f3f992bf7ba1d7.
Report an issue: GitHub.