microsoft/playwright · error · InvalidSelectorError

Invalid frame in aria-ref selector "${selector}"

Error message

Invalid frame in aria-ref selector "${selector}"

What it means

During internal aria-ref resolution (used by recorder/locator snapshots), Playwright extracts a frame sequence id from an aria-ref like 'f<seq>e<n>'. If no live frame in the page matches that sequence id, it throws InvalidSelectorError indicating the ref points at a frame that no longer exists.

Source

Thrown at packages/playwright-core/src/server/frameSelectors.ts:119

      return [];

    // Note: adopting elements one by one may be slow. If we encounter the issue here,
    // we might introduce 'useMainContext' option or similar to speed things up.
    const targetContext = await elementHandles[0]._frame.mainContext();
    return Promise.all(elementHandles.map(handle => adoptIfNeeded(handle, targetContext)));
  }

  private _jumpToAriaRefFrameIfNeeded(selector: string, info: SelectorInfo, frame: Frame): Frame {
    if (info.parsed.parts[0].name !== 'aria-ref')
      return frame;
    const body = info.parsed.parts[0].body as string;
    const match = body.match(/^f(\d+)e\d+$/);
    if (!match)
      return frame;
    const frameSeq = +match[1];
    const jumptToFrame = this.frame._page.frameManager.frames().find(frame => frame.seq === frameSeq);
    if (!jumptToFrame)
      throw new InvalidSelectorError(`Invalid frame in aria-ref selector "${selector}"`);
    return jumptToFrame;
  }

  private async _resolveFramesForSelector(selector: string, options: types.StrictOptions & { noDefaultPierce?: boolean } = {}, scope?: ElementHandle): Promise<SelectorInFrame[]> {
    const pierceByDefault = !!this.frame._page.browserContext._options.pierceFrames && !options.noDefaultPierce;
    const { pierce, chunks } = splitSelectorByFrame(selector, pierceByDefault);
    for (const chunk of chunks) {
      visitAllSelectorParts(chunk, (part, nested) => {
        if (nested && part.name === 'internal:control' && part.body === 'enter-frame') {
          const locator = asLocator(this.frame._page.browserContext._browser.sdkLanguage(), selector);
          throw new InvalidSelectorError(`Frame locators are not allowed inside composite locators, while querying "${locator}"`);
        }
        if (nested && pierce) {
          const locator = asLocator(this.frame._page.browserContext._browser.sdkLanguage(), selector);
          throw new InvalidSelectorError(`Composite locators are not supported with piercing frames, while querying "${locator}"`);
        }
      });
    }

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Re-capture the selector/locator against the current page state.
  2. Wait for the target iframe to attach (frameLocator + waitFor) before resolving.
  3. Prefer stable, semantic locators (getByRole) over recorded refs when possible.
  4. Avoid reusing aria-refs across page navigations or sessions.

Example fix

null
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the frame exists before using a recorded locator
const frames = page.frames();
if (!frames.some(f => f.url().includes(expectedHost))) await page.waitForURL(expectedHost);

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Replaying a captured aria-ref/locator snapshot against a page where the referenced iframe has been removed, navigated, or not yet attached. Typically produced by recorder/replay or by reusing a stale ref captured in a previous session.

Common situations: Locator recorded against one page state and replayed after the DOM/iframes changed; iframes added dynamically and not yet present; cross-frame recording in flaky tests; recorder artifacts from older builds.

Related errors


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