Skip to content

test(interaction-harness): a busy-window proof can judge a component against a compositor-only control - #8906

Merged
cixzhang merged 3 commits into
meta:mainfrom
vjeux:harness/busy-window-control
Oct 31, 2026
Merged

cixzhang merged 3 commits into
meta:mainfrom
vjeux:harness/busy-window-control

Conversation

@vjeux

@vjeux vjeux commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Why

A busy-window proof judges a compositor animation against a display-rate bound. On a two-core or loaded runner the compositor presents fewer frames than that bound allows though the animation moves on every one: Lightbox's double-tap zoom missed it in 7 of 10 two-core runs. A short fade gives one window too few frames.

What led to it

Both were written for Lightbox's proofs; only its pull requests carried them.

Options and tradeoffs

  • Loosen the bound: it can't then tell a starved compositor from a frozen element.
  • Keep them in Lightbox's pull requests: other proofs would carry its base.
  • Their own harness pull request, on main (this one).

Why this choice

startBusyControl(page) mounts a small top-layer box that the compositor animates, tracked from the same screencast. busyWindowProof(frames, window, {control}) passes when the component moves on every frame the control moves and never stalls longer. Same frames, so no tolerance. One exception: on an easing tail, a run of unchanged frames counts as moved when a single cast step ends it and it is at most a frame longer than the run before; a freeze ends in a larger catch-up. At least four control frames need a visible move.

  • pooledBusyProof(runs) judges short runs together.
  • {within: dialog} mounts the control inside an open dialog, for a backdrop proof. waitForAnimation and blockMainThread's during never count the control's own animation, so on a dialog they still wait for the backdrop's.
  • A hold that starts before the block is judged from its first presented change inside it: WebKit presents a just-started compositor animation 130 to 300 ms late. Every frame after that change is judged, so a freeze later in the block still fails.
  • Main's three busy self-tests judge against the control.

How to check it

  1. pnpm exec vitest run --project node internal/interaction-harness/src/busy.control.test.ts
  2. pnpm test:interaction internal/interaction-harness/src/harness.interaction.browser.spec.ts --grep "busy|control|pooled": a compositor animation passes; a rAF one, timed or frame-counted, fails.

Evidence

  • Noise floor: a compositor animation missed no control frame in 180 single and 90 pooled proofs.
  • Margin: the rAF mutant moved on at most 1 of 40 to 47 control frames (the one presented as the block starts) and always stalled longer; a frame-counted loop failed all 20 pools.
  • Lightbox's five busy proofs: Chromium 30 of 30; WebKit 33 of 40, the rest too few frames.
  • Load: main's first busy self-test failed 6 of 32 at 16 workers (every frame moved, stalls 63 to 109 ms); against the control, 48 of 48. Main's other two failed the same way (3 of 16 Chromium runs at 16 workers: "31/31 moved, longest stall 169 ms"); against the control, 32 of 32 per engine.
  • The first self-test's rAF run blocked right after cancelling the compositor run, so the compositor kept playing the cancelled animation into the block (the rAF mutant read 6 to 8 changed frames). With two animation frames before the block, a cancel-then-block probe kept moving in 0 of 8 Chromium runs, against 1 of 8 without them.
  • BottomSheet's dim in WebKit: under the top-layer control it held 155 to 225 ms into the block while the control moved; with within it moves in step with the sheet. On a dialog that animates nothing else, the two waits resolved at once on the control's animation; now they wait for the dialog's own, 6 of 6 per engine.
  • Wrapped self-test animations: a turnaround in the block failed 7 of 12; now 12 of 12.
  • Red first: every new case fails on main's busy.ts.

Local gates at 187ee49: Prettier, ESLint and the harness typecheck pass on the changed files; the root build passes; the core UI suite passes; the node project passes except two process-timing cases in packages/cli/foundation/release/cache.test.mjs, which pass when that file runs alone (34 of 34), on this head and on main; the harness browser spec passes in Chromium (56 of 56, in two runs) and in WebKit apart from the backward self-test, a flake on main that #8535 fixes.

@vercel

vercel Bot commented Oct 29, 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 Oct 30, 2026 10:22pm UTC

Request Review

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

