microsoft/playwright · error · InvalidSelectorError
Can not capture the selector before diving into the frame…
Error message
Can not capture the selector before diving into the frame. Only use * after the last frame has been selected
What it means
splitSelectorByFrame rejects a selector where the '*' capture modifier lands in any chunk other than the last (deepest) frame chunk. Capture must target an element in the final frame; capturing before diving into a frame is meaningless because the captured node would be abandoned when descending.
Solutions
- Place the '*' capture on the final (innermost) selector only: 'iframe >> internal:control=enter-frame >> *button'.
- Avoid manual '*' — use the locator API and resolve/extract elements through returned handles.
- Re-read the message: it tells you the capture must be after the last frame has been selected.
Example fix
// before
await page.locator('*iframe >> internal:control=enter-frame >> button').click();
// after
await page.locator('iframe >> internal:control=enter-frame >> *button').click(); Defensive patterns
Strategy: validation
Validate before calling
function captureIsInLastFrameChunk(sel: string): boolean {
const parts = sel.split('>>').map(p => p.trim());
const lastEnterFrame = Math.max(-1, ...parts.map((p,i)=>/enter-frame/.test(p)?i:-1));
const captureIdx = parts.findIndex(p => /^\*/.test(p));
return captureIdx === -1 || captureIdx > lastEnterFrame;
} Try / catch
try { await page.locator(sel).click(); }
catch (e) { if (isInvalidSelectorError(e) && /Can not capture the selector before diving/.test(e.message)) { /* move '*' to the last fragment */ } else throw e; } Prevention
- Place '*' capture only on the innermost (post-frame) selector.
- Avoid manual '*' — resolve elements via the locator API.
- When building strings, append '*' last.
When it happens
Trigger: Authoring a selector like '*iframe >> internal:control=enter-frame >> button' (capture on the iframe, before entering the frame), or any use of '*' that places the capture index in a non-final frame chunk.
Common situations: Hand-authoring capture modifiers across frame boundaries; misunderstanding that '*' must come after the last enter-frame; building selector strings programmatically and inserting '*' too early.
Related errors
- Can not *-capture inside a frame-piercing selector, while…
- Only one of the selectors can capture using * modifier
- " " is only allowed as the first selector token, while…
- Selector cannot be empty when piercing frames, while…
- Selector cannot end with entering frame, while parsing…
AI-assisted analysis of microsoft/playwright@de214f440b (2026-09-01).
Data as JSON: /api/errors/feaf81970f41eb91.
Report an issue: GitHub.
Appendix: source
Thrown at packages/isomorphic/selectorParser.ts:85
if (unescaped.length === 2) {
if (typeof unescaped[1] !== 'number' || !kNestedSelectorNamesWithDistance.has(part.name))
throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
distance = unescaped[1];
}
} catch (e) {
throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
}
const nested = { name: part.name, source: part.body, body: { parsed: parseSelector(innerSelector), distance } };
const lastFrame = [...nested.body.parsed.parts].reverse().find(part => part.name === 'internal:control' && part.body === 'enter-frame');
const lastFrameIndex = lastFrame ? nested.body.parsed.parts.indexOf(lastFrame) : -1;
// The "any-frame" token applies to the whole selector, so nested selectors must not repeat it.
const outerParts = parts[0]?.name === 'internal:control' && parts[0].body === 'any-frame' ? parts.slice(1) : parts;
// Allow nested selectors to start with the same frame selector.
if (lastFrameIndex !== -1 && selectorPartsEqual(nested.body.parsed.parts.slice(0, lastFrameIndex + 1), outerParts.slice(0, lastFrameIndex + 1))) {
nested.body.parsed.parts.splice(0, lastFrameIndex + 1);
if (nested.body.parsed.capture !== undefined) {
if (nested.body.parsed.capture <= lastFrameIndex)
throw new InvalidSelectorError(`Can not capture the selector before diving into the frame. Only use * after the last frame has been selected`);
// The capture refers to a part index, so it shifts along with the removed prefix.
nested.body.parsed.capture -= lastFrameIndex + 1;
}
}
parts.push(nested);
continue;
}
parts.push({ ...part, source: part.body });
}
if (kNestedSelectorNames.has(parts[0].name))
throw new InvalidSelectorError(`"${parts[0].name}" selector cannot be first`);
return {
capture: parsedStrings.capture,
parts
};
}
// Matches in any frame of the subtree, instead of the frame itself. Only allowed as the first token.View on GitHub (pinned to de214f440b)