- 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]>
53 lines
2.3 KiB
TypeScript
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);
|
|
});
|
|
});
|