Repository navigation
Conversation
Adds internal/interaction-harness (@astryxdesign/interaction-harness) and playwright.interaction.config.ts: phone-sized touch projects in Chromium and WebKit for *.interaction.browser.spec.ts specs. Touch scripts are data (tap, drag with hold and rest, flick with an exact release velocity and velocity profiles, hold-then-drag, pinch, rotate, any path, a second finger mid-gesture), played as real engine input: CDP Input.dispatchTouchEvent with scripted timestamps in Chromium, WebKit's own input command in WebKit. The harness's self-test proves the timestamps, a native fling and multi-touch delivery; a pr-interaction CI job runs the proofs against each PR's Storybook build, and a container script runs the WebKit project where host libraries are missing.
Springs, scroll physics, timing curves, navigation values and gesture thresholds for the touch presentations of core components, each with its source (Apple documentation, a WWDC session, WebKit's UIKit-matched source, or a measurement of UIKit on the iOS 26.2 simulator) or labelled a guess with its measurement task. A test enforces the source rules on every domain file, in their JSDoc and typed forms, and checks the list of simulator measurements still owed.
Pure functions for the touch presentations: a closed-form damped spring with every Apple parameterization and Core Animation's settling duration, a spring chasing a moving target, UIScrollView's coast, rubber band and release projection. Tests hold them to an RK4 integration, WebKit's spring solver, the WWDC conversion equations and values read from UIKit, and play the constants table through them.
…rena An internal gesture system for touch presentations, not exported from the package. GestureRecognizer carries UIKit's states and allowed transitions, discrete and continuous recognizers, require-to-fail, simultaneous recognition on request, shouldBegin and shouldReceiveTouch, cancelsTouchesInView and the touch delays. GestureArena settles each touch sequence without the DOM: participants, resolution order, claims, delivery to the view, click dispositions (allow, suppress, defer and replay) and timers as deadlines on the event clock. Its rules are tested with scripted recognizers, independent of any real one.
…sholds Tap recognizes at the lift within 45 pt of movement, taps 0.35 s and 45 pt apart, and a 1.5 s hold; long press begins at exactly touch-down + 0.5 s on the event clock within 10 pt, even when the main thread was blocked, and a zero duration begins on the touch itself. The arena tests with these real recognizers cover tap against double tap, delegate requirements, deferred clicks and cancellation.
Touch behaviours key on a coarse primary pointer, layout behaviours on a compact viewport, and presentations that need both (a sheet in place of a popover) ask for both. utils/touchPresentation.ts holds the three queries, SSR-safe hooks (useTouchPresentation; useMotionPresentation for components with no interaction to infer from: UIKit motion under a finger, theme tokens under a mouse) and a one-moment read. Styles write the same queries as StyleX media keys, and a test keeps core's compact-touch keys spelled as the shared query. A compact viewport is narrower than 744 px or shorter than 500 px, the closest a size gets to UIKit's compact size class: an iPad mini in portrait keeps the anchored popover and a phone in landscape gets the sheet; a Max-size iPhone in landscape is the known miss. The adaptive popover-or- sheet presentation reads the shared query; its docs, stories and tests follow.
A file that reads only defineConsts values from a .stylex.ts module lost
that import under the legacy {stylexOptions} form Storybook uses: the
babel wrapper's StyleX instance ran without treeshakeCompensation (the
unplugin applies its Vite default only to its own instance, which runs
after the wrapper), so once stylex.create was compiled the TypeScript
transform dropped the import and the defining module never reached the
build. A constant used as a media-query key failed the build with
"Invalid empty selector"; one used as a value shipped as a custom
property nothing declares. The wrapper now gets treeshakeCompensation by
default.
The layer split also substituted constants only within each layer, so a
constant defined on the library side and read on the product side stayed
a placeholder in either form; every layer now sees every constant.
A production build now warns, naming the variable and the rule that reads
it, when the StyleX stylesheet reads a generated custom property nothing
declares; undefinedVariables: 'error' fails the build instead, and this
repo's Storybook and Sandbox configs set it so CI catches the next case.
Pure functions for the touch presentations: a closed-form damped spring with every Apple parameterization and Core Animation's settling duration, a spring chasing a moving target, UIScrollView's coast, rubber band and release projection. Tests hold them to an RK4 integration, WebKit's spring solver, the WWDC conversion equations and values read from UIKit, and play the constants table through them.
createAnimator plays springs and UIScrollView coasts as Web Animations keyframes on translate, rotate, scale and opacity, on one explicit clock, so they keep running while JavaScript is busy. Presentation values and velocities come from the curves, so a finger can catch any animation and a new target keeps the velocity; the end state is committed inline and held by the keyframes through a main-thread block, and a finished animation retires when its own time reaches the curve's end, so a sampler can pause it; Reduce Motion substitutes UIKit's cross-fade.
… Chromium The Chromium driver sent a lift of one finger of several as a touchMove listing the fingers still down, assuming CDP releases a point a move omits. It does not: the lifted finger stayed down. A touchEnd that lists the lifting fingers releases exactly those while the others keep moving, so the driver sends that, and a self-test lifts one of two fingers and checks the release, the other finger's moves and the touchend's remaining touches. Playwright's WebKit sends that touchend with no remaining touches, which iOS never does. The harness does not work around it: drivers expose partialLifts, and PARTIAL_LIFT_SKIP is the reason a proof cites when it skips; the README lists it with the other engine limits.
Pause every Web Animation, seek it to a time and read the presented transform and opacity, then compare the curve with oracles that share no code with the motion under test: cubic-bezier, the SwiftUI spring (response, dampingFraction, initial velocity), UIScrollView deceleration and UIKit's rubber band. Storyboards photograph seeked animations at t = 0, 16.7, 33.3 ... ms or each step of a scripted touch, and write a PNG contact sheet plus the JSON trace a spec asserts, under the gitignored test-results/interaction-evidence. A timer gate holds a component's own timers while a filmstrip is taken. pnpm storyboards runs the tagged tests.
…sented values An animation that keeps its end value in a run of identical final keyframes (held so a later interruption can read it) reported its whole duration as the curve: storyboards filmed the hold, waitForAnimation's interval reached past the landing, and waitForRest, which also required every animation to leave the running state and read only transform, timed out. The curve now ends where that run starts (HeldAnimation.curveEndMs, waitForAnimation's to, the default storyboard times), and rest is no animation pending or before its curve's end plus the presented transform, translate, rotate, scale and opacity (and ::backdrop opacity) still for 3 frames, whatever the playState. sampleCurve composes translate, rotate and scale with transform, so curves of the individual properties are sampled.
Timestamped composited frames (Chromium's screencast and WebKit's own), a marker the frames can find, and the busy-main-thread proof: block the page's main thread mid-flight and show the element still moving every frame, with a requestAnimationFrame animation that freezes as the negative control. Chromium trace counts of Layout, Paint and style work inside an animation window, and long tasks. The exemplar spec proves the BottomSheet drag to a detent with all of it: the drag follows the finger, a flick dismisses, the settle matches its timing function, keeps advancing through a blocked main thread in Chromium and WebKit, does no Layout or Paint, and gets a drag and settle storyboard.
…ed machine The proof asserted a stall under 40 ms and near-exact frame counts, so a dropped screencast frame or two on a loaded runner failed it, and frames delivered out of order read as the element stepping backwards. busyWindowProof now returns a verdict that tolerates what load does to a screencast and nothing a frozen animation does: a median frame interval of at most 20 ms, no stall longer than three screencast intervals, and at least 90% of the window's whole frame intervals presenting a new position (one miss always allowed), with the raw numbers in a summary. Screencast frames are sorted by presentation time, the cadence comes from the frames around the window, and blockMainThread can block from inside the page in the frame an animation has run, so a short settle is still judged at length on a busy machine. The exemplar judges its settle until it is within 2 px of rest, since the screencast rounds to whole pixels. A unit test covers the limits; the rAF negative control still fails every criterion.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
The pan begins past 10 pt on either axis and leaves that travel out of its translation, as UIPanGestureRecognizer does; axis and one-way directions with axisRatio, axis lock, finger changes without jumps, scrollEdge, and an opt-in non-UIKit scrollHandoff. VelocityTracker defaults to UIKit's filter (0.2 x the newest event's velocity + 0.8 x the previous one's), which reproduces every value UIKit reported on recorded streams.
…zers The rest of UIKit's recognizer set on the gesture system: swipe recognized during the motion, pinch measured from the starting span moved by its hysteresis, unwrapped rotation, edge pans resolved against the writing direction that fail at touch-down outside their band, and hover for mice and hovering pens.
The touch-action an element gets from its recognizers (pinch-zoom stays unless a pinch or rotation recognizer is attached), what the scrollers under a touch could still do along the scroll chain, and UIKit's haptic feedback kinds on the web: navigator.vibrate where it exists, the switch tap haptic in iOS Safari.
…s takeover One host per document routes fingers (touch events, paired with their pointer events), mice and pens to the recognizers on the hit path. A recognizer that owns a touch cancels its touchmove through a non-passive listener; pointercancel, an uncancelable move or a scroll mean the browser took the touch; a claimed touch's touchend is cancelled and its click swallowed, a deferred click replayed when its recognizer fails. pointerIdOf names the pointer behind a tracked contact, so a held finger can be handed to code that follows pointers.
… and hooks PressTracker applies UIControl's events and its 70 pt tracking margin, the content-touch delay inside scrollers, the quick-tap flash and the scroll brake, with two profiles: control (UIButton) and cell (a row: delayed highlight everywhere, cancelled past the pan hysteresis, no flash). paintPress is the one writer of data-astryx-press and never clobbers a held row. installPressAttribute paints every marked surface with one tracker per touch, picks the profile from the marker's value and releases on its clock. useGestureRecognizer attaches a recognizer with a stable identity and no render per move.
The same touch scripts run in Chromium (CDP touches with timestamps) and WebKit (its own touch input): touch-action per element, claims over a scroller, no ghost click, scroll edge, pinch on its element only, a second finger, and a long press at exactly 500 ms under a blocked main thread. The README documents the system for component authors.
11d17f0 to
30320a7
Compare
…gesture constants The menu press model is shared code (hooks), and a component's motion table belongs to the component that animates with it (the table-import rule): the drift allowance and the opening finger's handoff move from `motion/uikit/menu.ts` to `UIKIT_GESTURES.menuPress` (gestures.ts), the guess with its measurement (M-G20). spec:AST-094 FR29 (meta#8378): a tap in a scroll view that ends before the content-touch delay opens the menu at its lift, now under test.
… the menu The module-internal useDropdownMenuPress returns continueGesture again: a pointer that went down elsewhere (a context menu's long press) is handed to the menu as it opens and becomes its opening press. A finger picks only after UIMenu's handoff distance, counted from the first position the menu sees after it opened (as spec:AST-094 FR29 counts a trigger's opening finger), whatever the touch model; a lift before that acts on nothing and leaves the menu open. A mouse follows the settle rule. Ignored while another gesture is live or the hook is disabled. The public useMenuPress has no continuation, in its type or its return.
…ts module Through the uikit barrel it reached every table module, including the menu table, which the table-import rule reserves for the component that animates with it once that component imports it.
…action and picks it UIKit's touch continuation: when the menu opens under a finger that is still down, ContextMenu hands that finger to the menu's press model (useMenuPress.continueGesture), found by the gesture host's pointerIdOf. Once it has moved UIMenu's handoff distance in the open menu, the highlight follows it and the row under its lift acts. A lift before that picks nothing and leaves the menu open. The touch story shows the last action picked.
…droid 15 Chromium 158)
86076e9 to
a885d27
Compare
|
Posted by Cindy's agent on her behalf. Requested change: slide-to-pick should cover every touch device, as |
The release note and a test header still described an iPhone/iPad gate that the code no longer has: a finger still down when the menu opens slides onto a row and picks it on every touch device, iOS and Android alike (spec:AST-094 FR13, FR26-FR29), as the suites run on both.
|
Fixed in 84a8bad. The code had no device gate left: a finger still down when the menu opens slides onto a row and picks it on every touch device, iOS and Android alike ( The jsdom suite runs the slide cases on iOS and Android stubs, the interaction suite runs them in Chromium and WebKit under each engine's own finger, and the Android 15 clip in the description shows the slide picking Share. The opening finger's handoff (10 pt from the first position the open menu sees) is AST-094 FR29 and the menu press model's own rule in #8078; ContextMenu only hands the finger over. |
There was a problem hiding this comment.
This pull request is on hold for an engineering owner's decision: letting the finger that opened a ContextMenu slide onto an action is a clear improvement on touch screens, but a lift before that finger has moved 10 px picks nothing even over a row, which departs from the current useMenuPress FR1 ("A row MUST act when a press is released over it"), and the branch adds spec text with no exact-head owner approval yet (AST-094, the rewritten useMenuPress FR1 and its new FR11, and edits to nine more records). An engineering owner can settle it by approving or amending that text and commenting /approve-spec 84a8bade625b8d33f5981aa164ae15a1df56e1c1 on this pull request; the author may do so as an engineering owner. Independently of that decision, please narrow this pull request to the ContextMenu change once its dependencies land (it carries about 38,700 authored lines today) and fix these:
presentation="popover"no longer opens the menu from a held finger, so on iPhone and iPad that ContextMenu cannot be opened by touch; keep the previous long press there, asAST-017FR3 requires of the restoring value.- ContextMenu's import carries the touch layer statically, including the whole springs table; load it lazily for coarse pointers or justify the cost, as
component-style-authoringINV6 requires. - The proposed
useMenuPressdelay row gives FR29's values as the default, while the public hook keeps 500 ms.
[Automated review]
| import {PRESSED_ATTRIBUTE, type pressableProps} from '../utils/pressFeedback'; | ||
| import {createAnimator, type Animator} from '../motion/animator'; | ||
| import {resolveMotion} from '../motion/resolveMotion'; | ||
| import {registerSpringsTable, springs} from '../motion/uikit/springs'; |
There was a problem hiding this comment.
This imports, and registers at line 111, the whole springs table (navigation, sheet, alert and switch rows included), while the feedback reads only springs.contextMenuDismiss (line 197). architecture:component-style-authoring INV6: a shared tuning table "ships to a component only the rows that component uses". Import only the row the feedback reads.
| | highlight | DOM focus, or a caller callback | one lit row; focus with `preventScroll` in a menu, `aria-activedescendant` in a picker | focus | `module:DropdownMenu/useMenuPress` | stable | | ||
| | stray-click window | 400 ms, or the next `pointerdown` | how long after a tracked release the browser's click is still swallowed | `MENU_PRESS_STRAY_CLICK_MS` | `module:DropdownMenu/useMenuPress` | stable | | ||
| | settle time | 300 ms | proposed: how long an opening press must last before its release may act | `MENU_PRESS_SETTLE_MS` | `module:DropdownMenu/useMenuPress` | proposed | | ||
| | long-press delay | as `spec:AST-094` FR29 states (none in a fixed bar; 150 ms in a scroll view) | proposed: how long a held finger waits on a trigger before the menu opens (`spec:AST-094` FR29). A caller that sets `longPressDelayMs` gets that wait in a fixed bar and in a scroll view alike, which keeps the released behavior. | `longPressDelayMs` | `module:DropdownMenu/useMenuPress` | proposed | |
There was a problem hiding this comment.
This row gives FR29's values as the long-press delay's default, but the public useMenuPress keeps MENU_PRESS_LONG_PRESS_MS = 500 (useMenuPress.ts:104), as its doc and .changeset/menu-press-uimenu.md say; FR29's values apply only to the internal uikit touch model. State 500 ms for the public hook and FR29's values for the internal model.
… long press With presentation="popover", or where the primary pointer is fine, a finger held on the trigger opens the menu at the touch point after 500 ms, as main does (useLongPress: the timer, the move cancel and the cleanup). The touch model replaced that long press with UIKit's timeline and left a finger outside it to the browser, so on iOS Safari, which sends no contextmenu for a long press, presentation="popover" opened nothing. The consumer's touch handlers still run. The doc, the release note and the story say what each device gets.
The same held finger opens the menu at UIKit's 735 ms where the primary pointer is coarse and at today's 500 ms where it is fine, with nothing else changed (spec:AST-094 FR13, FR33).
… the touch model) into the slide # Conflicts: # packages/core/src/ContextMenu/ContextMenu.tsx
The spec is cixzhang's record, now on main; code cites it by id.
FR29 covers DropdownMenu's trigger only; FR33 states the handoff after a held finger opens a ContextMenu.
There was a problem hiding this comment.
This is on hold for an engineering owner's decision: the ContextMenu slide itself follows spec:AST-094 FR33, now current on main, but the branch adds useMenuPress rules that current spec does not state yet (FR9's touch model, FR10, FR11 and AR5). An engineering owner can approve or amend them by commenting /approve-spec 098481492634dc5cb5727fdc9726a111ca2c94b6 on this pull request (the author may do so as an engineering owner), and the rebase this branch needs will ask for that again on its new head. Independently of that decision, please:
- rebase onto
mainonce the nine predecessor pull requests land, so this one carries only the ContextMenu change (it carries 38,503 authored lines today); - keep
main's text where your edits to theuser-states,ContextMenu,DropdownMenu,useMenuPressandPopoverrecords conflict with it, since the branch's versions contradict AST-094 FR11, FR29, FR32 and its menu scope; - load ContextMenu's touch layer lazily for coarse pointers or justify its cost, and import only the dismiss spring (
component-style-authoringINV6); - add real-device iOS Safari and Android Chrome recordings of the slide, as AST-094 asks.
[Automated review]
| | FR6 | Component anatomy contains the caller trigger and content, one painted Popover surface, and the optional fallback close control. Popover owns no Header, Body, or separate shared-hook surface part. | Current source, docs target inventory, and tests | Accepted anatomy; stale consumer anatomy corrected by this contract | | ||
| | FR7 | The painted surface has one broad canonical target, `popover`; composed components may add one authoritative component-specific refinement target. | Owner direction on 2026-08-31 plus current target inventory | Accepted canonical contract; covered by focused target tests | | ||
| | FR8 | The surface padding is one spacing-scale rung applied on the painted surface. Popover defaults to step 3 (the released inset); `0` paints a flush surface; a direct `usePopover` composition paints none unless it asks for a rung. The rung sits where a theme's `padding` on the `popover` target replaces it. | PR #6683 and its Popover tests; `architecture:container-padding/INV8` (padding alone does not enroll) | Pending owner review (DEC-4); implemented and covered by the Popover surface-padding suite | | ||
| | FR9 | Under a finger, Popover grows out of its trigger and shrinks back into it, a pan outside dismisses it while its outside dismissal is on, and a touch during its growth is ignored, as `spec:AST-094` FR31, FR32 and FR35 state. A mouse keeps today's entrance and dismissal. | `spec:AST-094` FR31, FR32, FR35 | Pending implementation; tests land with it | |
There was a problem hiding this comment.
Current spec:AST-094 FR32 says “A Popover's dismissal is unchanged.” This row adds a pan-outside dismissal for Popover and cites FR32 for it; main's FR9 keeps the dismissal unchanged. Please take main's text here when you rebase.
| | FR9 | Under a finger a tap on the trigger MUST open the menu through its click. A press held on the trigger for the long-press delay MUST open the menu with the finger down, prevent the default of the finger's later `touchmove`s until the gesture ends, and continue under FR1–FR4 with the same settle rule. | Proposed DEC-4; `DropdownMenu.test.tsx` held-touch case, `useMenuPress.test.tsx` | proposed; pending owner review | | ||
| | ID | Invariant | Basis | Verification state | | ||
| | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | | ||
| | FR1 | Under a mouse, a row MUST act when the press is released over it, never because the press began on it; a press that never left its row is a click on that row. Under a finger or pen, `spec:AST-094` FR26 governs. | macOS and iOS menu behavior; `menuPressGesture.test.ts`, `useMenuPress.test.tsx` | verify | |
There was a problem hiding this comment.
AST-094 applies FR26–FR30 only where the primary pointer is coarse; “where the primary pointer is fine, a finger in a menu keeps today's behavior”. Limiting FR1 (and FR2, FR6, FR7) to a mouse leaves that finger with no rule. main's FR1 keeps the coarse/fine split.
| | FR2 | The current `context-menu` target remains on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | Current source, docs, and tests | Current inventory; verified | | ||
| | FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs | Current delegation; verified | | ||
| | FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests | Current branches; verified | | ||
| | FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. Under a finger the press follows `spec:AST-094` FR26–FR30, and the menu's grow, dismissal, long-press lift and outside press follow FR31–FR33 and FR35; a mouse press outside is unchanged. Right-click, Shift+F10, long-press invocation and keyboard navigation keep their existing behavior. | `module:DropdownMenu/useMenuPress`; `ContextMenu.test.tsx` press model suite | Current module projection; verified in jsdom | |
There was a problem hiding this comment.
AST-094 FR29 “covers DropdownMenu's trigger only; ContextMenu, sub-menu rows and Selector keep their own opening”, so ContextMenu's press follows FR26–FR28 and FR30, as main says. Dropping “(pending implementation)” also claims FR31, FR32 and FR35, but the outside dismissal still listens for mousedown only.
| import {PRESSED_ATTRIBUTE, type pressableProps} from '../utils/pressFeedback'; | ||
| import {createAnimator, type Animator} from '../motion/animator'; | ||
| import {resolveMotion} from '../motion/resolveMotion'; | ||
| import {registerSpringsTable, springs} from '../motion/uikit/springs'; |
There was a problem hiding this comment.
This imports, and registers at line 111, the whole springs table while the feedback reads only springs.contextMenuDismiss (line 197). architecture:component-style-authoring INV6: a shared tuning table “ships to a component only the rows that component uses”, and a flagged touch-only feature loads lazily or justifies its cost.
|
Posted by Cindy's agent on her behalf. The vjeux, could you update the PR to match these? |
|
Posted by Cindy's agent on her behalf. Cindy's decision on context menus: a public, framework-agnostic For this PR, a small change: |
|
Posted by Cindy's agent on her behalf: closed and reopened only to restart this PR's CI on the current workflow after tonight's queue-wide cancel; nothing in the PR changed. |
Waits on #8622; it will be rebuilt on the shared action-group tracker, per the decision on #8084.
Depends on #8125 and #8078 (and their predecessors); review the last four non-merge commits.
Evidence
iOS 26.2 Safari simulator: the HUD shows ms since the touch, clicks, the picked action and scroll; dots are fingers.
Before (main): held (the first row lit at once), slid down onto the menu, lifted: nothing is picked.

