microsoft/playwright · error · InvalidSelectorError

Can not capture the selector before diving into the frame…

Error message

Can not capture the selector before diving into the frame. Only use * after the last frame has been selected

What it means

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.

Solutions

  1. Place the '*' capture on the final (innermost) selector only: 'iframe >> internal:control=enter-frame >> *button'.
  2. Avoid manual '*' — use the locator API and resolve/extract elements through returned handles.
  3. Re-read the message: it tells you the capture must be after the last frame has been selected.

Example fix

// before
await page.locator('*iframe >> internal:control=enter-frame >> button').click();

// after
await page.locator('iframe >> internal:control=enter-frame >> *button').click();
Defensive patterns

Strategy: validation

Validate before calling

function captureIsInLastFrameChunk(sel: string): boolean {
  const parts = sel.split('>>').map(p => p.trim());
  const lastEnterFrame = Math.max(-1, ...parts.map((p,i)=>/enter-frame/.test(p)?i:-1));
  const captureIdx = parts.findIndex(p => /^\*/.test(p));
  return captureIdx === -1 || captureIdx > lastEnterFrame;
}

Try / catch

try { await page.locator(sel).click(); }
catch (e) { if (isInvalidSelectorError(e) && /Can not capture the selector before diving/.test(e.message)) { /* move '*' to the last fragment */ } else throw e; }

Prevention

When it happens

Trigger: 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.

Common situations: Hand-authoring capture modifiers across frame boundaries; misunderstanding that '*' must come after the last enter-frame; building selector strings programmatically and inserting '*' too early.

Related errors


AI-assisted analysis of microsoft/playwright@de214f440b (2026-09-01). Data as JSON: /api/errors/feaf81970f41eb91. Report an issue: GitHub.

Appendix: source

Thrown at packages/isomorphic/selectorParser.ts:85

        if (unescaped.length === 2) {
          if (typeof unescaped[1] !== 'number' || !kNestedSelectorNamesWithDistance.has(part.name))
            throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
          distance = unescaped[1];
        }
      } catch (e) {
        throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
      }
      const nested = { name: part.name, source: part.body, body: { parsed: parseSelector(innerSelector), distance } };
      const lastFrame = [...nested.body.parsed.parts].reverse().find(part => part.name === 'internal:control' && part.body === 'enter-frame');
      const lastFrameIndex = lastFrame ? nested.body.parsed.parts.indexOf(lastFrame) : -1;
      // The "any-frame" token applies to the whole selector, so nested selectors must not repeat it.
      const outerParts = parts[0]?.name === 'internal:control' && parts[0].body === 'any-frame' ? parts.slice(1) : parts;
      // Allow nested selectors to start with the same frame selector.
      if (lastFrameIndex !== -1 && selectorPartsEqual(nested.body.parsed.parts.slice(0, lastFrameIndex + 1), outerParts.slice(0, lastFrameIndex + 1))) {
        nested.body.parsed.parts.splice(0, lastFrameIndex + 1);
        if (nested.body.parsed.capture !== undefined) {
          if (nested.body.parsed.capture <= lastFrameIndex)
            throw new InvalidSelectorError(`Can not capture the selector before diving into the frame. Only use * after the last frame has been selected`);
          // The capture refers to a part index, so it shifts along with the removed prefix.
          nested.body.parsed.capture -= lastFrameIndex + 1;
        }
      }
      parts.push(nested);
      continue;
    }
    parts.push({ ...part, source: part.body });
  }
  if (kNestedSelectorNames.has(parts[0].name))
    throw new InvalidSelectorError(`"${parts[0].name}" selector cannot be first`);
  return {
    capture: parsedStrings.capture,
    parts
  };
}

// Matches in any frame of the subtree, instead of the frame itself. Only allowed as the first token.

View on GitHub (pinned to de214f440b)