diff --git a/package.json b/package.json index 32ec461a7f6faa..fb564e638cceb2 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "test:e2e-website:dev": "cross-env PLAYWRIGHT_TEST_BASE_URL=http://localhost:3000 playwright test test/e2e-website --config test/e2e-website/playwright.config.ts", "test:karma": "nx run nx_test_karma", "test:karma:profile": "nx run nx_test_karma_profile", - "test:regressions": "cross-env NODE_ENV=production pnpm test:regressions:build && concurrently --success first --kill-others \"pnpm test:regressions:run\" \"pnpm test:regressions:server\"", + "test:regressions": "pnpm --filter @mui-internal/tests vitest", "test:regressions:build": "vite build test/regressions", "test:regressions:dev": "vite test/regressions --port 5001", "test:regressions:run": "nx run nx_test_regressions_run", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8080645469c828..8a682cbcfc5617 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2110,6 +2110,15 @@ importers: '@react-spring/web': specifier: ^9.7.5 version: 9.7.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@vitest/browser': + specifier: ^3.0.9 + version: 3.1.4(msw@2.7.3(@types/node@20.17.50)(typescript@5.8.3))(playwright@1.52.0)(vite@6.0.15(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1))(vitest@3.1.4) + vitest: + specifier: ^3.0.9 + version: 3.1.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/browser@3.1.4)(happy-dom@15.11.6)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.7.3(@types/node@20.17.50)(typescript@5.8.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1) + vitest-browser-react: + specifier: ^0.1.1 + version: 0.1.1(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(@vitest/browser@3.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@3.1.4) devDependencies: '@babel/runtime': specifier: ^7.27.1 @@ -2165,6 +2174,9 @@ importers: '@types/sinon': specifier: ^17.0.4 version: 17.0.4 + '@types/webfontloader': + specifier: ^1.6.38 + version: 1.6.38 chai: specifier: ^4.5.0 version: 4.5.0 @@ -13630,6 +13642,22 @@ packages: yaml: optional: true + vitest-browser-react@0.1.1: + resolution: {integrity: sha512-n9l+sIAexKqqfBuEkjVGdfZ4xAn1Gn/+wc4Mo8KsUSUOVoM9evSY0rVXdMIzCQqloT/zvmFGAtziFINkqu+t7g==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + '@types/react': '>18.0.0' + '@types/react-dom': '>18.0.0' + '@vitest/browser': '>=2.1.0' + react: '>18.0.0' + react-dom: '>18.0.0' + vitest: '>=2.1.0' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + vitest-fail-on-console@0.7.1: resolution: {integrity: sha512-/PjuonFu7CwUVrKaiQPIGXOtiEv2/Gz3o8MbLmovX9TGDxoRCctRC8CA9zJMRUd6AvwGu/V5a3znObTmlPNTgw==} peerDependencies: @@ -27739,6 +27767,16 @@ snapshots: tsx: 4.19.4 yaml: 2.7.1 + vitest-browser-react@0.1.1(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(@vitest/browser@3.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@3.1.4): + dependencies: + '@vitest/browser': 3.1.4(msw@2.7.3(@types/node@20.17.50)(typescript@5.8.3))(playwright@1.52.0)(vite@6.0.15(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1))(vitest@3.1.4) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + vitest: 3.1.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/browser@3.1.4)(happy-dom@15.11.6)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.7.3(@types/node@20.17.50)(typescript@5.8.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1) + optionalDependencies: + '@types/react': 19.1.6 + '@types/react-dom': 19.1.5(@types/react@19.1.6) + vitest-fail-on-console@0.7.1(vite@6.0.15(@types/node@20.17.50)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1))(vitest@3.1.4): dependencies: chalk: 5.3.0 diff --git a/test/package.json b/test/package.json index 2485f893432f6c..595a6886d5edba 100644 --- a/test/package.json +++ b/test/package.json @@ -2,7 +2,8 @@ "name": "@mui-internal/tests", "private": true, "scripts": { - "typescript": "tsc -p tsconfig.json" + "typescript": "tsc -p tsconfig.json", + "vitest": "vitest" }, "devDependencies": { "@babel/runtime": "^7.27.1", @@ -23,6 +24,7 @@ "@types/react": "^19.1.6", "@types/react-is": "^19.0.0", "@types/sinon": "^17.0.4", + "@types/webfontloader": "^1.6.38", "chai": "^4.5.0", "docs": "workspace:^", "fast-glob": "^3.3.3", @@ -43,6 +45,9 @@ "yargs": "^17.7.2" }, "dependencies": { - "@react-spring/web": "^9.7.5" + "@react-spring/web": "^9.7.5", + "@vitest/browser": "^3.0.9", + "vitest": "^3.0.9", + "vitest-browser-react": "^0.1.1" } } diff --git a/test/regressions.test.tsx b/test/regressions.test.tsx new file mode 100644 index 00000000000000..e45ccd5be141c7 --- /dev/null +++ b/test/regressions.test.tsx @@ -0,0 +1,193 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; +import { beforeAll, test, beforeEach, afterEach } from 'vitest'; +import { page } from '@vitest/browser/context'; +import { render } from 'vitest-browser-react'; +import webfontloader from 'webfontloader'; +import TestViewer from './regressions/TestViewer'; + +const importRegressionFixtures: Record Promise> = + import.meta.glob(['./fixtures/**/*.(js|ts|tsx)'], { + import: 'default', + }); + +const importDemos: Record Promise> = 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', + // ================== Exclusions ================== + '!docs/data/experiments', + '!docs/data/material/**/*NoSnap.*', + // Template + '!docs/data/material/getting-started/templates/blog/components', + '!docs/data/material/getting-started/templates/checkout/components', + '!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', + // Marketing Page Theme Customizations + '!docs/data/material/components/alert/TransitionAlerts', // Needs interaction + '!docs/data/material/components/app-bar/BackToTop', // Needs interaction + '!docs/data/material/components/app-bar/ElevateAppBar', // Needs interaction + '!docs/data/material/components/app-bar/HideAppBar', // Needs interaction + '!docs/data/material/components/app-bar/MenuAppBar', // Redundant + '!docs/data/material/components/autocomplete/Asynchronous', // Redundant + '!docs/data/material/components/autocomplete/CheckboxesTags', // Redundant + '!docs/data/material/components/autocomplete/CountrySelect', // Redundant + '!docs/data/material/components/autocomplete/DisabledOptions', // Redundant + '!docs/data/material/components/autocomplete/Filter', // Redundant + '!docs/data/material/components/autocomplete/FreeSolo', // Redundant + '!docs/data/material/components/autocomplete/GoogleMaps', // Redundant + '!docs/data/material/components/autocomplete/Grouped', // Redundant + '!docs/data/material/components/autocomplete/Highlights', // Redundant + '!docs/data/material/components/autocomplete/Playground', // Redundant + '!docs/data/material/components/autocomplete/UseAutocomplete', // Redundant + '!docs/data/material/components/autocomplete/Virtualize', // Redundant + '!docs/data/material/components/backdrop/SimpleBackdrop', // Needs interaction + '!docs/data/material/components/badges/BadgeAlignment', // Redux isolation + '!docs/data/material/components/badges/BadgeVisibility', // Needs interaction + '!docs/data/material/components/bottom-navigation/FixedBottomNavigation', // Redundant + '!docs/data/material/components/breadcrumbs/ActiveLastBreadcrumb', // Redundant + '!docs/data/material/components/chips/ChipsPlayground', // Redux isolation + '!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/drawers/SwipeableEdgeDrawer', // Needs interaction + '!docs/data/material/components/drawers/SwipeableTemporaryDrawer', // Needs interaction + '!docs/data/material/components/drawers/TemporaryDrawer', // Needs interaction + '!docs/data/material/components/floating-action-button/FloatingActionButtonZoom', // Needs interaction + '!docs/data/material/components/grid-legacy/InteractiveGrid', // Redux isolation + '!docs/data/material/components/grid-legacy/SpacingGrid', // Needs interaction + '!docs/data/material/components/image-list', // Image don't load + '!docs/data/material/components/masonry/ImageMasonry', // Image don't load + '!docs/data/material/components/masonry/Sequential', // Flaky + '!docs/data/material/components/material-icons/SearchIcons', + '!docs/data/material/components/menus', // Need interaction + '!docs/data/material/components/modal/BasicModal', // Needs interaction + '!docs/data/material/components/modal/KeepMountedModal', // Needs interaction + '!docs/data/material/components/modal/SpringModal', // Needs interaction + '!docs/data/material/components/modal/TransitionsModal', // Needs interaction + '!docs/data/material/components/no-ssr/FrameDeferring', // Needs interaction + '!docs/data/material/components/popover/AnchorPlayground', // Redux isolation + '!docs/data/material/components/popover/BasicPopover', // Needs interaction + '!docs/data/material/components/popover/PopoverPopupState', // Needs interaction + '!docs/data/material/components/popper/PopperPopupState', // Needs interaction + '!docs/data/material/components/popper/PositionedPopper', // Needs interaction + '!docs/data/material/components/popper/ScrollPlayground', // Redux isolation + '!docs/data/material/components/popper/SimplePopper', // Needs interaction + '!docs/data/material/components/popper/SpringPopper', // Needs interaction + '!docs/data/material/components/popper/TransitionsPopper', // Needs interaction + '!docs/data/material/components/popper/VirtualElementPopper', // Needs interaction + '!docs/data/material/components/progress', // Flaky + '!docs/data/material/components/selects/ControlledOpenSelect', // Needs interaction + '!docs/data/material/components/selects/DialogSelect', // Needs interaction + '!docs/data/material/components/selects/GroupedSelect', // Needs interaction + '!docs/data/material/components/skeleton/Animations', // Animation disabled + '!docs/data/material/components/skeleton/Facebook', // Flaky image loading + '!docs/data/material/components/skeleton/SkeletonChildren', // flaky image loading + '!docs/data/material/components/skeleton/YouTube', // Flaky image loading + '!docs/data/material/components/snackbars/ConsecutiveSnackbars', // Needs interaction + '!docs/data/material/components/snackbars/CustomizedSnackbars', // Redundant + '!docs/data/material/components/snackbars/DirectionSnackbar', // Needs interaction + '!docs/data/material/components/snackbars/FabIntegrationSnackbar', // Needs interaction + '!docs/data/material/components/snackbars/IntegrationNotistack', // Needs interaction + '!docs/data/material/components/snackbars/PositionedSnackbar', // Needs interaction + '!docs/data/material/components/snackbars/SimpleSnackbar', // Needs interaction + '!docs/data/material/components/snackbars/TransitionsSnackbar', // Needs interaction + '!docs/data/material/components/speed-dial', // Needs interaction + '!docs/data/material/components/stack/InteractiveStack', // Redundant + '!docs/data/material/components/steppers/HorizontalNonLinearStepper', // Redundant + '!docs/data/material/components/steppers/TextMobileStepper', // Flaky image loading + '!docs/data/material/components/tabs/AccessibleTabs1', // Need interaction + '!docs/data/material/components/tabs/AccessibleTabs2', // Need 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/joy/components/circular-progress/CircularProgressCountUp', // Flaky due to animation + '!docs/data/joy/components/divider/DividerChildPosition', // Needs interaction + '!docs/data/joy/components/linear-progress/LinearProgressCountUp', // Flaky due to animation + '!docs/data/joy/customization/theme-typography/TypographyThemeViewer', // No need for theme tokens + '!docs/data/joy/getting-started/templates/TemplateCollection', // No public components + '!docs/data/joy/**/*Variables.*', + '!docs/data/joy/**/*Usage.*', + '!docs/data/premium-themes', + ], + { + import: 'default', + }, +); + +beforeAll(async () => { + await new Promise((resolve, reject) => { + webfontloader.load({ + google: { + families: ['Roboto:300,400,500,700', 'Inter:300,400,500,600,700,800,900', 'Material+Icons'], + }, + custom: { + families: ['Font Awesome 5 Free:n9'], + urls: ['https://use.fontawesome.com/releases/v5.14.0/css/all.css'], + }, + timeout: 20000, + active: () => { + resolve(); + }, + inactive: () => { + reject(new Error('Font loading failed')); + }, + }); + }); +}); + +// Problem with fg-loadcss using `typeof global !== "undefined" ? global : this` to obtain the `window` +// See https://github.com/filamentgroup/loadCSS/blob/e2fa3939a641ae0501854d15f15443fe4d017c58/src/loadCSS.js#L88 +// Remove when `fg-loadcss` no loner appears in Examples +globalThis.global = window; + +let container; +let root; + +beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + root = ReactDOM.createRoot(container); +}); + +afterEach(() => { + root.unmount(); + document.body.removeChild(container); + container = null; +}); + +test.for(Object.entries({ ...importRegressionFixtures, ...importDemos }))( + 'Screenshot test fixture %s', + async ([path, mod]) => { + const Component = await mod(); + await root.render( + + + , + ); + await page.getByTestId('testcase'); + await page.screenshot(); + }, +); diff --git a/test/vitest.config.mts b/test/vitest.config.mts new file mode 100644 index 00000000000000..c4fb35fde8cbc8 --- /dev/null +++ b/test/vitest.config.mts @@ -0,0 +1,103 @@ +/// + +import * as path from 'path'; +import { defineConfig, mergeConfig } from 'vitest/config'; +import { transformWithEsbuild } from 'vite'; +import react from '@vitejs/plugin-react'; +import * as url from 'url'; + +const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); +const WORKSPACE_ROOT = path.resolve(currentDirectory, '../'); + +// https://vite.dev/config/ + +export default defineConfig({ + esbuild: { + minifyIdentifiers: false, + keepNames: true, + }, + plugins: [ + { + // Unfortunatelly necessary as we opted to write our jsx in js files + name: 'treat-js-files-as-jsx', + async transform(code, id) { + if (/\/node_modules\//.test(id)) { + return null; + } + if (!/.*\.js$/.test(id)) { + return null; + } + if (id.startsWith('\0')) { + return null; + } + // Use the exposed transform from vite, instead of directly + // transforming with esbuild + return transformWithEsbuild(code, id, { + loader: 'tsx', + jsx: 'automatic', + }); + }, + }, + react(), + ], + define: { + 'process.env': '{}', + }, + resolve: { + dedupe: ['react', 'react-dom'], + alias: { + '@mui/material': path.resolve(WORKSPACE_ROOT, './packages/mui-material/src'), + '@mui/docs': path.resolve(WORKSPACE_ROOT, './packages/mui-docs/src'), + '@mui/icons-material': path.resolve(WORKSPACE_ROOT, './packages/mui-icons-material/lib/esm'), + '@mui/lab': path.resolve(WORKSPACE_ROOT, './packages/mui-lab/src'), + '@mui/styled-engine': path.resolve(WORKSPACE_ROOT, './packages/mui-styled-engine/src'), + '@mui/styled-engine-sc': path.resolve(WORKSPACE_ROOT, './packages/mui-styled-engine-sc/src'), + '@mui/styles': path.resolve(WORKSPACE_ROOT, './packages/mui-styles/src'), + '@mui/system': path.resolve(WORKSPACE_ROOT, './packages/mui-system/src'), + '@mui/private-theming': path.resolve(WORKSPACE_ROOT, './packages/mui-private-theming/src'), + '@mui/utils': path.resolve(WORKSPACE_ROOT, './packages/mui-utils/src'), + '@mui/material-nextjs': path.resolve(WORKSPACE_ROOT, './packages/mui-material-nextjs/src'), + '@mui/joy': path.resolve(WORKSPACE_ROOT, './packages/mui-joy/src'), + '@mui/stylis-plugin-rtl': path.resolve( + WORKSPACE_ROOT, + './packages/mui-stylis-plugin-rtl/src', + ), + '@mui/internal-docs-utils': path.resolve( + WORKSPACE_ROOT, + './packages-internal/docs-utils/src', + ), + '@mui/internal-scripts/typescript-to-proptypes': path.resolve( + WORKSPACE_ROOT, + './packages-internal/scripts/typescript-to-proptypes/src', + ), + '@mui/internal-test-utils': path.resolve( + WORKSPACE_ROOT, + './packages-internal/test-utils/src', + ), + docs: path.resolve(WORKSPACE_ROOT, './docs'), + }, + }, + optimizeDeps: { + force: true, + esbuildOptions: { + loader: { + '.js': 'tsx', + }, + }, + }, + test: { + include: ['./regressions.test.tsx'], + browser: { + enabled: true, + provider: 'playwright', + headless: true, + screenshotDirectory: path.resolve(import.meta.dirname, './regressions/screenshots/chrome'), + instances: [ + { + browser: 'chromium', + viewport: { width: 1000, height: 700 }, + }, + ], + }, + }, +});