feat(quick-260916-bwo): Widget-Inhalt skaliert per Container-Queries (Uhr, Stoppuhr, Rechner), Punktgroesse der Uhrzeit einstellbar

- widget-wrapper.tsx: Rumpf ist Groessen-Container (@container-size) fuer cqw/cqh
- clock-font-size.ts (neu): Grenzen 8..200, resolveClockTimeFontSizePt nur fuer endliche Zahlen (T-BWO-01)
- clock-widget.tsx: Container-Query-Klassen mit clamp(), data-font-mode auto/fixed, Inline-pt nur bei Zahl, keine vw-Formel mehr, p-1
- stopwatch-widget.tsx: Anzeige per Container-Query, p-1.5; calculator-widget.tsx: Anzeige und Tasten skalieren, Tasten h-full min-h-7, p-1
- widget-settings-panel.tsx: Zahlenfeld Schriftgroesse der Uhrzeit (Blur/Enter, leer = automatisch, Fehlermeldung ausserhalb 8..200)
- de.json/en.json: widgets.clock.fontSizeLabel/fontSizeAuto/fontSizeHint/fontSizeInvalid (Sie-Form, Waechter gruen)
- Tests: Uhr +4, Stoppuhr +1, Rechner +1, widget-settings-panel.test.tsx neu (4)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
2026-09-16 09:12:02 +02:00
parent 3f5afb0f54
commit 2d8efe1a97
12 changed files with 364 additions and 20 deletions
@@ -0,0 +1,107 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import de from '@/messages/de.json';
import { updateWidgetConfig } from '@/lib/dashboard-api';
import { WidgetSettingsPanel } from './widget-settings-panel';
/**
* widget-settings-panel.test — NEU (quick-260916-bwo). Das Einstellungsfeld
* hatte bisher keine Testdatei. Vier Tests fuer das Zahlenfeld
* „Schriftgroesse der Uhrzeit (Punkt)“: Vorbelegung, Uebernahme bei Blur,
* leer = automatisch (null) bei Enter, Grenzen 8..200 mit Fehlermeldung und
* OHNE Aufruf (T-BWO-01 — die Grenzen leben in clock-font-size.ts und gelten
* fuer Formular und Widget gleichermassen). Texte aus der echten de.json.
*/
vi.mock('next-intl', async () => {
const messages = (await import('@/messages/de.json')).default as Record<string, unknown>;
const lookup = (path: string): string | undefined =>
path.split('.').reduce<unknown>((o, k) => (o && typeof o === 'object' ? (o as any)[k] : undefined), messages) as
| string
| undefined;
return {
useTranslations: (ns?: string) => (key: string) => lookup(ns ? `${ns}.${key}` : key) ?? key,
};
});
vi.mock('@/lib/dashboard-api', () => ({
updateWidgetConfig: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('next/link', () => ({
default: ({ href, children }: { href: string; children: React.ReactNode }) => <a href={href}>{children}</a>,
}));
vi.mock('@/components/settings/search-provider-form', () => ({
SearchProviderForm: () => null,
}));
const clockTexts = (de as { widgets: { clock: Record<string, string> } }).widgets.clock;
const WIDGET = { id: 'c1', widgetType: 'clock', config: { timezone: 'Europe/Berlin', timeFontSizePt: 24 } };
function renderExpanded() {
const onWidgetUpdate = vi.fn();
render(<WidgetSettingsPanel widgets={[WIDGET]} onWidgetUpdate={onWidgetUpdate} />);
fireEvent.click(screen.getByRole('button', { name: /Uhr #1/ }));
const input = screen.getByLabelText(clockTexts.fontSizeLabel) as HTMLInputElement;
return { onWidgetUpdate, input };
}
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
cleanup();
});
describe('WidgetSettingsPanel — Schriftgroesse der Uhrzeit (quick-260916-bwo)', () => {
it('Test 1: das Zahlenfeld ist aus der Konfiguration vorbelegt, mit Grenzen, Platzhalter und Hilfetext', () => {
const { input } = renderExpanded();
expect(input.value).toBe('24');
expect(input.type).toBe('number');
expect(input.min).toBe('8');
expect(input.max).toBe('200');
expect(input.placeholder).toBe(clockTexts.fontSizeAuto);
expect(screen.getByText(clockTexts.fontSizeHint)).toBeInTheDocument();
});
it('Test 2: eine Zahl im Bereich wird beim Verlassen des Felds uebernommen', async () => {
const { input, onWidgetUpdate } = renderExpanded();
fireEvent.change(input, { target: { value: '36' } });
fireEvent.blur(input);
await vi.waitFor(() => expect(onWidgetUpdate).toHaveBeenCalledWith('c1', { timeFontSizePt: 36 }));
expect(updateWidgetConfig).toHaveBeenCalledTimes(1);
expect(updateWidgetConfig).toHaveBeenCalledWith('c1', { timeFontSizePt: 36 });
});
it('Test 3: leeres Feld plus Enter -> automatisch (timeFontSizePt null), keine Fehlermeldung', async () => {
const { input } = renderExpanded();
fireEvent.change(input, { target: { value: '' } });
fireEvent.keyDown(input, { key: 'Enter' });
await vi.waitFor(() => expect(updateWidgetConfig).toHaveBeenCalledWith('c1', { timeFontSizePt: null }));
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
it('Test 4: ausserhalb 8..200 -> Fehlermeldung, aria-invalid, KEIN Aufruf', () => {
const { input } = renderExpanded();
fireEvent.change(input, { target: { value: '300' } });
fireEvent.blur(input);
expect(updateWidgetConfig).not.toHaveBeenCalled();
expect(screen.getByRole('alert')).toHaveTextContent(clockTexts.fontSizeInvalid);
expect(input.getAttribute('aria-invalid')).toBe('true');
fireEvent.change(input, { target: { value: '7' } });
fireEvent.blur(input);
expect(updateWidgetConfig).not.toHaveBeenCalled();
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});