microsoft/playwright · error · InvalidSelectorError

Composite locators are not supported with piercing frames…

Error message

Composite locators are not supported with piercing frames, while querying "${locator}"

What it means

When pierce-frames mode is active (browserContext option pierceFrames, or internal piercing), composite (multi-part chained) locators are not supported. If a nested selector part is encountered while pierce is on, InvalidSelectorError is thrown.

Solutions

  1. Disable pierceFrames if you need composite locators, or
  2. Simplify the locator to a single-part selector when piercing is enabled.
  3. Refactor the query into a frameLocator chain that does not produce nested composite parts.

Example fix

// before
const ctx = await browser.newContext({ pierceFrames: true });
await page.locator('div >> button').click(); // composite under pierce -> throws
// after
await page.locator('button').click();
Defensive patterns

Strategy: validation

Validate before calling

// Avoid composite (>>) selectors when piercing is on
const isPiercing = !!context.options()?.pierceFrames;
if (isPiercing && sel.includes('>>')) throw new Error('no composite under pierce');

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Enabling pierceFrames on a BrowserContext (or using piercing selectors) and then issuing a composite/chained locator that has nested parts, which the piercing resolver cannot decompose.

Common situations: Turning on pierceFrames experimentally and reusing existing composite locators; frameworks that auto-generate chained selectors; refactoring tests without re-validating against piercing.

Related errors


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

Appendix: source

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

    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.
      if (!resolved || seenFrames.has(resolved.frame))
        continue;
      seenFrames.add(resolved.frame);
      result.push(resolved);
    }

View on GitHub (pinned to 500c9c822c)