Files
familienarchiv/frontend/src/lib/document/WhoWhenSection.svelte.test.ts
Marcel 654ac1478c feat(document): surface end-before-start inline on the date form (#678)
Add an endBeforeStart $derived to WhoWhenSection (lexicographic ISO compare,
no Date object) that renders an inline error on the end-date field —
border-red-400, aria-invalid, aria-describedby, and a #end-date-error <p>
inside the existing aria-live region — with a ⚠ glyph so the cue is not
colour-alone (WCAG 1.4.1). Save is not disabled; the server stays the gate.

Wire ErrorCode INVALID_DATE_RANGE through errors.ts getErrorMessage and add
the single key error_invalid_date_range to de/en/es, so the same translated
string is used inline (client) and via getErrorMessage (server fallback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 09:27:57 +02:00

151 lines
5.1 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import WhoWhenSection from './WhoWhenSection.svelte';
afterEach(cleanup);
describe('WhoWhenSection — date input behavior', () => {
it('marks the date input as invalid when input has text but no valid ISO', async () => {
render(WhoWhenSection, {});
const dateInput = document.querySelector('input#documentDate') as HTMLInputElement;
dateInput.value = '32.13';
dateInput.dispatchEvent(new Event('input', { bubbles: true }));
await vi.waitFor(() => {
// Invalid → border-red-400 class
expect(dateInput.className).toContain('border-red-400');
expect(document.querySelector('#date-error')).not.toBeNull();
});
});
it('does not show the error before the user has typed', async () => {
render(WhoWhenSection, {});
const error = document.querySelector('#date-error');
expect(error).toBeNull();
});
it('updates the hidden ISO input when typing a valid German date', async () => {
render(WhoWhenSection, {});
const dateInput = document.querySelector('input#documentDate') as HTMLInputElement;
dateInput.value = '15.03.2024';
dateInput.dispatchEvent(new Event('input', { bubbles: true }));
await vi.waitFor(() => {
const hidden = document.querySelector(
'input[name="documentDate"][type="hidden"]'
) as HTMLInputElement;
expect(hidden.value).toBe('2024-03-15');
});
});
it('renders the location input outside editMode with initialLocation', async () => {
render(WhoWhenSection, { editMode: false, initialLocation: 'Hamburg' });
const loc = document.querySelector('input#location') as HTMLInputElement;
expect(loc.value).toBe('Hamburg');
});
it('hides the location input in editMode', async () => {
render(WhoWhenSection, { editMode: true });
const loc = document.querySelector('input#location');
expect(loc).toBeNull();
});
it('shows the FieldLabelBadge for receivers in editMode', async () => {
render(WhoWhenSection, { editMode: true });
// FieldLabelBadge with variant=additive is rendered (just check the heading area)
const labels = Array.from(document.querySelectorAll('p, label')).filter((el) =>
/empfänger/i.test(el.textContent ?? '')
);
expect(labels.length).toBeGreaterThan(0);
});
it('renders the date asterisk indicator (required field)', async () => {
render(WhoWhenSection, {});
const label = document.querySelector('label[for="documentDate"]');
expect(label?.textContent).toContain('*');
});
});
describe('WhoWhenSection — precision controls', () => {
it('renders a labelled precision select', async () => {
render(WhoWhenSection, {});
const label = document.querySelector('label[for="metaDatePrecision"]');
const select = document.querySelector('select#metaDatePrecision[name="metaDatePrecision"]');
expect(label).not.toBeNull();
expect(select).not.toBeNull();
});
it('hides the end-date field unless precision is RANGE', async () => {
render(WhoWhenSection, { precision: 'DAY' });
expect(document.querySelector('input#metaDateEnd')).toBeNull();
});
it('reveals the end-date field when precision is RANGE', async () => {
render(WhoWhenSection, { precision: 'RANGE' });
expect(document.querySelector('input#metaDateEnd')).not.toBeNull();
});
it('renders the raw cell as static text (not an editable input) and escapes it', async () => {
render(WhoWhenSection, { rawDate: '<b>Sommer</b> 1916' });
const raw = document.querySelector('[data-testid="who-when-raw"]');
expect(raw).not.toBeNull();
// Verbatim shown as escaped text; no injected <b> element.
expect(raw?.textContent).toContain('<b>Sommer</b> 1916');
expect(raw?.querySelector('b')).toBeNull();
});
});
describe('WhoWhenSection — end-before-start inline validation (#678)', () => {
it('shows an inline error on the end-date field when end is before start (AC1)', async () => {
render(WhoWhenSection, {
precision: 'RANGE',
dateIso: '1917-01-11',
endDateIso: '1917-01-10'
});
const end = document.querySelector('input#metaDateEnd') as HTMLInputElement;
await vi.waitFor(() => {
expect(document.querySelector('#end-date-error')).not.toBeNull();
expect(end.getAttribute('aria-invalid')).toBe('true');
expect(end.className).toContain('border-red-400');
});
});
it('clears the inline error once the end date is corrected, without reload (AC5)', async () => {
render(WhoWhenSection, {
precision: 'RANGE',
dateIso: '1917-01-11',
endDateIso: '1917-01-10'
});
await vi.waitFor(() => expect(document.querySelector('#end-date-error')).not.toBeNull());
const end = document.querySelector('input#metaDateEnd') as HTMLInputElement;
end.value = '12.01.1917'; // now after the start
end.dispatchEvent(new Event('input', { bubbles: true }));
await vi.waitFor(() => {
expect(document.querySelector('#end-date-error')).toBeNull();
expect(end.getAttribute('aria-invalid')).not.toBe('true');
});
});
it('does not show the inline error when precision is not RANGE', async () => {
render(WhoWhenSection, {
precision: 'DAY',
dateIso: '1917-01-11',
endDateIso: '1917-01-10'
});
expect(document.querySelector('#end-date-error')).toBeNull();
});
});