From 9d6c7b8605bef7ffd1f60a581b71bf887e1dabf3 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 30 Mar 2026 18:20:07 +0200 Subject: [PATCH] test(DateInput): add Vitest specs for DateInput component and date utils Co-Authored-By: Claude Sonnet 4.6 --- .../lib/components/DateInput.svelte.spec.ts | 206 ++++++++++++++++++ frontend/src/lib/utils/date.spec.ts | 109 +++++++++ 2 files changed, 315 insertions(+) create mode 100644 frontend/src/lib/components/DateInput.svelte.spec.ts create mode 100644 frontend/src/lib/utils/date.spec.ts diff --git a/frontend/src/lib/components/DateInput.svelte.spec.ts b/frontend/src/lib/components/DateInput.svelte.spec.ts new file mode 100644 index 00000000..5c682812 --- /dev/null +++ b/frontend/src/lib/components/DateInput.svelte.spec.ts @@ -0,0 +1,206 @@ +import { describe, expect, it, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import DateInput from './DateInput.svelte'; + +/** Wait one macrotask so Svelte can flush reactive DOM updates. */ +const domFlush = () => new Promise((r) => setTimeout(r, 50)); + +afterEach(() => cleanup()); + +// ─── Rendering ──────────────────────────────────────────────────────────────── + +describe('DateInput – rendering', () => { + it('renders a text input with inputmode=numeric and maxlength=10', async () => { + render(DateInput, {}); + const input = page.getByRole('textbox'); + await expect.element(input).toBeInTheDocument(); + await expect.element(input).toHaveAttribute('inputmode', 'numeric'); + await expect.element(input).toHaveAttribute('maxlength', '10'); + }); + + it('has default placeholder from paraglide', async () => { + render(DateInput, {}); + const input = page.getByRole('textbox'); + await expect.element(input).toHaveAttribute('placeholder', 'TT.MM.JJJJ'); + }); + + it('accepts a custom placeholder', async () => { + render(DateInput, { placeholder: 'Geburtsdatum' }); + const input = page.getByRole('textbox'); + await expect.element(input).toHaveAttribute('placeholder', 'Geburtsdatum'); + }); + + it('passes id prop to the input', async () => { + render(DateInput, { id: 'my-date' }); + const input = page.getByRole('textbox'); + await expect.element(input).toHaveAttribute('id', 'my-date'); + }); +}); + +// ─── Init from value ────────────────────────────────────────────────────────── + +describe('DateInput – init from value', () => { + it('displays ISO value in German format on mount', async () => { + render(DateInput, { value: '2024-12-20' }); + const input = page.getByRole('textbox'); + await expect.element(input).toHaveValue('20.12.2024'); + }); + + it('starts empty and error-free when no value is given', async () => { + let errorMessage: string | null = null; + render(DateInput, { + get errorMessage() { + return errorMessage; + }, + set errorMessage(v) { + errorMessage = v; + } + }); + const input = page.getByRole('textbox'); + await expect.element(input).toHaveValue(''); + expect(errorMessage).toBeNull(); + }); +}); + +// ─── Typing valid date ──────────────────────────────────────────────────────── + +describe('DateInput – typing a valid date', () => { + it('auto-formats to DD.MM.YYYY and updates value to ISO', async () => { + let value = ''; + let errorMessage: string | null = null; + render(DateInput, { + get value() { + return value; + }, + set value(v) { + value = v; + }, + get errorMessage() { + return errorMessage; + }, + set errorMessage(v) { + errorMessage = v; + } + }); + const input = page.getByRole('textbox'); + await input.fill('20122024'); + await expect.element(input).toHaveValue('20.12.2024'); + expect(value).toBe('2024-12-20'); + expect(errorMessage).toBeNull(); + }); +}); + +// ─── Typing invalid month ───────────────────────────────────────────────────── + +describe('DateInput – typing a date with invalid month', () => { + it('sets errorMessage and clears value when month > 12', async () => { + let value = ''; + let errorMessage: string | null = null; + render(DateInput, { + get value() { + return value; + }, + set value(v) { + value = v; + }, + get errorMessage() { + return errorMessage; + }, + set errorMessage(v) { + errorMessage = v; + } + }); + const input = page.getByRole('textbox'); + await input.fill('22222222'); + await expect.element(input).toHaveValue('22.22.2222'); + expect(value).toBe(''); + expect(errorMessage).not.toBeNull(); + }); +}); + +// ─── Typing partial date ────────────────────────────────────────────────────── + +describe('DateInput – typing a partial date', () => { + it('sets errorMessage and clears value when date is incomplete', async () => { + let value = ''; + let errorMessage: string | null = null; + render(DateInput, { + get value() { + return value; + }, + set value(v) { + value = v; + }, + get errorMessage() { + return errorMessage; + }, + set errorMessage(v) { + errorMessage = v; + } + }); + const input = page.getByRole('textbox'); + await input.fill('2212'); + await expect.element(input).toHaveValue('22.12'); + expect(value).toBe(''); + expect(errorMessage).not.toBeNull(); + }); +}); + +// ─── Clearing date ──────────────────────────────────────────────────────────── + +describe('DateInput – clearing the date', () => { + it('resets value and errorMessage to null when cleared', async () => { + let value = ''; + let errorMessage: string | null = null; + render(DateInput, { + get value() { + return value; + }, + set value(v) { + value = v; + }, + get errorMessage() { + return errorMessage; + }, + set errorMessage(v) { + errorMessage = v; + } + }); + const input = page.getByRole('textbox'); + // Type a valid date first + await input.fill('20122024'); + expect(value).toBe('2024-12-20'); + // Now clear + await input.fill(''); + expect(value).toBe(''); + expect(errorMessage).toBeNull(); + }); +}); + +// ─── Hidden input ───────────────────────────────────────────────────────────── + +describe('DateInput – hidden input for form submission', () => { + it('renders a hidden input with the given name when name prop is set', async () => { + render(DateInput, { name: 'documentDate' }); + const hidden = document.querySelector('input[type="hidden"][name="documentDate"]'); + expect(hidden).not.toBeNull(); + }); + + it('does not render a hidden input when name prop is absent', async () => { + render(DateInput, {}); + const hidden = document.querySelector('input[type="hidden"]'); + expect(hidden).toBeNull(); + }); + + it('hidden input value reflects the ISO value', async () => { + render(DateInput, { name: 'documentDate', value: '' }); + const input = page.getByRole('textbox'); + await input.fill('20122024'); + await domFlush(); + const hidden = document.querySelector( + 'input[type="hidden"][name="documentDate"]' + ); + expect(hidden?.value).toBe('2024-12-20'); + }); +}); diff --git a/frontend/src/lib/utils/date.spec.ts b/frontend/src/lib/utils/date.spec.ts new file mode 100644 index 00000000..27344f96 --- /dev/null +++ b/frontend/src/lib/utils/date.spec.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from 'vitest'; +import { formatGermanDateInput, isoToGerman, germanToIso } from './date'; + +// ─── isoToGerman ───────────────────────────────────────────────────────────── + +describe('isoToGerman', () => { + it('converts a valid ISO date to DD.MM.YYYY', () => { + expect(isoToGerman('2024-12-20')).toBe('20.12.2024'); + }); + + it('returns empty string for empty input', () => { + expect(isoToGerman('')).toBe(''); + }); + + it('returns empty string for invalid format', () => { + expect(isoToGerman('not-a-date')).toBe(''); + }); +}); + +// ─── germanToIso ───────────────────────────────────────────────────────────── + +describe('germanToIso', () => { + it('converts DD.MM.YYYY to ISO', () => { + expect(germanToIso('20.12.2024')).toBe('2024-12-20'); + }); + + it('returns empty string for partial input', () => { + expect(germanToIso('20.12')).toBe(''); + }); + + it('returns empty string for empty input', () => { + expect(germanToIso('')).toBe(''); + }); +}); + +// ─── formatGermanDateInput ──────────────────────────────────────────────────── + +describe('formatGermanDateInput – digit stream (no dots typed)', () => { + it('leaves 1–2 digits as-is', () => { + expect(formatGermanDateInput('2')).toBe('2'); + expect(formatGermanDateInput('20')).toBe('20'); + }); + + it('auto-inserts dot after 2 digits for 3–4 digit input', () => { + expect(formatGermanDateInput('201')).toBe('20.1'); + expect(formatGermanDateInput('2012')).toBe('20.12'); + }); + + it('auto-inserts two dots for 5–8 digit input', () => { + expect(formatGermanDateInput('20121')).toBe('20.12.1'); + expect(formatGermanDateInput('20122024')).toBe('20.12.2024'); + }); + + it('ignores digits beyond 8', () => { + expect(formatGermanDateInput('201220249')).toBe('20.12.2024'); + }); +}); + +describe('formatGermanDateInput – manual dot entry with padding', () => { + it('pads single-digit day to 2 digits when dot is typed after it', () => { + expect(formatGermanDateInput('3.')).toBe('03.'); + }); + + it('does not pad a 2-digit day', () => { + expect(formatGermanDateInput('03.')).toBe('03.'); + expect(formatGermanDateInput('20.')).toBe('20.'); + }); + + it('pads single-digit month to 2 digits when dot is typed after it', () => { + expect(formatGermanDateInput('03.3.')).toBe('03.03.'); + }); + + it('does not pad a 2-digit month', () => { + expect(formatGermanDateInput('03.12.')).toBe('03.12.'); + }); + + it('pads both day and month in a fully typed date', () => { + expect(formatGermanDateInput('3.3.2012')).toBe('03.03.2012'); + }); + + it('pads only day when month is already 2 digits', () => { + expect(formatGermanDateInput('3.12.2024')).toBe('03.12.2024'); + }); + + it('pads only month when day is already 2 digits', () => { + expect(formatGermanDateInput('20.3.2024')).toBe('20.03.2024'); + }); + + it('handles a complete date entered with manual dots and no padding needed', () => { + expect(formatGermanDateInput('20.12.2024')).toBe('20.12.2024'); + }); + + it('overflows excess day digits into month when dot follows', () => { + expect(formatGermanDateInput('123.')).toBe('12.3'); + }); + + it('caps year digits at 4', () => { + expect(formatGermanDateInput('03.03.20249')).toBe('03.03.2024'); + }); + + it('overflows excess month digits into year (digit stream then continue typing)', () => { + // User typed digits → auto-dot gave "20.12", then types "2" → raw becomes "20.122" + expect(formatGermanDateInput('20.122')).toBe('20.12.2'); + }); + + it('continues building year after overflow', () => { + expect(formatGermanDateInput('20.12.2024')).toBe('20.12.2024'); + }); +});