Skip to content

Commit

Permalink
[wip][poc] Add Pigment CSS screenshot test (mui#43280)
Browse files Browse the repository at this point in the history
  • Loading branch information
mnajdova authored and Michael-Hutchinson committed Sep 7, 2024
1 parent 1676ff4 commit 5fce989
Show file tree
Hide file tree
Showing 13 changed files with 1,069 additions and 20 deletions.
6 changes: 6 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,12 @@ jobs:
- run:
name: Run visual regression tests
command: xvfb-run pnpm test:regressions
- run:
name: Build packages for fixtures
command: xvfb-run pnpm release:build
- run:
name: Run visual regression tests using Pigment CSS
command: xvfb-run pnpm test:regressions-pigment-css
- run:
name: Upload screenshots to Argos CI
command: pnpm test:argos
Expand Down
121 changes: 121 additions & 0 deletions apps/pigment-css-next-app/src/app/material-ui/react-select/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use client';
import * as React from 'react';
import BasicSelect from '../../../../../../docs/data/material/components/selects/BasicSelect';
import ControlledOpenSelect from '../../../../../../docs/data/material/components/selects/ControlledOpenSelect';
import CustomizedSelects from '../../../../../../docs/data/material/components/selects/CustomizedSelects';
import DialogSelect from '../../../../../../docs/data/material/components/selects/DialogSelect';
import GroupedSelect from '../../../../../../docs/data/material/components/selects/GroupedSelect';
import MultipleSelect from '../../../../../../docs/data/material/components/selects/MultipleSelect';
import MultipleSelectCheckmarks from '../../../../../../docs/data/material/components/selects/MultipleSelectCheckmarks';
import MultipleSelectChip from '../../../../../../docs/data/material/components/selects/MultipleSelectChip';
import MultipleSelectNative from '../../../../../../docs/data/material/components/selects/MultipleSelectNative';
import MultipleSelectPlaceholder from '../../../../../../docs/data/material/components/selects/MultipleSelectPlaceholder';
import NativeSelectDemo from '../../../../../../docs/data/material/components/selects/NativeSelectDemo';
import SelectAutoWidth from '../../../../../../docs/data/material/components/selects/SelectAutoWidth';
import SelectLabels from '../../../../../../docs/data/material/components/selects/SelectLabels';
import SelectOtherProps from '../../../../../../docs/data/material/components/selects/SelectOtherProps';
import SelectSmall from '../../../../../../docs/data/material/components/selects/SelectSmall';
import SelectVariants from '../../../../../../docs/data/material/components/selects/SelectVariants';

export default function Selects() {
return (
<React.Fragment>
<section>
<h2> Basic Select</h2>
<div className="demo-container">
<BasicSelect />
</div>
</section>
<section>
<h2> Controlled Open Select</h2>
<div className="demo-container">
<ControlledOpenSelect />
</div>
</section>
<section>
<h2> Customized Selects</h2>
<div className="demo-container">
<CustomizedSelects />
</div>
</section>
<section>
<h2> Dialog Select</h2>
<div className="demo-container">
<DialogSelect />
</div>
</section>
<section>
<h2> Grouped Select</h2>
<div className="demo-container">
<GroupedSelect />
</div>
</section>
<section>
<h2> Multiple Select</h2>
<div className="demo-container">
<MultipleSelect />
</div>
</section>
<section>
<h2> Multiple Select Checkmarks</h2>
<div className="demo-container">
<MultipleSelectCheckmarks />
</div>
</section>
<section>
<h2> Multiple Select Chip</h2>
<div className="demo-container">
<MultipleSelectChip />
</div>
</section>
<section>
<h2> Multiple Select Native</h2>
<div className="demo-container">
<MultipleSelectNative />
</div>
</section>
<section>
<h2> Multiple Select Placeholder</h2>
<div className="demo-container">
<MultipleSelectPlaceholder />
</div>
</section>
<section>
<h2> Native Select Demo</h2>
<div className="demo-container">
<NativeSelectDemo />
</div>
</section>
<section>
<h2> Select Auto Width</h2>
<div className="demo-container">
<SelectAutoWidth />
</div>
</section>
<section>
<h2> Select Labels</h2>
<div className="demo-container">
<SelectLabels />
</div>
</section>
<section>
<h2> Select Other Props</h2>
<div className="demo-container">
<SelectOtherProps />
</div>
</section>
<section>
<h2> Select Small</h2>
<div className="demo-container">
<SelectSmall />
</div>
</section>
<section>
<h2> Select Variants</h2>
<div className="demo-container">
<SelectVariants />
</div>
</section>
</React.Fragment>
);
}
7 changes: 7 additions & 0 deletions apps/pigment-css-vite-app/.mocharc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
recursive: true,
slow: 500,
timeout: (process.env.CIRCLECI === 'true' ? 4 : 2) * 1000, // Circle CI has low-performance CPUs.
reporter: 'dot',
require: ['@mui/internal-test-utils/setupBabelPlaywright'],
};
4 changes: 3 additions & 1 deletion apps/pigment-css-vite-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@mui/material": "workspace:^",
"@mui/system": "workspace:^",
"clsx": "^2.1.1",
"playwright": "^1.46.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
Expand All @@ -32,7 +33,8 @@
"postcss": "^8.4.44",
"postcss-combine-media-query": "^1.0.1",
"vite": "5.4.2",
"vite-plugin-pages": "^0.32.3"
"vite-plugin-pages": "^0.32.3",
"vite-plugin-node-polyfills": "0.22.0"
},
"nx": {
"targets": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';

export default function BasicSelect() {
const [age, setAge] = React.useState(10);

const handleChange = (event) => {
setAge(event.target.value);
};

return (
<Box sx={{ minWidth: 120, minHeight: 250 }}>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">Age</InputLabel>
<Select
defaultOpen
labelId="demo-simple-select-label"
id="demo-simple-select"
value={age}
label="Age"
onChange={handleChange}
>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
</FormControl>
</Box>
);
}
117 changes: 117 additions & 0 deletions apps/pigment-css-vite-app/src/pages/fixtures/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as path from 'path';
import * as fse from 'fs-extra';
import * as playwright from 'playwright';

async function main() {
const baseUrl = 'http://localhost:5001/fixtures';
const screenshotDir = path.resolve('screenshots/chrome');
const browser = await playwright.chromium.launch({
args: ['--font-render-hinting=none'],
// otherwise the loaded google Roboto font isn't applied
headless: false,
});
// reuse viewport from `vrtest`
// https://github.com/nathanmarks/vrtest/blob/1185b852a6c1813cedf5d81f6d6843d9a241c1ce/src/server/runner.js#L44
const page = await browser.newPage({
viewport: { width: 1000, height: 700 },
reducedMotion: 'reduce',
});

// Block images since they slow down tests (need download).
// They're also most likely decorative for documentation demos
await page.route(/./, async (route, request) => {
const type = await request.resourceType();
if (type === 'image') {
route.abort();
} else {
route.continue();
}
});

// Wait for all requests to finish.
// This should load shared resources such as fonts.
await page.goto(`${baseUrl}`, { waitUntil: 'networkidle0' });
// If we still get flaky fonts after awaiting this try `document.fonts.ready`
// await page.waitForSelector('[data-webfontloader="active"]', { state: 'attached' });

// Simulate portrait mode for date pickers.
// See `useIsLandscape`.
await page.evaluate(() => {
Object.defineProperty(window.screen.orientation, 'angle', {
get() {
return 0;
},
});
});

let routes = await page.$$eval('#tests a', (links) => {
return links.map((link) => link.href);
});
routes = routes.map((route) => route.replace(baseUrl, ''));

async function renderFixture(index) {
// 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();
});
// Move cursor offscreen to not trigger unwanted hover effects.
page.mouse.move(0, 0);

const testcase = await page.waitForSelector('#root-demo');

return testcase;
}