astracat-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 30, 2026, 10:45 PM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks ready to merge: the control-relative verdict is opt-in, the default busy-window judgment is unchanged, and both new browser cases passed the compositor animation and failed the rAF one in every Chromium and WebKit run. One optional follow-up: in Chromium a frame stamped up to about 50 ms into a block can still show the main thread's last frames, so pooled 70 ms blocks credited the rAF animation with up to 58% of the control's frames (a few single runs fully) even though the pool always rejected it. The pooledBusyProof doc line "in every run the control moves and it does not" could say that one run's numbers don't separate the two, or each run's judged part could start past that lag.

[Automated review]

Comment on lines +795 to +796
* `maxMedianFrameMs`, when given, applies to each run. A main-thread
* animation still fails: in every run the control moves and it does not.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: in Chromium a frame stamped up to ~50 ms after the block starts can still show the main thread's last frames, and a 70 ms block's judged part (20 ms margins) ends at 50 ms. Pooling the self-test's rAF animation 25 times, it changed position inside 149 of 150 blocks and 7 runs credited it with every control frame of the run (pooled credit up to 58%); the pool still failed it every time. This sentence describes the animation rather than what a run reads: worth saying that one run's numbers (runs) don't separate the two and that the pool rejects a main-thread animation on its runs' later frames, or starting each run's judged part past that lag.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: the new sub-step credit lets pooledBusyProof pass a rAF loop that steps a fixed 1 or 2 px per frame, which passed 16 of 20 Chromium pools of six 70 ms blocks that the previous revision rejected, because each run's frozen frames end in a single step, stepPx reads 2 px when a run's judged part shows no change, and one late frame per run fills the pooled four-frame floor. Please bound the credit to runs the component entered already moving under one step a frame (for example, a run at most one frame longer than its previous run, which keeps all 18 busy.control.test.ts cases passing), stop stepPx defaulting to 2 px for a part with no change, and add a pooled frame-counted case to the unit and browser tests, red first. Otherwise the control verdict holds up: compositor animations passed and time-based rAF ones failed in Chromium and WebKit, single and pooled.

[Automated review]

