diff --git a/.gitignore b/.gitignore index dd6e803..92c0729 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,9 @@ node_modules/ dist/ *.log .DS_Store + +# Playwright screenshot testing +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/README.md b/README.md index 033a7d2..f442e0a 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,39 @@ npm run build # bundle the dev harness (dist/) 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 `
` 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 -- Test cases (unit + component tests) with mock pages will be added under - `dev/` / `src/__tests__/` as they are written. +- Unit + component tests with mock pages will be added under `dev/` / + `src/__tests__/` as they are written. diff --git a/package-lock.json b/package-lock.json index b7641d9..1735429 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@emotion/styled": "^11.14.1", "@mui/icons-material": "^9.3.1", "@mui/material": "^9.3.1", + "@playwright/test": "^1.62.1", "@vitejs/plugin-react": "6.0.5", "react": "^19.2.8", "react-dom": "^19.2.8", @@ -625,6 +626,22 @@ "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": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -1654,6 +1671,53 @@ "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": { "version": "8.5.26", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", diff --git a/package.json b/package.json index 44f1eee..642bab9 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "lint": "eslint ." + "lint": "eslint .", + "test:screenshots": "playwright test", + "test:screenshots:update": "playwright test --update-snapshots" }, "peerDependencies": { "@emotion/react": "^11.14.0", @@ -37,6 +39,7 @@ "@emotion/styled": "^11.14.1", "@mui/icons-material": "^9.3.1", "@mui/material": "^9.3.1", + "@playwright/test": "^1.62.1", "@vitejs/plugin-react": "6.0.5", "react": "^19.2.8", "react-dom": "^19.2.8", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..242e432 --- /dev/null +++ b/playwright.config.js @@ -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, + }, +}); diff --git a/screenshots.html b/screenshots.html new file mode 100644 index 0000000..278bb1b --- /dev/null +++ b/screenshots.html @@ -0,0 +1,12 @@ + + + + + + dashgui screenshot harness + + +
+ + + diff --git a/src/dev/screenshots/ScreenshotHarness.jsx b/src/dev/screenshots/ScreenshotHarness.jsx new file mode 100644 index 0000000..fbc5cdc --- /dev/null +++ b/src/dev/screenshots/ScreenshotHarness.jsx @@ -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) => ( + + ), + }, +]; + +class MockFolderTile extends Tile { + renderIcon() { + return ; + } +} + +class MockAddTile extends Tile { + renderIcon() { + return ; + } +} + +/** + * 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 ( + + + {label} + + {children} + + ); +} + +class ScreenshotHarness extends Component { + render() { + return ( + +
+ + + + + + + {}} /> + {}} /> + + + } + onClick={() => {}} + /> + } + onClick={() => {}} + /> + + +
+ +
+ + {}} /> + {}} + helperText="Mit Hilfetext" + /> + {}} + /> + {}} + /> + {}} + /> + +
+ +
+ + + + + + + tileBaseSx direkt + + + +
+ +
+ + + ACME GmbH + + + Mit Hintergrundfarbe + + {} }} + > + Idle Aktion + + +
+ +
+ + {}, + }} + > + Wird gespeichert … + + +
+ +
+ + {}} + onError={() => {}} + display={ACME GmbH} + edit={({ draft, onChange, inputRef }) => ( + + )} + /> + +
+ +
+ {}} + onEdit={() => {}} + onDelete={() => {}} + emptyMessage="Noch keine Lieferungen." + color="#fed7aa" + /> +
+ +
+ {}} + onEdit={() => {}} + onDelete={() => {}} + emptyMessage="Noch keine Lieferungen." + color="#fed7aa" + /> +
+ +
+ +
+ +
+ + Backend-Fehler: Verbindung fehlgeschlagen. + + } + /> +
+ +
+ +
+
+ ); + } +} + +export { ScreenshotHarness }; diff --git a/src/dev/screenshotsMain.jsx b/src/dev/screenshotsMain.jsx new file mode 100644 index 0000000..cd6c74d --- /dev/null +++ b/src/dev/screenshotsMain.jsx @@ -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( + + + + , +); diff --git a/tests/screenshot.spec.js b/tests/screenshot.spec.js new file mode 100644 index 0000000..9039368 --- /dev/null +++ b/tests/screenshot.spec.js @@ -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'); +}); diff --git a/tests/screenshots/reference/editable-dirty.png b/tests/screenshots/reference/editable-dirty.png new file mode 100644 index 0000000..da45cfb Binary files /dev/null and b/tests/screenshots/reference/editable-dirty.png differ diff --git a/tests/screenshots/reference/editable-editing.png b/tests/screenshots/reference/editable-editing.png new file mode 100644 index 0000000..b7a1605 Binary files /dev/null and b/tests/screenshots/reference/editable-editing.png differ diff --git a/tests/screenshots/reference/editable-read.png b/tests/screenshots/reference/editable-read.png new file mode 100644 index 0000000..e8a2903 Binary files /dev/null and b/tests/screenshots/reference/editable-read.png differ diff --git a/tests/screenshots/reference/lists-empty.png b/tests/screenshots/reference/lists-empty.png new file mode 100644 index 0000000..f7e8e3f Binary files /dev/null and b/tests/screenshots/reference/lists-empty.png differ diff --git a/tests/screenshots/reference/lists-failed.png b/tests/screenshots/reference/lists-failed.png new file mode 100644 index 0000000..069cf3c Binary files /dev/null and b/tests/screenshots/reference/lists-failed.png differ diff --git a/tests/screenshots/reference/lists-loading.png b/tests/screenshots/reference/lists-loading.png new file mode 100644 index 0000000..3a787d1 Binary files /dev/null and b/tests/screenshots/reference/lists-loading.png differ diff --git a/tests/screenshots/reference/lists-no-actions.png b/tests/screenshots/reference/lists-no-actions.png new file mode 100644 index 0000000..cfd5467 Binary files /dev/null and b/tests/screenshots/reference/lists-no-actions.png differ diff --git a/tests/screenshots/reference/lists-ready.png b/tests/screenshots/reference/lists-ready.png new file mode 100644 index 0000000..926c4d0 Binary files /dev/null and b/tests/screenshots/reference/lists-ready.png differ diff --git a/tests/screenshots/reference/panels-basic.png b/tests/screenshots/reference/panels-basic.png new file mode 100644 index 0000000..4f9618d Binary files /dev/null and b/tests/screenshots/reference/panels-basic.png differ diff --git a/tests/screenshots/reference/panels-busy.png b/tests/screenshots/reference/panels-busy.png new file mode 100644 index 0000000..6d298ba Binary files /dev/null and b/tests/screenshots/reference/panels-busy.png differ diff --git a/tests/screenshots/reference/pills.png b/tests/screenshots/reference/pills.png new file mode 100644 index 0000000..245e9f2 Binary files /dev/null and b/tests/screenshots/reference/pills.png differ diff --git a/tests/screenshots/reference/textfields.png b/tests/screenshots/reference/textfields.png new file mode 100644 index 0000000..3b01ba7 Binary files /dev/null and b/tests/screenshots/reference/textfields.png differ diff --git a/tests/screenshots/reference/tiles.png b/tests/screenshots/reference/tiles.png new file mode 100644 index 0000000..204c01a Binary files /dev/null and b/tests/screenshots/reference/tiles.png differ