{"record":{"id":"0a108c1b5c6858e0","repo":"microsoft/playwright","slug":"parts-0-name-selector-cannot-be-first","errorCode":null,"errorMessage":"\"${parts[0].name}\" selector cannot be first","messagePattern":"\"(.+?)\" selector cannot be first","errorType":"exception","errorClass":"InvalidSelectorError","httpStatus":null,"severity":"error","filePath":"packages/isomorphic/selectorParser.ts","lineNumber":87,"sourceCode":"            throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);\n          distance = unescaped[1];\n        }\n      } catch (e) {\n        throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);\n      }\n      const nested = { name: part.name, source: part.body, body: { parsed: parseSelector(innerSelector), distance } };\n      const lastFrame = [...nested.body.parsed.parts].reverse().find(part => part.name === 'internal:control' && part.body === 'enter-frame');\n      const lastFrameIndex = lastFrame ? nested.body.parsed.parts.indexOf(lastFrame) : -1;\n      // Allow nested selectors to start with the same frame selector.\n      if (lastFrameIndex !== -1 && selectorPartsEqual(nested.body.parsed.parts.slice(0, lastFrameIndex + 1), parts.slice(0, lastFrameIndex + 1)))\n        nested.body.parsed.parts.splice(0, lastFrameIndex + 1);\n      parts.push(nested);\n      continue;\n    }\n    parts.push({ ...part, source: part.body });\n  }\n  if (kNestedSelectorNames.has(parts[0].name))\n    throw new InvalidSelectorError(`\"${parts[0].name}\" selector cannot be first`);\n  return {\n    capture: parsedStrings.capture,\n    parts\n  };\n}\n\n// Splits a selector into per-frame chunks separated by \"enter-frame\" boundaries in non-piercing mode.\n// In piercing mode, \"enter-frame\" tokens are preserved, so `chunks` holds a single chunk.\nexport function splitSelectorByFrame(selectorText: string, pierceByDefault?: boolean): { pierce: boolean, chunks: ParsedSelector[] } {\n  const selector = parseSelector(selectorText);\n  const chunks: ParsedSelector[] = [];\n  let chunk: ParsedSelector = {\n    parts: [],\n  };\n  let pierce = !!pierceByDefault;\n  let pierceToken = false;\n  let chunkStartIndex = 0;\n  for (let i = 0; i < selector.parts.length; ++i) {","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/microsoft/playwright/blob/c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6/packages/isomorphic/selectorParser.ts#L69-L105","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait page.locator('internal:has=[\"span\"]').click();\n\n// after\nawait page.locator('div').filter({ has: page.locator('span') }).click();","handlingStrategy":"try-catch","validationCode":"const NESTED = new Set(['internal:has','internal:has-not','internal:and','internal:or','internal:chain','left-of','right-of','above','below','near']);\nfunction startsWithConcreteSelector(sel: string): boolean {\n  const first = sel.split('>>')[0].trim();\n  const name = first.split('=')[0].replace(/^\\*/, '');\n  return !NESTED.has(name);\n}","typeGuard":null,"tryCatchPattern":"try { await page.locator(sel).click(); }\ncatch (e) { if (isInvalidSelectorError(e) && /cannot be first/.test(e.message)) { sel = 'div >> ' + sel; } else throw e; }","preventionTips":["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."],"tags":["selector","nested-selector","ordering","invalid-selector"],"backgroundTag":null,"analyzedSha":"c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6","analyzedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}