diff --git a/apps/web/src/components/dashboard/widget-registry.test.tsx b/apps/web/src/components/dashboard/widget-registry.test.tsx new file mode 100644 index 0000000..6f90e61 --- /dev/null +++ b/apps/web/src/components/dashboard/widget-registry.test.tsx @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; +import { WIDGET_CONSTRAINTS, type WidgetType } 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', + 'link', + 'stopwatch', +]; + +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('link'); + expect(Object.keys(WIDGET_CONSTRAINTS)).toContain('stopwatch'); + }); +}); diff --git a/apps/web/src/components/dashboard/widgets/calculator-widget.test.tsx b/apps/web/src/components/dashboard/widgets/calculator-widget.test.tsx new file mode 100644 index 0000000..5f57548 --- /dev/null +++ b/apps/web/src/components/dashboard/widgets/calculator-widget.test.tsx @@ -0,0 +1,85 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +// Mock next-intl — returns the key so tests are locale-agnostic +vi.mock('next-intl', () => ({ + useTranslations: () => (key: string) => key, +})); + +// Must import after mocks +import { CalculatorWidget } from './calculator-widget'; + +const defaultProps = { + instanceId: 'calc-1', + config: {}, + isEditMode: false, +}; + +describe('CalculatorWidget', () => { + it('renders with initial display "0"', () => { + render(); + const display = screen.getByLabelText('Anzeige'); + expect(display).toHaveTextContent('0'); + }); + + it('calculates 7 + 3 = 10', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: '7' })); + fireEvent.click(screen.getByRole('button', { name: '+' })); + fireEvent.click(screen.getByRole('button', { name: '3' })); + fireEvent.click(screen.getByRole('button', { name: /gleich/i })); + + expect(screen.getByLabelText('Anzeige')).toHaveTextContent('10'); + }); + + it('calculates 8 × 2 = 16', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: '8' })); + fireEvent.click(screen.getByRole('button', { name: '×' })); + fireEvent.click(screen.getByRole('button', { name: '2' })); + fireEvent.click(screen.getByRole('button', { name: /gleich/i })); + + expect(screen.getByLabelText('Anzeige')).toHaveTextContent('16'); + }); + + it('shows "Fehler" on division by zero', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: '5' })); + fireEvent.click(screen.getByRole('button', { name: '÷' })); + fireEvent.click(screen.getByRole('button', { name: '0' })); + fireEvent.click(screen.getByRole('button', { name: /gleich/i })); + + expect(screen.getByLabelText('Anzeige')).toHaveTextContent('Fehler'); + }); + + it('handles keyboard input: 5 * 6 Enter = 30', () => { + render(); + + const container = screen.getByRole('application'); + + fireEvent.keyDown(container, { key: '5' }); + fireEvent.keyDown(container, { key: '*' }); + fireEvent.keyDown(container, { key: '6' }); + fireEvent.keyDown(container, { key: 'Enter' }); + + expect(screen.getByLabelText('Anzeige')).toHaveTextContent('30'); + }); + + it('handles decimal comma: 1,5 + 1,5 = 3', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: '1' })); + fireEvent.click(screen.getByRole('button', { name: ',' })); + fireEvent.click(screen.getByRole('button', { name: '5' })); + fireEvent.click(screen.getByRole('button', { name: '+' })); + fireEvent.click(screen.getByRole('button', { name: '1' })); + fireEvent.click(screen.getByRole('button', { name: ',' })); + fireEvent.click(screen.getByRole('button', { name: '5' })); + fireEvent.click(screen.getByRole('button', { name: /gleich/i })); + + expect(screen.getByLabelText('Anzeige')).toHaveTextContent('3'); + }); +});