u
6
.gitignore
vendored
@@ -2,3 +2,9 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Playwright screenshot testing
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
|||||||
36
README.md
@@ -42,7 +42,39 @@ npm run build # bundle the dev harness (dist/)
|
|||||||
npm run lint
|
npm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Screenshot testing
|
||||||
|
|
||||||
|
Variations of the GUI elements are covered by **reference screenshot tests**
|
||||||
|
(Playwright). Each variation is rendered by the static harness at
|
||||||
|
[`/screenshots.html`](screenshots.html) (`src/dev/screenshots/ScreenshotHarness.jsx`)
|
||||||
|
inside a `data-shot` section, and every section is compared against a committed
|
||||||
|
reference image in `tests/screenshots/reference/`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run test:screenshots # compare against references
|
||||||
|
npm run test:screenshots:update # regenerate references (after intentional changes)
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- The tests reuse the running dev server (`npm run dev` on port 5199) or start
|
||||||
|
it automatically.
|
||||||
|
- Reference images are committed so visual regressions fail the run; the
|
||||||
|
actual + diff images land in `test-results/` (gitignored).
|
||||||
|
- Animations are frozen during capture so spinners/transitions render
|
||||||
|
deterministically.
|
||||||
|
- Interactive states (e.g. the `EditablePanelTile` edit/dirty rings) are driven
|
||||||
|
from the test itself, on top of the static sections.
|
||||||
|
|
||||||
|
### Adding a new variation
|
||||||
|
|
||||||
|
1. Add a `<Section id="…">` block to `ScreenshotHarness.jsx`.
|
||||||
|
2. Add the id to the test in `tests/screenshot.spec.js` (or write a dedicated
|
||||||
|
interaction test).
|
||||||
|
3. Run `npm run test:screenshots:update` to generate the new reference, review
|
||||||
|
it, and commit it.
|
||||||
|
|
||||||
## Future
|
## Future
|
||||||
|
|
||||||
- Test cases (unit + component tests) with mock pages will be added under
|
- Unit + component tests with mock pages will be added under `dev/` /
|
||||||
`dev/` / `src/__tests__/` as they are written.
|
`src/__tests__/` as they are written.
|
||||||
|
|||||||
64
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^9.3.1",
|
"@mui/icons-material": "^9.3.1",
|
||||||
"@mui/material": "^9.3.1",
|
"@mui/material": "^9.3.1",
|
||||||
|
"@playwright/test": "^1.62.1",
|
||||||
"@vitejs/plugin-react": "6.0.5",
|
"@vitejs/plugin-react": "6.0.5",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
@@ -625,6 +626,22 @@
|
|||||||
"url": "https://github.com/sponsors/Boshen"
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.62.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz",
|
||||||
|
"integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.62.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@popperjs/core": {
|
"node_modules/@popperjs/core": {
|
||||||
"version": "2.11.8",
|
"version": "2.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
@@ -1654,6 +1671,53 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.62.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||||
|
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.62.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.62.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||||
|
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.26",
|
"version": "8.5.26",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint ."
|
"lint": "eslint .",
|
||||||
|
"test:screenshots": "playwright test",
|
||||||
|
"test:screenshots:update": "playwright test --update-snapshots"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^9.3.1",
|
"@mui/icons-material": "^9.3.1",
|
||||||
"@mui/material": "^9.3.1",
|
"@mui/material": "^9.3.1",
|
||||||
|
"@playwright/test": "^1.62.1",
|
||||||
"@vitejs/plugin-react": "6.0.5",
|
"@vitejs/plugin-react": "6.0.5",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
|
|||||||
28
playwright.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
// Screenshot testing against the existing Vite dev harness (localhost:5199).
|
||||||
|
// Each `data-shot` section of /screenshots.html becomes one reference image
|
||||||
|
// under tests/screenshots/reference/. Refs are generated on first run; on
|
||||||
|
// changes regenerate them with `npm run test:screenshots:update`.
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
fullyParallel: false,
|
||||||
|
snapshotPathTemplate: '{testDir}/screenshots/reference/{arg}{ext}',
|
||||||
|
expect: {
|
||||||
|
toHaveScreenshot: {
|
||||||
|
// Freeze spinners / transitions so animated states render deterministically.
|
||||||
|
animations: 'disabled',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:5199',
|
||||||
|
viewport: { width: 1200, height: 800 },
|
||||||
|
screenshot: 'off',
|
||||||
|
},
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev',
|
||||||
|
url: 'http://localhost:5199',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 60_000,
|
||||||
|
},
|
||||||
|
});
|
||||||
12
screenshots.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>dashgui screenshot harness</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/dev/screenshotsMain.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
300
src/dev/screenshots/ScreenshotHarness.jsx
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import FolderIcon from '@mui/icons-material/FolderOutlined';
|
||||||
|
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||||
|
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
|
||||||
|
import { EditablePanelTile, PanelTile, PanelTileGrid } from '../../panels';
|
||||||
|
import RecordList from '../../lists/RecordList';
|
||||||
|
import { Tile, tileBaseSx } from '../../tiles';
|
||||||
|
import ElevatedPill, {
|
||||||
|
ActionElevatedPill,
|
||||||
|
ToggleElevatedPill,
|
||||||
|
} from '../../ElevatedPill';
|
||||||
|
import SingleLineTextEdit from '../../SingleLineTextEdit';
|
||||||
|
|
||||||
|
const MOCK_ROWS = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
number: 'L-1001',
|
||||||
|
date: '2026-08-01',
|
||||||
|
party: 'Müller GmbH',
|
||||||
|
status: 'fertig',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
number: 'L-1002',
|
||||||
|
date: '2026-08-03',
|
||||||
|
party: 'Schmidt & Co',
|
||||||
|
status: 'offen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
number: 'L-1003',
|
||||||
|
date: '2026-08-05',
|
||||||
|
party: 'Weber OHG',
|
||||||
|
status: 'offen',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const COLUMNS = [
|
||||||
|
{ key: 'number', label: 'Nummer' },
|
||||||
|
{ key: 'date', label: 'Lieferdatum' },
|
||||||
|
{ key: 'party', label: 'Lieferant' },
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: 'Status',
|
||||||
|
render: (row) => (
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={row.status}
|
||||||
|
color={row.status === 'fertig' ? 'success' : 'default'}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
class MockFolderTile extends Tile {
|
||||||
|
renderIcon() {
|
||||||
|
return <FolderIcon color="action" sx={{ fontSize: 40 }} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockAddTile extends Tile {
|
||||||
|
renderIcon() {
|
||||||
|
return <AddIcon color="primary" sx={{ fontSize: 40 }} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed, deterministic gallery of every GUI variation — one `data-shot`
|
||||||
|
* section per reference screenshot. Served under /screenshots.html and
|
||||||
|
* captured by the Playwright screenshot tests (tests/screenshot.spec.js).
|
||||||
|
*
|
||||||
|
* Deliberately static: no timers, no routing, no user state. Only stable
|
||||||
|
* props, so references stay comparable across runs. Interactive states
|
||||||
|
* (EditablePanelTile editing/dirty) are driven from the test, not here.
|
||||||
|
*/
|
||||||
|
function Section({ id, label, children }) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
component="section"
|
||||||
|
data-shot={id}
|
||||||
|
sx={{ background: '#f4f4f5', p: 2, borderRadius: 1, mb: 3 }}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="overline"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ display: 'block', mb: 1.5 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScreenshotHarness extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: 1000, mx: 'auto', py: 3, px: 1 }}>
|
||||||
|
<Section id="pills" label="ElevatedPill / Toggle / Action">
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||||
|
<ElevatedPill label="Statisch" />
|
||||||
|
<ElevatedPill label="Aktiv" active />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||||
|
<ToggleElevatedPill label="Aus" checked={false} onToggle={() => {}} />
|
||||||
|
<ToggleElevatedPill label="An" checked onToggle={() => {}} />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||||
|
<ActionElevatedPill
|
||||||
|
label="Inaktiv"
|
||||||
|
icon={<ToggleOnIcon sx={{ fontSize: 18 }} />}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
<ActionElevatedPill
|
||||||
|
label="Aktiv"
|
||||||
|
active
|
||||||
|
icon={<ToggleOnIcon sx={{ fontSize: 18 }} />}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="textfields" label="SingleLineTextEdit">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
maxWidth: 480,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SingleLineTextEdit label="Name" value="ACME GmbH" onChange={() => {}} />
|
||||||
|
<SingleLineTextEdit
|
||||||
|
label="Name"
|
||||||
|
value=""
|
||||||
|
onChange={() => {}}
|
||||||
|
helperText="Mit Hilfetext"
|
||||||
|
/>
|
||||||
|
<SingleLineTextEdit
|
||||||
|
label="Nur lesen"
|
||||||
|
value="Schreibgeschützt"
|
||||||
|
readOnly
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
<SingleLineTextEdit
|
||||||
|
label="Deaktiviert"
|
||||||
|
value="Ausgegraut"
|
||||||
|
disabled
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
<SingleLineTextEdit
|
||||||
|
label="Klein"
|
||||||
|
value="Small"
|
||||||
|
size="small"
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="tiles" label="Tile / tileBaseSx">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(4, 140px)',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MockFolderTile label="Ordner" secondary="3 Artikel" />
|
||||||
|
<MockAddTile label="Neu" secondary="anlegen" />
|
||||||
|
<MockFolderTile label="Deaktiviert" secondary="nicht klickbar" disabled />
|
||||||
|
<Box sx={{ ...tileBaseSx, bgcolor: 'primary.main', color: 'white' }}>
|
||||||
|
<Typography variant="body2" fontWeight={600}>
|
||||||
|
tileBaseSx direkt
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="panels-basic" label="PanelTile">
|
||||||
|
<PanelTileGrid>
|
||||||
|
<PanelTile wide={2} title="Firma">
|
||||||
|
<Typography variant="body1">ACME GmbH</Typography>
|
||||||
|
</PanelTile>
|
||||||
|
<PanelTile wide={1} title="Farbe" color="#fef3c7">
|
||||||
|
<Typography variant="body2">Mit Hintergrundfarbe</Typography>
|
||||||
|
</PanelTile>
|
||||||
|
<PanelTile
|
||||||
|
wide={1}
|
||||||
|
title="Mit Aktion"
|
||||||
|
action={{ label: 'Speichern', icon: SaveOutlinedIcon, onClick: () => {} }}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">Idle Aktion</Typography>
|
||||||
|
</PanelTile>
|
||||||
|
</PanelTileGrid>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="panels-busy" label="PanelTile – Aktion busy">
|
||||||
|
<PanelTileGrid>
|
||||||
|
<PanelTile
|
||||||
|
wide={2}
|
||||||
|
title="Speichern läuft"
|
||||||
|
action={{
|
||||||
|
label: 'Speichern',
|
||||||
|
icon: SaveOutlinedIcon,
|
||||||
|
busy: true,
|
||||||
|
onClick: () => {},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">Wird gespeichert …</Typography>
|
||||||
|
</PanelTile>
|
||||||
|
</PanelTileGrid>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="editable" label="EditablePanelTile">
|
||||||
|
<PanelTileGrid>
|
||||||
|
<EditablePanelTile
|
||||||
|
wide={2}
|
||||||
|
title="Firma"
|
||||||
|
value="ACME GmbH"
|
||||||
|
onSave={async () => {}}
|
||||||
|
onError={() => {}}
|
||||||
|
display={<Typography variant="body1">ACME GmbH</Typography>}
|
||||||
|
edit={({ draft, onChange, inputRef }) => (
|
||||||
|
<SingleLineTextEdit
|
||||||
|
label="Name"
|
||||||
|
value={draft}
|
||||||
|
onChange={onChange}
|
||||||
|
inputRef={inputRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</PanelTileGrid>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="lists-ready" label="RecordList – ready">
|
||||||
|
<RecordList
|
||||||
|
status="ready"
|
||||||
|
rows={MOCK_ROWS}
|
||||||
|
columns={COLUMNS}
|
||||||
|
createLabel="Neue Lieferung"
|
||||||
|
onCreate={() => {}}
|
||||||
|
onEdit={() => {}}
|
||||||
|
onDelete={() => {}}
|
||||||
|
emptyMessage="Noch keine Lieferungen."
|
||||||
|
color="#fed7aa"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="lists-empty" label="RecordList – leer">
|
||||||
|
<RecordList
|
||||||
|
status="ready"
|
||||||
|
rows={[]}
|
||||||
|
columns={COLUMNS}
|
||||||
|
createLabel="Neue Lieferung"
|
||||||
|
onCreate={() => {}}
|
||||||
|
onEdit={() => {}}
|
||||||
|
onDelete={() => {}}
|
||||||
|
emptyMessage="Noch keine Lieferungen."
|
||||||
|
color="#fed7aa"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="lists-loading" label="RecordList – laden">
|
||||||
|
<RecordList status="loading" rows={[]} columns={COLUMNS} />
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="lists-failed" label="RecordList – Fehler">
|
||||||
|
<RecordList
|
||||||
|
status="failed"
|
||||||
|
rows={[]}
|
||||||
|
columns={COLUMNS}
|
||||||
|
error={
|
||||||
|
<Alert severity="error">
|
||||||
|
Backend-Fehler: Verbindung fehlgeschlagen.
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="lists-no-actions" label="RecordList – ohne Aktionen">
|
||||||
|
<RecordList
|
||||||
|
status="ready"
|
||||||
|
rows={MOCK_ROWS}
|
||||||
|
columns={COLUMNS}
|
||||||
|
color="#e2e8f0"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ScreenshotHarness };
|
||||||
17
src/dev/screenshotsMain.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||||
|
import { ScreenshotHarness } from './screenshots/ScreenshotHarness';
|
||||||
|
|
||||||
|
// Standalone root for the screenshot harness (/screenshots.html). Mirrors the
|
||||||
|
// dev gallery's theme so reference screenshots match real-app rendering.
|
||||||
|
const theme = createTheme({
|
||||||
|
shape: { borderRadius: 4 },
|
||||||
|
});
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<ScreenshotHarness />
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
51
tests/screenshot.spec.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screenshot tests for the GUI variations rendered by the harness
|
||||||
|
* (/screenshots.html). Every `data-shot` section becomes one reference image;
|
||||||
|
* interactive states (EditablePanelTile editing/dirty) are driven here.
|
||||||
|
*
|
||||||
|
* First run generates the reference images (and passes). After that the test
|
||||||
|
* fails on any visual drift; see the actual/diff images in test-results/.
|
||||||
|
* Regenerate intentionally changed references with:
|
||||||
|
* npm run test:screenshots:update
|
||||||
|
*/
|
||||||
|
const STATIC_SECTIONS = [
|
||||||
|
'pills',
|
||||||
|
'textfields',
|
||||||
|
'tiles',
|
||||||
|
'panels-basic',
|
||||||
|
'panels-busy',
|
||||||
|
'lists-ready',
|
||||||
|
'lists-empty',
|
||||||
|
'lists-loading',
|
||||||
|
'lists-failed',
|
||||||
|
'lists-no-actions',
|
||||||
|
];
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/screenshots.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const id of STATIC_SECTIONS) {
|
||||||
|
test(`screenshot ${id}`, async ({ page }) => {
|
||||||
|
await expect(page.locator(`[data-shot="${id}"]`)).toHaveScreenshot(
|
||||||
|
`${id}.png`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('editable tile: read, edit, dirty', async ({ page }) => {
|
||||||
|
const section = page.locator('[data-shot="editable"]');
|
||||||
|
|
||||||
|
// Read state — edit button visible, no ring.
|
||||||
|
await expect(section).toHaveScreenshot('editable-read.png');
|
||||||
|
|
||||||
|
// Editing state — clean ring, input focused, save/cancel buttons.
|
||||||
|
await section.getByRole('button', { name: 'Bearbeiten' }).click();
|
||||||
|
await expect(section).toHaveScreenshot('editable-editing.png');
|
||||||
|
|
||||||
|
// Dirty state — rose pending ring once the draft differs from the saved value.
|
||||||
|
await section.locator('input').fill('ACME GmbH 2');
|
||||||
|
await expect(section).toHaveScreenshot('editable-dirty.png');
|
||||||
|
});
|
||||||
BIN
tests/screenshots/reference/editable-dirty.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
tests/screenshots/reference/editable-editing.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
tests/screenshots/reference/editable-read.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
tests/screenshots/reference/lists-empty.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
tests/screenshots/reference/lists-failed.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
tests/screenshots/reference/lists-loading.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
tests/screenshots/reference/lists-no-actions.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
tests/screenshots/reference/lists-ready.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
tests/screenshots/reference/panels-basic.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
tests/screenshots/reference/panels-busy.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
tests/screenshots/reference/pills.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
tests/screenshots/reference/textfields.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
tests/screenshots/reference/tiles.png
Normal file
|
After Width: | Height: | Size: 14 KiB |