Comment on lines +711 to +714
return (
k < series.length &&
Math.abs(series[k].value - series[i].value) < 1.5 * stepPx
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This credits any run of unchanged frames whose ending change is under 1.5 × stepPx, however long the run and however fast the component moved before it. With stepPx reading 2 px for a judged part that shows no change (line 640) and the pooled new-position floor summed over the runs (line 895), a rAF loop that steps a fixed 1 or 2 px per frame passed pooledBusyProof in 16 of 20 Chromium pools of six 70 ms blocks; the previous revision failed all 10 it judged on the same frames. Crediting a run only when it is at most one frame longer than the component's previous run kept all 18 busy.control.test.ts cases passing and failed those pools 10 of 10.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
…eta#8666 08a2a28) into the web-input drivers helper

The harness files are 8d35612's verified union, byte for byte, plus meta#8535's change to the backward self-test, which this line carries; the two helpers hold the same content apart from that change.

This line's own harness additions leave it, as no open pull request builds on them: the earlier busy-window control and pooled proof (now meta#8906), openStory waiting for Storybook to finish a render, the touch driver's synthesizesClicks and SCRIPTED_CLICK_SKIP, and the wheel-at-DPR-3 self-test.
@vjeux
vjeux marked this pull request as draft October 29, 2026 22:40

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks ready to merge: judged against the control, the busy proof passed the compositor animation and failed both the time-based and the fixed-step rAF loops in Chromium and WebKit, single and pooled, and the fixed-step loop that passed pooled runs on the previous revision now fails every pool. One optional cleanup: BusyProof.stepPx's JSDoc in busy.ts still describes the old position-based step, while the value now comes from the judged part's changes and is 0.5 when there are none (as the README says), and the file header could also name pooledBusyProof.

[Automated review]

Comment on lines +625 to +628
// The cast's position step for the tracked value: the largest of 2, 1 and
// 0.5 CSS px that every change between its consecutive positions in the
// judged part is a multiple of. Changes rather than positions: a position
// the page commits once the block ends can sit off that grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: BusyProof.stepPx's JSDoc (lines 261-265) still says "the largest of 0.5, 1 and 2 that every tracked position is a multiple of". With this change it is the largest dividing every change between consecutive positions in the judged part, and 0.5 when the part shows no change, as the README now says; rewording the field's doc keeps a frozen part's reported 0.5 from reading as the cast's grid.

Comment on lines +7 to +10
* that reports exactly when it ran —, `startBusyControl` — a plain
* compositor-only control animation recorded in the same window — and
* `busyWindowProof`, which reads frame evidence for that window, against
* the control when it has one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: the header's @output now names startBusyControl but not pooledBusyProof, which this file also exports.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: when a loaded runner starts the cast late enough that the 700 ms self-test's block holds the box's turnaround at 1,600 ms, two cast frames that straddle the linear turnaround evenly read the same position and the control verdict fails the correct compositor animation (in Chromium, 2 of 2 runs at one of six one-in-six cast phases: "moved on 6 of the 7 frames the control moved"). Dropping direction: 'alternate' from that animation (harness.interaction.browser.spec.ts:404) so it wraps from 560 px back to 0 passed every phase, and the pooled case's animation alternates the same way. Otherwise the control verdict holds up at this head: compositor animations passed and the time-based and fixed-step rAF loops failed in Chromium and WebKit, single and pooled. Optional: BusyProof.stepPx's JSDoc in busy.ts still describes the old position-based step, and the file header could name pooledBusyProof.

[Automated review]

duration: 1600,
easing: 'linear',
iterations: Infinity,
direction: 'alternate',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

direction: 'alternate' adds a linear turnaround at 1,600 ms. When the cast starts about 0.7–1.4 s after the animation (the slow runner this commit targets), the turnaround lands inside the judged part, and two cast frames that straddle it evenly read the same position; the run they form ends in a full frame's motion, so underStep doesn't credit it and the compositor run fails. In Chromium, with the block about 1 s in, one of six one-in-six cast phases failed 2 of 2 runs ("moved on 6 of the 7 frames the control moved"). Without direction, the endless animation wraps from 560 px back to 0, a change on every frame, and passed all six phases.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: the reworked first busy self-test failed 7 of 7 Chromium runs here, because its rAF run blocks right after cancelling the previous run's still-running compositor animation, so the compositor keeps moving the box into the block and the rAF mutant reads as moving (at one frame in six it even passed the control verdict). Letting a rendering update land between that cancel and the rAF run's block, as the 200 ms wait in the control case does, made it pass 4 of 4 with the mutant frozen.

  • The backdrop case's during: dialog gate is satisfied by the within control's own animation, so its block no longer waits for the backdrop; skipping the harness's control in during (or gating on the ::backdrop animation) restores the wait and lets the WebKit comparison use one gate on both sides.
  • The description still links an earlier compare range and does not mention within or the first self-test's rework.

[Automated review]

Comment on lines +379 to +383
const window = await blockMainThread(
page,
700,
driver === 'compositor' ? {during: box} : {},
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

For the rAF driver this blocks right after the evaluate that cancels the previous run's animation. The compositor run's 1,600 ms animation is still running then (1,050–1,467 ms in, in every replay here), and with no rendering update between the cancel and this block the compositor keeps playing it into the judged part: the rAF mutant read "moved on 9 of its 39 frames" (raf.changed 9 at line 416) and, at one frame in six, passed the control verdict in 2 of 4 replays. With await page.waitForTimeout(200) before this call for the rAF driver, as the control case keeps at line 506, the test passed 4 of 4 with the mutant at 0 changed frames.

pseudoElement: '::backdrop',
});
});
const window = await blockMainThread(page, 600, {during: dialog});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

during counts every running animation in the dialog's subtree (busy.ts:228-235), and within (line 569) put the control's endless animation there, so this gate is satisfied in the first frame it looks rather than once the backdrop runs: in Chromium the block started 5.4–23.4 ms after the dim was created, against 25.6–27.2 ms when the gate counted only the ::backdrop animation. Skipping the harness's own control in during, or gating on the backdrop animation, keeps the hand-off wait and makes the WebKit with/without-within comparison use one gate on both sides.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
…nst a compositor-only control [skip ci]

The 1 s busy proof marked one row and asked that every frame the compositor
presented show it moved. Two things made it fail on a busy machine. Late in
the block the coast moves about a pixel a frame, under the screencast's
step, so a frame delivered early shows no change; and over a whole second
the coast travels further than the column shows, so the marked row was out
of sight at one end, turned and faded at the other.

