{"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":96,"sourceCode":"      // The \"any-frame\" token applies to the whole selector, so nested selectors must not repeat it.\n      const outerParts = parts[0]?.name === 'internal:control' && parts[0].body === 'any-frame' ? parts.slice(1) : parts;\n      // Allow nested selectors to start with the same frame selector.\n      if (lastFrameIndex !== -1 && selectorPartsEqual(nested.body.parsed.parts.slice(0, lastFrameIndex + 1), outerParts.slice(0, lastFrameIndex + 1))) {\n        nested.body.parsed.parts.splice(0, lastFrameIndex + 1);\n        if (nested.body.parsed.capture !== undefined) {\n          if (nested.body.parsed.capture <= lastFrameIndex)\n            throw new InvalidSelectorError(`Can not capture the selector before diving into the frame. Only use * after the last frame has been selected`);\n          // The capture refers to a part index, so it shifts along with the removed prefix.\n          nested.body.parsed.capture -= lastFrameIndex + 1;\n        }\n      }\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// Matches in any frame of the subtree, instead of the frame itself. Only allowed as the first token.\nexport const kAnyFrameSelector = 'internal:control=any-frame';\n\n// Splits a selector into per-frame chunks separated by \"enter-frame\" boundaries.\n// The optional leading \"any-frame\" token is consumed and reported separately.\nexport function splitSelectorByFrame(selectorText: string): { anyFrame: boolean, chunks: ParsedSelector[] } {\n  const selector = parseSelector(selectorText);\n  const chunks: ParsedSelector[] = [];\n  let chunk: ParsedSelector = {\n    parts: [],\n  };\n  let anyFrame = false;","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/microsoft/playwright/blob/de214f440b7e34937fe4886f046b78b757136087/packages/isomorphic/selectorParser.ts#L78-L114","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":"de214f440b7e34937fe4886f046b78b757136087","analyzedAt":"2026-08-12T07:26:36.950Z","contentChangedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}