{"record":{"id":"feaf81970f41eb91","repo":"microsoft/playwright","slug":"can-not-capture-the-selector-before-diving-into-th","errorCode":null,"errorMessage":"Can not capture the selector before diving into the frame. Only use * after the last frame has been selected","messagePattern":"Can not capture the selector before diving into the frame\\. Only use \\* after the last frame has been selected","errorType":"exception","errorClass":"InvalidSelectorError","httpStatus":null,"severity":"error","filePath":"packages/isomorphic/selectorParser.ts","lineNumber":85,"sourceCode":"        if (unescaped.length === 2) {\n          if (typeof unescaped[1] !== 'number' || !kNestedSelectorNamesWithDistance.has(part.name))\n            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      // 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.","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/microsoft/playwright/blob/de214f440b7e34937fe4886f046b78b757136087/packages/isomorphic/selectorParser.ts#L67-L103","documentation":"splitSelectorByFrame rejects a selector where the '*' capture modifier lands in any chunk other than the last (deepest) frame chunk. Capture must target an element in the final frame; capturing before diving into a frame is meaningless because the captured node would be abandoned when descending.","triggerScenarios":"Authoring a selector like '*iframe >> internal:control=enter-frame >> button' (capture on the iframe, before entering the frame), or any use of '*' that places the capture index in a non-final frame chunk.","commonSituations":"Hand-authoring capture modifiers across frame boundaries; misunderstanding that '*' must come after the last enter-frame; building selector strings programmatically and inserting '*' too early.","solutions":["Place the '*' capture on the final (innermost) selector only: 'iframe >> internal:control=enter-frame >> *button'.","Avoid manual '*' — use the locator API and resolve/extract elements through returned handles.","Re-read the message: it tells you the capture must be after the last frame has been selected."],"exampleFix":"// before\nawait page.locator('*iframe >> internal:control=enter-frame >> button').click();\n\n// after\nawait page.locator('iframe >> internal:control=enter-frame >> *button').click();","handlingStrategy":"validation","validationCode":"function captureIsInLastFrameChunk(sel: string): boolean {\n  const parts = sel.split('>>').map(p => p.trim());\n  const lastEnterFrame = Math.max(-1, ...parts.map((p,i)=>/enter-frame/.test(p)?i:-1));\n  const captureIdx = parts.findIndex(p => /^\\*/.test(p));\n  return captureIdx === -1 || captureIdx > lastEnterFrame;\n}","typeGuard":null,"tryCatchPattern":"try { await page.locator(sel).click(); }\ncatch (e) { if (isInvalidSelectorError(e) && /Can not capture the selector before diving/.test(e.message)) { /* move '*' to the last fragment */ } else throw e; }","preventionTips":["Place '*' capture only on the innermost (post-frame) selector.","Avoid manual '*' — resolve elements via the locator API.","When building strings, append '*' last."],"tags":["selector","frames","capture","invalid-selector"],"backgroundTag":null,"analyzedSha":"de214f440b7e34937fe4886f046b78b757136087","analyzedAt":"2026-09-01T05:48:36.844Z","contentChangedAt":"2026-09-01T05:48:36.844Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}