After:

Android 15 emulator, Chromium 158 (Chrome 124 there lacks anchor positioning):
User need
The finger that opens a context menu isn't done when the menu appears: it slides onto an action and lifts to pick it, in one gesture. With #8125 the menu opens under a held finger at UIKit's moment, but that finger is stranded: sliding does nothing.
Problem and solution fit
continueGesture, DropdownMenu: under a finger the trigger opens as the finger presses (AST-094 FR29) #8078) and the gesture host'spointerIdOf. Where it is fine, or withpresentation="popover", a finger keeps today's long press and no handoff.useMenuPress's own (Date.now()).Current specification
component:ContextMenuis a draft; AST-094 (FR13, FR26–FR28, FR33) governs the touch model.module:DropdownMenu/useMenuPressFR1–FR7 hold, through #8078's continuation.Owner decision, proposed; pending owner review: a context menu's opening finger continues into the menu, under UIMenu's handoff distance.
Public delta and caller burden
No API change; touch only. Size: +341/−34, about 75% tests.
Design and alternatives
useMenuPressalready owns the highlight, the pick, autoscroll and the stray click.Scope
Testing
pnpm vitest run packages/core/src/{ContextMenu,hooks,DropdownMenu,utils,Layer,gestures,motion}: 2427 pass, including the table-import rule. The slide-to-pick, lift-before-handoff and no-handover-at-a-lift cases run on iOS and Android stubs alike.pnpm test:interaction ContextMenuLongPress DropdownMenu: Chromium 28/28, WebKit 24/24, under each engine's own finger:check:changesets, the experimental-isolation golden.