Skip to content

[test] Stabilize Data Grid demo data in Argos screenshots#48654

Merged
LukasTy merged 11 commits into
mui:masterfrom
LukasTy:claude/argos-stable-grid-data
Jun 16, 2026
Merged

[test] Stabilize Data Grid demo data in Argos screenshots#48654
LukasTy merged 11 commits into
mui:masterfrom
LukasTy:claude/argos-stable-grid-data

Conversation

@LukasTy

@LukasTy LukasTy commented Jun 10, 2026

Copy link
Copy Markdown
Member

Problem

#48589 added Argos coverage for the product landing-page composites, but several render non-deterministically, so their screenshots diffed on every Argos run (e.g. build 49389). Three independent causes:

  1. Random Data Grid data -- XHero, XGridFullDemo, XTheming use useDemoData from @mui/x-data-grid-generator, which generates random rows per page visit.
  2. Loading skeleton captured -- useDemoData loads rows asynchronously; the screenshot's aria-busy gate only tracks fonts, so the grid could be captured mid-load showing its skeleton.
  3. MaterialStyling scroll -- it scrolls its code panel on mount with behavior: 'smooth', captured mid-scroll at a different offset each run.

Fixes

1. Deterministic grid data (vite.config.mts) -- a Vite define sets __DISABLE_CHANCE_RANDOM__ to true, the token @mui/x-data-grid-generator checks to seed its Chance instances (new Chance(() => 0.5) / new Chance(42)). Same mechanism mui-x's regression bundle uses. Verified the built chunk resolves to the seeded branch, so the define reaches the dependency in node_modules. With a constant () => 0.5 source every Chance call returns the same value, so data is identical across loads regardless of render order; the Math.random seed-extension path (only XHero at rowLength: 10000) copies from seed rows whose columns are now all identical, so it doesn't affect output either -- which is why mui-x never needed to seed Math.random.

2. Wait for grid rows (demoMeta.ts) -- a waitForSelector: '.MuiDataGrid-row .MuiDataGrid-cell' rule for the three grid composites gates the screenshot on a real data cell (the skeleton uses .MuiDataGrid-skeletonRow).

3. Respect prefers-reduced-motion (MaterialStyling.tsx) -- gate the mount scroll on matchMedia('(prefers-reduced-motion: reduce)') ('auto' when reduced), matching the existing AppLayout/components/BackToTop.tsx pattern. An explicit JS behavior: 'smooth' isn't governed by prefers-reduced-motion (only CSS scroll-behavior is), so this both respects the preference in the live docs and -- because the regression runner emulates reduced-motion: reduce -- makes the screenshot deterministic. Fixes the issue at the source rather than patching the scroll API in the test harness.

Test plan

  • pnpm eslint test/regressions/, pnpm prettier --check, pnpm typescript (all 19 projects incl. docs), build (9 composites) -- all green.
  • Built chunk verified: seeded Chance branch present; prefers-reduced-motion check present in MaterialStyling.
  • demoMeta.test.ts (9/9); rule resolution verified (grid composites get width 1440 + waitForSelector; others width only).
  • Argos baseline stable across no-op reruns -- confirm the grid composites and MaterialStyling no longer diff.

🤖 Generated with Claude Code

The product composites that render a Data Grid (XHero, XGridFullDemo,
XDataGrid, XTheming via `useDemoData` from `@mui/x-data-grid-generator`)
generated random rows on every page visit, so their Argos screenshots
diffed on every run (e.g. build 49389).

