fix(extension): support Playwright-style negative nth() indices
why
locator().nth(-1) and deepLocator().nth(-1) threw RangeError, while Playwright's contract is that negative indices count from the end of the match list (nth(-1) selects the last match). The -1 sentinel additionally carried three different meanings across the locator classes (see #3415): "no narrowing" in Locator, "preserve all matches" in DeepLocatorDelegate, and a hard error from the public nth() methods.
Closes #3415
what changed
Locatornow tracks explicit narrowing separately from the-1sentinel (hasExplicitNth), so an explicitnth(-1)no longer collides with the internal default.Locator.nth()accepts finite negative indices;resolveNode()andresolveNodesForMask()resolve an explicit negative index against the match count (count, then index from the end) viaresolveActionIndex().DeepLocatorDelegate.nth()accepts finite negative indices and threads the explicit flag through to the underlyingLocator; the default delegate keeps its narrowing-free masking semantics.- Non-negative
nth()behavior and the not-found error path (e.g.nth(-5)on 3 matches → "Could not find an element") are unchanged.
test plan
- New
packages/extension/tests/locator-nth.test.tscovers:nth(-1)no longer throws on either class, explicit-flag/sentinel separation, unchanged non-negative semantics,RangeErrorstill thrown for non-finite indices, and delegate identity-stability for repeated identicalnth()calls. - Existing suites pass:
pnpm --filter ./packages/extension test:unit→ 74 files / 1196 tests passing (incl. the 8 new tests), extension typecheck clean, rootpnpm typecheckclean,pnpm lintclean (pre-existing warnings in unrelated packages only). - Manual reasoning for the end-to-end path:
nth(-1)→resolveActionIndex()→count()+resolveAtIndex(total - 1); the same selector with a non-negative index hits the unchangedresolveAtIndexfast path.
Summary by cubic
Locator.nth() and DeepLocatorDelegate.nth() now accept negative indices, counting from the end of the match list like Playwright. nth(-1) selects the last match instead of throwing a RangeError. Explicit narrowing is tracked separately from the internal -1 sentinel, so default behavior and explicit nth(-1) don't collide.
Changes
- Negative indices are resolved against the match count at resolution time.
- Non-finite indices still throw RangeError.
- Non-negative behavior and not-found error paths are unchanged.