Repository navigation
test(interaction): frame evidence #8045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 115 commits
Commits
Show all changes
117 commits
Select commit
Hold shift + click to select a range
dc6ae1e
test(interaction): real touch input for browser interaction proofs
vjeux 75fc6cb
fix(interaction): release only the lifted finger on a partial lift in…
vjeux 0a07aa1
fix(interaction): join the proofs to test, keep their setup in scope,…
vjeux 5f2ed50
fix(interaction): releaseVelocity measures from when the finger reach…
vjeux d3c13cc
test(interaction): curve sampling and deterministic storyboards
vjeux 65c017e
fix(interaction): curves end before a held end value; rest judges pre…
vjeux d8af73a
fix(interaction): hold only running animations at their own offsets, …
vjeux 2e170f5
fix(interaction): film from each animation's own offset, label sample…
vjeux 04cbfc5
fix(interaction): waitForAnimation sees a short animation that ended …
vjeux d29981d
refactor(interaction): the storyboard layer moves to its own pull req…
vjeux beecc80
test(interaction): a second hold adds to the held set
vjeux 4a6dfbe
test(interaction): deterministic storyboards
vjeux eec2404
test(interaction): a touch filmstrip's handler clock follows its pacing
vjeux 0954697
test(interaction): frame evidence and the BottomSheet exemplar
vjeux 6be18c6
test(interaction): make the busy-main-thread verdict robust to a load…
vjeux 3f39b21
test(BottomSheet): pin the settle's easing and duration instead of re…
vjeux 03eadf9
test(interaction): block after the frame's rendering update; rebase W…
vjeux ffc2bd4
Merge branch 'main' into interaction-harness-2-curves
vjeux e918a2f
Merge branch 'interaction-harness-2-curves' into uikit/f3-storyboards
vjeux 79a1fed
Merge branch 'uikit/f3-storyboards' into interaction-harness-3-frames
vjeux 8d0a829
test(interaction): a held animation the page cancels leaves the held set
vjeux cb37bd7
Merge branch 'interaction-harness-2-curves' into uikit/f3-storyboards
vjeux 0b4d12b
Merge branch 'uikit/f3-storyboards' into interaction-harness-3-frames
vjeux a499860
docs(interaction): the touch filmstrip's clock limit names no missing…
vjeux 212ec1d
Merge branch 'uikit/f3-storyboards' into interaction-harness-3-frames
vjeux b36bf63
docs(interaction): the touch filmstrip's clock limit names the screen…
vjeux 91f8ddd
test(interaction): a hold stays where it was read
vjeux b5783ed
Merge branch 'interaction-harness-2-curves' into uikit/f3-storyboards
vjeux 2a718ad
Merge branch 'uikit/f3-storyboards' into interaction-harness-3-frames
vjeux 3f44fe9
test(interaction): a block never starts on a pending animation
vjeux bc744cb
test(interaction): the WebKit container keeps compiled shaders betwee…
vjeux f898984
Merge commit 'refs/f3/main' into interaction-harness-1-touch
vjeux 824bc84
Merge branch 'interaction-harness-1-touch' into interaction-harness-2…
vjeux 0e3e379
Merge branch 'interaction-harness-2-curves' into uikit/f3-storyboards
vjeux 9716595
Merge branch 'uikit/f3-storyboards' into interaction-harness-3-frames
vjeux 4538572
ci: the harness's typecheck runs through pnpm typecheck, once
vjeux 26f2b98
test(interaction): a watch on first contact ignores what already runs
vjeux d8fbea2
Merge branch 'interaction-harness-1-touch' into interaction-harness-2…
vjeux 5850e7a
test(interaction): a later hold never moves the first hold's times
vjeux c6d91e8
ci: pin the interaction routing's fail-closed edges by running them
vjeux 15a1a76
Merge branch 'interaction-harness-1-touch' into interaction-harness-2…
vjeux 0a9e7bf
docs(interaction): seek and sample times count from the held origin
vjeux 2438fc7
ci: a head cut before the interaction proofs passes with a notice
vjeux 295c0da
Merge branch 'interaction-harness-1-touch' into interaction-harness-2…
vjeux eeef165
ci: a head cut before the interaction proofs passes with a notice
vjeux a9b2acf
fix(interaction): a watch keeps the start of an animation the page ca…
vjeux cf0ad08
chore(interaction): the WebKit container script moves up the stack
vjeux 8afa44a
Merge branch 'interaction-harness-1-touch' into interaction-harness-2…
vjeux 990fa36
chore(interaction): the BottomSheet detent exemplar leaves the harnes…
vjeux 2642e06
fix(interaction): a storyboard retakes a photograph Chromium returns …
vjeux ca4fee0
test(interaction): pin what the long-task log sees; the cache test wa…
vjeux 18b3bd3
docs(interaction): the storyboard retake waits two frames, three atte…
vjeux a68b352
test(interaction): the self-test films a cast window
vjeux 44278bf
Merge branch 'interaction-harness-2-curves' into uikit/f3-storyboards
vjeux 56d5de0
chore(interaction): the WebKit container script lands with the storyb…
vjeux cc613ef
Merge branch 'uikit/f3-storyboards' into interaction-harness-3-frames
vjeux 618ed4e
Merge remote-tracking branch 'refs/remotes/origin/main' into interact…
vjeux 37050d3
ci: the interaction jobs keep to the 15-minute budget and report thei…
vjeux cd141ab
Merge branch 'interaction-harness-1-touch' into interaction-harness-2…
vjeux 9864ff8
Merge branch 'interaction-harness-2-curves' into uikit/f3-storyboards
vjeux fd1e7e1
Merge branch 'uikit/f3-storyboards' into interaction-harness-3-frames
vjeux b6cb3d8
Merge main (#8043 landed) into the curve sampling
cixzhang b9c5d7b
Merge branch 'uikit/f3-storyboards' into interaction-harness-3-frames
vjeux 7ee8fea
Merge branch 'interaction-harness-2-curves' into uikit/f3-storyboards
vjeux cd9e5cf
fix(interaction): curve samples stay true for percentage translates, …
cixzhang 804b28e
fix(interaction): compareCurve fails a point it cannot compare
cixzhang 19ef35c
fix(interaction): the busy-main-thread verdict judges the frames the …
vjeux 4f6dd9c
Merge main (#8462 curve oracles and #8053 landed) into the curve samp…
cixzhang 2912517
Merge remote-tracking branch 'refs/remotes/fork/interaction-harness-2…
vjeux e5a32e1
Merge branch 'uikit/f3-storyboards' into interaction-harness-3-frames
vjeux 432ef44
fix(interaction): curve ends under a non-linear effect easing, and ro…
cixzhang 8082d27
Merge remote-tracking branch 'refs/remotes/fork/interaction-harness-2…
vjeux c4e0f0d
Merge branch 'uikit/f3-storyboards' into interaction-harness-3-frames
vjeux 0cc6d8e
fix(interaction): a held end is solved through a cubic-bézier easing,…
cixzhang e2da8b9
fix(interaction): judge a just-started animation from when it reaches…
vjeux fb3423f
fix(interaction): an animation reversed by reverse() reads to its ful…
cixzhang e2e775a
fix(interaction): a reverse()d animation is reported from its real st…
cixzhang 2efaf04
fix(interaction): a held run counts only when it reaches offset 1 and…
cixzhang 7c8ab74
fix(interaction): the busy verdict reads the compositor's own frames …
vjeux ad21d14
test(interaction): held offsets against the runners' own start times
vjeux 35b0f22
fix(interaction): a reversed run's progress counts from its end; wait…
vjeux a261384
fix(interaction): the busy verdict counts the frames a screencast del…
vjeux 10ba166
fix(interaction): a run going backward is dated from when it began
cixzhang 92bc9c2
fix(interaction): a finish read before its event goes through the rat…
cixzhang a2ac6f9
fix(interaction): waitForRest waits out a backward run's end delay an…
cixzhang 1e242be
fix(interaction): a rendering trace reports DevTools-run tasks apart …
vjeux efdffbe
refactor(interaction): the animation-state waits move to their own pu…
cixzhang c833823
feat(interaction): animation-state waits for interaction proofs
cixzhang 2bcd5b2
docs(interaction): the waits read a run reversed before it starts; a …
cixzhang e8b0bb6
Merge main (#8044 landed) into the animation-state waits
cixzhang 4eac57c
Merge #8505 (e8b0bb62: main with #8044, plus the animation-state wait…
cixzhang 772a4b5
Merge #8348 (main with #8044 and #8505's waits) into the frame evidence
cixzhang 571f088
fix(interaction): openStory turns Storybook's highlight module off
vjeux 4c41132
fix(interaction): a trace sets apart only the harness's own reads, ne…
vjeux 9320644
fix(interaction): a wait keeps waiting while the action's animation i…
cixzhang 156e4b3
test(interaction): openStory's highlight self-test runs in both engines
vjeux b4234d7
Merge #8505 (9320644717: a wait keeps waiting while the action's anim…
vjeux cb907b7
fix(interaction): the busy verdict judges a sparse cast from its firs…
vjeux 462d82c
fix(interaction): a scroll- or view-timeline animation does not hold …
cixzhang 803e6d6
test(interaction): the long-task self-test judges the harness read, n…
vjeux fa8c8e2
Merge #8505 (462d82caaf: a scroll- or view-timeline animation does no…
vjeux a9be409
fix(interaction): a scroll-driven animation at its end is not a finis…
cixzhang 02ff48f
fix(interaction): a watch stamps after the newest entry, and an exit …
cixzhang 528dd2d
fix(interaction): after an on-screen start, the busy verdict judges t…
vjeux e05cdd1
Merge #8505 (02ff48fafe: a watch stamps after the newest entry, and a…
vjeux 93c611d
Merge main (#8505 landed) into the storyboards
cixzhang 79aa498
Merge #8348 (main with #8505 landed) into the frame evidence
cixzhang 5ad1170
fix(interaction): the WebKit container runs as the invoking user unde…
cixzhang 38ea7d9
fix(interaction): rootless docker runs the WebKit container without -…
cixzhang 5712c49
Merge #8348 (38ea7d9c: rootless docker, WebKit container user) into t…
cixzhang 4045092
chore(interaction): move four instruments out of the frame evidence
cixzhang 6762cc5
chore(interaction): move rendering traces and the long-task log out o…
cixzhang 03058f6
chore(interaction): move the compositor presentation record out of th…
cixzhang 3e3c4e7
Merge main (#8348 landed) into the frame evidence
cixzhang 5613501
docs(interaction): the README's evidence list and busy recipe match w…
cixzhang bac4669
fix(interaction): a window that delivered under four frames is judged…
cixzhang 8715133
docs(interaction): the busy verdict's docs name the thin-window rule
cixzhang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,22 @@ | ||
| # @astryxdesign/interaction-harness | ||
|
|
||
| Playwright instruments for touch interaction and animation proofs: real touch | ||
| input, curve sampling and storyboards. **Internal and unpublished.** Specs live | ||
| next to their component as `*.interaction.browser.spec.ts` and run under | ||
| input, frame evidence, curve sampling and storyboards. **Internal and | ||
| unpublished.** Specs live next to their component as | ||
| `*.interaction.browser.spec.ts` and run under | ||
| [`playwright.interaction.config.ts`](../../playwright.interaction.config.ts) on | ||
| a phone-sized touch screen (402 × 714 CSS px page, 402 × 874 screen, 3 device | ||
| pixels per CSS px, touch, mobile viewport rules), in Chromium and WebKit. | ||
|
|
||
| The harness proves its own instruments in | ||
| [`src/harness.interaction.browser.spec.ts`](src/harness.interaction.browser.spec.ts), | ||
| on fixture pages written inline. | ||
| [`src/harness.interaction.browser.spec.ts`](src/harness.interaction.browser.spec.ts): | ||
| every positive check has a negative control beside it (a rAF animation that | ||
| freezes under a blocked main thread). | ||
|
|
||
| ## Running | ||
|
|
||
| ```sh | ||
| pnpm storybook:build # component specs read stories from the built Storybook | ||
| pnpm storybook:build # the specs read stories from the built Storybook | ||
| pnpm exec playwright install chromium webkit | ||
| pnpm test:interaction --project chromium # everything, Chromium | ||
| pnpm test:interaction <spec file> # one spec, both engines | ||
|
|
@@ -42,6 +44,8 @@ committed: a pull request links or attaches it. | |
| | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `scripts.ts` | Touch scripts as data: `tap`, `drag` (hold, rest), `flick` (exact final-move speed, velocity profiles), `holdThenDrag`, `pinch`, `rotate`, `path`, `merge` (a second finger mid-gesture), `releaseVelocity`, `toTouchEvents`. | | ||
| | `touch.ts` | `touchDriver(page).play(script)`: the script as real engine input. `installInputLog` / `readInputLog`: what the page received. | | ||
| | `frames.ts` | `startScreencast` (timestamped composited frames), `markElement` (a marker chip or outline), `trackMarker` (marker box per frame), `frameStats`. | | ||
| | `busy.ts` | `blockMainThread(page, ms)` and `busyWindowProof(tracked, window)`: frames presented, frames that moved, longest stall, a verdict; `presented` adds the compositor's own frames. | | ||
| | `curves.ts` | `holdAnimations`, `seekAnimations`, `sampleCurve` (presented transform / opacity at any t), `waitForAnimation`, `watchAnimations`, `waitForRest`, `releaseAnimations`. | | ||
| | `math.ts` | Oracles independent of the code under test: `cubicBezier`, `timingFunction`, `springStep` (SwiftUI response / dampingFraction, initial velocity), `projectDecay`, `rubberBand`, `parseTransform`, `compareCurve`, `timeline`. | | ||
| | `storyboard.ts` | `animationStoryboard` (seek to t = 0, 16.7, 33.3 … ms), `touchStoryboard` (a frame per scripted event): PNG contact sheet + JSON trace. | | ||
|
|
@@ -97,23 +101,89 @@ set): a short entrance no longer races the first look on a cold page. When the | |
| action may be the page's first contact with the harness, take | ||
| `watchAnimations(locator)` before it and read `watch.started()` after. | ||
|
|
||
| **Busy main thread.** Mark the element, start the screencast, start the | ||
| animation, wait until it runs (`waitForAnimation`: an animation cannot start | ||
| while the thread is held), block the main thread for 1 s (`spec:AST-094` | ||
| FR5's verification row), stop. Always | ||
| start the gesture or animation first and block mid-flight, never before: | ||
| in headless Chromium a touch that starts while the main thread is busy does | ||
| not scroll or start a gesture even with only passive listeners (the input | ||
| waits for the main thread). Tracking a finger under a busy main thread is | ||
| therefore proven on the iOS simulator, not here. The | ||
| screencast is captured off the main thread, so a compositor animation shows a | ||
| new position every frame of the block, and a main-thread animation shows none | ||
| (nothing changed, so no frame is sent at all: `busyWindowProof` counts the | ||
| whole window as one stall). `settledWithin` (2 px) stops judging at the easing | ||
| tail: the screencast rounds to whole pixels, so a tail that still moves shows | ||
| plateaus within a pixel or two of rest. A judged part shorter than four of the | ||
| screencast's intervals fails: the animation under test was not moving during | ||
| the block. With | ||
| `during`, `blockMainThread` starts the loop from a task queued in the frame | ||
| that saw the animation, after that frame's rendering update: WebKit hands a | ||
| newly started animation to its compositor there, and a loop inside the rAF | ||
| callback would freeze even a compositor animation. Call it before the release | ||
| that starts a settle and await it after, so the page is already waiting when | ||
| the settle begins. An animation counts once it is no longer pending and has | ||
| run at least one frame: a `minProgressMs` under 16 is raised to 16, since one | ||
| created in the frame the page looks is not on the compositor yet. The `verdict` judges the | ||
| frames the screencast delivered, whatever its rate (a loaded machine's can | ||
| deliver one frame in five or six), and tolerates nothing a frozen animation | ||
| does: at least one frame in the judged part, no stall longer than three of the | ||
| screencast's intervals (two dropped frames), and at least 90% of the frames it | ||
| delivered in the judged part presenting a new position (one miss is always | ||
| allowed; a frame it did not deliver is a gap, for the stall check). Pass | ||
| `maxMedianFrameMs` (20 at 60 Hz) only in a proof that claims display rate. | ||
| `verdict.failures` names each miss with its numbers, and `summary` carries the | ||
| raw numbers for the report. Frames are judged in presentation order: a loaded | ||
| machine can deliver them out of order. | ||
|
|
||
| A just-started animation can reach the screen after the block began (50–100 ms | ||
| later in WebKit on a loaded machine). When the cast saw the element at rest | ||
| before the block (wait for `cast.frameCount()` to pass 0 before starting the | ||
| animation), judging starts at its first presented change: the judged part still | ||
| ends with the block and still needs four intervals, and `onScreenFromMs` and the | ||
| summary report when it came. An element already moving is judged from the | ||
| block's start. WebKit's casts are 2 px quantized: the frames come at the | ||
| viewport's size, but the page in them is captured at half resolution, so | ||
| positions move in 2 CSS px steps (Chromium's in 1). With under two steps of | ||
| motion per frame, a frame delivered a few ms early shows no step and reads as | ||
| not moved. The proof reports `stepPx` and `pxPerFrame` and notes motion under | ||
| two steps per frame, which never fails it. Move a proof's element at least two | ||
| steps a frame: the self-tests move 560 px in 1,600 ms, about 4.8 px a frame. | ||
|
|
||
| On a loaded machine Chromium's screencast can skip frames the compositor | ||
| still presents. Pass when the compositor presented each frame (epoch ms on the | ||
| page clock, from a trace of `Display::FrameDisplayed`) as `presented`. A gap between two moved | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No harness instrument records |
||
| frames with nothing delivered inside it is then measured between the | ||
| compositor's own frames: the frames it presented there count as presented | ||
| (`recorderSkipped`, in the summary), and a gap it did not present through is | ||
| a stall as before. A gap that opens or closes the judged part, or holds a | ||
| delivered frame that did not move, is judged from the screencast, so a | ||
| frozen element fails whatever else the compositor draws (a rAF animation's | ||
| block presents no compositor frame at all). With the record, the 90% counts the | ||
| compositor's frames from the first to the last moved frame; the gaps before and | ||
| after are the stall check's. Without a record (WebKit has no such trace), the | ||
| screencast is judged alone. | ||
|
|
||
| **Storyboards.** Deterministic: frames come from seeking held animations or | ||
| from stepping scripted input, never from wall-clock capture. The JSON trace | ||
| carries each frame's measurements and the spec asserts on it; the sheet is | ||
| evidence. A component that finishes an animation from a timer (a transitionend | ||
| backstop, a cleanup render) would do so behind a held storyboard's back: | ||
| `installTimerGate` before the animation starts and `freezeTimers` while | ||
| photographing. The page can still cancel a held callback, a held frame callback | ||
| gets the time it actually runs, and the harness's own frame waits are not held. | ||
| gets the time it actually runs, and the harness's own waits (its frame waits, | ||
| `blockMainThread` with `during`) are not held. | ||
| An animation that repeats forever does not set a storyboard's default length; | ||
| with nothing else to film, pass `times`. A touch filmstrip keeps the scripted | ||
| event `timeStamp`s (Chromium), but a handler that reads `performance.now()` or | ||
| `Date.now()` sees one event per photograph, about 100 ms apart, so a | ||
| clock-timed release flings slower when filmed than when played: a storyboard | ||
| cannot show such a gesture; record it in real time instead. Chromium can return | ||
| an empty copy of the page's surface right after it changes ("Unable to capture | ||
| screenshot"); a storyboard takes that photograph again two frames later, up to | ||
| three attempts in all, and lists each retake under `recaptures` in the trace. | ||
| cannot show such a gesture; record it in real time instead, with | ||
| `startScreencast`. Chromium can return an empty copy of | ||
| the page's surface right after it changes ("Unable to capture screenshot"); a | ||
| storyboard takes that photograph again two frames later, up to three attempts | ||
| in all, and lists each retake under `recaptures` in the trace. | ||
|
|
||
| ## Engine limits (measured with Playwright 1.61) | ||
|
|
||
|
|
@@ -126,11 +196,13 @@ three attempts in all, and lists each retake under `recaptures` in the trace. | |
| | Lifting one finger while others stay down | yes | pointer events right, but that `touchend` lists no remaining `touches`: skip with `PARTIAL_LIFT_SKIP` | | ||
| | Click synthesised from a touch | yes | only from `driver.tap` (WebKit's tap gesture), not from a raw touch sequence | | ||
| | Curve sampling, storyboards, input log | yes | yes | | ||
| | Screencast frame evidence (busy main thread) | yes | yes (WebKit's own screencast; frames keep coming while the main thread is blocked) | | ||
|
|
||
| What WebKit cannot show here — native scroll hand-off, flings, a finger | ||
| lifting while another stays down, and anything iOS-only — needs Mobile Safari | ||
| itself, on a device or the iOS simulator. Skip such a proof on the driver's | ||
| flag, citing the reason: `test.skip(!driver.partialLifts, PARTIAL_LIFT_SKIP)`. | ||
| lifting while another stays down, finger tracking under a busy main thread, and | ||
| anything iOS-only — needs Mobile Safari itself, on a device or the iOS | ||
| simulator. Skip such a proof on the driver's flag, citing the reason: | ||
| `test.skip(!driver.partialLifts, PARTIAL_LIFT_SKIP)`. | ||
|
|
||
| The WebKit input command is reached through Playwright's in-process server | ||
| object, so it needs a locally launched browser (the default), not `connect()`. | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only the busy-main-thread proof has a negative control; the two
duringtests, the curve-sampling tests and the timer-gate wait test assert only the positive outcome. Scoping this to "the busy-main-thread proof has a negative control beside it" keeps it true.