microsoft/playwright · error · InvalidSelectorError
Can not *-capture inside a frame-piercing selector, while pa
Error message
Can not *-capture inside a frame-piercing selector, while parsing selector ${selectorText} What it means
splitSelectorByFrame rejects combining the '*' capture modifier with frame-piercing mode (pierce-frames). A pierce selector matches elements across many frames simultaneously, so singling out one captured index is ambiguous/unsupported.
Source
Thrown at packages/isomorphic/selectorParser.ts:145
continue;
}
if (selector.capture === i)
chunk.capture = i - chunkStartIndex;
chunk.parts.push(part);
}
if (!chunk.parts.length) {
if (pierceToken)
throw new InvalidSelectorError(`Selector cannot be empty when piercing frames, while parsing selector ${selectorText}`);
throw new InvalidSelectorError(`Selector cannot end with entering frame, while parsing selector ${selectorText}`);
}
const lastPart = chunk.parts[chunk.parts.length - 1];
if (lastPart.name === 'internal:control' && lastPart.body === 'enter-frame')
throw new InvalidSelectorError(`Selector cannot end with entering frame, while parsing selector ${selectorText}`);
chunks.push(chunk);
if (typeof selector.capture === 'number' && typeof chunks[chunks.length - 1].capture !== 'number')
throw new InvalidSelectorError(`Can not capture the selector before diving into the frame. Only use * after the last frame has been selected`);
if (typeof selector.capture === 'number' && pierce)
throw new InvalidSelectorError(`Can not *-capture inside a frame-piercing selector, while parsing selector ${selectorText}`);
return { pierce, chunks };
}
function selectorPartsEqual(list1: ParsedSelectorPart[], list2: ParsedSelectorPart[]) {
return stringifySelector({ parts: list1 }) === stringifySelector({ parts: list2 });
}
export function stringifySelector(selector: string | ParsedSelector, forceEngineName?: boolean): string {
if (typeof selector === 'string')
return selector;
return selector.parts.map((p, i) => {
let includeEngine = true;
if (!forceEngineName && i !== selector.capture) {
if (p.name === 'css')
includeEngine = false;
else if (p.name === 'xpath' && (p.source.startsWith('//') || p.source.startsWith('..')))
includeEngine = false;
}View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Drop the '*' capture modifier when using pierce-frames.
- If you need a specific element, switch to non-piercing frame-by-frame navigation with frameLocator and capture there.
- Re-evaluate whether piercing is needed at all for the use case.
Example fix
// before
await page.locator('internal:control=pierce-frames >> *div').click();
// after
await page.locator('internal:control=pierce-frames >> div').click(); Defensive patterns
Strategy: validation
Validate before calling
function notCapturingWhilePiercing(sel: string): boolean {
const pierces = /internal:control=pierce-frames/.test(sel);
const captures = /(^|>>)\s*\*/.test(sel);
return !(pierce && captures);
} Try / catch
try { await page.locator(sel).click(); }
catch (e) { if (isInvalidSelectorError(e) && /frame-piercing/.test(e.message)) { sel = sel.replace(/(^|>>)\s*\*/g, '$1'); } else throw e; } Prevention
- Do not combine '*' capture with pierce-frames.
- If a specific element is needed, navigate frame-by-frame with frameLocator.
- Drop '*' when switching a selector to pierce mode.
When it happens
Trigger: Authoring a selector that uses both 'internal:control=pierce-frames' and a '*' capture modifier anywhere, e.g. 'internal:control=pierce-frames >> *div'.
Common situations: Trying to capture a specific matched element while also piercing frames; converting a normal captured selector to pierce mode without dropping the '*'; dynamic selector builders that combine both features.
Related errors
- "${part.body}" is only allowed as the first selector token,
- Selector cannot be empty when piercing frames, while parsing
- Can not capture the selector before diving into the frame. O
- Selector cannot start with entering frame, select the iframe
- Selector cannot end with entering frame, while parsing selec
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/305325203032d5d6.
Report an issue: GitHub.