From 65efa9f9c96f4399236188273c3acb083ade95cd Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:04:57 +0200 Subject: [PATCH] [code-infra] Optimize regression tests (#43889) --- test/regressions/TestViewer.js | 5 +- test/regressions/index.js | 129 ++++++++++++++++++++------------- test/regressions/index.test.js | 37 +++++----- 3 files changed, 99 insertions(+), 72 deletions(-) diff --git a/test/regressions/TestViewer.js b/test/regressions/TestViewer.js index e12734f9d8fa05..f261809e3c69b0 100644 --- a/test/regressions/TestViewer.js +++ b/test/regressions/TestViewer.js @@ -7,7 +7,7 @@ import JoyBox from '@mui/joy/Box'; import { CssVarsProvider } from '@mui/joy/styles'; function TestViewer(props) { - const { children } = props; + const { children, path } = props; // We're simulating `act(() => ReactDOM.render(children))` // In the end children passive effects should've been flushed. @@ -82,6 +82,7 @@ function TestViewer(props) { {children} @@ -91,6 +92,7 @@ function TestViewer(props) { {children} @@ -103,6 +105,7 @@ function TestViewer(props) { TestViewer.propTypes = { children: PropTypes.node.isRequired, + path: PropTypes.string.isRequired, }; export default TestViewer; diff --git a/test/regressions/index.js b/test/regressions/index.js index 658f14f967ad94..8d377681dfb032 100644 --- a/test/regressions/index.js +++ b/test/regressions/index.js @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import * as ReactDOMClient from 'react-dom/client'; -import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; +import { BrowserRouter as Router, Routes, Route, Link, useNavigate } from 'react-router-dom'; import webfontloader from 'webfontloader'; import { Globals } from '@react-spring/web'; import TestViewer from './TestViewer'; @@ -11,6 +11,12 @@ Globals.assign({ skipAnimation: true, }); +window.muiFixture = { + navigate: () => { + throw new Error(`muiFixture.navigate is not ready`); + }, +}; + // Get all the fixtures specifically written for preventing visual regressions. const importRegressionFixtures = require.context('./fixtures', true, /\.(js|ts|tsx)$/, 'lazy'); const regressionFixtures = []; @@ -295,13 +301,13 @@ if (unusedBlacklistPatterns.size > 0) { const viewerRoot = document.getElementById('test-viewer'); -function FixtureRenderer({ component: FixtureComponent }) { +function FixtureRenderer({ component: FixtureComponent, path }) { const viewerReactRoot = React.useRef(null); React.useLayoutEffect(() => { const renderTimeout = setTimeout(() => { const children = ( - + ); @@ -320,38 +326,43 @@ function FixtureRenderer({ component: FixtureComponent }) { viewerReactRoot.current = null; }); }; - }, [FixtureComponent]); + }, [FixtureComponent, path]); return null; } FixtureRenderer.propTypes = { component: PropTypes.elementType, + path: PropTypes.string.isRequired, }; -function App(props) { - const { fixtures } = props; - - function computeIsDev() { - if (window.location.hash === '#dev') { - return true; - } - if (window.location.hash === '#no-dev') { - return false; - } - return process.env.NODE_ENV === 'development'; - } - const [isDev, setDev] = React.useState(computeIsDev); - React.useEffect(() => { - function handleHashChange() { - setDev(computeIsDev()); - } - window.addEventListener('hashchange', handleHashChange); - +function useHash() { + const subscribe = React.useCallback((callback) => { + window.addEventListener('hashchange', callback); return () => { - window.removeEventListener('hashchange', handleHashChange); + window.removeEventListener('hashchange', callback); }; }, []); + const getSnapshot = React.useCallback(() => window.location.hash, []); + const getServerSnapshot = React.useCallback(() => '', []); + return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); +} + +function computeIsDev(hash) { + if (hash === '#dev') { + return true; + } + if (hash === '#no-dev') { + return false; + } + return process.env.NODE_ENV === 'development'; +} + +function App(props) { + const { fixtures } = props; + + const hash = useHash(); + const isDev = computeIsDev(hash); // Using does not apply the google Roboto font in chromium headless/headfull. const [fontState, setFontState] = React.useState('pending'); @@ -380,8 +391,13 @@ function App(props) { return `/${fixture.suite}/${fixture.name}`; } + const navigate = useNavigate(); + React.useEffect(() => { + window.muiFixture.navigate = navigate; + }, [navigate]); + return ( - + {fixtures.map((fixture) => { const path = computePath(fixture); @@ -396,36 +412,43 @@ function App(props) { key={path} exact path={path} - element={fixturePrepared ? : null} + element={ + fixturePrepared ? ( + + ) : null + } /> ); })} - - + {isDev ? ( +
+
webfontloader: {fontState}
+

+ Devtools can be enabled by appending #dev in the addressbar or disabled by + appending #no-dev. +

+ Hide devtools +
+ nav for all tests + + +
+
+ ) : null} + ); } @@ -434,6 +457,10 @@ App.propTypes = { }; const container = document.getElementById('react-root'); -const children = ; +const children = ( + + {' '} + +); const reactRoot = ReactDOMClient.createRoot(container); reactRoot.render(children); diff --git a/test/regressions/index.test.js b/test/regressions/index.test.js index a0f4b5ad30c89d..a612b7daca3929 100644 --- a/test/regressions/index.test.js +++ b/test/regressions/index.test.js @@ -31,7 +31,7 @@ async function main() { // Wait for all requests to finish. // This should load shared resources such as fonts. - await page.goto(`${baseUrl}#no-dev`, { waitUntil: 'networkidle0' }); + await page.goto(`${baseUrl}#dev`, { waitUntil: 'networkidle0' }); // If we still get flaky fonts after awaiting this try `document.fonts.ready` await page.waitForSelector('[data-webfontloader="active"]', { state: 'attached' }); @@ -50,18 +50,21 @@ async function main() { }); routes = routes.map((route) => route.replace(baseUrl, '')); - async function renderFixture(index) { + /** + * @param {string} route + */ + async function renderFixture(route) { // Use client-side routing which is much faster than full page navigation via page.goto(). - // Could become an issue with test isolation. - // If tests are flaky due to global pollution switch to page.goto(route); - // puppeteers built-in click() times out - await page.$eval(`#tests li:nth-of-type(${index + 1}) a`, (link) => { - link.click(); - }); + await page.evaluate((_route) => { + window.muiFixture.navigate(`${_route}#no-dev`); + }, route); + // Move cursor offscreen to not trigger unwanted hover effects. - page.mouse.move(0, 0); + await page.mouse.move(0, 0); - const testcase = await page.waitForSelector('[data-testid="testcase"]:not([aria-busy="true"])'); + const testcase = await page.waitForSelector( + `[data-testid="testcase"][data-testpath="${route}"]:not([aria-busy="true"])`, + ); return testcase; } @@ -94,24 +97,21 @@ async function main() { await browser.close(); }); - routes.forEach((route, index) => { + routes.forEach((route) => { it(`creates screenshots of ${route}`, async function test() { // With the playwright inspector we might want to call `page.pause` which would lead to a timeout. if (process.env.PWDEBUG) { this.timeout(0); } - const testcase = await renderFixture(index); + const testcase = await renderFixture(route); await takeScreenshot({ testcase, route }); }); }); describe('Rating', () => { it('should handle focus-visible correctly', async () => { - const index = routes.findIndex( - (route) => route === '/regression-Rating/FocusVisibleRating', - ); - const testcase = await renderFixture(index); + const testcase = await renderFixture('/regression-Rating/FocusVisibleRating'); await page.keyboard.press('Tab'); await takeScreenshot({ testcase, route: '/regression-Rating/FocusVisibleRating2' }); await page.keyboard.press('ArrowLeft'); @@ -119,10 +119,7 @@ async function main() { }); it('should handle focus-visible with precise ratings correctly', async () => { - const index = routes.findIndex( - (route) => route === '/regression-Rating/PreciseFocusVisibleRating', - ); - const testcase = await renderFixture(index); + const testcase = await renderFixture('/regression-Rating/PreciseFocusVisibleRating'); await page.keyboard.press('Tab'); await takeScreenshot({ testcase, route: '/regression-Rating/PreciseFocusVisibleRating2' }); await page.keyboard.press('ArrowRight');