refactor(quick-260922-m1h): Widget-Typen an einer Stelle, Katalog aus der Registry, Kachel kennt ihr Modul
Ein neuer Widget-Typ war an sieben Stellen einzutragen; vergass man eine, fehlte die Kachel im Katalog oder die API lehnte sie mit 400 ab. - WIDGET_TYPES/WidgetType/WIDGET_MODULE_SLUGS stehen jetzt einmal in packages/shared; Registry, Katalog und die @IsIn-Whitelist der API leiten davon ab - neun wireXWidget()-Funktionen durch ein generisches registerWidget() ersetzt (idempotent, unbekannter Typ wirft in der Entwicklung) - der Katalog fuehrt keine zweite Typliste mehr, sondern leitet sie aus der Registry ab und filtert nach Modulzugriff (fail-closed, wenn die Modulliste unbekannt ist); der Abruf von /modules/active liegt auf der Dashboard-Seite, nicht im Dialog - widget-module-map.ts liest die geteilte Tabelle statt einer Kopie, die oeffentliche Funktion bleibt unveraendert Der Katalogfilter ist Komfort (T-M1H-01) — verbindlich bleibt der serverseitige Filter in DashboardService.getWidgets. Abweichung vom Plan: apps/web hing entgegen der Planannahme noch nicht von @tessera/shared ab; die Abhaengigkeit wurde ergaenzt (Lockfile). Die Dockerfiles kopieren packages/shared bereits, der Produktionsbau von Next.js und der nest build laufen unveraendert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { WIDGET_CONSTRAINTS, type WidgetType } from './widget-registry';
|
||||
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
|
||||
@@ -82,3 +91,118 @@ describe('WIDGET_CONSTRAINTS (DASH-11)', () => {
|
||||
expect(counted).toBe(36);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 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 neun 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('heute traegt keine der neun Kacheln einen moduleSlug (alle sind Plattform-Kacheln)', () => {
|
||||
for (const type of WIDGET_TYPES) {
|
||||
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('proxmox' as WidgetType, makeComponent()),
|
||||
).toThrow(/proxmox/);
|
||||
});
|
||||
|
||||
it('ein unbekannter Typ wird in der Produktion still ignoriert', () => {
|
||||
const previous = process.env.NODE_ENV;
|
||||
vi.stubEnv('NODE_ENV', 'production');
|
||||
|
||||
expect(() => registerWidget('proxmox' 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<string, Pick<WidgetDefinition, 'moduleSlug'>> = {
|
||||
clock: {},
|
||||
proxmox: { moduleSlug: 'proxmox' },
|
||||
note: {},
|
||||
};
|
||||
|
||||
it('behaelt die Reihenfolge der Registry bei', () => {
|
||||
expect(visibleWidgetTypes(WIDGET_REGISTRY, [])).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']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user