It now follows markers that ride the strip itself, every three rows in three
colours, placed before the flick so the block meets them already on screen.
They move with the coast and never turn or fade, and each is judged while
within reach of the band; together they cover the block. The judgment is
busyWindowProof's against a compositor-only control recorded in the same
frames (meta#8906): the coast moves on every frame the control moves, a run
under one step that a single step ends counts as moved, and its longest
stall is no longer than the control's.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: the reworked first busy self-test fails in Chromium, the browser CI runs it in, because its rAF run blocks right after cancelling the previous run's still-running 1,600 ms compositor animation, so the compositor keeps moving the box into the block and raf.changed reads 6 to 8 against the <= 1 assertion (the base version passes). Letting a rendering update land before that block removed every changed frame in replays here; keeping await page.waitForTimeout(200) there, as the base version and the control case do, is one way. In the same push, please make blockMainThread's during and waitForAnimation skip the harness's own control, or say in the within docs that a backdrop proof must wait on the backdrop's own animation, since a control mounted within a dialog satisfies both gates on a dialog that animates nothing else.

[Automated review]

Comment on lines +384 to +388
const window = await blockMainThread(
page,
blockMs,
driver === 'compositor' ? {during: box} : {},
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

For the rAF driver this blocks right after the evaluate that cancels the previous run's animation. The compositor run's 1,600 ms animation is still running then (1,417 to 1,467 ms in, in every replay here), and with no rendering update between that cancel and this block the compositor keeps playing it into the block: the rAF mutant's marker kept moving until 146 to 179 ms into the block, raf.changed read 6 to 8, and line 421 failed in every Chromium run. Awaiting two animation frames before this block left the mutant with 0 changed frames in every replay, and the base version, which waited 200 ms before its block as the control case still does at line 516, passed every run.

document.body.appendChild(box);
box.showPopover();
} else {
host.appendChild(box);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With within, the control's endless animation lives inside the dialog, so blockMainThread's during: dialog (lines 363-365: "Any animation inside during counts") and waitForAnimation(dialog) both count it: on a dialog that animates nothing else, both returned at once in Chromium and WebKit, where with the control in the top layer both reject "no animation started". A backdrop proof that follows the README's recipe can therefore start its block before its dim runs. Skipping [data-harness-control] animations in both gates, or saying in the within docs that the proof must wait on the backdrop's own animation, would close it.

vjeux added 3 commits October 30, 2026 14:15
…against a compositor-only control

A busy-window proof judged a compositor animation against a display-rate bound. On a two-core or loaded runner the compositor presents fewer frames than that bound allows, though the animation moves on each one, so Lightbox's double-tap zoom missed the bound in 7 of 10 two-core runs.

startBusyControl(page) mounts a small top-layer box that the compositor animates, tracked from the same screencast. busyWindowProof(frames, window, {control}) passes when the component moves on every frame the control moves and never stalls longer. The frames are the same, so there is no tolerance. One exception: on an easing tail, unchanged frames count as moved when one cast step ends the run and it is at most a frame longer than the run before. At least four control frames must move.

- pooledBusyProof(runs) judges short runs together.
- {within: dialog} mounts the control inside an open dialog, for backdrop proofs.
- A hold that starts before the block is judged from its first presented change inside it. WebKit presents a just-started compositor animation 130 to 300 ms late, and its cast reaches Node about 0.7 s late on a loaded runner, too late to wait on. Every frame after that change is judged, so a freeze later in the block still fails.
- Main's first busy self-test judges against the control too.
…pdate land before its rAF block, and the gates never count the busy control's own animation

astracat found two gaps on 1ca51a4.

1. The rAF run blocked right after cancelling the compositor run's still-running animation. With no rendering update between the cancel and the block, the compositor kept playing the cancelled animation into the block, so the rAF mutant read 6 to 8 changed frames. The run now waits two animation frames before blocking. A probe that cancels and then blocks at once kept moving in 1 of 8 Chromium runs here (27 changed frames); with two frames first, 0 of 8.

2. Inside a dialog (`within`), the control's endless animation answered `waitForAnimation(dialog)` and `blockMainThread`'s `during: dialog`, so a backdrop proof could block before its dim ran. Both gates now skip any animation inside `[data-harness-control]`.

Red first: on a dialog that animates nothing else, both gates resolved at once. Now both keep waiting until the dialog's own animation runs, 6 of 6 in Chromium and in WebKit.
…inst the busy control

"A block that starts during a just-started transition" and "a block never starts on an animation that is not on the compositor yet" still judged their compositor animation against the display-rate bound alone. On a loaded runner that bound fails a component that moved on every frame. The second one failed this way on a gate run of meta#8911 (WebKit, 8 workers: "43/43 moved, longest stall 52 ms"), and in 3 of 16 Chromium runs at 16 workers ("31/31 moved, longest stall 169 ms").

Both now start the busy control before the cast and judge against what the compositor presented in the same window. Both pass 32 of 32 per engine at 16 workers. Their intent is unchanged: a block in the frame the transition has run 16 ms, and a block never on a pending animation, each still fails if the compositor's motion stalls longer than the control's.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
meta#8360's change on the internal touch foundation: one click guard shared
by the menu press and the gesture host (its click listener stays once
the menu press arms it, as its record has it, and lasts only while armed
for the gesture host, FR24), the host as the one touch owner (finger
scroll, page holds, attachments, the press tracker hook and a held
control's kept touch, the long press's single source, presented hit
testing on iOS WebKit), and the gesture browser proof in Chromium and
WebKit with the README. It carries meta#8074, meta#8270, meta#8293, meta#8315, meta#8320 and
meta#8356, and folds in meta#8358 (the long press's tolerance comment and an
unused import). Rebased onto section 2 as one commit; the touch-action
rule now comes with meta#8071, and meta#8072's claimed-touchend fix, which this
line never had, comes back with meta#8309.

The animator's busy proof is judged against the harness's compositor-only
control (meta#8906), with a main-thread mutant as its negative, and the
spring's curve is placed against the control's own clock, so the late
frames a loaded runner sends fail nothing.

With the host serving every pointer (meta#8071), this change's own pieces
follow it: a press tracker takes a mouse press as a press, painting at
once and braking no scroll (FR11); a finger's or a pen's hold that does
something keeps text unselected under any primary pointer, while a mouse
keeps its released behavior (FR41); a claimed pen's stylus touchend is
cancelled as a finger's (FR21); and `perEventPointer`, from before FR43,
changes nothing. These come from meta#8665, which keeps only the undecided
rule for text selection during a mouse or pen drag.

The hover recognizer and the host's hover routing land with hover's
first user.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
@vjeux
vjeux force-pushed the harness/busy-window-control branch from 1ca51a4 to 187ee49 Compare October 30, 2026 22:14
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

No new or modified components detected.

Bundle Size Summary

No component packages changed.

Accessibility Audit

Status: The accessibility audit did not run on this pull request. It runs after merges to main and before each release; pull requests that touch components run it with the ci:heavy-lanes label.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks good to merge: judged against the control, every busy self-test passes the compositor animation and fails the script-driven ones in Chromium and WebKit, and the first self-test that failed on the previous revision now passes. Two optional follow-ups: a control mounted within a dialog still makes waitForRest(dialog) reject "still moving" and is paused by holdAnimations(page, dialog), so either skip [data-harness-control] there as during and waitForAnimation now do or say that a backdrop proof removes the control first; and a hold excused before the block is summarized as "nothing moved before the block", which the summary and onScreenFromMs's doc could name instead.

[Automated review]

@@ -554,6 +895,9 @@ export function busyWindowProof(
(onScreenFromMs == null
? ''
: `, on screen from +${onScreenFromMs.toFixed(0)} ms (nothing moved before the block)`) +

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This label now also covers a hold that started before the block (lines 583-593), where the element did move first: frames held from 120 ms before a 700 ms block to +600 ms pass and read "on screen from +600 ms (nothing moved before the block)". Naming the excused hold here and in onScreenFromMs's doc (lines 289-295), for example "held from -120 ms, judged from +600 ms", would keep the annotation accurate.

};
// The busy-window control (busy.ts) animates on its own, inside a
// dialog with `within`: it never answers for the component.
const isControl = (a: Animation) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

waitForRest (line 1185) and holdAnimations (line 368) still count a within control: on a still dialog hosting one, waitForRest(dialog) rejects "still moving" and holdAnimations(page, dialog) pauses the control, in Chromium and WebKit. Sharing this predicate with them, or saying in the within docs that the control is removed first, would close the gap.

@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Oct 31, 2026
@cixzhang
cixzhang marked this pull request as ready for review October 31, 2026 00:44
@cixzhang
cixzhang merged commit 8bb30b5 into meta:main Oct 31, 2026
98 checks passed
@vjeux
vjeux deleted the harness/busy-window-control branch October 31, 2026 01:07
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
meta#8360's change on the internal touch foundation: one click guard shared
by the menu press and the gesture host (its click listener stays once
the menu press arms it, as its record has it, and lasts only while armed
for the gesture host, FR24), the host as the one touch owner (finger
scroll, page holds, attachments, the press tracker hook and a held
control's kept touch, the long press's single source, presented hit
testing on iOS WebKit), its cases in the gesture browser proof (Chromium
and WebKit) and the README. It carries meta#8074, meta#8270, meta#8293, meta#8315, meta#8320
and meta#8356, and folds in meta#8358 (the long press's tolerance comment and an
unused import). Rebased onto section 2 as one commit; the touch-action
rule now comes with meta#8071, and meta#8072's claimed-touchend fix, which this
line never had, comes back with meta#8309.

The animator's busy proof is judged against the harness's compositor-only
control (meta#8906), with a main-thread mutant as its negative, and the
spring's curve is placed against the control's own clock, so the late
frames a loaded runner sends fail nothing.

With the host serving every pointer (meta#8071), this change's own pieces
follow it: a press tracker takes a mouse press as a press, painting at
once and braking no scroll (FR11); a finger's or a pen's hold that does
something keeps text unselected under any primary pointer, while a mouse
keeps its released behavior (FR41); and `perEventPointer`, from before
FR43, changes nothing. These come from meta#8665, which keeps only the
undecided rule for text selection during a mouse or pen drag.

The hover recognizer and the host's hover routing land with hover's
first user.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
meta#8360's change on the internal touch foundation: one click guard shared
by the menu press and the gesture host (its click listener stays once
the menu press arms it, as its record has it, and lasts only while armed
for the gesture host, FR24), the host as the one touch owner (finger
scroll, page holds, attachments, the press tracker hook and a held
control's kept touch, the long press's single source, presented hit
testing on iOS WebKit), its cases in the gesture browser proof (Chromium
and WebKit) and the README. It carries meta#8074, meta#8270, meta#8293, meta#8315, meta#8320
and meta#8356, and folds in meta#8358 (the long press's tolerance comment and an
unused import). Rebased onto main after section 2 as one commit; the
touch-action rule came with meta#8071. Where fingers come from their pointer
events, a claimed finger's touchend is cancelled from its element
(FR21), under a fine primary pointer wherever the browser sends Touch
Events and inside an open shadow root too.

The animator's busy proof is judged against the harness's compositor-only
control (meta#8906), with a main-thread mutant as its negative, and the
spring's curve is placed against the control's own clock, so the late
frames a loaded runner sends fail nothing.

With the host serving every pointer (meta#8071), this change's own pieces
follow it: a press tracker takes a mouse press as a press, painting at
once and braking no scroll (FR11); a finger's or a pen's hold that does
something keeps text unselected under any primary pointer, while a mouse
keeps its released behavior (FR41); and `perEventPointer`, from before
FR43, changes nothing. These come from meta#8665, which keeps only the
undecided rule for text selection during a mouse or pen drag.

The hover recognizer and the host's hover routing land with hover's
first user.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
meta#8360's change on the internal touch foundation: one click guard shared
by the menu press and the gesture host (its click listener stays once
the menu press arms it, as its record has it, and lasts only while armed
for the gesture host, FR24), the host as the one touch owner (finger
scroll, page holds, attachments, the press tracker hook and a held
control's kept touch, the long press's single source, presented hit
testing on iOS WebKit), its cases in the gesture browser proof (Chromium
and WebKit) and the README. It carries meta#8074, meta#8270, meta#8293, meta#8315, meta#8320
and meta#8356, and folds in meta#8358 (the long press's tolerance comment and an
unused import). Rebased onto main after section 2 as one commit; the
touch-action rule came with meta#8071. Where fingers come from their pointer
events, a claimed finger's touchend is cancelled from its element
(FR21), under a fine primary pointer wherever the browser sends Touch
Events and inside an open shadow root too; at most eight such lifts wait
for their touchend, and each goes after 500 ms or with the host (FR24).

The animator's busy proof is judged against the harness's compositor-only
control (meta#8906), with a main-thread mutant as its negative, and the
spring's curve is placed against the control's own clock, so the late
frames a loaded runner sends fail nothing.

With the host serving every pointer (meta#8071), this change's own pieces
follow it: a press tracker takes a mouse press as a press, painting at
once and braking no scroll (FR11); a finger's or a pen's hold that does
something keeps text unselected under any primary pointer, while a mouse
keeps its released behavior (FR41); and `perEventPointer`, from before
FR43, changes nothing. These come from meta#8665, which keeps only the
undecided rule for text selection during a mouse or pen drag.

The hover recognizer and the host's hover routing land with hover's
first user.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
meta#8360's change on the internal touch foundation: one click guard shared
by the menu press and the gesture host (its click listener stays once
the menu press arms it, as its record has it, and lasts only while armed
for the gesture host, FR24), the host as the one touch owner (finger
scroll, page holds, attachments, the press tracker hook and a held
control's kept touch, the long press's single source, presented hit
testing on iOS WebKit), its cases in the gesture browser proof (Chromium
and WebKit) and the README. It carries meta#8074, meta#8270, meta#8293, meta#8315, meta#8320
and meta#8356, and folds in meta#8358 (the long press's tolerance comment and an
unused import). Rebased onto main after section 2 as one commit; the
touch-action rule came with meta#8071. Where fingers come from their pointer
events, a claimed finger's touchend is cancelled from its element
(FR21), under a fine primary pointer wherever the browser sends Touch
Events and inside an open shadow root too; at most eight such lifts wait
for their touchend, and each goes after 500 ms or with the host (FR24).

The animator's busy proof is judged against the harness's compositor-only
control (meta#8906), with a main-thread mutant as its negative, and the
spring's curve is placed against the control's own clock, so the late
frames a loaded runner sends fail nothing.

With the host serving every pointer (meta#8071), this change's own pieces
follow it: a press tracker takes a mouse press as a press, painting at
once and braking no scroll (FR11); a finger's or a pen's hold that does
something keeps text unselected under any primary pointer, while a mouse
keeps its released behavior (FR41); and `perEventPointer`, from before
FR43, changes nothing. These come from meta#8665, which keeps only the
undecided rule for text selection during a mouse or pen drag.

The hover recognizer and the host's hover routing land with hover's
first user.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
meta#8360's change on the internal touch foundation: one click guard shared
by the menu press and the gesture host (its click listener stays once
the menu press arms it, as its record has it, and lasts only while armed
for the gesture host, FR24), the host as the one touch owner (finger
scroll, page holds, attachments, the press tracker hook and a held
control's kept touch, the long press's single source, presented hit
testing on iOS WebKit), its cases in the gesture browser proof (Chromium
and WebKit) and the README. It carries meta#8074, meta#8270, meta#8293, meta#8315, meta#8320
and meta#8356, and folds in meta#8358 (the long press's tolerance comment and an
unused import). Rebased onto main after section 2 as one commit; the
touch-action rule came with meta#8071. Where fingers come from their pointer
events, a claimed finger's touchend is cancelled from its element
(FR21), under a fine primary pointer wherever the browser sends Touch
Events and inside an open shadow root too; at most eight such lifts wait
for their touchend, and each goes once taken or after 500 ms, kept
across the host's uninstall since the touchend comes in a later task
(FR24).

The animator's busy proof is judged against the harness's compositor-only
control (meta#8906), with a main-thread mutant as its negative, and the
spring's curve is placed against the control's own clock, so the late
frames a loaded runner sends fail nothing.

With the host serving every pointer (meta#8071), this change's own pieces
follow it: a press tracker takes a mouse press as a press, painting at
once and braking no scroll (FR11); a finger's or a pen's hold that does
something keeps text unselected under any primary pointer, while a mouse
keeps its released behavior (FR41); and `perEventPointer`, from before
FR43, changes nothing. These come from meta#8665, which keeps only the
undecided rule for text selection during a mouse or pen drag.

The hover recognizer and the host's hover routing land with hover's
first user.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
meta#8360's change on the internal touch foundation: one click guard shared
by the menu press and the gesture host (its click listener stays once
the menu press arms it, as its record has it, and lasts only while armed
for the gesture host, FR24), the host as the one touch owner (finger
scroll, page holds, attachments, the press tracker hook and a held
control's kept touch, the long press's single source, presented hit
testing on iOS WebKit), its cases in the gesture browser proof (Chromium
and WebKit) and the README. It carries meta#8074, meta#8270, meta#8293, meta#8315, meta#8320
and meta#8356, and folds in meta#8358 (the long press's tolerance comment and an
unused import). Rebased onto main after section 2 as one commit; the
touch-action rule came with meta#8071. Where fingers come from their pointer
events, a claimed finger's touchend is cancelled from its element
(FR21), under a fine primary pointer wherever the browser sends Touch
Events and inside an open shadow root too; at most eight such lifts wait
for their touchend, and each goes once taken or after 500 ms, kept
across the host's uninstall, which can run in the microtask after the
pointerup, before the touchend that comes later in the same task (FR24).
An armed press's page listeners go once the arena has decided its last
touch, a tap that waits on a double tap included, and after its last
detach the host holds no listener, note, frame or timer (FR24).

The animator's busy proof is judged against the harness's compositor-only
control (meta#8906), with a main-thread mutant as its negative, and the
spring's curve is placed against the control's own clock, so the late
frames a loaded runner sends fail nothing.

With the host serving every pointer (meta#8071), this change's own pieces
follow it: a press tracker takes a mouse press as a press, painting at
once and braking no scroll (FR11); a finger's or a pen's hold that does
something keeps text unselected under any primary pointer, while a mouse
keeps its released behavior (FR41); and `perEventPointer`, from before
FR43, changes nothing. These come from meta#8665, which keeps only the
undecided rule for text selection during a mouse or pen drag.

The hover recognizer and the host's hover routing land with hover's
first user.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
meta#8360's change on the internal touch foundation: one click guard shared
by the menu press and the gesture host (its click listener stays once
the menu press arms it, as its record has it, and lasts only while armed
for the gesture host, FR24), the host as the one touch owner (finger
scroll, page holds, attachments, the press tracker hook and a held
control's kept touch, the long press's single source, presented hit
testing on iOS WebKit), its cases in the gesture browser proof (Chromium
and WebKit) and the README. It carries meta#8074, meta#8270, meta#8293, meta#8315, meta#8320
and meta#8356, and folds in meta#8358 (the long press's tolerance comment and an
unused import). Rebased onto main after section 2 as one commit; the
touch-action rule came with meta#8071. Where fingers come from their pointer
events, a claimed finger's touchend is cancelled from its element
(FR21), under a fine primary pointer wherever the browser sends Touch
Events and inside an open shadow root too; at most eight such lifts wait
for their touchend, and each goes once taken or after 500 ms, kept
across the host's uninstall, which can run in the microtask after the
pointerup, before the touchend that comes later in the same task (FR24).
An armed press's page listeners go once the arena has decided its last
touch, a tap that waits on a double tap included, and after its last
detach the host holds no listener, note, frame or timer (FR24).

The animator's busy proof is judged against the harness's compositor-only
control (meta#8906), with a main-thread mutant as its negative, and the
spring's curve is placed against the control's own clock, so the late
frames a loaded runner sends fail nothing.

With the host serving every pointer (meta#8071), this change's own pieces
follow it: a press tracker takes a mouse press as a press, painting at
once and braking no scroll (FR11); a finger's or a pen's hold that does
something keeps text unselected under any primary pointer, while a mouse
keeps its released behavior (FR41); and `perEventPointer`, from before
FR43, changes nothing. These come from meta#8665, which keeps only the
undecided rule for text selection during a mouse or pen drag.

The hover recognizer and the host's hover routing land with hover's
first user.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>

This branch was successfully deployed

1 active deployment
Preview — 187ee491 Deployed Oct 30, 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