Skip to content

test(interaction-harness): name a double tap the page read as two taps - #10916

Merged
cixzhang merged 4 commits into
meta:mainfrom
vjeux:bank/touch-tap-interval
Nov 10, 2026
Merged

cixzhang merged 4 commits into
meta:mainfrom
vjeux:bank/touch-tap-interval

Conversation

@vjeux

@vjeux vjeux commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Maintainer impact

A double tap's second touch-down must reach the page within UIKit's 350 ms of the first lift. A starved runner can send it late. The page then reads two taps, and a proof waiting for the zoom fails with a bare "no animation started". Lightbox's busy proof saw one arrive 419 ms after the lift, where the script spaces them 120 ms.

Intended invariant

A play fails by name when the page read separate taps where the script taps once, and the error names the cause. It never fails a play the component paired.

Change

  • A play takes boundaries.tapIntervalMs, the component's tap interval, and every driver judges it. It reads each touch's stamp and receipt, and each frame, on the page's clock. Chromium's page records them. WebKit's arrival stamps and probe frames serve.
  • The window closes at the lift's stamp plus the interval. A touch-down received later is named only where the page drew a frame in between. The component's timer then ran first. Otherwise the component decides. A gap the script spaces past the interval is no fault.
  • TouchTapIntervalError names the cause by the frame gap across the close. Up to FRAME_STARVATION_MS, the page kept drawing, so a dispatch stalled before the page, as on a starved runner. Past it, the page's main thread was held across the close, by a task of its own or a starved machine. In Chromium, such a task ends with a frame before the touch-down it held.
  • Lightbox's double-tap plays pass UIKit's interval.

Evidence

  • Failure before the change: with main's touch.ts, a runner stalls 450 ms after the lift. The play passes, though the page read two taps (red arm 0d21481). On aa1422c, a page task across the close is named a starved runner, 3/3 in Chromium (red arm fde30f3).
  • Success after the change: at 758da14, the runner's stall names the runner, and the page's task names its held main thread. A task that ends before the close leaves the double tap on time.
  • Product/runtime behavior verified unchanged: test code only.
  • CI impact: none.

Scope

  • No intended public API, product behavior, visual direction, or policy change.
  • Product changes discovered during the work were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

  • pnpm exec vitest run internal/interaction-harness
  • pnpm exec tsc --noEmit -p internal/interaction-harness/tsconfig.json
  • pnpm lint:strict
  • Playwright, one worker, in Chromium and WebKit: every Lightbox and harness interaction spec, 11 files.

Heads: this head is 070cee3. It merges main b1566fd into 758da14, and the change's own diff is unchanged (patch-id 5fec85e25168). At 070cee3, at one worker, the 11 Lightbox and harness interaction specs pass 286 in Chromium, with 20 skipped. In WebKit, 268 pass and 38 are skipped. The harness units pass 313, and strict lint has 0 errors.

At 758da14, on aa1422c, the harness spec and the three Lightbox specs passed 225 in Chromium, with 15 skipped. In WebKit, 206 passed and 33 were skipped. The shader-cache case failed only because the container ran without its setup script. With that setup it passed too. The harness units passed 311.

The tap-interval self-tests, three cases at one worker, each run three times:

  • A runner that stalls 450 ms after the lift. It fails by name with main's touch.ts, 3/3 in Chromium and WebKit (red arm 0d21481). At 758da14 it names the runner, with the close-frame gap at most FRAME_STARVATION_MS.
  • The page's own 400 ms task, from 50 ms after the lift, across the close. On aa1422c, Chromium names a starved runner, 3/3 (red arm fde30f3 holds the new cases on aa1422c). At 758da14, Chromium names the held main thread, 3/3. WebKit draws no frame between the task and the touch-down it held. So the play resolves there, and the case asserts that. The named page stall is Chromium-only. touch.taps.test.ts covers the frameless case.
  • The same task cut to 250 ms ends before the close. The double tap stays on time and the play resolves, 3/3 in both engines, before and after.

Units: touch.taps.test.ts covers the frame gate, the close-frame gap and the cause each gap names. It also covers a frameless arrival, arrival stamps, spaced taps, and a lift that leaves a finger down.

Mutants, each a not-for-landing child of 758da14:

  • 2236097 names the runner whatever the gap. The cause unit fails, and so does the Chromium page-stall case, 3/3.
  • 61095ee counts the gap from the lift, not the last frame before the close. Two gap units fail.
  • 86dfc78 names a held main thread at exactly FRAME_STARVATION_MS. The cause unit fails.

A log-only instrument of the rule (1a62eff) logged every double tap's page times and whether the zoom ran:

  • Under CI's profile, at one worker, all 120 double taps zoomed. None was named, under either rule.
  • Under a heavier load (two pinned CPUs, six workers, four busy loops), 33 of 76 taps had a frame after the close. All 33 are named, and none zoomed. By the close-frame gap, 1 names the runner and 32 a held main thread. Those pages drew a frame only every 55 to 505 ms across the close. Leaving gaps past 50 ms to the component would have named 1 of the 33.
  • One touch-down arrived 19.5 ms after the close with no frame between, and the zoom did not run. The check leaves such a tap to the component, so that failure stays unnamed.