Seed the generator's Chance instances deterministically via a Vite
`define` that sets `__DISABLE_CHANCE_RANDOM__` to `true` — the same token
`@mui/x-data-grid-generator` checks at module scope to switch from
`new Chance()` to `new Chance(() => 0.5)` / `new Chance(42)`. This is the
mechanism mui-x's regression bundle uses. With a constant `() => 0.5`
source every Chance call returns the same value, so the generated columns
are identical across loads regardless of render order; the `Math.random`
seed-extension path (only hit by XHero's `rowLength: 10000`) copies from
seed rows whose columns are now all identical, so it no longer affects
the output either.

Verified the built bundle resolves to the seeded branch
(`new Chance(() => .5)` / `new ...(42)`), so the `define` reaches the
dependency in node_modules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@code-infra-dashboard

code-infra-dashboard Bot commented Jun 10, 2026

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-48654--material-ui.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/material 0B(0.00%) 0B(0.00%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


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

The `aria-busy` gate only tracks font loading, not `useDemoData`'s async
row generation, so XHero/XGridFullDemo/XTheming could be screenshotted
while their Data Grid still showed the loading skeleton — a source of
Argos diffs. Add a `waitForSelector` for a real data cell
(`.MuiDataGrid-row .MuiDataGrid-cell`; the skeleton uses
`.MuiDataGrid-skeletonRow`). Each rule restates the productX width since
rules are last-match-wins.

XDataGrid isn't in the composite allowlist, so it needs no rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zannager zannager added the test label Jun 12, 2026
@zannager zannager requested a review from Janpot June 12, 2026 15:50
Comment thread test/regressions/disableSmoothScroll.ts Outdated
@@ -0,0 +1,44 @@
// Force programmatic scrolling to be instant so screenshots are deterministic.
//
// Some composites scroll on mount with `{ behavior: 'smooth' }` (e.g.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤔 Doesn't smooth scroll not respect reduced motion?

`MaterialStyling` scrolled its code panel on mount with
`behavior: 'smooth'` unconditionally. An explicit JS `behavior: 'smooth'`
isn't governed by `prefers-reduced-motion` (only CSS `scroll-behavior`
is), so it animated even for users who asked to reduce motion — and it
made the Argos screenshot non-deterministic (captured mid-scroll at a
varying offset).

Gate the behavior on `matchMedia('(prefers-reduced-motion: reduce)')`,
matching the existing pattern in `AppLayout/components/BackToTop.tsx`.
This respects the preference in the live docs and, because the
regression runner emulates `reduced-motion: reduce`, also makes the
screenshot deterministic — so the test-harness scroll patch from the
previous revision is dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@LukasTy LukasTy force-pushed the claude/argos-stable-grid-data branch from e67d321 to 4cb681f Compare June 15, 2026 13:15
LukasTy and others added 3 commits June 15, 2026 16:20
The previous `waitForSelector: '.MuiDataGrid-row .MuiDataGrid-cell'`
wasn't enough: the loading overlay renders above the rows (zIndex 5), so
the selector matched while the spinner was still painted on top — XHero
(`rowLength: 10000`) renders its first rows but keeps the overlay up
during seed extrapolation, and its Argos screenshot still showed the
spinner.

Add a `waitForSelectorDetached` rule field and wait for
`.MuiDataGrid-overlayWrapper` to detach (it unmounts when `loading`
clears) for the three grid composites. Also scope both waits to the
route's testcase element instead of the whole page, so a pooled page's
leftover DOM from the previous route can't satisfy the wait early.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`__DISABLE_CHANCE_RANDOM__` makes `@mui/x-data-grid-generator` seed its
data `Chance` with `new Chance(() => 0.5)` — a constant source, so every
generated row is identical (same name/commodity/quantity, differing only
by id). Deterministic, but the grid demos look boring and unrealistic.

Add a Vite transform that rewrites that one seed to `new Chance(42)`, a
fixed integer that yields a varied-but-deterministic sequence. Verified
the rows now differ across rows and stay byte-identical across runs.

Safe for the regression runner: each composite renders on its own
browser page (its own module-scoped `chance`) and does a single
generation pass via `useDemoData`, so concurrent grids never interleave
the shared sequence. The transform throws if upstream reformats the seed
line, so it can't silently regress to constant data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread test/regressions/index.test.js Outdated
await page.waitForSelector(screenshotRule.waitForSelector);
await testcase.waitForSelector(screenshotRule.waitForSelector);
}
if (screenshotRule?.waitForSelectorDetached) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is an extra option really needed? Looks like we can just waitForSelector on some cell content in the grid. waitForSelectorDetached feels wrong as it looks like technically it could also fire before react has the chance to render the loading overlay. Looks like this could be another source of flakyness in a way that waitForSelector could never be.

