feat(quick-260916-bwo): Dashboard-Raster verdoppelt (24 Spalten, 20 px, 8 px Abstand), Konstanten x2, einmalige Umrechnung gespeicherter Anordnungen mit Marker __gridVersion

- dashboard-grid.tsx: COLS 24/20/12/8/2, rowHeight 20, margin 8 (containerPadding folgt), Rueckfallwerte 4
- widget-registry.tsx: alle 32 Werte in WIDGET_CONSTRAINTS verdoppelt
- grid-layout-migration.ts (neu): migrateGridLayouts/withGridVersion, Marker nur im JSON, Idempotenz (T-BWO-02)
- dashboard-store.ts: Umrechnung beim Laden, Sofort-Speichern mit Marker, withGridVersion bei jedem saveLayout
- Tests: Migration 7 (neu), Store 6 (neu), Registry +1 (Tabelle), Grid +2 (Props ueber Mock), API-Spec +2 (Durchreichung __gridVersion, timeFontSizePt)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
2026-09-16 09:09:00 +02:00
parent 50f201ecc1
commit 3f5afb0f54
9 changed files with 511 additions and 25 deletions
@@ -0,0 +1,123 @@
import { describe, expect, it } from 'vitest';
import {
GRID_SCALE_FACTOR,
GRID_VERSION,
GRID_VERSION_KEY,
migrateGridLayouts,
withGridVersion,
} from './grid-layout-migration';
/**
* grid-layout-migration.test — NEU (quick-260916-bwo, feineres Dashboard-Raster).
*
* Sieben Tests fuer die einmalige Umrechnung gespeicherter Anordnungen von
* den alten Einheiten (12 Spalten / 40 px) in die neuen (24 Spalten / 20 px).
* Kern ist T-BWO-02: die Verdopplung darf GENAU EINMAL geschehen. Der Marker
* `__gridVersion` steht dafuer im gespeicherten JSON, nie im Zustand — Test 4
* (Idempotenz) wird rot, sobald jemand die Marker-Pruefung entfernt.
*/
function altLayouts() {
return {
lg: [
{ i: 'a', x: 1, y: 2, w: 2, h: 3, minW: 2, minH: 2, moved: false, static: false },
{ i: 'b', x: 2, y: 0, w: 6, h: 2, maxW: 12, maxH: 8 },
],
md: [{ i: 'a', x: 0, y: 0, w: 2, h: 2 }],
sm: [],
xs: [],
xxs: [],
};
}
describe('grid-layout-migration (quick-260916-bwo)', () => {
it('Test 1: alte Anordnung ohne Marker -> alle Positionen und Groessen x2, migrated true, kein Marker im Ergebnis', () => {
const { layouts, migrated } = migrateGridLayouts(altLayouts());
expect(layouts.lg[0]).toEqual({
i: 'a', x: 2, y: 4, w: 4, h: 6, minW: 4, minH: 4, moved: false, static: false,
});
expect(layouts.lg[1]).toEqual({ i: 'b', x: 4, y: 0, w: 12, h: 4, maxW: 24, maxH: 16 });
expect(layouts.md[0]).toEqual({ i: 'a', x: 0, y: 0, w: 4, h: 4 });
expect(layouts.sm).toEqual([]);
expect(layouts.xs).toEqual([]);
expect(layouts.xxs).toEqual([]);
expect(migrated).toBe(true);
expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY);
expect(GRID_SCALE_FACTOR).toBe(2);
});
it('Test 2: markierte Anordnung (__gridVersion 2) bleibt unveraendert, migrated false', () => {
const alt = altLayouts();
const { layouts, migrated } = migrateGridLayouts({ ...alt, __gridVersion: 2 });
expect(layouts).toEqual(alt);
expect(migrated).toBe(false);
expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY);
});
it('Test 3: leere Anordnung bleibt leer, migrated false (kein Speichern noetig)', () => {
const empty = { lg: [], md: [], sm: [], xs: [], xxs: [] };
const r1 = migrateGridLayouts(empty);
expect(r1.layouts).toEqual(empty);
expect(r1.migrated).toBe(false);
const r2 = migrateGridLayouts({});
expect(r2.layouts).toEqual({});
expect(r2.migrated).toBe(false);
});
it('Test 4: Idempotenz — einmal umgerechnet und markiert wird nicht erneut verdoppelt (T-BWO-02)', () => {
const once = migrateGridLayouts(altLayouts());
const twice = migrateGridLayouts(withGridVersion(once.layouts));
expect(twice.layouts).toEqual(once.layouts);
expect(twice.migrated).toBe(false);
});
it('Test 5: withGridVersion haengt den Marker an, ohne die Eingabe zu veraendern', () => {
const { layouts } = migrateGridLayouts(altLayouts());
const marked = withGridVersion(layouts);
expect(marked[GRID_VERSION_KEY]).toBe(GRID_VERSION);
expect(GRID_VERSION).toBe(2);
expect(marked.lg).toBe(layouts.lg);
expect(marked.md).toBe(layouts.md);
expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY);
expect((layouts as Record<string, unknown>)[GRID_VERSION_KEY]).toBeUndefined();
});
it('Test 6: Zukunft und Robustheit — Marker 3 bleibt, Zeichenketten-Marker zaehlt nicht, nicht-numerische Felder bleiben', () => {
const alt = altLayouts();
const future = migrateGridLayouts({ ...alt, __gridVersion: 3 });
expect(future.layouts).toEqual(alt);
expect(future.migrated).toBe(false);
const stringMarker = migrateGridLayouts({ ...alt, __gridVersion: '2' });
expect(stringMarker.migrated).toBe(true);
expect(stringMarker.layouts.lg[0].x).toBe(2);
expect(Object.keys(stringMarker.layouts)).not.toContain(GRID_VERSION_KEY);
const odd = migrateGridLayouts({
lg: [{ i: 'c', x: 'kaputt', y: 1, w: 2, h: 2, resizeHandles: ['se'], moved: true, static: false }],
});
expect(odd.layouts.lg[0]).toEqual({
i: 'c', x: 'kaputt', y: 2, w: 4, h: 4, resizeHandles: ['se'], moved: true, static: false,
});
expect(Number.isNaN(odd.layouts.lg[0].x)).toBe(false);
expect(odd.migrated).toBe(true);
});
it('Test 7: Fremdwerte — Nicht-Arrays werden weggelassen, Nicht-Objekte liefern eine leere Anordnung', () => {
const r = migrateGridLayouts({ lg: 'kaputt', md: null, sm: [{ i: 'a', x: 1, y: 1, w: 1, h: 1 }] });
expect(Object.keys(r.layouts)).toEqual(['sm']);
expect(r.layouts.sm[0]).toEqual({ i: 'a', x: 2, y: 2, w: 2, h: 2 });
expect(r.migrated).toBe(true);
expect(migrateGridLayouts(null)).toEqual({ layouts: {}, migrated: false });
expect(migrateGridLayouts(undefined)).toEqual({ layouts: {}, migrated: false });
expect(migrateGridLayouts(42)).toEqual({ layouts: {}, migrated: false });
expect(migrateGridLayouts([1, 2])).toEqual({ layouts: {}, migrated: false });
});
});