import { describe, expect, it, vi } from 'vitest'; import { WIDGET_TYPES } from '@tessera/shared'; import { WIDGET_CONSTRAINTS, WIDGET_REGISTRY, type WidgetDefinition, type WidgetProps, type WidgetType, registerWidget, visibleWidgetTypes, } from './widget-registry'; /** * DASH-11: Every WidgetType entry in WIDGET_CONSTRAINTS must have * the four numeric grid-constraint fields: minW, minH, defaultW, defaultH. */ const ALL_WIDGET_TYPES: WidgetType[] = [ // Existing types 'clock', 'search', 'calendar', 'note', // New types (Phase 8) 'calculator', 'favorites', 'stopwatch', // Bilderrahmen (quick-260921-pi9) 'picture-frame', // XFrame (quick-260921-qd3) 'xframe', // Proxmox — erste Modul-Kachel (quick-260924-i8v) 'proxmox', ]; describe('WIDGET_CONSTRAINTS (DASH-11)', () => { it('contains entries for all expected widget types', () => { for (const type of ALL_WIDGET_TYPES) { expect( WIDGET_CONSTRAINTS, `WIDGET_CONSTRAINTS must contain key "${type}"`, ).toHaveProperty(type); } }); it.each(ALL_WIDGET_TYPES)( 'WIDGET_CONSTRAINTS["%s"] has numeric minW, minH, defaultW, defaultH', (type) => { const constraints = WIDGET_CONSTRAINTS[type]; expect(typeof constraints.minW, `${type}.minW must be a number`).toBe('number'); expect(typeof constraints.minH, `${type}.minH must be a number`).toBe('number'); expect(typeof constraints.defaultW, `${type}.defaultW must be a number`).toBe('number'); expect(typeof constraints.defaultH, `${type}.defaultH must be a number`).toBe('number'); expect(constraints.minW, `${type}.minW must be >= 1`).toBeGreaterThanOrEqual(1); expect(constraints.minH, `${type}.minH must be >= 1`).toBeGreaterThanOrEqual(1); expect(constraints.defaultW, `${type}.defaultW >= minW`).toBeGreaterThanOrEqual(constraints.minW); expect(constraints.defaultH, `${type}.defaultH >= minH`).toBeGreaterThanOrEqual(constraints.minH); }, ); it('WIDGET_CONSTRAINTS contains new Phase-8 keys', () => { expect(Object.keys(WIDGET_CONSTRAINTS)).toContain('calculator'); expect(Object.keys(WIDGET_CONSTRAINTS)).toContain('favorites'); expect(Object.keys(WIDGET_CONSTRAINTS)).toContain('stopwatch'); }); it('Test A (quick-260916-dyv): Minima = kleinste bedienbare Kachel je Typ, Vorgaben unveraendert (quick-260916-iex: Link-Widget entfernt; quick-260921-pi9: Bilderrahmen dazu; quick-260921-qd3: XFrame dazu; quick-260924-i8v: Proxmox dazu, zehn Typen)', () => { // Raster 48 Spalten / 20 px (quick-260929-dmx, vorher 24 Spalten). Alle // Breitenwerte sind gegenueber dem 24er-Raster verdoppelt (gleiche // Bildschirmbreite), Hoehen unveraendert; einzige Ausnahme Kalender-minW 8 // (rund 250 px, gemessene kleinste benutzbare Breite; vorher 6 von 24). expect(WIDGET_CONSTRAINTS).toEqual({ clock: { minW: 4, minH: 2, defaultW: 8, defaultH: 4 }, search: { minW: 12, minH: 2, defaultW: 24, defaultH: 4 }, calendar: { minW: 8, minH: 8, defaultW: 16, defaultH: 16 }, note: { minW: 8, minH: 4, defaultW: 12, defaultH: 8 }, calculator: { minW: 6, minH: 10, defaultW: 12, defaultH: 10 }, favorites: { minW: 2, minH: 3, defaultW: 12, defaultH: 10 }, stopwatch: { minW: 8, minH: 3, defaultW: 12, defaultH: 6 }, 'picture-frame': { minW: 8, minH: 4, defaultW: 16, defaultH: 12 }, xframe: { minW: 8, minH: 4, defaultW: 24, defaultH: 12 }, proxmox: { minW: 6, minH: 4, defaultW: 16, defaultH: 8 }, }); let counted = 0; for (const type of ALL_WIDGET_TYPES) { for (const field of ['minW', 'minH', 'defaultW', 'defaultH'] as const) { expect(typeof WIDGET_CONSTRAINTS[type][field]).toBe('number'); counted++; } } expect(counted).toBe(40); }); }); /** * quick-260922-m1h: Deckungsgleichheit. Die Typliste steht seit diesem Umbau * EINMAL in `packages/shared`; Registry, Constraints-Tabelle und die * Erwartungsliste dieses Tests muessen dieselben Schluessel in derselben * Reihenfolge tragen. Dieser Test faengt kuenftig jede vergessene Stelle. */ describe('Typliste ist an einer Stelle definiert (quick-260922-m1h)', () => { it('WIDGET_TYPES aus @tessera/shared, Registry-Schluessel und Constraints-Schluessel sind deckungsgleich (gleiche Reihenfolge)', () => { expect(Object.keys(WIDGET_REGISTRY)).toEqual([...WIDGET_TYPES]); expect(Object.keys(WIDGET_CONSTRAINTS)).toEqual([...WIDGET_TYPES]); }); it('die zehn erwarteten Kacheln stehen unveraendert und in unveraenderter Reihenfolge in WIDGET_TYPES', () => { expect([...WIDGET_TYPES]).toEqual(ALL_WIDGET_TYPES); }); it('jeder Registry-Eintrag traegt seinen eigenen Typ als `type`', () => { for (const type of WIDGET_TYPES) { expect(WIDGET_REGISTRY[type].type).toBe(type); } }); it("nur proxmox traegt moduleSlug 'proxmox', alle anderen Kacheln keinen (quick-260924-i8v)", () => { for (const type of WIDGET_TYPES) { if (type === 'proxmox') { expect(WIDGET_REGISTRY[type].moduleSlug).toBe('proxmox'); } else { expect(WIDGET_REGISTRY[type].moduleSlug).toBeUndefined(); } } }); }); describe('registerWidget (quick-260922-m1h)', () => { function makeComponent(): (props: WidgetProps) => null { return () => null; } it('meldet eine Komponente fuer ihren Typ an', () => { const before = WIDGET_REGISTRY.clock.component; const component = makeComponent(); registerWidget('clock', component); expect(WIDGET_REGISTRY.clock.component).toBe(component); WIDGET_REGISTRY.clock.component = before; }); it('ist idempotent: eine zweite Anmeldung desselben Typs ist ein No-Op (wie die alten wireX-Flags)', () => { const before = WIDGET_REGISTRY.note.component; const first = makeComponent(); const second = makeComponent(); registerWidget('note', first); registerWidget('note', second); expect(WIDGET_REGISTRY.note.component).toBe(first); WIDGET_REGISTRY.note.component = before; }); it('ein unbekannter Typ wirft in der Entwicklung', () => { expect(() => // Absichtlich ein Typ ausserhalb der Union — genau der Fall, den der // Wurf melden soll (eine Kachel, die in WIDGET_TYPES vergessen wurde). registerWidget('gibt-es-nicht' as WidgetType, makeComponent()), ).toThrow(/gibt-es-nicht/); }); it('ein unbekannter Typ wird in der Produktion still ignoriert', () => { const previous = process.env.NODE_ENV; vi.stubEnv('NODE_ENV', 'production'); expect(() => registerWidget('gibt-es-nicht' as WidgetType, makeComponent())).not.toThrow(); vi.stubEnv('NODE_ENV', previous ?? 'test'); vi.unstubAllEnvs(); }); }); /** * quick-260922-m1h: Der Katalogfilter als reine Funktion — so testbar ohne * eine echte Modul-Kachel zu erfinden (Muster picture-frame-config.ts). * WICHTIG (T-M1H-01): Dieser Filter ist Komfort. Die verbindliche * Durchsetzung bleibt serverseitig in DashboardService.getWidgets. */ describe('visibleWidgetTypes (quick-260922-m1h)', () => { const testRegistry: Record> = { clock: {}, proxmox: { moduleSlug: 'proxmox' }, note: {}, }; it('behaelt die Reihenfolge der Registry bei; ohne Modulzugriff fehlt nur Proxmox', () => { expect(visibleWidgetTypes(WIDGET_REGISTRY, [])).toEqual( WIDGET_TYPES.filter((type) => type !== 'proxmox'), ); }); it("mit Zugriff auf 'proxmox' erscheinen alle zehn Kacheln in WIDGET_TYPES-Reihenfolge (quick-260924-i8v)", () => { expect(visibleWidgetTypes(WIDGET_REGISTRY, ['proxmox'])).toEqual([...WIDGET_TYPES]); }); it('Kacheln ohne moduleSlug sind immer sichtbar', () => { expect(visibleWidgetTypes(testRegistry, [])).toEqual(['clock', 'note']); }); it('eine Kachel mit moduleSlug fehlt, wenn der Slug nicht in den zugaenglichen Modulen steht', () => { expect(visibleWidgetTypes(testRegistry, ['domaincheck'])).toEqual(['clock', 'note']); }); it('eine Kachel mit moduleSlug erscheint, wenn der Slug in den zugaenglichen Modulen steht', () => { expect(visibleWidgetTypes(testRegistry, ['domaincheck', 'proxmox'])).toEqual([ 'clock', 'proxmox', 'note', ]); }); it('fail-closed: ist die Modulliste unbekannt (null, z. B. fehlgeschlagener Abruf), verschwinden alle Kacheln MIT moduleSlug', () => { expect(visibleWidgetTypes(testRegistry, null)).toEqual(['clock', 'note']); }); });