microsoft/playwright · error · InvalidSelectorError

nth can only be the last locator when piercing frames, while

Error message

nth can only be the last locator when piercing frames, while querying "${locator}"

What it means

In pierce-frames mode, the nth pseudo-part (>> nth=N) is only valid as the final part of a selector. If nth appears anywhere except the last position, InvalidSelectorError is thrown because the piercing resolver cannot reorder it.

Source

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

    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}"`);
        }
      });
    }

    if (pierce) {
      const parsed = chunks[0];  // Only one chunk is allowed with pierce, it may contain enter-frame parts.
      if (parsed.parts.some((part, index) => part.name === 'nth' && index !== parsed.parts.length - 1)) {
        const locator = asLocator(this.frame._page.browserContext._browser.sdkLanguage(), selector);
        throw new InvalidSelectorError(`nth can only be the last locator when piercing frames, while querying "${locator}"`);
      }
      return await this._resolveFramePiercingSelector(parsed, options, scope);
    }

    const result = await this._resolveChainedSelector(selector, options, chunks, scope);
    return result ? [result] : [];
  }

  private async _resolveChainedSelector(selector: string, options: types.StrictOptions, frameChunks: ParsedSelector[], scope: ElementHandle | undefined): Promise<SelectorInFrame | null> {
    let frame: Frame = this.frame;
    for (let i = 0; i < frameChunks.length - 1; ++i) {
      const info = this._parseSelector(frameChunks[i], options);
      frame = this._jumpToAriaRefFrameIfNeeded(selector, info, frame);
      const context = await frame.context(info.world);
      const injectedScript = await context.injectedScript();
      const handle = await injectedScript.evaluateHandle((injected, { info, scope, selectorString }) => {
        const element = injected.querySelector(info.parsed, scope || document, info.strict);
        if (element && element.nodeName !== 'IFRAME' && element.nodeName !== 'FRAME')

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Move nth to the end of the locator string.
  2. Re-record/re-generate the locator so nth is last.
  3. Wrap with frameLocator/locator API and call .first()/.nth() as a method on the final locator instead of an inline token.

Example fix

// before
page.locator('iframe >> button >> nth=0 >> span'); // nth not last
// after
page.frameLocator('iframe').locator('button').first().locator('span');
Defensive patterns

Strategy: validation

Validate before calling

// Ensure nth is only the last token when piercing
const parts = sel.split('>>').map(s => s.trim());
const nthIdx = parts.findIndex(p => p.startsWith('nth='));
if (nthIdx !== -1 && nthIdx !== parts.length - 1) throw new Error('nth must be last');

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Using a piercing locator whose nth component is not last, e.g. locator('iframe >> button >> nth=0 >> span') with pierceFrames on, or chaining after nth.

Common situations: Authoring locators by hand with nth in the middle; generated locators from recorder placing nth before trailing parts; enabling piercing on existing nth-bearing locators.

Related errors


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