microsoft/playwright · error · InvalidSelectorError

Frame locators are not allowed inside composite locators…

Error message

Frame locators are not allowed inside composite locators, while querying "${locator}"

What it means

Frame locators (the internal:control enter-frame part, i.e. frameLocator(...).find(...)) cannot be nested inside a composite (chained) locator. visitAllSelectorParts flags any nested enter-frame part and throws InvalidSelectorError naming the offending locator.

Solutions

  1. Restructure so the frameLocator is the outer chain and only descendant locators live inside it.
  2. Use the frameLocator(...).locator(...).locator(...) API rather than string concatenation.
  3. Split the operation into explicit frame resolution + a within-frame locator query.

Example fix

// before (conceptual): enter-frame nested inside a composite
page.locator('iframe >> [data-x] >> iframe >> button'); // invalid nesting
// after: chain frame locators via the API
page.frameLocator('iframe').locator('[data-x]').locator('button');
Defensive patterns

Strategy: validation

Validate before calling

// Reject nested frame-locator strings before sending them
function hasNestedFrameLocator(sel: string) {
  return (sel.match(/>>(?:[^>]*>>){2,}/) || []).length > 0;
}

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Composing a selector where an iframe-descending locator is used as an inner/non-final part of a larger chain, e.g. combining frameLocator(...).locator(...) as an intermediate segment in a multi-part locator string.

Common situations: Building complex selectors by concatenation; converting hand-written query strings that mix iframe traversal with other composite operators; migrations from older selector syntax.

Related errors


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

Appendix: source

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

      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;
  }

  async resolveFramesForSelector(selector: string, options: types.StrictOptions & { anyFrame?: boolean } = {}, scope?: ElementHandle): Promise<SelectorInFrame[]> {
    const { anyFrame, chunks } = splitSelectorByFrame(selector);
    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 && part.name === 'internal:control' && part.body === 'any-frame') {
          const locator = asLocator(this.frame._page.browserContext._browser.sdkLanguage(), selector);
          throw new InvalidSelectorError(`frameLocator() is not allowed inside composite locators, while querying "${locator}"`);
        }
      });
    }

    if (!anyFrame && !options.anyFrame) {
      const result = await this._resolveChainedSelector(this.frame, selector, options, chunks, scope, false /* noStall */);
      return result ? [result] : [];
    }

    const result: SelectorInFrame[] = [];
    const seenFrames = new Set<Frame>();
    for (const startFrame of await this._anyFrameCandidates(scope)) {
      const resolved = await this._resolveChainedSelector(startFrame, selector, options, chunks, startFrame === this.frame ? scope : undefined, startFrame !== this.frame /* noStall */);
      // Different starting frames may resolve into the same frame, e.g. with an aria-ref selector.

View on GitHub (pinned to 500c9c822c)