325c5ddbf2
- Reminder-Tabelle mit Zeilenschutz (Mandant+Benutzer, Systemlesen fuer den E-Mail-Planer), API reminders (Liste, Anlegen) - Kachel "Erinnerungen", globaler Melder im Portalrahmen (Browser und Desktop, je Faelligkeit einmal) - Desktop: Laufzeit-Berechtigung fuer Benachrichtigungen nur fuer die gespeicherte Server-Adresse - Zugriffsklassifikation nachgemessen Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
225 lines
8.5 KiB
TypeScript
225 lines
8.5 KiB
TypeScript
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',
|
|
// Erinnerungen (quick-260929-if2)
|
|
'reminder',
|
|
];
|
|
|
|
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; quick-260929-if2: Erinnerungen dazu, elf 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 },
|
|
reminder: { minW: 8, minH: 4, defaultW: 12, defaultH: 10 },
|
|
});
|
|
|
|
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(44);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 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 elf 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<string, Pick<WidgetDefinition, 'moduleSlug'>> = {
|
|
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']);
|
|
});
|
|
});
|