feat(locator): tap, doubleTap, and secondaryTap hold modifiers
Merged
OK
22d ago+357+357+012345678901234567890123456789-41-41-01234567890123456789
Actual Budget's "shift-click range selection skips hidden reconciled transactions" needs click({ modifiers: ['Shift'] }). e2e had no modifiers (migration table: missing), so the port faked a MouseEvent with shiftKey through web.evaluate.
await screen.getByRole('row', { name: 'Transaction 1' }).click();
await screen.getByRole('row', { name: 'Transaction 4' }).click({ modifiers: ['Shift'] });
await row.doubleTap({ modifiers: ['Alt'] });
await row.secondaryTap({ modifiers: ['ControlOrMeta'] });
tap/click,doubleTap,secondaryTaptakemodifiers(theKeyModifiernames of the key grammar, now also exported frome2e). The step label names them:getByRole("row", ...) with Shift.- Unknown, repeated, or non-array modifiers, and
modifiersbesideposition, areINVALID_ARGUMENTbefore any lookup. - Engine contract: the tap kinds of
LocatorActioncarrymodifiers?. An engine opts in with a newEnginemember,tapModifiers: true;defineEnginerequiresperformfor it. - An engine without
tapModifiers(mobile, any third-party engine written before this) getsUNSUPPORTED_CAPABILITYbefore the node resolves (packages/e2e/src/locator/engine.tscheckAction, withsession.performas the backstop), so it never drops them silently. @e2e-dev/webdeclares it and hands the list to Playwright'sclick/dblclick.- Changesets:
e2eminor,@e2e-dev/webminor. No mobile changeset: its code is untouched, and the refusal is runner behavior covered by thee2eentry (mobile docs updated). - Docs:
reference/engine.mdx,writing-an-engine.mdx,reference/screen.mdx, the Playwright migration table, mobile pages,skills/e2e.
flowchart LR
A["click({ modifiers })"] --> B{valid list?}
B -- no --> X[INVALID_ARGUMENT]
B -- yes --> C{engine declares tapModifiers?}
C -- no --> Y[UNSUPPORTED_CAPABILITY, before resolve]
C -- yes --> D["resolve, perform({ kind: 'tap', modifiers })"]
Tests: locator-action-options (options, label, pre-resolve refusal), engine.test.ts (manifest), engine-contract.test.ts (fake engine with and without the flag through the real runner), web-platform.test.ts (click/dblclick/contextmenu observe shift, alt, ctrl in Chromium; fails if the web engine drops them).
Verified
Ran it locally: yes
- Scratch project, a page of five transaction buttons with a Shift-click range selection (
node server.mjsonhttp://127.0.0.1:0), one test: click row 1, Shift-click row 4, expect4 selected. Same test against a main build and this branch,e2e run --reporter list.
| main | this branch |
|---|---|
INVALID_ARGUMENT: click options has no key "modifiers"; it takes timeout, position | passes, 4 selected |
main:
INVALID_ARGUMENT: click options has no key "modifiers"; it takes timeout, position
❯ tests/range.e2e.ts:6:63
Tests 1 failed | 0 passed (1)
this branch:
✓ |web| tests/range.e2e.ts (1 test) 227ms
✓ shift-click selects a range 227ms
Tests 1 passed (1)
Steps in .e2e/report.json:
app.open | / | passed
locator.click | getByRole("button", name: "Transaction 1") | passed
locator.click | getByRole("button", name: "Transaction 4") with Shift | passed
expect.toHaveText | getByRole("status") | passed
- The engine-contract test fails when the session gate is removed; the pre-resolve unit test fails (waits,
LOCATOR_NOT_FOUND) without thecheckActiongate. - Not run on a device: the mobile refusal is proven by the contract test's non-declaring fake engine.
pnpm checkstops atcheck:peer-ranges(canary peers left by the release, fixed by #619); every other step passes.