{"record":{"id":"3354383261fc402b","repo":"microsoft/playwright","slug":"nth-can-only-be-the-last-locator-when-piercing-fra","errorCode":null,"errorMessage":"nth can only be the last locator when piercing frames, while querying \"${locator}\"","messagePattern":"nth can only be the last locator when piercing frames, while querying \"(.+?)\"","errorType":"exception","errorClass":"InvalidSelectorError","httpStatus":null,"severity":"error","filePath":"packages/playwright-core/src/server/frameSelectors.ts","lineNumber":143,"sourceCode":"    const { pierce, chunks } = splitSelectorByFrame(selector, pierceByDefault);\n    for (const chunk of chunks) {\n      visitAllSelectorParts(chunk, (part, nested) => {\n        if (nested && part.name === 'internal:control' && part.body === 'enter-frame') {\n          const locator = asLocator(this.frame._page.browserContext._browser.sdkLanguage(), selector);\n          throw new InvalidSelectorError(`Frame locators are not allowed inside composite locators, while querying \"${locator}\"`);\n        }\n        if (nested && pierce) {\n          const locator = asLocator(this.frame._page.browserContext._browser.sdkLanguage(), selector);\n          throw new InvalidSelectorError(`Composite locators are not supported with piercing frames, while querying \"${locator}\"`);\n        }\n      });\n    }\n\n    if (pierce) {\n      const parsed = chunks[0];  // Only one chunk is allowed with pierce, it may contain enter-frame parts.\n      if (parsed.parts.some((part, index) => part.name === 'nth' && index !== parsed.parts.length - 1)) {\n        const locator = asLocator(this.frame._page.browserContext._browser.sdkLanguage(), selector);\n        throw new InvalidSelectorError(`nth can only be the last locator when piercing frames, while querying \"${locator}\"`);\n      }\n      return await this._resolveFramePiercingSelector(parsed, options, scope);\n    }\n\n    const result = await this._resolveChainedSelector(selector, options, chunks, scope);\n    return result ? [result] : [];\n  }\n\n  private async _resolveChainedSelector(selector: string, options: types.StrictOptions, frameChunks: ParsedSelector[], scope: ElementHandle | undefined): Promise<SelectorInFrame | null> {\n    let frame: Frame = this.frame;\n    for (let i = 0; i < frameChunks.length - 1; ++i) {\n      const info = this._parseSelector(frameChunks[i], options);\n      frame = this._jumpToAriaRefFrameIfNeeded(selector, info, frame);\n      const context = await frame.context(info.world);\n      const injectedScript = await context.injectedScript();\n      const handle = await injectedScript.evaluateHandle((injected, { info, scope, selectorString }) => {\n        const element = injected.querySelector(info.parsed, scope || document, info.strict);\n        if (element && element.nodeName !== 'IFRAME' && element.nodeName !== 'FRAME')","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/microsoft/playwright/blob/c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6/packages/playwright-core/src/server/frameSelectors.ts#L125-L161","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Move nth to the end of the locator string.","Re-record/re-generate the locator so nth is last.","Wrap with frameLocator/locator API and call .first()/.nth() as a method on the final locator instead of an inline token."],"exampleFix":"// before\npage.locator('iframe >> button >> nth=0 >> span'); // nth not last\n// after\npage.frameLocator('iframe').locator('button').first().locator('span');","handlingStrategy":"validation","validationCode":"// Ensure nth is only the last token when piercing\nconst parts = sel.split('>>').map(s => s.trim());\nconst nthIdx = parts.findIndex(p => p.startsWith('nth='));\nif (nthIdx !== -1 && nthIdx !== parts.length - 1) throw new Error('nth must be last');","typeGuard":"null","tryCatchPattern":"null","preventionTips":["Use .first()/.nth() methods on the Locator object rather than inline nth tokens.","Re-validate locators after enabling pierceFrames."],"tags":["selector","pierce-frames","nth"],"backgroundTag":null,"analyzedSha":"c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6","analyzedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}