import { describe, it, expect, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import * as m from '$lib/paraglide/messages.js'; import TimelineView from './TimelineView.svelte'; import { makeEntry, makeYear, makeTimelineDTO } from './test-factories'; afterEach(() => cleanup()); describe('TimelineView', () => { it('shows the empty state and no ol when there are no years and no undated (REQ-017)', () => { render(TimelineView, { timeline: makeTimelineDTO() }); expect(document.body.textContent).toContain('Noch keine Ereignisse.'); expect(document.querySelector('ol')).toBeNull(); expect(document.querySelector('section')).toBeNull(); }); it('renders the timeline as a single
    with each band a
    , ascending (REQ-006)', () => { render(TimelineView, { timeline: makeTimelineDTO({ years: [ makeYear(1914, [makeEntry({ documentId: 'a' })]), makeYear(1916, [makeEntry({ documentId: 'b' })]) ] }) }); expect(document.querySelectorAll('ol')).toHaveLength(1); const headings = Array.from(document.querySelectorAll('section h2')).map((h) => h.textContent); expect(headings.some((t) => t?.includes('1914'))).toBe(true); const order = headings.map((t) => t?.trim()); expect(order.indexOf('1914')).toBeLessThan(order.indexOf('1916')); }); it('folds an interior run of empty years into one GapSpan (REQ-015)', () => { render(TimelineView, { timeline: makeTimelineDTO({ years: [ makeYear(1909, [makeEntry({ documentId: 'a' })]), makeYear(1915, [makeEntry({ documentId: 'b' })]) ] }) }); expect(document.body.textContent).toContain('1910–1914'); expect(document.body.textContent).toContain('keine Einträge'); }); it('folds a single empty interior year as a single year (REQ-015)', () => { render(TimelineView, { timeline: makeTimelineDTO({ years: [ makeYear(1911, [makeEntry({ documentId: 'a' })]), makeYear(1913, [makeEntry({ documentId: 'b' })]) ] }) }); expect(document.body.textContent).toContain('1912'); expect(document.body.textContent).not.toContain('1912–1912'); }); it('renders an "Ohne Datum" section when undated is non-empty (REQ-016)', () => { render(TimelineView, { timeline: makeTimelineDTO({ years: [makeYear(1914, [makeEntry({ documentId: 'a' })])], undated: [makeEntry({ precision: 'UNKNOWN', eventDate: undefined, documentId: 'u1' })] }) }); expect(document.querySelector('[data-testid="undated-section"]')).not.toBeNull(); expect(document.body.textContent).toContain('Ohne Datum'); }); it('omits the "Ohne Datum" section from the DOM when undated is empty (REQ-016)', () => { render(TimelineView, { timeline: makeTimelineDTO({ years: [makeYear(1914, [makeEntry({ documentId: 'a' })])] }) }); expect(document.querySelector('[data-testid="undated-section"]')).toBeNull(); }); it('frames the undated section with a dashed border and shows the count (REQ-012)', () => { const undated = Array.from({ length: 11 }, (_, i) => makeEntry({ precision: 'UNKNOWN', eventDate: undefined, documentId: `u-${i}` }) ); render(TimelineView, { timeline: makeTimelineDTO({ undated }) }); const section = document.querySelector('[data-testid="undated-section"]') as HTMLElement; expect(section).not.toBeNull(); expect(section.classList.contains('border-dashed')).toBe(true); const h2 = section.querySelector('h2'); expect(h2?.textContent).toContain(m.timeline_undated_section()); expect(h2?.textContent).toContain('11'); }); it('renders all years and undated entries with personId undefined, no filtering (REQ-025)', () => { render(TimelineView, { timeline: makeTimelineDTO({ years: [ makeYear(1914, [makeEntry({ documentId: 'a' })]), makeYear(1915, [makeEntry({ documentId: 'b' })]) ], undated: [makeEntry({ precision: 'UNKNOWN', eventDate: undefined, documentId: 'u1' })] }), personId: undefined }); // Two year bands inside the
      , plus the separate undated section. expect(document.querySelectorAll('ol section h2')).toHaveLength(2); expect(document.querySelector('[data-testid="undated-section"]')).not.toBeNull(); }); it('renders an undated curated EVENT as a pill, not a broken letter card (REQ-008/016)', () => { render(TimelineView, { timeline: makeTimelineDTO({ undated: [ makeEntry({ kind: 'EVENT', type: 'PERSONAL', derived: false, eventId: 'e1', precision: 'UNKNOWN', eventDate: undefined, title: 'Auswanderung', senderName: '', receiverName: '', documentId: undefined }) ] }) }); // The event renders inside the undated section… expect(document.querySelector('[data-testid="undated-section"]')).not.toBeNull(); expect(document.body.textContent).toContain('Auswanderung'); // …as an EventPill (its edit affordance), never as a letter card linking // to /documents/undefined with "Unbekannt → Unbekannt". expect(document.querySelector('[data-testid="event-edit"]')).not.toBeNull(); expect(document.querySelector('a[href="/documents/undefined"]')).toBeNull(); expect(document.body.textContent).not.toContain('Unbekannt'); }); it('renders an undated HISTORICAL EVENT as a world band, not a letter card (REQ-009/016)', () => { render(TimelineView, { timeline: makeTimelineDTO({ undated: [ makeEntry({ kind: 'EVENT', type: 'HISTORICAL', derived: false, precision: 'UNKNOWN', eventDate: undefined, title: 'Weltwirtschaftskrise', senderName: '', receiverName: '', documentId: undefined }) ] }) }); expect(document.querySelector('[data-testid="undated-section"]')).not.toBeNull(); expect(document.body.textContent).toContain('Weltwirtschaftskrise'); // HISTORICAL → WorldBand carries the sr-only "Weltgeschehen" cue (REQ-018), // not a broken document link. expect(document.body.textContent).toContain('Weltgeschehen'); expect(document.querySelector('a[href="/documents/undefined"]')).toBeNull(); }); it('still renders an undated LETTER as a letter card (REQ-016)', () => { render(TimelineView, { timeline: makeTimelineDTO({ undated: [makeEntry({ precision: 'UNKNOWN', eventDate: undefined, documentId: 'u1' })] }) }); expect(document.querySelector('a[href="/documents/u1"]')).not.toBeNull(); }); it('renders two derived events in one band without key collision (no-double-null-key)', () => { const a = makeEntry({ kind: 'EVENT', derived: true, derivedType: 'BIRTH', title: 'Geburt: Anna', senderName: '', receiverName: '', documentId: undefined, eventId: undefined, linkedPersonIds: ['p1'] }); const b = makeEntry({ kind: 'EVENT', derived: true, derivedType: 'BIRTH', title: 'Geburt: Bertha', senderName: '', receiverName: '', documentId: undefined, eventId: undefined, linkedPersonIds: ['p2'] }); render(TimelineView, { timeline: makeTimelineDTO({ years: [makeYear(1915, [a, b])] }) }); expect(document.body.textContent).toContain('Geburt: Anna'); expect(document.body.textContent).toContain('Geburt: Bertha'); }); it('shows the redundant non-color cue label for each layer (REQ-018)', () => { render(TimelineView, { timeline: makeTimelineDTO({ years: [ makeYear(1914, [ makeEntry({ kind: 'EVENT', derived: true, derivedType: 'BIRTH', title: 'Geburt: Hans', senderName: '', receiverName: '', documentId: undefined }), makeEntry({ kind: 'EVENT', derived: false, type: 'PERSONAL', eventId: 'e1', title: 'Auswanderung', senderName: '', receiverName: '', documentId: undefined }), makeEntry({ kind: 'EVENT', derived: false, type: 'HISTORICAL', precision: 'RANGE', eventDate: '1914-01-01', eventDateEnd: '1918-12-31', title: 'Erster Weltkrieg', senderName: '', receiverName: '', documentId: undefined }) ]) ] }) }); expect(document.body.textContent).toContain('Weltgeschehen'); expect(document.body.textContent).toContain('Familie'); expect(document.body.textContent).toContain('Geburt'); }); it('paints the axis with a three-stop mint→navy→slate gradient (REQ-006)', () => { // The palette tokens live in layout.css, which component tests don't load, // so define exactly the three the gradient must reference; an undefined // fourth token would invalidate the declaration and yield "none". const root = document.documentElement; root.style.setProperty('--palette-mint', '#a1dcd8'); root.style.setProperty('--palette-navy', '#012851'); root.style.setProperty('--c-tag-slate', '#607080'); try { render(TimelineView, { timeline: makeTimelineDTO({ years: [makeYear(1914, [makeEntry()])] }) }); const axis = document.querySelector('.timeline-axis') as HTMLElement; expect(axis).not.toBeNull(); const bg = getComputedStyle(axis, '::before').backgroundImage; expect(bg).toContain('gradient'); // three colour stops: mint, navy, slate expect((bg.match(/rgb/g) ?? []).length).toBe(3); } finally { root.style.removeProperty('--palette-mint'); root.style.removeProperty('--palette-navy'); root.style.removeProperty('--c-tag-slate'); } }); it('places consecutive letter cards on alternating sides (REQ-004 surrogate)', () => { const letters = Array.from({ length: 4 }, (_, i) => makeEntry({ documentId: `d${i}` })); render(TimelineView, { timeline: makeTimelineDTO({ years: [makeYear(1909, letters)] }) }); const sides = Array.from(document.querySelectorAll('.letter-row')).map((el) => el.getAttribute('data-side') ); expect(sides).toEqual(['left', 'right', 'left', 'right']); }); });