{"record":{"id":"fa60657b13e0e84f","repo":"microsoft/playwright","slug":"selector-cannot-be-empty-when-piercing-frames-whi","errorCode":null,"errorMessage":"Selector cannot be empty when piercing frames, while parsing selector ${selectorText}","messagePattern":"Selector cannot be empty when piercing frames, while parsing selector (.+?)","errorType":"exception","errorClass":"InvalidSelectorError","httpStatus":null,"severity":"error","filePath":"packages/isomorphic/selectorParser.ts","lineNumber":135,"sourceCode":"      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    }\n    if (selector.capture === i)\n      chunk.capture = i - chunkStartIndex;\n    chunk.parts.push(part);\n  }\n  if (!chunk.parts.length) {\n    if (pierceToken)\n      throw new InvalidSelectorError(`Selector cannot be empty when piercing frames, while parsing selector ${selectorText}`);\n    throw new InvalidSelectorError(`Selector cannot end with entering frame, while parsing selector ${selectorText}`);\n  }\n  const lastPart = chunk.parts[chunk.parts.length - 1];\n  if (lastPart.name === 'internal:control' && lastPart.body === 'enter-frame')\n    throw new InvalidSelectorError(`Selector cannot end with entering frame, while parsing selector ${selectorText}`);\n  chunks.push(chunk);\n  if (typeof selector.capture === 'number' && typeof chunks[chunks.length - 1].capture !== 'number')\n    throw new InvalidSelectorError(`Can not capture the selector before diving into the frame. Only use * after the last frame has been selected`);\n  if (typeof selector.capture === 'number' && pierce)\n    throw new InvalidSelectorError(`Can not *-capture inside a frame-piercing selector, while parsing selector ${selectorText}`);\n  return { pierce, chunks };\n}\n\nfunction selectorPartsEqual(list1: ParsedSelectorPart[], list2: ParsedSelectorPart[]) {\n  return stringifySelector({ parts: list1 }) === stringifySelector({ parts: list2 });\n}\n\nexport function stringifySelector(selector: string | ParsedSelector, forceEngineName?: boolean): string {","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/microsoft/playwright/blob/c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6/packages/isomorphic/selectorParser.ts#L117-L153","documentation":"splitSelectorByFrame throws this when, after processing all parts, the trailing chunk is empty AND a pierce-frames/no-pierce-frames token was used. The pierce mode promises to apply a selector across frames, so ending with no actual selector content is meaningless — you cannot pierce into nothing.","triggerScenarios":"A selector that sets pierce mode but then has no selector body, e.g. 'internal:control=pierce-frames >>' (trailing >>), or a pierce token immediately followed by enter-frame with nothing usable.","commonSituations":"Dynamically building a pierce selector and appending '>>' then nothing; truncating a selector string by mistake; copy-paste leaving a dangling combinator after the pierce token.","solutions":["Ensure a real selector follows the pierce token: 'internal:control=pierce-frames >> button'.","Remove trailing '>>' or empty fragments when constructing selectors dynamically.","If you do not need piercing, drop the pierce-frames token."],"exampleFix":"// before\nconst sel = 'internal:control=pierce-frames >> ' + (sub || ''); // sub empty\nawait page.locator(sel).click();\n\n// after\nif (!sub) throw new Error('sub selector required when piercing');\nawait page.locator('internal:control=pierce-frames >> ' + sub).click();","handlingStrategy":"validation","validationCode":"function pierceHasBody(sel: string): boolean {\n  if (!/internal:control=(pierce|no-pierce)-frames/.test(sel)) return true;\n  const parts = sel.split('>>').map(p => p.trim()).filter(Boolean);\n  // after the pierce token there must be at least one non-empty selector part\n  return parts.length >= 2 && parts.every(p => !/^(>>|\\s*)$/.test(p));\n}","typeGuard":null,"tryCatchPattern":"try { await page.locator(sel).click(); }\ncatch (e) { if (isInvalidSelectorError(e) && /cannot be empty when piercing/.test(e.message)) { sel = sel.replace(/>>\\s*$/,''); } else throw e; }","preventionTips":["Ensure a real selector follows a pierce-frames token.","Strip trailing '>>' and empty fragments from dynamic selectors.","Validate pierce selectors have a body before use."],"tags":["selector","frames","pierce","empty","invalid-selector"],"backgroundTag":null,"analyzedSha":"c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6","analyzedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}