-
-
Notifications
You must be signed in to change notification settings - Fork 32.6k
[test] Cover docs landing-page composites with Argos #48589
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
LukasTy
wants to merge
17
commits into
mui:master
Choose a base branch
from
LukasTy:claude/cover-landing-composites-argos
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+509
−155
Open
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
372fef5
[test] Cover docs landing-page composites with Argos
LukasTy e7691db
[test] Fix prettier formatting
LukasTy ec5d9f9
[test] Fix TypeScript errors surfaced by composite coverage
LukasTy 93d0d93
[test] Dedupe pnpm lockfile
LukasTy 46b6203
[test] Insulate test typecheck from branding theme augmentations
LukasTy 4ddf533
Merge remote-tracking branch 'upstream/master' into claude/cover-land…
LukasTy 3c05e15
[test] Narrow composite coverage to meaningful integrations
LukasTy 437d663
[test] Freeze the clock for date-dependent composites
LukasTy 29dab5a
[test] Align date freeze with base-ui's regression approach
LukasTy 7e01d9a
[test] Simplify composite route mapping to single-word products
LukasTy 0be52a0
[test] Move date freeze into a module, matching base-ui's structure
LukasTy 2ac0f92
[test] Fix next/router stub default export
LukasTy 430d93e
[test] Resolve test types like a bundler, drop branding shim
LukasTy 96203d8
[test] Make composite viewport overrides width-only
LukasTy af9d064
[test] Make next/link stub honor `as` and object href query/hash
LukasTy 74a180b
[test] Reset viewport per page acquisition, drop redundant newPage vi…
LukasTy bd90845
Merge branch 'master' into claude/cover-landing-composites-argos
LukasTy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import * as React from 'react'; | ||
| import { BrandingProvider } from '@mui/internal-core-docs/branding'; | ||
|
|
||
| // `docs/src/components/product*/*.tsx` composites are authored to run inside | ||
| // the Next.js docs site and read the docs branding theme | ||
| // (`palette.primaryDark`, `palette.gradients`, `applyDarkStyles`, ...). | ||
| // Supply it so they render in the regression fixture. `mode="light"` matches | ||
| // the live `/material-ui` / `/x` pages' default. | ||
| export default function MarketingWrapper({ children }: { children: React.ReactNode }) { | ||
| return ( | ||
| <BrandingProvider mode="light"> | ||
| <div style={{ width: '100%' }}>{children}</div> | ||
| </BrandingProvider> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| // Override `Date` so `new Date()` and `Date.now()` return a stable instant for | ||
| // Argos — otherwise the `today` in date-aware composites (XHero / | ||
| // XDateRangeDemo build picker defaults from `dayjs()`) shifts the baseline | ||
| // every day. | ||
| // | ||
| // This MUST be imported before any module that reads `Date` at module scope. | ||
| // `index.jsx` imports it on its first line, ahead of `./fixtures` (which hosts | ||
| // the eager `import.meta.glob` calls). Eager globs *prepend* their imports to | ||
| // the module that contains them rather than appending (see mui/base-ui#4370), | ||
| // so the globs live in a separate `./fixtures` module that is imported after | ||
| // this one — guaranteeing the demos' top-level `dayjs()` runs after the | ||
| // override is installed. | ||
| // | ||
| // `Reflect.construct` preserves the native `Date.prototype` (timezone-aware | ||
| // date libraries enumerate its own methods) and forwards `new.target` for | ||
| // correct subclass construction. Follows mui/base-ui#4337. The playwright page | ||
| // also pins `timezoneId: 'UTC'` (see `index.test.js`) so the frozen instant | ||
| // renders identically regardless of the machine's local timezone. | ||
|
|
||
| const fakeNow = new Date('2025-06-15T09:00:00.000Z').valueOf(); | ||
|
|
||
| const OriginalDate = Date; | ||
| const offset = fakeNow - OriginalDate.now(); | ||
|
|
||
| function FakeDate(...args: any[]): Date | string { | ||
| if (new.target) { | ||
| return args.length === 0 | ||
| ? Reflect.construct(OriginalDate, [OriginalDate.now() + offset], new.target) | ||
| : Reflect.construct(OriginalDate, args, new.target); | ||
| } | ||
| return new OriginalDate(OriginalDate.now() + offset).toString(); | ||
| } | ||
|
LukasTy marked this conversation as resolved.
|
||
|
|
||
| FakeDate.prototype = OriginalDate.prototype; | ||
| // Static methods can't be assigned to a plain function type under `strict`, so | ||
| // copy them through `Object.assign` (base-ui's version relies on a looser | ||
| // tsconfig; this is the typecheck-clean equivalent). | ||
| Object.assign(FakeDate, { | ||
| parse: OriginalDate.parse, | ||
| UTC: OriginalDate.UTC, | ||
| now: () => OriginalDate.now() + offset, | ||
| }); | ||
|
|
||
| (globalThis as { Date: unknown }).Date = FakeDate; | ||
|
|
||
| export {}; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| // Fixture discovery for the visual-regression bundle. | ||
| // | ||
| // This module exists to host the eager `import.meta.glob` calls in isolation. | ||
| // Eager globs *prepend* their resolved imports to the top of the module that | ||
| // contains them, ahead of that module's own `import` statements (see | ||
| // mui/base-ui#4370). If these globs lived in `index.jsx`, the demo modules — | ||
| // including their top-level `dayjs()` calls — would evaluate before | ||
| // `index.jsx`'s first import (`./fakeDateSetup`) could install the frozen | ||
| // clock. Keeping them here, imported *after* `./fakeDateSetup`, guarantees the | ||
| // override is in place first. | ||
|
|
||
| // Get all the fixtures specifically written for preventing visual regressions. | ||
| const importRegressionFixtures = import.meta.glob(['./fixtures/**/*.(js|ts|tsx)'], { | ||
| import: 'default', | ||
| eager: true, | ||
| }); | ||
|
|
||
| const regressionFixtures = []; | ||
|
|
||
| Object.keys(importRegressionFixtures).forEach((path) => { | ||
| const [suite, name] = path | ||
| .replace('./fixtures/', '') | ||
| .replace(/\.\w+$/, '') | ||
| .split('/'); | ||
|
|
||
| // TODO: Why does webpack include a key for the absolute and relative path? | ||
| // We just want the relative path | ||
| if (path.startsWith('./')) { | ||
| regressionFixtures.push({ | ||
| path, | ||
| suite: `regression-${suite}`, | ||
| name, | ||
| Component: importRegressionFixtures[path], | ||
| }); | ||
| } | ||
| }, []); | ||
|
|
||
| // Also use some of the demos to avoid code duplication. | ||
| // | ||
| // Two exclusion layers: | ||
| // - Slug-level (whole slug has no tool consumer, or path can't be imported) lives here, | ||
| // dropping the demo from the bundle entirely. | ||
| // - Per-demo (a specific demo inside an otherwise-enrolled slug is skipped by one tool | ||
| // or the other) lives in `demoMeta.ts`, so screenshot-specific reasons | ||
| // ("Redundant", "Flaky image loading") don't also drop a11y coverage. | ||
| // | ||
| // Enrolling a new component for a11y: un-negate the slug glob below if needed, | ||
| // then add an `A11Y_RULES` entry in `demoMeta.ts` | ||
| // (e.g. `{ test: 'docs/data/material/components/foo/{BasicFoo,FooVariants}', enabled: true }`). | ||
| const importDemos = import.meta.glob( | ||
| [ | ||
| 'docs/data/**/[A-Z]*.js', | ||
| 'docs/data/base/**/[A-Z]*/css/index.js', | ||
| 'docs/data/base/**/[A-Z]*/tailwind/index.js', | ||
| 'docs/data/base/**/[A-Z]*/system/index.js', | ||
| // ================== Structural — cannot be imported safely ================== | ||
| '!docs/data/experiments', | ||
| '!docs/data/material/**/*NoSnap.*', | ||
| // Templates — not demos | ||
| '!docs/data/material/getting-started/templates/blog/components', | ||
| '!docs/data/material/getting-started/templates/checkout/components', | ||
| '!docs/data/material/getting-started/templates/crud-dashboard/components', | ||
| '!docs/data/material/getting-started/templates/crud-dashboard/theme/customizations', | ||
| '!docs/data/material/getting-started/templates/crud-dashboard/hooks', | ||
| '!docs/data/material/getting-started/templates/crud-dashboard/context', | ||
| '!docs/data/material/getting-started/templates/dashboard/components', | ||
| '!docs/data/material/getting-started/templates/dashboard/internals/components', | ||
| '!docs/data/material/getting-started/templates/dashboard/theme/customizations', | ||
| '!docs/data/material/getting-started/templates/marketing-page/components', | ||
| '!docs/data/material/getting-started/templates/marketing-page/MarketingPage', | ||
| '!docs/data/material/getting-started/templates/shared-theme', | ||
| '!docs/data/material/getting-started/templates/sign-in/components', | ||
| '!docs/data/material/getting-started/templates/sign-in-side/components', | ||
| '!docs/data/material/getting-started/templates/sign-up/components', | ||
| // Customization demos — not component pages | ||
| '!docs/data/material/customization/breakpoints', | ||
| '!docs/data/material/customization/color', | ||
| '!docs/data/material/customization/container-queries/ResizableDemo', | ||
| '!docs/data/material/customization/default-theme', | ||
| '!docs/data/material/customization/density/DensityTool', | ||
| '!docs/data/material/customization/right-to-left/RtlDemo', | ||
| '!docs/data/material/customization/transitions/TransitionHover', | ||
| '!docs/data/material/customization/typography/ResponsiveFontSizesChart', | ||
| // Other non-demo subtrees | ||
| '!docs/data/material/components/menubar/components', // Source subdir, not demos | ||
| '!docs/data/material/getting-started/supported-components/MaterialUIComponents', | ||
| '!docs/data/material/guides', | ||
| '!docs/data/base/getting-started/quickstart/BaseButtonTailwind', | ||
| '!docs/data/base/guides/working-with-tailwind-css/PlayerFinal', | ||
| '!docs/data/premium-themes', | ||
| // ================== Slug-level — no tool consumer ================== | ||
| '!docs/data/material/components/backdrop', // Needs interaction | ||
| '!docs/data/material/components/click-away-listener', // Needs interaction | ||
| '!docs/data/material/components/container', // Can't see the impact | ||
| '!docs/data/material/components/dialogs', // Needs interaction | ||
| '!docs/data/material/components/image-list', // Images don't load | ||
| '!docs/data/material/components/material-icons/SearchIcons', // Heavy icon grid | ||
| '!docs/data/material/components/menus', // Needs interaction | ||
| '!docs/data/material/components/popper', // Needs interaction | ||
| '!docs/data/material/components/progress', // Flaky | ||
| '!docs/data/material/components/speed-dial', // Needs interaction | ||
| '!docs/data/material/components/textarea-autosize', // Superseded by a dedicated regression test | ||
| '!docs/data/material/components/tooltips', // Needs interaction | ||
| '!docs/data/material/components/transitions', // Needs interaction | ||
| '!docs/data/material/components/use-media-query', // Need to dynamically resize to test | ||
| '!docs/data/material/customization/breakpoints', // Need to dynamically resize to test | ||
| '!docs/data/material/customization/color', // Escape viewport | ||
| '!docs/data/material/customization/container-queries/ResizableDemo', // No public components | ||
| '!docs/data/material/customization/default-theme', // Redux isolation | ||
| '!docs/data/material/customization/density/DensityTool', // Redux isolation | ||
| '!docs/data/material/customization/right-to-left/RtlDemo', | ||
| '!docs/data/material/customization/transitions/TransitionHover', // Need interaction | ||
| '!docs/data/material/customization/typography/ResponsiveFontSizesChart', | ||
| '!docs/data/material/getting-started/supported-components/MaterialUIComponents', // No public components | ||
| '!docs/data/material/guides', | ||
| '!docs/data/base/getting-started/quickstart/BaseButtonTailwind', // CodeSandbox | ||
| '!docs/data/base/guides/working-with-tailwind-css/PlayerFinal', // No public components | ||
| '!docs/data/premium-themes', | ||
| '!docs/data/material/getting-started/versions/LatestVersions', // not a component | ||
| '!docs/data/material/getting-started/versions/ReleasedVersions', // not a component | ||
| ], | ||
| { | ||
| import: 'default', | ||
| eager: true, | ||
| }, | ||
| ); | ||
|
|
||
| const demoFixtures = []; | ||
| Object.keys(importDemos).forEach((path) => { | ||
| const [name, ...suiteArray] = path | ||
| .replace('../../docs/data/', '') | ||
| .replace('.js', '') | ||
| .split('/') | ||
| .reverse(); | ||
| const suite = `docs-${suiteArray | ||
| .reverse() | ||
| .join('-') | ||
| .replace(/^material-/, '')}`; | ||
|
|
||
| demoFixtures.push({ | ||
| path, | ||
| suite, | ||
| name, | ||
| Component: importDemos[path], | ||
| }); | ||
| }, []); | ||
|
|
||
| // Bespoke composites under `docs/src/components/product*/` that assemble the | ||
| // product landing pages (`/material-ui`, `/x`). They aren't covered by the | ||
| // `docs/data/**` glob above. See `test/regressions/MarketingWrapper.tsx` for | ||
| // the branding wrapper and `test/regressions/stubs/` for the `next/*` shims | ||
| // that let composites with transitive `next/router` imports render outside | ||
| // the Next.js docs host. | ||
| // | ||
| // This is an explicit allowlist rather than a `product*/[A-Z]*.tsx` glob: most | ||
| // composites are marketing chrome (heroes with links, pricing tables, Figma | ||
| // kit promos, template thumbnails) with no meaningful component integration to | ||
| // guard. We only cover the ones that render real MUI / MUI X components in a | ||
| // composed layout — where a CSS/theme regression would actually show up. | ||
| const importComposites = import.meta.glob( | ||
| [ | ||
| // Material UI — component showcases under the docs branding theme. | ||
| 'docs/src/components/productMaterial/MaterialHero.tsx', | ||
| 'docs/src/components/productMaterial/MaterialStyling.tsx', | ||
| 'docs/src/components/productMaterial/MaterialTheming.tsx', | ||
| // MUI X — one isolated demo per product (the switcher composite | ||
| // `XComponents` renders these same demos behind tabs; isolating them | ||
| // drops the tab chrome and avoids screenshotting one product at a time). | ||
| 'docs/src/components/productX/XHero.tsx', | ||
| 'docs/src/components/productX/XGridFullDemo.tsx', | ||
| 'docs/src/components/productX/XDateRangeDemo.tsx', | ||
| 'docs/src/components/productX/XChartsDemo.tsx', | ||
| 'docs/src/components/productX/XTreeViewDemo.tsx', | ||
| 'docs/src/components/productX/XTheming.tsx', | ||
| ], | ||
| { | ||
| import: 'default', | ||
| eager: true, | ||
| }, | ||
| ); | ||
|
|
||
| const compositeFixtures = []; | ||
| Object.keys(importComposites).forEach((path) => { | ||
| const productMatch = path.match(/components\/product([^/]+)\//); | ||
| if (!productMatch) { | ||
| return; | ||
| } | ||
| // Only single-word products are covered (`productMaterial`, `productX`), so | ||
| // the suite name is just the lower-cased segment (`material`, `x`). | ||
| // `parseRoute` in `demoMeta.ts` re-capitalizes it to rebuild the docs path. | ||
| const product = productMatch[1].toLowerCase(); | ||
| const name = path | ||
| .split('/') | ||
| .pop() | ||
| .replace(/\.tsx$/, ''); | ||
| compositeFixtures.push({ | ||
| path, | ||
| suite: `docs-product-${product}`, | ||
| name, | ||
| Component: importComposites[path], | ||
| isComposite: true, | ||
| }); | ||
| }, []); | ||
|
|
||
| const fixtures = regressionFixtures.concat(demoFixtures).concat(compositeFixtures); | ||
|
|
||
| export default fixtures; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
iirc playwright has a
fullPageoption for its screenshots. Something we can leverage over hardcoding the height?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The screenshots here are element-scoped -
screenshotTarget.screenshot()on the[data-testid="testcase"]element rather thanpage.screenshot({ fullPage: true })- and an element screenshot already captures the element's full rendered height regardless of viewport. So the hardcoded height in these rules isn't really doing the work; it's the width that matters (the composites key off desktop breakpoints at 1280/1440).Rather than introduce page-level
fullPage(different code path from how every other demo is shot, and it'd pull in chrome/scrollbars), would you be okay with dropping the magic height and keeping these as width-only overrides? That removes the hardcoded height while keeping the element-screenshot consistency.