Files
ApiCore/ApiCore-Frontend/scripts/check-motion-budget.test.ts
T
Nick.RosenloecherandClaude Opus 5.5 cda3e02a31
Backend Image / build-and-push (push) Successful in 11m29s
Frontend Image / build-and-push (push) Successful in 23m35s
feat(t5): Bewegungs-Sprache mit Tokens, Stufen und Budget (T5a A11)
- src/lib/motion/tokens.ts und --motion-* in globals.css: Dauern,
  Kurven und Feder an einer Stelle; Beute-Bühne liest dieselben Werte.
- AppMotion: LazyMotion strict, Bewegungs-Code (domAnimation) als ein
  asynchroner Chunk, 28 kB gzip; check:motion-budget im Docker-Build
  neben dem 3D-Budget (Spec 17.6).
- PresenceList: abgehakte Zeilen blenden aus, die übrigen rücken nur bei
  „Voll“ nach; „Dezent“ blendet nur, „Aus“ sofort. Die Zeile geht nach
  der festen Verzögerung, auch wenn der Server schweigt (Spec 17.5);
  Fokus wandert zum nächsten Haken, auch ohne Bewegungs-Chunk.
- Overlays (Sheet, Dialog, Menü, Select) über Base-UI-Start/Ende-Stile
  mit Token-Zeiten; Verschiebung und Skalierung nur bei motion-full.
- Kein transition-all, kein Hover-Heben, keine Breiten-Animation von
  Daten; Skeleton erst nach 150 ms; Wächter-Regeln für alles davon.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 21:15:42 +02:00

53 lines
2.3 KiB
TypeScript

// @vitest-environment node
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { randomBytes } from "node:crypto";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { BUDGET_BYTES, MOTION_MARKERS, checkMotionBudget, isMotionChunk } from "./check-motion-budget.mjs";
const motionChunk = (filler: string, marker = MOTION_MARKERS[0]) => `/*${marker}*/var a="${marker}";${filler}`;
// Hex of random bytes is barely compressible by gzip (about 50 %).
const noise = (bytes: number) => randomBytes(bytes).toString("hex");
describe("check-motion-budget (spec 17.6: motion at most 30 kB gzip)", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(path.join(tmpdir(), "motion-budget-"));
});
afterEach(() => rmSync(dir, { recursive: true, force: true }));
it("adds up every motion chunk (the shell part and the lazily loaded features)", () => {
writeFileSync(path.join(dir, "core.js"), motionChunk(noise(500), MOTION_MARKERS[0]));
writeFileSync(path.join(dir, "features.js"), motionChunk(noise(500), MOTION_MARKERS.at(-1)));
writeFileSync(path.join(dir, "app.js"), "console.log('Außerdem heute')");
const result = checkMotionBudget(dir);
expect(result.ok).toBe(true);
expect(result.files.map(({ file }) => file).sort()).toEqual(["core.js", "features.js"]);
expect(result.gzipBytes).toBe(result.files.reduce((sum, { gzipBytes }) => sum + gzipBytes, 0));
});
it("fails when the motion chunks together are over budget", () => {
writeFileSync(path.join(dir, "a.js"), motionChunk(noise(400)));
writeFileSync(path.join(dir, "b.js"), motionChunk(noise(400)));
const result = checkMotionBudget(dir, 600);
expect(result.ok).toBe(false);
expect(result.gzipBytes).toBeGreaterThan(600);
});
it("fails when no motion chunk exists (check broken or bundle rebuilt)", () => {
writeFileSync(path.join(dir, "app.js"), "export const a = 1;");
const result = checkMotionBudget(dir);
expect(result.ok).toBe(false);
expect(result.files).toEqual([]);
});
it("does not count the three.js chunk", () => {
expect(isMotionChunk(`/*WebGLRenderer*/window.__THREE__="186";${MOTION_MARKERS[0]}`)).toBe(false);
});
it("uses the spec budget of 30 kB by default", () => {
expect(BUDGET_BYTES).toBe(30_000);
});
});