[test] Cover docs landing-page composites with Argos#48589
Conversation
Add Argos screenshot coverage for the ~27 bespoke composites under `docs/src/components/product*/*.tsx` that assemble the product landing pages (`/material-ui`, `/x`, `/core`, `/design-kits`, `/templates`). The existing fixture glob walks `docs/data/**` only, so these composites have historically regressed silently until someone opened the page. Ports the approach from mui/mui-x#22283 with one material-ui-specific addition: a tiny `next/*` stub layer in the regression bundle's Vite config. Nearly every product composite directly or transitively imports `Link` from `@mui/internal-core-docs/Link`, which calls `useRouter()` from `next/router`; without the stub, render either crashes at runtime or fails to bundle (the `AppLayout` barrel pulls `next/router` in via `AppLayoutHead`/`AppLayoutDocs`/`AppSearch`). - `stubs/{next-router,next-link,next-head}.{ts,tsx}` (~30 lines total): `useRouter` returns safe defaults, `next/link` renders a plain anchor (visually equivalent for Argos), `next/head` is `() => null`. - `vite.config.mts`: alias `next/router`, `next/link`, `next/head` to the stubs. - `MarketingWrapper.tsx`: wraps composite fixtures with `BrandingProvider` so they can read docs branding tokens (`palette.primaryDark`, `palette.gradients`, `applyDarkStyles`). - `index.jsx`: second `import.meta.glob` for `docs/src/components/product*/[A-Z]*.tsx`; PascalCase → kebab-case for the suite name (`productDesignKit` → suite `docs-product-design-kit`); composites get wrapped in `MarketingWrapper`. - `demoMeta.ts`: adds a `viewport` field on `ScreenshotRule`, exports `DEFAULT_VIEWPORT`, extends `parseRoute` to recognise composite routes and round-trip the kebab-case back to a PascalCase directory, and appends two viewport rules — `product*/**` → 1280×800, `productX/**` → 1440×900 (last-match-wins). - `index.test.js`: per-route `page.setViewportSize(rule?.viewport ?? DEFAULT_VIEWPORT)` before each fixture render. - `demoMeta.test.ts`: new cases for the composite route regex, including the kebab-case → PascalCase round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deploy previewhttps://deploy-preview-48589--material-ui.netlify.app/ Bundle size
Check out the code infra dashboard for more information about this PR. |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `stubs/next-link.tsx`: `NextLinkStubProps` was extending
`AnchorHTMLAttributes<HTMLAnchorElement>` but widening `href` from
`string | undefined` to `string | { pathname?: string }`. Omit
`href` from the base before redeclaring it.
- `MarketingWrapper.tsx`: importing `BrandingProvider` from
`@mui/internal-core-docs/branding` pulled in the package's barrel,
whose sibling modules use extensionless relative imports
(`from './brandingTheme'`). That's fine under the package's own
`moduleResolution: 'bundler'`, but the `test/tsconfig.json` typecheck
runs under `nodenext` and surfaced them as TS2835. Deep-import
`brandingLightTheme` from the leaf module (which has zero relative
imports) and inline the minimal light-mode `ThemeProvider` wrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Caught by CI's `pnpm dedupe --check`. The `eslint-plugin-import` peer info had a nested `eslint-import-resolver-typescript` resolver specifier that pnpm dedupe simplifies to the flat form. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's `test_types` job (which runs `tsc -p test/tsconfig.json` under `moduleResolution: 'nodenext'`) was failing to resolve `@mui/material/themeCssVarsAugmentation` and friends when typechecking walked into `packages-internal/core-docs/src/branding/brandingTheme.ts` via `MarketingWrapper.tsx`'s deep import. The augmentations resolve fine in the package's own typecheck (`bundler` resolution) and in the Vite bundle, but not in `test/`'s nodenext context against the test package's node_modules layout. Workaround: route the import through a local `brandingThemeShim` — the `.d.ts` declares the theme as a `@mui/material/styles` `Theme`, and the `.js` does the runtime re-export. TS picks the `.d.ts` and never walks into the augmentation source; Vite picks the `.js` and resolves it normally. Also added `regressions/brandingThemeShim.js` to `test/tsconfig.json`'s exclude as belt-and-suspenders so TS doesn't accidentally include the runtime shim and start chasing imports through it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ing-composites-argos # Conflicts: # test/regressions/index.test.js
The broad `product*/[A-Z]*.tsx` glob pulled in all 27 composites, but most are marketing chrome — heroes that are mostly links/images, pricing tables, Figma-kit promos, template thumbnails — with no real component integration for Argos to guard. Replace it with an explicit allowlist of the 9 composites that render real MUI / MUI X components in a composed layout, where a CSS/theme regression would actually show. Kept: - productMaterial: MaterialHero, MaterialStyling, MaterialTheming - productX: XHero, XGridFullDemo, XDateRangeDemo, XChartsDemo, XTreeViewDemo, XTheming For MUI X, each product is covered by its isolated demo rather than via the `XComponents` switcher composite (which renders these same demos behind tabs) — that drops the tab chrome and the duplicate grid section (`XDataGrid`), giving one clean screenshot per product. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
XHero and XDateRangeDemo build their picker default values from `dayjs()` at *module scope* (e.g. `const startDate = dayjs().date(10)`), which runs when the eager fixture glob loads the bundle — before any render-time or per-test mock could intervene. Without a fixed clock the calendars (and any date columns in the grid demos) would shift every run and churn the Argos baseline. Install a fixed `Date` via an inline script in `index.html`, ahead of the module bundle, so it is in place before any composite module evaluates. Native `new Date(arg)`, `Date.parse`, `Date.UTC` and `instanceof Date` keep working; only the "now" reading is pinned. Mirrors the fixed-date approach used by mui-x's regression bundle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous inline `Date` override was hand-rolled and I incorrectly described it as mirroring mui-x (mui-x uses sinon `useFakeTimers`; this bundle does not). Replace the shim with the reviewed approach from mui/base-ui#4337: `Reflect.construct` preserves the native `Date.prototype` (timezone-aware date libraries enumerate its own methods) and forwards `new.target`. Also pin `timezoneId: 'UTC'` on the playwright page — without it the frozen instant would render in the CI machine's local timezone and still churn the baseline. This was missing before. Kept as an inline classic <script> (rather than base-ui's module + fixture-extraction in mui#4370): a classic script runs synchronously before the deferred module bundle, so it beats the eager-glob import hoisting that mui#4370 worked around — without refactoring the glob loading that drives the entire VRT suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With coverage narrowed to `productMaterial` and `productX`, the kebab-case <-> PascalCase round-trip (added for multi-word products like `productDesignKit`) is dead code. Replace it with a plain lower-case / re-capitalize of the single-word product segment in both `index.jsx` (suite name) and `demoMeta.ts` (`parseRoute`), and drop the now-irrelevant multi-segment test case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follows mui/base-ui#4337 + mui#4370 fully, instead of the earlier inline `<script>` shortcut: - `fakeDateSetup.ts` holds the `Reflect.construct`-based `Date` override (was inline in `index.html`). - `fixtures.js` hosts the three eager `import.meta.glob` calls, extracted from `index.jsx`. Eager globs *prepend* their resolved imports to the top of their host module (base-ui#4370), so if they stayed in `index.jsx` the demo modules' top-level `dayjs()` would evaluate before `index.jsx`'s first import could install the clock. Hosting them in a separate module imported *after* `./fakeDateSetup` guarantees the override is in place first. - `index.jsx` imports `./fakeDateSetup` on its first line, then `./fixtures`; the inline `<script>` is removed from `index.html`. Verified the ordering: in the built entry chunk the `globalThis.Date` install lands at byte ~2k while the module-scope `dayjs().date(10)` / `.add(28)` from XHero/XDateRangeDemo land at ~3.85M, and an import-order simulation freezes those module-scope dates to 2025-06-10 / 2025-07-13. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds Argos visual-regression coverage for selected docs product landing-page composites (/material-ui, /x) by extending the regression fixture loader beyond docs/data/**, and ensuring these composites can render safely outside a Next.js runtime.
Changes:
- Add an explicit allowlist of 9 product-page composites to the regression fixtures and route parsing/config.
- Introduce a safe
next/*stub layer and a branding-theme wrapper/shim so composites can render in the Vite regression bundle. - Stabilize screenshots by applying per-route viewports and a deterministic
Datesetup + pinned timezone in Playwright.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/tsconfig.json | Excludes the runtime-only branding theme shim JS from nodenext typechecking. |
| test/regressions/vite.config.mts | Adds next/* module alias stubs alongside existing monorepo aliases. |
| test/regressions/stubs/next-router.ts | Provides a stubbed router for non-Next rendering. |
| test/regressions/stubs/next-link.tsx | Stubs next/link as a plain anchor for screenshots. |
| test/regressions/stubs/next-head.tsx | Stubs next/head to avoid module-scope crashes. |
| test/regressions/MarketingWrapper.tsx | Wraps composites with the docs branding light theme. |
| test/regressions/index.test.js | Applies per-route viewport sizing and pins timezone for deterministic output. |
| test/regressions/index.jsx | Installs fake date first, imports fixtures from a separate module, and wraps composites in MarketingWrapper. |
| test/regressions/fixtures.js | Centralizes eager globs, adds allowlisted composite fixtures, and marks composites for special wrapping. |
| test/regressions/fakeDateSetup.ts | Overrides Date so date-dependent composites render deterministically. |
| test/regressions/demoMeta.ts | Adds viewport rules for product composites and extends route parsing for /docs-product-*. |
| test/regressions/demoMeta.test.ts | Adds coverage for composite route parsing. |
| test/regressions/brandingThemeShim.js | Runtime re-export of the docs branding theme for the Vite bundle. |
| test/regressions/brandingThemeShim.d.ts | Declaration-only shim for typechecking consumers of the branding theme. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function useRouter() { | ||
| return router; | ||
| } | ||
|
|
||
| export default { useRouter }; |
| // file declares augmentations to `@mui/material/styles` and friends, which | ||
| // resolve fine in the package's own `bundler`-resolution typecheck and in | ||
| // the Vite build, but fail under `test/tsconfig.json`'s `nodenext` | ||
| // resolution (`@mui/material/themeCssVarsAugmentation` can't be found in |
There was a problem hiding this comment.
Sorry, but no. This file shouldn't be necessary, if vitest can resolve like a bundler then we should configure typescript to resolve like a bundler.
| // X composites (large grids, dense charts) want a wider canvas to match | ||
| // their live-docs desktop layout. Last-match-wins so this overrides the | ||
| // broader product*/** viewport above. | ||
| { test: 'docs/src/components/productX/**', viewport: { width: 1440, height: 900 } }, |
There was a problem hiding this comment.
iirc playwright has a fullPage option for its screenshots. Something we can leverage over hardcoding the height?
Summary
Adds Argos screenshot coverage for the bespoke composites that assemble the product landing pages (
/material-ui,/x). They live underdocs/src/components/product*/*.tsxand were uncovered by the fixture loader, which globsdocs/data/**only.Coverage is an explicit allowlist of 9 composites that render real MUI / MUI X components in a composed layout — the ones where a CSS/theme regression would actually show. The other ~18
product*composites are marketing chrome (heroes that are mostly links/images, pricing tables, Figma-kit promos, template thumbnails) and are intentionally left out.docs-product-materialMaterialHero,MaterialStyling,MaterialThemingdocs-product-xXHero,XGridFullDemo,XDateRangeDemo,XChartsDemo,XTreeViewDemo,XThemingFor MUI X, each product is covered by its isolated demo rather than via the
XComponentsswitcher composite (which renders these same demos behind tabs) — that drops the tab chrome and the duplicateXDataGridsection, giving one clean screenshot per product (grid, pickers, charts, tree view) plus the hero and the custom-theme grid.Ported from mui/mui-x#22283, with two material-ui-specific additions (stub layer + date freeze, below).
How it works
next/*stub layer (test/regressions/stubs/, aliased invite.config.mts). Every kept composite imports from the@mui/internal-core-docs/AppLayoutbarrel, whose siblings (AppLayoutHead/AppLayoutDocs/AppSearch) importnext/router,next/link,next/head;MaterialHeroalso importsLinkdirectly. Without the stubs the real Next modules get bundled and crash at runtime outside a Next router context (verified: removing them swaps the stubs for the real modules and the bundle balloons). The stubs return safe defaults so<Link>renders a plain anchor (visually equivalent for Argos).MarketingWrappersupplies the docs branding theme (light) so composites can read branding tokens (palette.primaryDark,palette.gradients,applyDarkStyles). It routes through a.d.ts/.jsshim (brandingThemeShim) so thenodenexttest typecheck doesn't choke on the theme file's@mui/material/*module augmentations.demoMeta.ts):product*/**→ 1280×800,productX/**→ 1440×900. The runner appliespage.setViewportSizeper route (important now that pages come from a pool and may have been resized by a prior test).fakeDateSetup.ts+timezoneId: 'UTC').XHeroandXDateRangeDemocompute picker defaults fromdayjs()at module scope, which runs when the eager fixture glob loads — too early for a render-time mock. Follows mui/base-ui#4337 + #4370: theDateoverride (Reflect.construct, preserves the native prototype) lives infakeDateSetup.ts, imported onindex.jsx's first line; the eager globs are extracted intofixtures.js(imported after it) because eager globs prepend their resolved imports to the host module, which would otherwise evaluate the demos' module-scopedayjs()before the clock is installed. The page is pinned to UTC so the frozen instant renders identically across CI machines. (Note: this is not mui-x's mechanism — mui-x uses sinonuseFakeTimers.)Why
These composites combine many components into marketing-style layouts and regress easily (CSS bleed, theme tweaks, internal style changes) without anyone noticing until the page is opened — and they sit outside the
docs/data/**glob, so they had no coverage.Test plan
pnpm test:regressions:build— bundle builds clean; exactly the 9 allowlisted composites are included.pnpm eslint test/regressions/,pnpm prettier --check,pnpm typescript(all 19 projects) — all green.demoMeta.test.ts(9/9) — incl. the composite route regex.dayjs:dayjs()pins to the fixed instant,dayjs().date(10)/.add(28,'day')are deterministic, explicitnew Date(arg),Date.UTC, andinstanceof Datestill work.XHero/grids, the per-product viewport layout, and the frozen calendars inXHero/XDateRangeDemo) before accepting.🤖 Generated with Claude Code