af87c2c112
Je Reiter wird die Flaeche des ersten Desktop-Bildschirms als __canvas im Layout-JSON gespeichert (ohne API/DB-Aenderung, GRID_VERSION bleibt 3). Das Raster rendert in Leinwandbreite und wird per transform: scale(min(bw/cw, bh/ch)) eingepasst, Schrift eingeschlossen, ohne Scrollen; unter 768 px wie bisher. Ziehen/Groesse aendern unter Skalierung ueber eine eigene Positionsstrategie (createScaledStrategy aus react-grid-layout 2.2.3 rechnet den Rasterversatz falsch). Im Browser nachgewiesen: 1920x1080 -> 1366x768 (Faktor 0,66), Ziehen +200 px folgt der Maus, 700 px ohne Skalierung. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
288 lines
9.7 KiB
TypeScript
288 lines
9.7 KiB
TypeScript
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<string, unknown>)[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);
|
|
});
|
|
});
|