Skip to content

[scheduler] Absorb un-acted ResizeObserver deliveries after rendering the timeline - #23444

Open
rita-codes wants to merge 8 commits into
mui:masterfrom
rita-codes:scheduler-shared-observer-absorb
Open

[scheduler] Absorb un-acted ResizeObserver deliveries after rendering the timeline#23444
rita-codes wants to merge 8 commits into
mui:masterfrom
rita-codes:scheduler-shared-observer-absorb

Conversation

@rita-codes

@rita-codes rita-codes commented Aug 28, 2026

Copy link
Copy Markdown
Member

Fixes the recurring test_browser_react_18 failures in the scheduler browser suites: the dependency drag suites have been red on every branch since #23441 landed, plus the header act-warning flake.

Why it flakes: the timeline measures itself with ResizeObservers. The browser delivers those callbacks on the frames after the render, and each delivery sets React state. Whether that update lands inside an act() window depends on runner timing — when it doesn't, React warns, and vitest-fail-on-console turns the warning into a failure. The race predates #23441; that PR added the first tests shaped to lose it almost every run (perturb layout, assert synchronously, end — nothing act-covered follows), and only the React 18 scheduler leaves that window uncovered, which is why the other jobs stay green.

The fix: drain the pending deliveries inside act before the test moves on, so they can never land un-acted.

  • absorbObserverFrames() (new, in test/utils/scheduler): waits two native frames inside act. The rAF is captured at module load so fake-timer tests can't stall it — and it hard-fails with a descriptive error if a test ever leaks fake timers into module collection, instead of hanging. In jsdom (no ResizeObserver) it flushes pending microtasks inside act, so async work started by the render (e.g. a data source fetch) lands acted in both environments.
  • renderSettled on createSchedulerRenderer (render + absorb in one call) is the single render path in the adopted suites; explicit absorbObserverFrames() calls remain only where a render can't be awaited (toErrorDev) or after scrolls that mount observed elements.
  • Adopted in every premium suite that mounts the timeline in browser mode, including all scroll sites in the dependency suites and their dual-timeline helper — the whole class is closed, not just the file that flaked. The conformance suite is jsdom-only instead: its sync render driver can't settle, and its checks are environment-agnostic.
  • The calendar also mounts ResizeObservers and can adopt renderSettled the day it flakes.

Verified deterministically, not by rerunning until green: a probe forcing the delivery to land outside act (two raw frames after render) reproduced the exact CI error 3/3 runs; with absorbObserverFrames() first it passed 3/3. That probe is committed as a regression test (absorbObserverFrames.test.tsx) — if the absorb ever stops draining the queue, the console guard fails it automatically — alongside pins for the fake-timers and jsdom branches.

The title-column and viewport observers deliver on the frames after the
render, outside act; depending on runner timing the un-acted update
fails the run through the console guard (the recurring
test_browser_react_18 header failure). The absorb helper waits two
native frames inside act — rAF captured at module load so fake timers
cannot stall it — and now lives in test/utils/scheduler for any suite
rendering the timeline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDEgka4iDRFrenWeWFhunq
@rita-codes rita-codes added test scope: scheduler Changes related to the scheduler. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. labels Aug 28, 2026
@rita-codes rita-codes self-assigned this Aug 28, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Aug 28, 2026

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-23444--material-ui-x.netlify.app/
QR code for https://deploy-preview-23444--material-ui-x.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)
@mui/x-scheduler 0B(0.00%) 0B(0.00%)
@mui/x-scheduler-premium 0B(0.00%) 0B(0.00%)
@mui/x-chat 0B(0.00%) 0B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@rita-codes rita-codes changed the title [test][scheduler] Absorb un-acted ResizeObserver deliveries after rendering the timeline [scheduler] Absorb un-acted ResizeObserver deliveries after rendering the timeline Aug 28, 2026
rita-codes and others added 4 commits August 28, 2026 13:31
Every suite mounting the timeline in browser mode is exposed to the
same un-acted delivery; renderSettled packages render + absorb in the
shared renderer so adopting it is one call, and the seven remaining
timeline suites now settle their renders. Drag/scroll perturbations
keep the explicit absorbObserverFrames calls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDEgka4iDRFrenWeWFhunq
The dependency harness still rendered synchronously with no absorb, so
the arrows and terminals suites were failing test_browser_react_18 on
CI for every branch (build 902915): the muting drag tests landed with
master through mui#23441, but their absorb harness only exists on the
auto-scheduling branch. The harness now settles its renders and the
drag/scroll steps absorb explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDEgka4iDRFrenWeWFhunq
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

…observer-absorb

# Conflicts:
#	packages/x-scheduler-premium/src/event-timeline-premium/EventTimelinePremium.test.tsx
#	packages/x-scheduler-premium/src/event-timeline-premium/tests/EventDialog.test.tsx
#	packages/x-scheduler-premium/src/event-timeline-premium/tests/dependencyTerminals.EventTimelinePremium.test.tsx
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Sep 1, 2026

@noraleonte noraleonte 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.

LGTM 🎉 Thanks for fixing it 🙏

* Renders and absorbs the post-render ResizeObserver deliveries (see
* `absorbObserverFrames`), so browser-mode tests start from an acted, settled
* layout. Use it for anything mounting a scheduler surface.
*/

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.

Nit:
Nothing stops the flake from coming back - createSchedulerRenderer now exposes both render and renderSettled, and a new timeline test reaching for render silently reintroduces the race.
We can't just make render absorb (conformance needs the sync one), but a one-line comment on render in createSchedulerRenderer.tsx pointing at renderSettled for timeline surfaces — or an eslint no-restricted-syntax scoped to event-timeline-premium/ — would make the invariant enforceable rather than tribal.

* Waits two native frames inside act, so pending ResizeObserver deliveries land as
* acted updates instead of between test steps. Call it after rendering a scheduler
* surface (prefer `renderSettled`) or after a scroll that mounts observed elements.
* No-op in jsdom, which has no ResizeObserver.

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.

Claude highlighted this: the branch below awaits act(async () => {}) and flushes pending microtasks — the inline comment says so directly. That's a real behavior change applied after every render across ~13 jsdom suites, not a no-op. The behavior looks deliberate and useful; we should just fix the doc line so nobody debugging jsdom timing is misled by it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: scheduler Changes related to the scheduler. test type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants