Skip to content

Commit

Permalink
[code-infra] Optimize regression tests (#43889)
Browse files Browse the repository at this point in the history
  • Loading branch information
Janpot authored Sep 30, 2024
1 parent 590f4ff commit 65efa9f
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 72 deletions.
5 changes: 4 additions & 1 deletion test/regressions/TestViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -82,6 +82,7 @@ function TestViewer(props) {
<JoyBox
aria-busy={!ready}
data-testid="testcase"
data-testpath={path}
sx={{ bgcolor: 'background.body', ...viewerBoxSx }}
>
{children}
Expand All @@ -91,6 +92,7 @@ function TestViewer(props) {
<Box
aria-busy={!ready}
data-testid="testcase"
data-testpath={path}
sx={{ bgcolor: 'background.default', ...viewerBoxSx }}
>
{children}
Expand All @@ -103,6 +105,7 @@ function TestViewer(props) {

TestViewer.propTypes = {
children: PropTypes.node.isRequired,
path: PropTypes.string.isRequired,
};

export default TestViewer;
129 changes: 78 additions & 51 deletions test/regressions/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 = [];
Expand Down Expand Up @@ -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 = (
<TestViewer>
<TestViewer path={path}>
<FixtureComponent />
</TestViewer>
);
Expand All @@ -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 <link rel="stylesheet" /> does not apply the google Roboto font in chromium headless/headfull.
const [fontState, setFontState] = React.useState('pending');
Expand Down Expand Up @@ -380,8 +391,13 @@ function App(props) {
return `/${fixture.suite}/${fixture.name}`;
}

const navigate = useNavigate();
React.useEffect(() => {
window.muiFixture.navigate = navigate;
}, [navigate]);

return (
<Router>
<React.Fragment>
<Routes>
{fixtures.map((fixture) => {
const path = computePath(fixture);
Expand All @@ -396,36 +412,43 @@ function App(props) {
key={path}
exact
path={path}
element={fixturePrepared ? <FixtureRenderer component={FixtureComponent} /> : null}
element={
fixturePrepared ? (
<FixtureRenderer component={FixtureComponent} path={path} />
) : null
}
/>
);
})}
</Routes>

<div hidden={!isDev}>
<div data-webfontloader={fontState}>webfontloader: {fontState}</div>
<p>
Devtools can be enabled by appending <code>#dev</code> in the addressbar or disabled by
appending <code>#no-dev</code>.
</p>
<a href="#no-dev">Hide devtools</a>
<details>
<summary id="my-test-summary">nav for all tests</summary>
<nav id="tests">
<ol>
{fixtures.map((fixture) => {
const path = computePath(fixture);
return (
<li key={path}>
<Link to={path}>{path}</Link>
</li>
);
})}
</ol>
</nav>
</details>
</div>
</Router>
{isDev ? (
<div>
<div data-webfontloader={fontState}>webfontloader: {fontState}</div>
<p>
Devtools can be enabled by appending <code>#dev</code> in the addressbar or disabled by
appending <code>#no-dev</code>.
</p>
<a href="#no-dev">Hide devtools</a>
<details>
<summary id="my-test-summary">nav for all tests</summary>

<nav id="tests">
<ol>
{fixtures.map((fixture) => {
const path = computePath(fixture);
return (
<li key={path}>
<Link to={path}>{path}</Link>
</li>
);
})}
</ol>
</nav>
</details>
</div>
) : null}
</React.Fragment>
);
}

Expand All @@ -434,6 +457,10 @@ App.propTypes = {
};

const container = document.getElementById('react-root');
const children = <App fixtures={regressionFixtures.concat(demoFixtures)} />;
const children = (
<Router>
<App fixtures={regressionFixtures.concat(demoFixtures)} />{' '}
</Router>
);
const reactRoot = ReactDOMClient.createRoot(container);
reactRoot.render(children);
37 changes: 17 additions & 20 deletions test/regressions/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });

Expand All @@ -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;
}
Expand Down Expand Up @@ -94,35 +97,29 @@ 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');
await takeScreenshot({ testcase, route: '/regression-Rating/FocusVisibleRating3' });
});

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');
Expand Down

0 comments on commit 65efa9f

Please sign in to comment.