@vercel

vercel Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 10, 2026 8:54pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 9, 2026
@astracat-bot

astracat-bot Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 10, 2026, 9:16 PM UTC

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 070cee3

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
Lightbox · Storybook 123.7 kB (0) · 10.6 kB CSS (0) 3,412 (0) ✓ · ✓ · ✓

Package size: measured; main has no measurement of the merge base to compare yet.

API changes: unknown (base surface unavailable): richtext, theme-butter, theme-chocolate, theme-gothic, theme-matcha, theme-neutral, theme-stone, theme-y2k

Committed API report behind this build: cli 36 rows, core 36 rows; run pnpm api:report.

Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: the tap-interval check is well evidenced, and two documentation fixes in internal/interaction-harness remain.

  • The README (lines 690-692), the tapIntervalMs JSDoc in touch.ts (lines 178-180) and the title say any late touch-down fails the play, but tapIntervalFault also requires a page frame between the window's close and the touch-down, so state that condition in both places and narrow the title to match (for example, "a second tap the runner delays past the tap interval fails the play by name").
  • The new block in touch.ts is inserted between TouchLiftStallError's doc comment (lines 907-915) and its class, and CHROMIUM_TOUCH_TYPES between TOUCH_TYPES's doc comment (lines 1385-1388) and that constant, so editor hovers now show no documentation for TouchLiftStallError or TOUCH_TYPES and the pointer-events description on CHROMIUM_TOUCH_TYPES; move the insertions above those comments.

The /approve-spec comment does not settle these, since no spec record changes here and both items are documentation fixes. Optional: the self-test's frameAfterCloseMs > 0 assertion holds for every returned fault, so it cannot fail. CI has not run on this draft head yet.

[Automated review]

Comment thread internal/interaction-harness/README.md Resolved
@cixzhang

cixzhang commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 82bb885

@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 9, 2026
A double tap's second touch-down must reach the page within UIKit's
maximumIntervalBetweenTaps (350 ms) of the first lift. The gesture
arena times that window from the lift's stamp on the real clock. On a
starved runner a play can send the second touch-down late. In Chromium
the page then sees the scripted stamps, but the window's timer fires
before the touch arrives. Lightbox's double-tap busy proof saw a second
touch-down arrive 419 ms after the lift's stamp, where the script
spaces them 120 ms. The page read two single taps, no zoom started, and
the proof failed with a bare "no animation started".

A play now takes boundaries.tapIntervalMs, the component's interval
between taps. Every driver judges it. A tap play reads, on the page's
clock, each touch's stamp, when the page received it, and when the page
drew each frame. In Chromium the page sees the scripted stamps, so the
play arms a page-side record of its touch events and frames. In WebKit
the arrival stamps and the probe's frames serve. The window closes at
the lift's stamp plus the interval. tapIntervalFault names a touch-down
received after the close only where the page drew a frame between the
close and the touch-down: the main thread was free first, so the
component's timer ran first. Without such a frame the component can
still pair the taps, so it decides. A gap the script itself spaces past
the interval is no fault. The play then throws TouchTapIntervalError
with both spacings and the frame. Lightbox's double-tap plays pass
UIKit's interval.

Under a heavier load than CI's (two pinned CPUs, six workers, four busy
loops), all 37 of 57 double taps with a frame after the close failed to
zoom, and all 20 others zoomed, two of them received after the close
with no frame between. Under CI's profile (one worker), all 120 double
taps zoomed, and none was named.

touch.taps.test.ts covers the frame gate, a main thread held across the
close, arrival stamps, taps the script spaces apart, and a lift that
leaves a finger down. A new self-test stalls the runner 450 ms after
the first lift. On main the play passes, though the page read two taps;
with the change it throws TouchTapIntervalError, naming the frame.
@vjeux
vjeux force-pushed the bank/touch-tap-interval branch from 82bb885 to 9650421 Compare November 10, 2026 00:02
@vjeux vjeux changed the title test(interaction-harness): a late second tap fails the play by name test(interaction-harness): name a double tap the page read as two taps Nov 10, 2026
@cixzhang

Copy link
Copy Markdown
Contributor

/approve-spec 9650421

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: CI has not run on this head yet, because the pull request is still a draft; marking it ready for review starts the run.

The tap-interval check is well evidenced: the new self-test fails with the base's touch.ts and passes with this change, and the README and tapIntervalMs docs now state the frame condition. The earlier documentation findings are resolved.

  • Optional: in harness.interaction.browser.spec.ts, the toContain('The page drew a frame') assertion cannot fail once the error name matches, since every TouchTapIntervalError message includes that sentence; drop it, or add a case where the page's own main thread is held across the close and the play resolves.

[Automated review]

@cixzhang

Copy link
Copy Markdown
Contributor

/approve-spec aa1422c

