Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions test/regressions/MarketingWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';
import { ThemeProvider } from '@mui/material/styles';
// Go through a local `.d.ts` shim rather than importing the docs branding
// theme directly. The theme source file declares `@mui/material/*` module
// augmentations that resolve fine in the package's own typecheck and in the
// Vite bundle, but fail under `test/tsconfig.json`'s `nodenext` resolution.
// The runtime side is in `brandingThemeShim.js`, excluded from the test
// typecheck.
import brandingLightTheme from './brandingThemeShim';

// `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. Inlines the minimal
// light-mode subset of `BrandingProvider` (a thin `ThemeProvider` wrapper);
// composites that read `theme.palette.mode` get `'light'`, which matches
// the live `/material-ui` / `/x` pages' default.
export default function MarketingWrapper({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider theme={brandingLightTheme}>
<div style={{ width: '100%' }}>{children}</div>
</ThemeProvider>
);
}
15 changes: 15 additions & 0 deletions test/regressions/brandingThemeShim.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Theme } from '@mui/material/styles';

// Declaration-only shim for the docs branding theme.
//
// The runtime export lives in the matching `.js` file (excluded from the
// test typecheck). We declare the shape here so `MarketingWrapper.tsx` can
// consume it without TS transitively pulling in
// `packages-internal/core-docs/src/branding/brandingTheme.ts`. That source
// 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
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.

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.

// the test package's node_modules layout).
declare const brandingLightTheme: Theme;
export default brandingLightTheme;
7 changes: 7 additions & 0 deletions test/regressions/brandingThemeShim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Runtime re-export of the docs branding theme. See `brandingThemeShim.d.ts`
// for the rationale — this file is excluded from `test/tsconfig.json` so TS
// uses the declaration; Vite resolves the import here at bundle time.
// eslint-disable-next-line import/no-relative-packages
import { brandingLightTheme } from '../../packages-internal/core-docs/src/branding/brandingTheme';

export default brandingLightTheme;
13 changes: 13 additions & 0 deletions test/regressions/demoMeta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ describe('parseRoute', () => {
demo: 'BasicButtons',
});
});

it('parses a docs-product route into the matching product*/ docs/src path', () => {
expect(parseRoute('/docs-product-material/MaterialHero')).to.deep.equal({
path: 'docs/src/components/productMaterial/MaterialHero',
slug: 'material',
demo: 'MaterialHero',
});
expect(parseRoute('/docs-product-x/XGridFullDemo')).to.deep.equal({
path: 'docs/src/components/productX/XGridFullDemo',
slug: 'x',
demo: 'XGridFullDemo',
});
});
});

describe('getConfig', () => {
Expand Down
43 changes: 35 additions & 8 deletions test/regressions/demoMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* screenshots, one for axe — so editing one tool can never stomp on the
* other. Each list is evaluated last-match-wins (no inheritance: an override
* rule must restate every field it cares about) against the docs path
* `docs/data/material/components/{slug}/{Demo}`.
* `docs/data/material/components/{slug}/{Demo}` (component demos) or
* `docs/src/components/product{Product}/{Name}` (landing-page composites).
*
* Whole-slug exclusions where *no* tool wants anything live in the
* `index.jsx` glob — dropping them from the bundle entirely, not just from
Expand All @@ -12,12 +13,17 @@

import { minimatch } from 'minimatch';

/** Default playwright viewport when no `ScreenshotRule.viewport` matches. */
export const DEFAULT_VIEWPORT = { width: 1000, height: 700 };

export interface ScreenshotRule {
/** Minimatch glob against `docs/data/material/components/{slug}/{Demo}`. */
/** Minimatch glob against the docs path (see file-level comment). */
test: string;
enabled?: boolean;
/** Playwright waits for this selector after navigation, before axe + screenshot. */
waitForSelector?: string;
/** Per-route playwright viewport override. Defaults to {@link DEFAULT_VIEWPORT}. */
viewport?: { width: number; height: number };
}

export interface A11yRule {
Expand Down Expand Up @@ -98,6 +104,14 @@ export const SCREENSHOT_RULES: ScreenshotRule[] = [
test: 'docs/data/material/components/table/ReactVirtualizedTable',
waitForSelector: '[data-index="1"]',
}, // Wait for virtualized rows to render

// Landing-page composites under `docs/src/components/product*/` use
// desktop breakpoints (`md`+) and look clipped at the 1000×700 default.
{ test: 'docs/src/components/product*/**', viewport: { width: 1280, height: 800 } },
// 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 } },
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.

iirc playwright has a fullPage option for its screenshots. Something we can leverage over hardcoding the height?

];

/**
Expand All @@ -117,19 +131,32 @@ export interface ParsedRoute {
demo: string;
}

const ROUTE_REGEX = /^\/docs-components-([^/]+)\/(.+)$/;
const COMPONENT_ROUTE_REGEX = /^\/docs-components-([^/]+)\/(.+)$/;
const COMPOSITE_ROUTE_REGEX = /^\/docs-product-([^/]+)\/(.+)$/;

/**
* Map a VRT route to its docs path + slug + demo, or `null` for non-component
* routes (regression fixtures).
*
* Recognises two route shapes:
* - `/docs-components-{slug}/{Demo}` → `docs/data/material/components/{slug}/{Demo}`
* - `/docs-product-{product}/{Name}` → `docs/src/components/product{Product}/{Name}`
*/
export function parseRoute(route: string): ParsedRoute | null {
const match = route.match(ROUTE_REGEX);
if (!match) {
return null;
const componentMatch = route.match(COMPONENT_ROUTE_REGEX);
if (componentMatch) {
const [, slug, demo] = componentMatch;
return { path: `docs/data/material/components/${slug}/${demo}`, slug, demo };
}
const compositeMatch = route.match(COMPOSITE_ROUTE_REGEX);
if (compositeMatch) {
const [, product, demo] = compositeMatch;
// Re-capitalize the single-word product segment from `index.jsx`'s glob
// (`material` → `Material`, `x` → `X`) to rebuild the directory name.
const dir = `product${product.charAt(0).toUpperCase()}${product.slice(1)}`;
return { path: `docs/src/components/${dir}/${demo}`, slug: product, demo };
}
const [, slug, demo] = match;
return { path: `docs/data/material/components/${slug}/${demo}`, slug, demo };
return null;
}

/**
Expand Down
46 changes: 46 additions & 0 deletions test/regressions/fakeDateSetup.ts
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();
}

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 {};
Loading
Loading