@Janpot Janpot Jun 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Otherwise, even if we do want a negative selector, perhaps it could be something like

 waitForSelector: '.MuiDataGrid-root:not(:has(.MuiDataGrid-overlayWrapper))'

?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for observing this. I backtracked the solution.
AFAIK, X relies on targeting specific cell.
Trying it out. Two demos are still captured with loaders, will refine it to ensure screenshots are captured after data is loaded

LukasTy and others added 2 commits June 15, 2026 17:21
Addresses @Janpot: `waitForSelectorDetached` was both an unnecessary
extra option and a potential race — it could fire before React has even
rendered the loading overlay (at first paint the overlay isn't mounted
yet, so "detached" is trivially true).

`useDemoData` sets the rows and clears `loading` in the same tick, so a
real `.MuiDataGrid-row` cell only exists once loading has finished (the
skeleton uses `.MuiDataGrid-skeletonRow`). So a plain
`waitForSelector: '.MuiDataGrid-row .MuiDataGrid-cell'` is sufficient and
strictly safer — it can only resolve once, when content is present.
Reverts the custom rule field and its runner branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LukasTy and others added 2 commits June 15, 2026 17:44
The wait `.MuiDataGrid-row .MuiDataGrid-cell` matched the loading
skeleton, not just real data: a skeleton cell composes *both*
`.MuiDataGrid-cell` and `.MuiDataGrid-cellSkeleton`, and a skeleton row
both `.MuiDataGrid-row` and `.MuiDataGrid-rowSkeleton` (additive classes,
not separate ones). So the wait resolved against the skeleton and the
screenshot captured the loading state — XTheming showed the skeleton and
XHero (10000 rows, slower) showed an empty grid.

Exclude skeleton rows: `.MuiDataGrid-row:not(.MuiDataGrid-rowSkeleton)
.MuiDataGrid-cell`. This only matches a cell once real data rows have
rendered (`useDemoData` sets rows and clears loading in the same tick).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@Janpot Janpot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice

@LukasTy LukasTy added the type: regression A bug, but worse, it used to behave as expected. label Jun 16, 2026
@LukasTy LukasTy self-assigned this Jun 16, 2026
@LukasTy LukasTy force-pushed the claude/argos-stable-grid-data branch from a3be9eb to 57266bf Compare June 16, 2026 09:01
XHero (`rowLength: 10000`) intermittently timed out the 30s grid wait in
CI: `useDemoData`'s `asyncWorker` chunks row generation across
`requestIdleCallback`, and under CI load those idle callbacks get
starved, so the 9000-row `extrapolateSeed` pass occasionally never
finished before the screenshot wait. The lighter grids (<=1000 rows)
weren't affected.

`asyncWorker` already has a synchronous branch, taken when
`requestIdleCallback.clock` is set — the affordance mui-x gets from
sinon fake timers (`test/utils/setupFakeClock`). Use sinon's `toFake` to
fake *only* `requestIdleCallback`: it still gets the `.clock` tag that
flips `asyncWorker` to its synchronous branch, while `Date` (handled by
`fakeDateSetup`), `setTimeout`, rAF, etc. stay real. Rows are now built
before first paint, eliminating both the skeleton race and the
starvation timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@LukasTy LukasTy force-pushed the claude/argos-stable-grid-data branch from 57266bf to f819779 Compare June 16, 2026 10:31
@LukasTy LukasTy enabled auto-merge (squash) June 16, 2026 10:34
@LukasTy LukasTy disabled auto-merge June 16, 2026 12:06
@LukasTy LukasTy merged commit 55cb1e6 into mui:master Jun 16, 2026
22 checks passed
@LukasTy LukasTy deleted the claude/argos-stable-grid-data branch June 16, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test type: regression A bug, but worse, it used to behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants