import { describe, expect, it } from 'vitest'; import { GRID_CANVAS_KEY, GRID_SCALE_FACTOR, GRID_VERSION, GRID_VERSION_KEY, migrateGridLayouts, withGridVersion, } from './grid-layout-migration'; /** * grid-layout-migration.test (quick-260916-bwo, erweitert quick-260929-dmx). * * Tests fuer die stufenweise Umrechnung gespeicherter Anordnungen: * v1 (12 Spalten / 40 px) -> v2 (24 / 20 px, alles x2) -> v3 (48 Spalten, * nur Breite x2). Kern ist T-BWO-02: jede Stufe darf GENAU EINMAL geschehen. * Der Marker `__gridVersion` steht dafuer im gespeicherten JSON, nie im * Zustand — der Idempotenz-Test 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: v1-Anordnung ohne Marker -> beide Stufen: x/w/minW/maxW x4, y/h/minH/maxH x2, migrated true, kein Marker im Ergebnis', () => { const { layouts, migrated } = migrateGridLayouts(altLayouts()); expect(layouts.lg[0]).toEqual({ i: 'a', x: 4, y: 4, w: 8, h: 6, minW: 8, minH: 4, moved: false, static: false, }); expect(layouts.lg[1]).toEqual({ i: 'b', x: 8, y: 0, w: 24, h: 4, maxW: 48, maxH: 16 }); expect(layouts.md[0]).toEqual({ i: 'a', x: 0, y: 0, w: 8, 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: v2-Anordnung (__gridVersion 2) -> nur die zweite Stufe: x/w/minW/maxW x2, y/h/minH/maxH unveraendert, migrated true', () => { const alt = altLayouts(); const { layouts, migrated } = migrateGridLayouts({ ...alt, __gridVersion: 2 }); expect(layouts.lg[0]).toEqual({ i: 'a', x: 2, y: 2, w: 4, h: 3, minW: 4, minH: 2, moved: false, static: false, }); expect(layouts.lg[1]).toEqual({ i: 'b', x: 4, y: 0, w: 12, h: 2, maxW: 24, maxH: 8 }); expect(layouts.md[0]).toEqual({ i: 'a', x: 0, y: 0, w: 4, h: 2 }); expect(migrated).toBe(true); expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY); }); it('Test 2b: v3-Anordnung (__gridVersion 3) bleibt unveraendert, migrated false', () => { const alt = altLayouts(); const { layouts, migrated } = migrateGridLayouts({ ...alt, __gridVersion: 3 }); expect(layouts).toEqual(alt); expect(migrated).toBe(false); expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY); }); it('Test 2c: v1 und v2 -> v3 ergeben dieselbe Anordnung, wenn die v2-Eingabe die v1-Eingabe x2 ist', () => { const v1 = altLayouts(); const v2 = { lg: [ { i: 'a', x: 2, y: 4, w: 4, h: 6, minW: 4, minH: 4, moved: false, static: false }, { i: 'b', x: 4, y: 0, w: 12, h: 4, maxW: 24, maxH: 16 }, ], md: [{ i: 'a', x: 0, y: 0, w: 4, h: 4 }], sm: [], xs: [], xxs: [], __gridVersion: 2, }; expect(migrateGridLayouts(v2).layouts).toEqual(migrateGridLayouts(v1).layouts); }); 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); // Auch die einstufige v2 -> v3 ist idempotent, und der Marker 3 wird geschrieben. const onceV2 = migrateGridLayouts({ ...altLayouts(), __gridVersion: 2 }); const marked = withGridVersion(onceV2.layouts); expect(marked[GRID_VERSION_KEY]).toBe(3); const twiceV2 = migrateGridLayouts(marked); expect(twiceV2.layouts).toEqual(onceV2.layouts); expect(twiceV2.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(3); 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)[GRID_VERSION_KEY]).toBeUndefined(); }); it('Test 6: Zukunft und Robustheit — Marker 4 bleibt, Zeichenketten-Marker zaehlt nicht, nicht-numerische Felder bleiben', () => { const alt = altLayouts(); const future = migrateGridLayouts({ ...alt, __gridVersion: 4 }); expect(future.layouts).toEqual(alt); expect(future.migrated).toBe(false); expect(future.newer).toBe(true); const stringMarker = migrateGridLayouts({ ...alt, __gridVersion: '2' }); expect(stringMarker.migrated).toBe(true); expect(stringMarker.layouts.lg[0].x).toBe(4); 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: 8, 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: 4, y: 2, w: 4, h: 2 }); expect(r.migrated).toBe(true); expect(migrateGridLayouts(null)).toEqual({ layouts: {}, migrated: false, newer: false, canvas: null, }); expect(migrateGridLayouts(undefined)).toEqual({ layouts: {}, migrated: false, newer: false, canvas: null, }); expect(migrateGridLayouts(42)).toEqual({ layouts: {}, migrated: false, newer: false, canvas: null, }); expect(migrateGridLayouts([1, 2])).toEqual({ layouts: {}, migrated: false, newer: false, canvas: null, }); }); it('Test 8 (quick-260930): nur eine HOEHERE Marke meldet newer — v1/v2/v3 nicht', () => { const alt = altLayouts(); expect(migrateGridLayouts(alt).newer).toBe(false); expect(migrateGridLayouts({ ...alt, [GRID_VERSION_KEY]: 2 }).newer).toBe(false); expect(migrateGridLayouts({ ...alt, [GRID_VERSION_KEY]: GRID_VERSION }).newer).toBe(false); const future = migrateGridLayouts({ ...alt, [GRID_VERSION_KEY]: GRID_VERSION + 1 }); expect(future.newer).toBe(true); // Unveraendert uebernommen, nichts skaliert. expect(future.layouts).toEqual(alt); }); }); describe('grid-layout-migration: Leinwand `__canvas` (Dashboard wie ein Bild mitskalieren)', () => { const v3 = () => ({ lg: [{ i: 'a', x: 0, y: 0, w: 8, h: 4 }], [GRID_VERSION_KEY]: GRID_VERSION }); it('liest eine gueltige Leinwand und laesst sie aus `layouts` heraus', () => { const r = migrateGridLayouts({ ...v3(), [GRID_CANVAS_KEY]: { w: 1600, h: 820 } }); expect(r.canvas).toEqual({ w: 1600, h: 820 }); expect(Object.keys(r.layouts)).toEqual(['lg']); expect(r.migrated).toBe(false); }); it('fehlende oder ungueltige Werte -> null', () => { const bad: unknown[] = [ undefined, null, 'x', [1600, 800], { w: 1600 }, { w: '1600', h: 800 }, { w: Number.NaN, h: 800 }, { w: Number.POSITIVE_INFINITY, h: 800 }, { w: 767, h: 800 }, { w: 1600, h: 199 }, ]; for (const value of bad) { expect(migrateGridLayouts({ ...v3(), [GRID_CANVAS_KEY]: value }).canvas).toBeNull(); } // Grenzwerte sind gueltig. expect(migrateGridLayouts({ ...v3(), [GRID_CANVAS_KEY]: { w: 768, h: 200 } }).canvas).toEqual({ w: 768, h: 200, }); }); it('withGridVersion schreibt die Leinwand nur, wenn eine da ist', () => { const { layouts } = migrateGridLayouts(v3()); expect(withGridVersion(layouts)).not.toHaveProperty(GRID_CANVAS_KEY); expect(withGridVersion(layouts, null)).not.toHaveProperty(GRID_CANVAS_KEY); expect(withGridVersion(layouts, { w: 1920, h: 950 })).toEqual({ ...layouts, [GRID_VERSION_KEY]: GRID_VERSION, [GRID_CANVAS_KEY]: { w: 1920, h: 950 }, }); }); it('Idempotenz mit Leinwand: Laden -> Speichern -> Laden liefert dasselbe, nichts skaliert', () => { const once = migrateGridLayouts({ ...altLayouts(), [GRID_CANVAS_KEY]: { w: 1400, h: 700 } }); expect(once.migrated).toBe(true); expect(once.canvas).toEqual({ w: 1400, h: 700 }); const twice = migrateGridLayouts(withGridVersion(once.layouts, once.canvas)); expect(twice.migrated).toBe(false); expect(twice.layouts).toEqual(once.layouts); expect(twice.canvas).toEqual(once.canvas); }); });