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:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user