microsoft/playwright · error · InvalidSelectorError

Selector cannot start with entering frame, select the iframe

Error message

Selector cannot start with entering frame, select the iframe first

What it means

splitSelectorByFrame rejects an 'enter-frame' control token when it begins a chunk — i.e. there is no preceding selector that picks the iframe element. Two consecutive enter-frame tokens also trip this (the chunk's last part is itself enter-frame). You must select the iframe element before descending into it.

Source

Thrown at packages/isomorphic/selectorParser.ts:119

  };
  let pierce = !!pierceByDefault;
  let pierceToken = false;
  let chunkStartIndex = 0;
  for (let i = 0; i < selector.parts.length; ++i) {
    const part = selector.parts[i];
    if (part.name === 'internal:control' && (part.body === 'pierce-frames' || part.body === 'no-pierce-frames')) {
      // Piercing applies to the whole selector, so the token only makes sense as the very first one.
      if (i !== 0)
        throw new InvalidSelectorError(`"${part.body}" is only allowed as the first selector token, while parsing selector ${selectorText}`);
      pierce = part.body === 'pierce-frames';
      pierceToken = true;
      chunkStartIndex = i + 1;
      continue;
    }
    if (part.name === 'internal:control' && part.body === 'enter-frame') {
      const lastPart = chunk.parts[chunk.parts.length - 1];
      if (!lastPart || (lastPart.name === 'internal:control' && lastPart.body === 'enter-frame'))
        throw new InvalidSelectorError('Selector cannot start with entering frame, select the iframe first');
      if (pierce) {
        chunk.parts.push(part);
        continue;
      }
      chunks.push(chunk);
      chunk = { parts: [] };
      chunkStartIndex = i + 1;
      continue;
    }
    if (selector.capture === i)
      chunk.capture = i - chunkStartIndex;
    chunk.parts.push(part);
  }
  if (!chunk.parts.length) {
    if (pierceToken)
      throw new InvalidSelectorError(`Selector cannot be empty when piercing frames, while parsing selector ${selectorText}`);
    throw new InvalidSelectorError(`Selector cannot end with entering frame, while parsing selector ${selectorText}`);
  }

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Precede enter-frame with a selector matching the iframe element: 'iframe >> internal:control=enter-frame >> button'.
  2. Use the frameLocator API: page.frameLocator('iframe').locator('button') — Playwright handles enter-frame for you.
  3. Never put two enter-frame tokens back-to-back; always select an iframe between them.

Example fix

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

// after
await page.frameLocator('iframe').locator('button').click();
Defensive patterns

Strategy: try-catch

Validate before calling

function selectorEntersFrameSafely(sel: string): boolean {
  const parts = sel.split('>>').map(p => p.trim());
  if (/internal:control=enter-frame/.test(parts[0])) return false;
  for (let i = 1; i < parts.length; i++)
    if (/internal:control=enter-frame/.test(parts[i]) && /internal:control=enter-frame/.test(parts[i-1])) return false;
  return true;
}

Try / catch

try { await page.locator(sel).click(); }
catch (e) { if (isInvalidSelectorError(e) && /select the iframe first/.test(e.message)) { sel = 'iframe >> ' + sel; } else throw e; }

Prevention

When it happens

Trigger: Writing a selector that starts with frame descent, e.g. internal:control=enter-frame >> button (no iframe chosen first), or two enter-frame tokens back to back without an iframe selector between them.

Common situations: Authoring iframe selectors by hand and forgetting the iframe locator prefix; chaining frame() calls incorrectly; converting a frame locator chain to a raw selector string and dropping the iframe token.

Related errors


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/d6548a7d9e101052. Report an issue: GitHub.