{"record":{"id":"361565cfc2784c7b","repo":"microsoft/playwright","slug":"part-body-is-only-allowed-as-the-first-select","errorCode":null,"errorMessage":"\"${part.body}\" is only allowed as the first selector token, while parsing selector ${selectorText}","messagePattern":"\"(.+?)\" is only allowed as the first selector token, while parsing selector (.+?)","errorType":"exception","errorClass":"InvalidSelectorError","httpStatus":null,"severity":"error","filePath":"packages/isomorphic/selectorParser.ts","lineNumber":110,"sourceCode":"}\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) {\n    const part = selector.parts[i];\n    if (part.name === 'internal:control' && (part.body === 'pierce-frames' || part.body === 'no-pierce-frames')) {\n      // Piercing applies to the whole selector, so the token only makes sense as the very first one.\n      if (i !== 0)\n        throw new InvalidSelectorError(`\"${part.body}\" is only allowed as the first selector token, while parsing selector ${selectorText}`);\n      pierce = part.body === 'pierce-frames';\n      pierceToken = true;\n      chunkStartIndex = i + 1;\n      continue;\n    }\n    if (part.name === 'internal:control' && part.body === 'enter-frame') {\n      const lastPart = chunk.parts[chunk.parts.length - 1];\n      if (!lastPart || (lastPart.name === 'internal:control' && lastPart.body === 'enter-frame'))\n        throw new InvalidSelectorError('Selector cannot start with entering frame, select the iframe first');\n      if (pierce) {\n        chunk.parts.push(part);\n        continue;\n      }\n      chunks.push(chunk);\n      chunk = { parts: [] };\n      chunkStartIndex = i + 1;\n      continue;\n    }","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/microsoft/playwright/blob/c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6/packages/isomorphic/selectorParser.ts#L92-L128","documentation":"splitSelectorByFrame rejects a selector where the pierce-frames / no-pierce-frames control token appears anywhere except position 0. Piercing applies to the whole selector, so the control token is only meaningful as the very first token.","triggerScenarios":"Authoring a selector like 'div >> internal:control=pierce-frames' or placing pierce mode after other tokens. The pierce/no-pierce token must be the leading token.","commonSituations":"Dynamically concatenating a pierce prefix in the wrong order; refactoring frame-piercing selectors and moving the token; mixing manual pierce tokens with locator chaining.","solutions":["Move the pierce/no-pierce control token to the absolute start of the selector string.","Prefer the frame-piercing API option rather than authoring the control token by hand.","If you do not need cross-frame piercing, remove the token entirely."],"exampleFix":"// before\nawait page.locator('div >> internal:control=pierce-frames >> span').click();\n\n// after\nawait page.locator('internal:control=pierce-frames >> div >> span').click();","handlingStrategy":"validation","validationCode":"function pierceTokenIsFirst(sel: string): boolean {\n  const parts = sel.split('>>').map(p => p.trim());\n  const nonFirstPierce = parts.slice(1).some(p => /internal:control=(pierce|no-pierce)-frames/.test(p));\n  return !nonFirstPierce;\n}","typeGuard":null,"tryCatchPattern":"try { await page.locator(sel).click(); }\ncatch (e) { if (isInvalidSelectorError(e) && /only allowed as the first/.test(e.message)) { sel = movePierceToFront(sel); } else throw e; }","preventionTips":["Put pierce-frames/no-pierce-frames only at the very start of a selector.","Prefer the frameLocator/pierce API over hand-authoring control tokens.","Avoid dynamically inserting control tokens mid-selector."],"tags":["selector","frames","pierce","ordering","invalid-selector"],"backgroundTag":null,"analyzedSha":"c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6","analyzedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}