async function takeScreenshot({ testcase, route }) {
const screenshotPath = path.resolve(screenshotDir, `.${route}.png`);
await fse.ensureDir(path.dirname(screenshotPath));

const explicitScreenshotTarget = await page.$('[data-testid="screenshot-target"]');
const screenshotTarget = explicitScreenshotTarget || testcase;

await screenshotTarget.screenshot({
path: screenshotPath,
type: 'png',
animations: 'disabled',
});
}

// prepare screenshots
await fse.emptyDir(screenshotDir);

describe('visual regressions', () => {
beforeEach(async () => {
await page.evaluate(() => {
localStorage.clear();
});
});

after(async () => {
await browser.close();
});

routes.forEach((route, index) => {
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);
await takeScreenshot({ testcase, route });
});
});
});

run();
}

main().catch((error) => {
// error during setup.
// Throwing lets mocha hang.
console.error(error);
process.exit(1);
});
80 changes: 80 additions & 0 deletions apps/pigment-css-vite-app/src/pages/fixtures/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as React from 'react';
import { useLocation, matchRoutes, Link } from 'react-router-dom';
import routes from '~react-pages';
import IndexLayout from '../../Layout';

export default function Layout() {
const location = useLocation();
const matchedRoute = React.useMemo(
() => matchRoutes(routes, location.pathname)?.[0],
[location.pathname],
);

const materialUIRoute = React.useMemo(
() => matchRoutes(routes, location.pathname.replace('fixtures', 'material-ui'))?.[0],
[location.pathname],
);

const demo = new URLSearchParams(location.search).get('demo');
const fixturesRoutes = (matchedRoute?.route.children ?? []).filter(
(item) => !!item.path && item.path !== 'index.test',
);

const demosRoutes = (materialUIRoute?.route.children ?? []).filter(
(item) => !!item.path && item.path.indexOf('react-pagination') < 0,
);

return (
<IndexLayout>
{demo && (
<div id="root-demo">
{fixturesRoutes.find((item) => item.path === demo)?.element}
{demosRoutes.find((item) => item.path === demo)?.element}
</div>
)}
<div>
<h1>Fixtures Material UI + Pigment CSS</h1>
<nav id="tests">
<ul
sx={{
margin: 0,
marginBlock: '1rem',
padding: 0,
paddingLeft: '1.5rem',
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
}}
>
{fixturesRoutes.map((item) => (
<li key={item.path}>
<Link
to={`/fixtures/?demo=${item.path}`}
sx={{
textDecoration: 'underline',
fontSize: '17px',
}}
>
{item.path}
</Link>
</li>
))}
{demosRoutes.map((item) => (
<li key={item.path}>
<Link
to={`/fixtures/?demo=${item.path}`}
sx={{
textDecoration: 'underline',
fontSize: '17px',
}}
>
{item.path}
</Link>
</li>
))}
</ul>
</nav>
</div>
</IndexLayout>
);
}
Loading

0 comments on commit 5fce989

Please sign in to comment.