fix(extension): support Playwright-style negative nth() indices

BR
4d ago
+106-14

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

  • Locator now tracks explicit narrowing separately from the -1 sentinel (hasExplicitNth), so an explicit nth(-1) no longer collides with the internal default.
  • Locator.nth() accepts finite negative indices; resolveNode() and resolveNodesForMask() resolve an explicit negative index against the match count (count, then index from the end) via resolveActionIndex().
  • DeepLocatorDelegate.nth() accepts finite negative indices and threads the explicit flag through to the underlying Locator; 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.ts covers: nth(-1) no longer throws on either class, explicit-flag/sentinel separation, unchanged non-negative semantics, RangeError still thrown for non-finite indices, and delegate identity-stability for repeated identical nth() calls.
  • Existing suites pass: pnpm --filter ./packages/extension test:unit → 74 files / 1196 tests passing (incl. the 8 new tests), extension typecheck clean, root pnpm typecheck clean, pnpm lint clean (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 unchanged resolveAtIndex fast 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.