microsoft/playwright · error · InvalidSelectorError
"${parts[0].name}" selector cannot be first
Error message
"${parts[0].name}" selector cannot be first What it means
After parsing, if the very first part of the selector is a nested engine name (internal:has, internal:has-not, internal:and, internal:or, internal:chain, left-of, right-of, above, below, near) Playwright rejects it. These engines are relative — they need a base selector to qualify, they cannot stand alone as the first token.
Source
Thrown at packages/isomorphic/selectorParser.ts:87
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;
// Allow nested selectors to start with the same frame selector.
if (lastFrameIndex !== -1 && selectorPartsEqual(nested.body.parsed.parts.slice(0, lastFrameIndex + 1), parts.slice(0, lastFrameIndex + 1)))
nested.body.parsed.parts.splice(0, 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
};
}
// Splits a selector into per-frame chunks separated by "enter-frame" boundaries in non-piercing mode.
// In piercing mode, "enter-frame" tokens are preserved, so `chunks` holds a single chunk.
export function splitSelectorByFrame(selectorText: string, pierceByDefault?: boolean): { pierce: boolean, chunks: ParsedSelector[] } {
const selector = parseSelector(selectorText);
const chunks: ParsedSelector[] = [];
let chunk: ParsedSelector = {
parts: [],
};
let pierce = !!pierceByDefault;
let pierceToken = false;
let chunkStartIndex = 0;
for (let i = 0; i < selector.parts.length; ++i) {View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Put a concrete selector (css/text/role) before the nested engine: 'div >> internal:has=...'.
- Use the locator API which enforces a base: page.locator('div').filter({ has: ... }).
- Reorder selector construction so relative engines always follow a base element.
Example fix
// before
await page.locator('internal:has=["span"]').click();
// after
await page.locator('div').filter({ has: page.locator('span') }).click(); Defensive patterns
Strategy: try-catch
Validate before calling
const NESTED = new Set(['internal:has','internal:has-not','internal:and','internal:or','internal:chain','left-of','right-of','above','below','near']);
function startsWithConcreteSelector(sel: string): boolean {
const first = sel.split('>>')[0].trim();
const name = first.split('=')[0].replace(/^\*/, '');
return !NESTED.has(name);
} Try / catch
try { await page.locator(sel).click(); }
catch (e) { if (isInvalidSelectorError(e) && /cannot be first/.test(e.message)) { sel = 'div >> ' + sel; } else throw e; } Prevention
- Always precede relative engines (has/and/or/near/...) with a concrete base selector.
- Build selectors via the locator API which enforces a base.
- When constructing strings dynamically, prepend a base fragment.
When it happens
Trigger: Writing a selector that starts with a combinator engine, e.g. internal:has=..., or near=[...], with no preceding concrete selector. Equivalent userland: calling .has()/.and() on nothing, or building a selector string that begins with a relative engine.
Common situations: Building selectors dynamically and prepending the wrong fragment; converting a chained locator to a string and dropping the base; refactors that left a relative engine at the head.
Related errors
- Malformed selector: ${part.name}=${part.body}
- "${part.body}" is only allowed as the first selector token,
- Selector cannot start with entering frame, select the iframe
- Unsupported token "${unsupportedToken.toSource()}" while par
- Error while parsing css selector "${selector}". Did you mean
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/0a108c1b5c6858e0.
Report an issue: GitHub.