@vjeux
vjeux marked this pull request as ready for review November 10, 2026 08:15

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: the tap-interval check names a runner that splits a double tap, but it also blames the runner when the page's own main thread splits it.

  • Miss: in headless Chromium, a double tap whose runner keeps its schedule, while the page runs a 400 ms task from about 50 ms after the first lift, rejects with TouchTapIntervalError saying "a dispatch stalled before the page, as on a starved runner" (3 of 3, and Lightbox's "With zoom" story does not zoom), because Chromium draws a frame after the long task before it dispatches the queued touch-down; name the runner only when no frame gap longer than FRAME_STARVATION_MS spans the close, otherwise leave the pairing to the component or report that the page drew no frame across it, and add a browser self-test where the page holds its main thread across the close (touch.ts lines 962-964, 994-997 and 941).

The same task cut to 250 ms, ending before the close, resolves and zooms, so the runner delivers the touch-down on time in that setup. That browser case would also replace the self-test's message assertion on line 598, which cannot fail once the name assertion passes.

[Automated review]

@vjeux
vjeux marked this pull request as draft November 10, 2026 08:24
A page task that holds the main thread across the tap interval's close
ends, in Chromium, with a frame before the touch-down it held. The tap
check read that frame as proof that a dispatch stalled before the page,
so it named a starved runner.

The fault now keeps the frame gap across the close, and the error names
the cause by it. Where the page kept drawing, within
FRAME_STARVATION_MS, a dispatch stalled before the page, as on a
starved runner. Where it drew no frame for longer, its main thread was
held across the close, by a task of its own or a starved machine.
@cixzhang

Copy link
Copy Markdown
Contributor

/approve-spec 758da14

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: CI has not run on this head yet, because the pull request is still a draft; marking it ready for review starts the run.

The earlier finding is resolved: a page's own 400 ms task across the tap window's close is now named a held main thread (3 of 3 in headless Chromium), and the new browser self-test fails in Chromium against the previous touch.ts and passes in Chromium and WebKit with this change.

  • Optional: in touch.ts (lines 951-952), a 450 ms runner stall beside an unrelated 70 ms page task across the close is named a held main thread ("the touch-down waited for that main thread"), although the page drew 9 frames before the touch-down arrived and the same task with an on-time runner resolves; naming the held main thread only when the frame gap also spans the touch-down's due time would keep the runner named there.
  • Optional: in harness.interaction.browser.spec.ts, the new helpers sit between the "A real navigation" comment (lines 184-185) and fixture, so that comment now describes DOUBLE_TAP; move the helpers above it.

[Automated review]

Comment on lines +951 to +952
fault.closeFrameGapMs > FRAME_STARVATION_MS
? `The page drew no frame for ${fault.closeFrameGapMs.toFixed(0)} ms across that window's close, then a frame ${after} ms after it, before the touch-down. Its main thread was held across the close, by a task of its own or a starved machine. So the page read separate taps: the touch-down waited for that main thread.`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this names the cause by the close-frame gap alone. In headless Chromium, a runner that stalls 450 ms after the first lift, while the page runs an unrelated 70 ms task from 290 to 360 ms, gets this branch 3 of 3 (gap 77 ms): "the touch-down waited for that main thread". The page then drew 9 frames over about 107 ms before the touch-down arrived, and the same task with an on-time runner resolves, since its touch-down lands at 117 ms, before the task. Suggest naming the held main thread only when the gap also spans the touch-down's due time (page.stamps[index] in Chromium, or page.stamps[lift] + scriptedGapMs), and naming the runner otherwise, with a unit case where frames resume well before the touch-down.

Comment on lines 184 to +186
// A real navigation (not setContent, whose document.open() erases the window
// listeners the input log installs).
/** A double tap: the second touch lands 120 ms after the first lift. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this comment explains why fixture (line 239) navigates instead of calling setContent. The new helpers now sit between them, so it reads as a note on DOUBLE_TAP. Move the helpers above line 184.

@vjeux
vjeux marked this pull request as ready for review November 10, 2026 17:56
@vjeux
vjeux marked this pull request as draft November 10, 2026 20:06
@cixzhang

Copy link
Copy Markdown
Contributor

/approve-spec 070cee3

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: CI has not run on this head yet, because the pull request is still a draft; marking it ready for review starts the run.

This update only merges main. The tap-interval self-tests still pass in Chromium and WebKit, and the Lightbox double-tap specs pass in Chromium.

  • Optional, still open: in touch.ts (lines 951-952), a runner stall beside an unrelated short page task across the close is named a held main thread; naming the held main thread only when the frame gap also spans the touch-down's due time would keep the runner named there.
  • Optional, still open: in harness.interaction.browser.spec.ts, the new helpers sit between the "A real navigation" comment (lines 184-185) and fixture; move them above that comment.

[Automated review]

@vjeux
vjeux marked this pull request as ready for review November 10, 2026 21:19
@cixzhang
cixzhang merged commit 0b85490 into meta:main Nov 10, 2026
128 checks passed
@vjeux
vjeux deleted the bank/touch-tap-interval branch November 10, 2026 23:38

This branch was successfully deployed

1 active deployment
Preview — 070cee3c Deployed Nov 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants