Compare commits

...

3 Commits

Author SHA1 Message Date
seb
cba59532ef u 2026-10-07 22:40:16 +02:00
seb
878f558a3a u 2026-10-07 22:26:40 +02:00
seb
3d1601595e u 2026-10-07 22:10:24 +02:00
46 changed files with 115 additions and 33 deletions

View File

@@ -481,21 +481,7 @@ class ArtikelVorlagenPage extends PureComponent {
</Button>
</Box>
{templates.length === 0 ? (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 1,
py: 6,
color: 'text.disabled',
}}
>
<LabelIcon sx={{ fontSize: 48 }} />
<Typography variant="body2">Noch keine Vorlagen angelegt.</Typography>
</Box>
) : (
{templates.length > 0 && (
<Box
sx={{
display: 'grid',

View File

@@ -28,7 +28,7 @@ npm run rig:compare
| `test/collect.mjs` | Fixture collector (mints JWT via dashback env + signing lib) |
| `test/dummyBackend.mjs` | Always-authed stub serving `test/fixtures/snapshot.json` + `images/` |
| `test/cdp.mjs` | Zero-dep CDP client (fresh page target per shot, settle wait, PNG capture) |
| `test/capture.mjs` | Orchestrates dummy + vite + Chrome |
| `test/capture.mjs` | Builds prod bundle, serves via preview + dummy, drives Chrome |
| `test/compare.mjs` | Zero-dep PNG decoder + comparator (0.1% tolerance, red diff PNGs) |
| `test/fixtures/` | Committed snapshot + binary thumbs (2.5 MiB) |
| `test/screenshots/reference/` | Committed reference PNGs (7.5 MiB) |

View File

@@ -4,15 +4,21 @@
* Usage:
* node test/capture.mjs [--out test/screenshots/current]
*
* Flow: spawns the dummy backend (default :4218) + `vite --port 4229` with
* RIG_API_TARGET pointing at the dummy, then drives a persistent headless
* Chrome (remote-debugging port) via CDP: one fresh page target per shot,
* Flow: builds the production bundle (`vite build`), serves `dist/` via
* `vite preview` (default :4229) with RIG_API_TARGET pointing at the dummy
* backend (default :4218), then drives a persistent headless Chrome
* (remote-debugging port) via CDP: one fresh page target per shot,
* waits for React hydration + data, captures full-page PNGs. No npm deps.
*
* Production (not dev) so screenshots reflect the shipped app: no HMR client,
* no AutomatInspector, minified bundles. Each run uses a fresh Chrome profile
* so the PWA service worker / caches never leak between runs.
*
* The dummy backend is always-authed, so no login step is needed.
*/
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { ROUTES, VIEWPORTS } from './routes.mjs';
@@ -65,17 +71,68 @@ function startProc(cmd, procArgs, env, tag) {
cwd: FRONT_ROOT,
env: { ...process.env, ...env },
stdio: ['ignore', 'pipe', 'pipe'],
// Own process group per child so stopProc can kill the whole tree
// (e.g. `npx vite` wrapper + the real vite process).
detached: process.platform !== 'win32',
});
// Piped stdio keeps the parent alive even after kill(); detach the streams
// so teardown below can actually exit.
child.stdout.on('data', (d) => process.stdout.write(`[${tag}] ${d}`));
child.stderr.on('data', (d) => {
const text = String(d);
// vite proxy logs are noise during capture; keep errors.
if (tag === 'vite' && text.includes('[vite-proxy]')) return;
// preview proxy logs are noise during capture; keep errors.
if ((tag === 'preview' || tag === 'vite') && text.includes('[vite-proxy]')) return;
process.stderr.write(`[${tag}] ${d}`);
});
child.stdout.unref?.();
child.stderr.unref?.();
child.unref?.();
return child;
}
async function stopProc(child, tag, timeoutMs = 8000) {
if (!child || child.exitCode != null || child.signalCode != null) return;
// Kill the whole process group (child + grandchildren like npx/vite).
const killTree = (sig) => {
try {
if (child.pid && process.platform !== 'win32') {
process.kill(-child.pid, sig);
return;
}
} catch {
// fall through to single kill
}
try {
child.kill(sig);
} catch {
// already gone
}
};
await new Promise((resolve) => {
const timer = setTimeout(() => {
killTree('SIGKILL');
resolve();
}, timeoutMs);
timer.unref?.();
child.once('exit', () => {
clearTimeout(timer);
resolve();
});
killTree('SIGTERM');
});
// Drain any buffered pipe listeners so they can't hold the loop open.
try {
child.stdout?.destroy();
} catch {
// ignore
}
try {
child.stderr?.destroy();
} catch {
// ignore
}
}
async function main() {
if (!fs.existsSync(CHROME)) {
throw new Error(`Chrome not found at ${CHROME} (pass --chrome=...)`);
@@ -85,19 +142,38 @@ async function main() {
}
fs.mkdirSync(OUT_DIR, { recursive: true });
// Production bundle first — screenshots must reflect the shipped app.
console.log('[capture] vite build…');
await new Promise((resolve, reject) => {
const build = spawn('npx', ['vite', 'build'], {
cwd: FRONT_ROOT,
env: { ...process.env },
stdio: ['ignore', 'pipe', 'pipe'],
});
build.stdout.on('data', () => process.stdout.write('[build] '));
build.stderr.on('data', (d) => process.stderr.write(`[build] ${d}`));
build.on('exit', (code) => {
if (code === 0) resolve();
else reject(new Error(`vite build exited with ${code}`));
});
build.on('error', reject);
});
const dummy = startProc('node', ['test/dummyBackend.mjs', `--port=${DUMMY_PORT}`], {}, 'dummy');
await waitForPort(DUMMY_PORT);
const vite = startProc(
const server = startProc(
'npx',
['vite', '--port', String(VITE_PORT), '--strictPort'],
['vite', 'preview', '--port', String(VITE_PORT), '--strictPort'],
{ RIG_API_TARGET: `http://localhost:${DUMMY_PORT}` },
'vite',
'preview',
);
await waitForPort(VITE_PORT);
await wait(3000);
await wait(2000);
const profile = '/tmp/rig-chrome-profile';
// Fresh profile per run: the production SW registers and caches dist/ —
// never let it leak between reference/compare runs.
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'rig-chrome-'));
fs.mkdirSync(profile, { recursive: true });
const chrome = startProc(
CHROME,
@@ -135,15 +211,24 @@ async function main() {
}
}
} finally {
chrome.kill();
vite.kill();
dummy.kill();
await stopProc(chrome, 'chrome');
await stopProc(server, 'preview');
await stopProc(dummy, 'dummy');
// Remove the throwaway Chrome profile (SW + caches).
try {
fs.rmSync(profile, { recursive: true, force: true });
} catch {
// ignore
}
}
if (failed > 0) {
console.error(`[capture] ${failed} shots failed`);
process.exit(1);
}
console.log(`[capture] done → ${OUT_DIR}`);
// Explicit exit: spawned grandchildren (preview server, chrome helpers) can
// otherwise keep the event loop alive after teardown.
process.exit(0);
}
main().catch((err) => {

View File

@@ -89,14 +89,25 @@ class CdpConnection {
send(method, params = {}, timeoutMs = 30000) {
const callId = ++this.id;
return new Promise((resolve, reject) => {
this.pending.set(callId, { resolve, reject });
this.sendFrame(0x1, Buffer.from(JSON.stringify({ id: callId, method, params })));
setTimeout(() => {
const timer = setTimeout(() => {
if (this.pending.has(callId)) {
this.pending.delete(callId);
reject(new Error(`CDP timeout: ${method}`));
}
}, timeoutMs);
// Don't let a settled call's timer hold the process open.
timer.unref?.();
this.pending.set(callId, {
resolve: (v) => {
clearTimeout(timer);
resolve(v);
},
reject: (e) => {
clearTimeout(timer);
reject(e);
},
});
this.sendFrame(0x1, Buffer.from(JSON.stringify({ id: callId, method, params })));
});
}

View File

@@ -1,6 +1,6 @@
{
"meta": {
"collectedAt": "2026-10-07T19:27:26.707Z",
"collectedAt": "2026-10-07T20:26:08.334Z",
"backend": "http://localhost:4208",
"companyId": 1,
"googleClientId": "928121624463-m9du45ek2d5f6p9cke8qpnf89ii9no20.apps.googleusercontent.com"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 KiB

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 529 KiB

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 568 KiB

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 592 KiB

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 KiB

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 593 KiB

After

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 126 KiB