Skip to content

Commit 46698de

Browse files
authored
[wip][poc] Add Pigment CSS screenshot test (#43280)
1 parent bcff288 commit 46698de

File tree

13 files changed

+1069
-20
lines changed

13 files changed

+1069
-20
lines changed

.circleci/config.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,12 @@ jobs:
495495
- run:
496496
name: Run visual regression tests
497497
command: xvfb-run pnpm test:regressions
498+
- run:
499+
name: Build packages for fixtures
500+
command: xvfb-run pnpm release:build
501+
- run:
502+
name: Run visual regression tests using Pigment CSS
503+
command: xvfb-run pnpm test:regressions-pigment-css
498504
- run:
499505
name: Upload screenshots to Argos CI
500506
command: pnpm test:argos
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
'use client';
2+
import * as React from 'react';
3+
import BasicSelect from '../../../../../../docs/data/material/components/selects/BasicSelect';
4+
import ControlledOpenSelect from '../../../../../../docs/data/material/components/selects/ControlledOpenSelect';
5+
import CustomizedSelects from '../../../../../../docs/data/material/components/selects/CustomizedSelects';
6+
import DialogSelect from '../../../../../../docs/data/material/components/selects/DialogSelect';
7+
import GroupedSelect from '../../../../../../docs/data/material/components/selects/GroupedSelect';
8+
import MultipleSelect from '../../../../../../docs/data/material/components/selects/MultipleSelect';
9+
import MultipleSelectCheckmarks from '../../../../../../docs/data/material/components/selects/MultipleSelectCheckmarks';
10+
import MultipleSelectChip from '../../../../../../docs/data/material/components/selects/MultipleSelectChip';
11+
import MultipleSelectNative from '../../../../../../docs/data/material/components/selects/MultipleSelectNative';
12+
import MultipleSelectPlaceholder from '../../../../../../docs/data/material/components/selects/MultipleSelectPlaceholder';
13+
import NativeSelectDemo from '../../../../../../docs/data/material/components/selects/NativeSelectDemo';
14+
import SelectAutoWidth from '../../../../../../docs/data/material/components/selects/SelectAutoWidth';
15+
import SelectLabels from '../../../../../../docs/data/material/components/selects/SelectLabels';
16+
import SelectOtherProps from '../../../../../../docs/data/material/components/selects/SelectOtherProps';
17+
import SelectSmall from '../../../../../../docs/data/material/components/selects/SelectSmall';
18+
import SelectVariants from '../../../../../../docs/data/material/components/selects/SelectVariants';
19+
20+
export default function Selects() {
21+
return (
22+
<React.Fragment>
23+
<section>
24+
<h2> Basic Select</h2>
25+
<div className="demo-container">
26+
<BasicSelect />
27+
</div>
28+
</section>
29+
<section>
30+
<h2> Controlled Open Select</h2>
31+
<div className="demo-container">
32+
<ControlledOpenSelect />
33+
</div>
34+
</section>
35+
<section>
36+
<h2> Customized Selects</h2>
37+
<div className="demo-container">
38+
<CustomizedSelects />
39+
</div>
40+
</section>
41+
<section>
42+
<h2> Dialog Select</h2>
43+
<div className="demo-container">
44+
<DialogSelect />
45+
</div>
46+
</section>
47+
<section>
48+
<h2> Grouped Select</h2>
49+
<div className="demo-container">
50+
<GroupedSelect />
51+
</div>
52+
</section>
53+
<section>
54+
<h2> Multiple Select</h2>
55+
<div className="demo-container">
56+
<MultipleSelect />
57+
</div>
58+
</section>
59+
<section>
60+
<h2> Multiple Select Checkmarks</h2>
61+
<div className="demo-container">
62+
<MultipleSelectCheckmarks />
63+
</div>
64+
</section>
65+
<section>
66+
<h2> Multiple Select Chip</h2>
67+
<div className="demo-container">
68+
<MultipleSelectChip />
69+
</div>
70+
</section>
71+
<section>
72+
<h2> Multiple Select Native</h2>
73+
<div className="demo-container">
74+
<MultipleSelectNative />
75+
</div>
76+
</section>
77+
<section>
78+
<h2> Multiple Select Placeholder</h2>
79+
<div className="demo-container">
80+
<MultipleSelectPlaceholder />
81+
</div>
82+
</section>
83+
<section>
84+
<h2> Native Select Demo</h2>
85+
<div className="demo-container">
86+
<NativeSelectDemo />
87+
</div>
88+
</section>
89+
<section>
90+
<h2> Select Auto Width</h2>
91+
<div className="demo-container">
92+
<SelectAutoWidth />
93+
</div>
94+
</section>
95+
<section>
96+
<h2> Select Labels</h2>
97+
<div className="demo-container">
98+
<SelectLabels />
99+
</div>
100+
</section>
101+
<section>
102+
<h2> Select Other Props</h2>
103+
<div className="demo-container">
104+
<SelectOtherProps />
105+
</div>
106+
</section>
107+
<section>
108+
<h2> Select Small</h2>
109+
<div className="demo-container">
110+
<SelectSmall />
111+
</div>
112+
</section>
113+
<section>
114+
<h2> Select Variants</h2>
115+
<div className="demo-container">
116+
<SelectVariants />
117+
</div>
118+
</section>
119+
</React.Fragment>
120+
);
121+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
recursive: true,
3+
slow: 500,
4+
timeout: (process.env.CIRCLECI === 'true' ? 4 : 2) * 1000, // Circle CI has low-performance CPUs.
5+
reporter: 'dot',
6+
require: ['@mui/internal-test-utils/setupBabelPlaywright'],
7+
};

apps/pigment-css-vite-app/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@mui/material": "workspace:^",
1818
"@mui/system": "workspace:^",
1919
"clsx": "^2.1.1",
20+
"playwright": "^1.46.1",
2021
"react": "^18.3.1",
2122
"react-dom": "^18.3.1",
2223
"react-error-boundary": "^4.0.13",
@@ -32,7 +33,8 @@
3233
"postcss": "^8.4.44",
3334
"postcss-combine-media-query": "^1.0.1",
3435
"vite": "5.4.2",
35-
"vite-plugin-pages": "^0.32.3"
36+
"vite-plugin-pages": "^0.32.3",
37+
"vite-plugin-node-polyfills": "0.22.0"
3638
},
3739
"nx": {
3840
"targets": {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as React from 'react';
2+
import Box from '@mui/material/Box';
3+
import InputLabel from '@mui/material/InputLabel';
4+
import MenuItem from '@mui/material/MenuItem';
5+
import FormControl from '@mui/material/FormControl';
6+
import Select from '@mui/material/Select';
7+
8+
export default function BasicSelect() {
9+
const [age, setAge] = React.useState(10);
10+
11+
const handleChange = (event) => {
12+
setAge(event.target.value);
13+
};
14+
15+
return (
16+
<Box sx={{ minWidth: 120, minHeight: 250 }}>
17+
<FormControl fullWidth>
18+
<InputLabel id="demo-simple-select-label">Age</InputLabel>
19+
<Select
20+
defaultOpen
21+
labelId="demo-simple-select-label"
22+
id="demo-simple-select"
23+
value={age}
24+
label="Age"
25+
onChange={handleChange}
26+
>
27+
<MenuItem value={10}>Ten</MenuItem>
28+
<MenuItem value={20}>Twenty</MenuItem>
29+
<MenuItem value={30}>Thirty</MenuItem>
30+
</Select>
31+
</FormControl>
32+
</Box>
33+
);
34+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import * as path from 'path';
2+
import * as fse from 'fs-extra';
3+
import * as playwright from 'playwright';
4+
5+
async function main() {
6+
const baseUrl = 'http://localhost:5001/fixtures';
7+
const screenshotDir = path.resolve('screenshots/chrome');
8+
const browser = await playwright.chromium.launch({
9+
args: ['--font-render-hinting=none'],
10+
// otherwise the loaded google Roboto font isn't applied
11+
headless: false,
12+
});
13+
// reuse viewport from `vrtest`
14+
// https://github.com/nathanmarks/vrtest/blob/1185b852a6c1813cedf5d81f6d6843d9a241c1ce/src/server/runner.js#L44
15+
const page = await browser.newPage({
16+
viewport: { width: 1000, height: 700 },
17+
reducedMotion: 'reduce',
18+
});
19+
20+
// Block images since they slow down tests (need download).
21+
// They're also most likely decorative for documentation demos
22+
await page.route(/./, async (route, request) => {
23+
const type = await request.resourceType();
24+
if (type === 'image') {
25+
route.abort();
26+
} else {
27+
route.continue();
28+
}
29+
});
30+
31+
// Wait for all requests to finish.
32+
// This should load shared resources such as fonts.
33+
await page.goto(`${baseUrl}`, { waitUntil: 'networkidle0' });
34+
// If we still get flaky fonts after awaiting this try `document.fonts.ready`
35+
// await page.waitForSelector('[data-webfontloader="active"]', { state: 'attached' });
36+
37+
// Simulate portrait mode for date pickers.
38+
// See `useIsLandscape`.
39+
await page.evaluate(() => {
40+
Object.defineProperty(window.screen.orientation, 'angle', {
41+
get() {
42+
return 0;
43+
},
44+
});
45+
});
46+
47+
let routes = await page.$$eval('#tests a', (links) => {
48+
return links.map((link) => link.href);
49+
});
50+
routes = routes.map((route) => route.replace(baseUrl, ''));
51+
52+
async function renderFixture(index) {
53+
// Use client-side routing which is much faster than full page navigation via page.goto().
54+
// Could become an issue with test isolation.
55+
// If tests are flaky due to global pollution switch to page.goto(route);
56+
// puppeteers built-in click() times out
57+
await page.$eval(`#tests li:nth-of-type(${index + 1}) a`, (link) => {
58+
link.click();
59+
});
60+
// Move cursor offscreen to not trigger unwanted hover effects.
61+
page.mouse.move(0, 0);
62+
63+
const testcase = await page.waitForSelector('#root-demo');
64+
65+
return testcase;
66+
}
67+
68+
async function takeScreenshot({ testcase, route }) {
69+
const screenshotPath = path.resolve(screenshotDir, `.${route}.png`);
70+
await fse.ensureDir(path.dirname(screenshotPath));
71+
72+
const explicitScreenshotTarget = await page.$('[data-testid="screenshot-target"]');
73+
const screenshotTarget = explicitScreenshotTarget || testcase;
74+
75+
await screenshotTarget.screenshot({
76+
path: screenshotPath,
77+
type: 'png',
78+
animations: 'disabled',
79+
});
80+
}
81+
82+
// prepare screenshots
83+
await fse.emptyDir(screenshotDir);
84+
85+
describe('visual regressions', () => {
86+
beforeEach(async () => {
87+
await page.evaluate(() => {
88+
localStorage.clear();
89+
});
90+
});
91+
92+
after(async () => {
93+
await browser.close();
94+
});
95+
96+
routes.forEach((route, index) => {
97+
it(`creates screenshots of ${route}`, async function test() {
98+
// With the playwright inspector we might want to call `page.pause` which would lead to a timeout.
99+
if (process.env.PWDEBUG) {
100+
this.timeout(0);
101+
}
102+
103+
const testcase = await renderFixture(index);
104+
await takeScreenshot({ testcase, route });
105+
});
106+
});
107+
});
108+
109+
run();
110+
}
111+
112+
main().catch((error) => {
113+
// error during setup.
114+
// Throwing lets mocha hang.
115+
console.error(error);
116+
process.exit(1);
117+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as React from 'react';
2+
import { useLocation, matchRoutes, Link } from 'react-router-dom';
3+
import routes from '~react-pages';
4+
import IndexLayout from '../../Layout';
5+
6+
export default function Layout() {
7+
const location = useLocation();
8+
const matchedRoute = React.useMemo(
9+
() => matchRoutes(routes, location.pathname)?.[0],
10+
[location.pathname],
11+
);
12+
13+
const materialUIRoute = React.useMemo(
14+
() => matchRoutes(routes, location.pathname.replace('fixtures', 'material-ui'))?.[0],
15+
[location.pathname],
16+
);
17+
18+
const demo = new URLSearchParams(location.search).get('demo');
19+
const fixturesRoutes = (matchedRoute?.route.children ?? []).filter(
20+
(item) => !!item.path && item.path !== 'index.test',
21+
);
22+
23+
const demosRoutes = (materialUIRoute?.route.children ?? []).filter(
24+
(item) => !!item.path && item.path.indexOf('react-pagination') < 0,
25+
);
26+
27+
return (
28+
<IndexLayout>
29+
{demo && (
30+
<div id="root-demo">
31+
{fixturesRoutes.find((item) => item.path === demo)?.element}
32+
{demosRoutes.find((item) => item.path === demo)?.element}
33+
</div>
34+
)}
35+
<div>
36+
<h1>Fixtures Material UI + Pigment CSS</h1>
37+
<nav id="tests">
38+
<ul
39+
sx={{
40+
margin: 0,
41+
marginBlock: '1rem',
42+
padding: 0,
43+
paddingLeft: '1.5rem',
44+
display: 'flex',
45+
flexDirection: 'column',
46+
gap: '0.5rem',
47+
}}
48+
>
49+
{fixturesRoutes.map((item) => (
50+
<li key={item.path}>
51+
<Link
52+
to={`/fixtures/?demo=${item.path}`}
53+
sx={{
54+
textDecoration: 'underline',
55+
fontSize: '17px',
56+
}}
57+
>
58+
{item.path}
59+
</Link>
60+
</li>
61+
))}
62+
{demosRoutes.map((item) => (
63+
<li key={item.path}>
64+
<Link
65+
to={`/fixtures/?demo=${item.path}`}
66+
sx={{
67+
textDecoration: 'underline',
68+
fontSize: '17px',
69+
}}
70+
>
71+
{item.path}
72+
</Link>
73+
</li>
74+
))}
75+
</ul>
76+
</nav>
77+
</div>
78+
</IndexLayout>
79+
);
80+
}

0 commit comments

Comments
 (0)