microsoft/playwright · error · InvalidSelectorError
Selector cannot be empty when piercing frames, while parsing
Error message
Selector cannot be empty when piercing frames, while parsing selector ${selectorText} What it means
splitSelectorByFrame throws this when, after processing all parts, the trailing chunk is empty AND a pierce-frames/no-pierce-frames token was used. The pierce mode promises to apply a selector across frames, so ending with no actual selector content is meaningless — you cannot pierce into nothing.
Source
Thrown at packages/isomorphic/selectorParser.ts:135
const lastPart = chunk.parts[chunk.parts.length - 1];
if (!lastPart || (lastPart.name === 'internal:control' && lastPart.body === 'enter-frame'))
throw new InvalidSelectorError('Selector cannot start with entering frame, select the iframe first');
if (pierce) {
chunk.parts.push(part);
continue;
}
chunks.push(chunk);
chunk = { parts: [] };
chunkStartIndex = i + 1;
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 {View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Ensure a real selector follows the pierce token: 'internal:control=pierce-frames >> button'.
- Remove trailing '>>' or empty fragments when constructing selectors dynamically.
- If you do not need piercing, drop the pierce-frames token.
Example fix
// before
const sel = 'internal:control=pierce-frames >> ' + (sub || ''); // sub empty
await page.locator(sel).click();
// after
if (!sub) throw new Error('sub selector required when piercing');
await page.locator('internal:control=pierce-frames >> ' + sub).click(); Defensive patterns
Strategy: validation
Validate before calling
function pierceHasBody(sel: string): boolean {
if (!/internal:control=(pierce|no-pierce)-frames/.test(sel)) return true;
const parts = sel.split('>>').map(p => p.trim()).filter(Boolean);
// after the pierce token there must be at least one non-empty selector part
return parts.length >= 2 && parts.every(p => !/^(>>|\s*)$/.test(p));
} Try / catch
try { await page.locator(sel).click(); }
catch (e) { if (isInvalidSelectorError(e) && /cannot be empty when piercing/.test(e.message)) { sel = sel.replace(/>>\s*$/,''); } else throw e; } Prevention
- Ensure a real selector follows a pierce-frames token.
- Strip trailing '>>' and empty fragments from dynamic selectors.
- Validate pierce selectors have a body before use.
When it happens
Trigger: A selector that sets pierce mode but then has no selector body, e.g. 'internal:control=pierce-frames >>' (trailing >>), or a pierce token immediately followed by enter-frame with nothing usable.
Common situations: Dynamically building a pierce selector and appending '>>' then nothing; truncating a selector string by mistake; copy-paste leaving a dangling combinator after the pierce token.
Related errors
- "${part.body}" is only allowed as the first selector token,
- Selector cannot end with entering frame, while parsing selec
- Can not *-capture inside a frame-piercing selector, while pa
- Selector cannot start with entering frame, select the iframe
- Can not capture the selector before diving into the frame. O
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/fa60657b13e0e84f.
Report an issue: GitHub.