import { describe, it, expect, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import YearBand from './YearBand.svelte'; import { makeEntry, makeYear } from './test-factories'; afterEach(async () => { cleanup(); await page.viewport(1280, 720); }); function manyLetters(year: number, count: number) { return Array.from({ length: count }, (_, i) => makeEntry({ eventDate: `${year}-01-10`, documentId: `doc-${i}` }) ); } describe('YearBand', () => { it('renders a section with a sticky h2 at top:4rem showing the year (REQ-006)', () => { render(YearBand, { year: makeYear(1914, [makeEntry()]) }); const section = document.querySelector('section'); expect(section).not.toBeNull(); const h2 = section?.querySelector('h2'); expect(h2?.textContent).toContain('1914'); const cs = getComputedStyle(h2 as HTMLElement); expect(cs.position).toBe('sticky'); expect(cs.top).toBe('64px'); }); it('renders each letter as a card when the band holds <= 12 letters (REQ-011)', () => { render(YearBand, { year: makeYear(1909, manyLetters(1909, 3)) }); expect(document.querySelectorAll('a')).toHaveLength(3); expect(document.querySelector('[data-testid="strip-expand"]')).toBeNull(); }); it('renders a single strip when the band holds > 12 letters (REQ-012)', () => { render(YearBand, { year: makeYear(1915, manyLetters(1915, 30)) }); expect(document.querySelector('[data-testid="strip-expand"]')).not.toBeNull(); // collapsed: no individual letter links yet expect(document.querySelectorAll('a')).toHaveLength(0); }); it('renders entries in DTO order — DAY-precision letter above a YEAR-precision letter (REQ-003)', () => { const dayLetter = makeEntry({ precision: 'DAY', eventDate: '1923-04-12', title: 'Tagesgenau', documentId: 'day' }); const yearLetter = makeEntry({ precision: 'YEAR', eventDate: '1923-01-01', title: 'Nur Jahr', documentId: 'year' }); render(YearBand, { year: makeYear(1923, [dayLetter, yearLetter]) }); const links = Array.from(document.querySelectorAll('a')); expect(links[0].getAttribute('href')).toBe('/documents/day'); expect(links[1].getAttribute('href')).toBe('/documents/year'); }); it('renders an EVENT as a pill and a HISTORICAL event as a band', () => { const pill = makeEntry({ kind: 'EVENT', derived: true, derivedType: 'MARRIAGE', title: 'Heirat', senderName: '', receiverName: '', documentId: undefined }); const band = makeEntry({ kind: 'EVENT', derived: false, type: 'HISTORICAL', precision: 'RANGE', eventDate: '1914-01-01', eventDateEnd: '1918-12-31', title: 'Erster Weltkrieg', senderName: '', receiverName: '', documentId: undefined }); render(YearBand, { year: makeYear(1914, [pill, band]) }); expect(document.body.textContent).toContain('Heirat'); expect(document.querySelector('[data-testid="world-range"]')).not.toBeNull(); }); it('centers the year badge on the axis at desktop (REQ-003)', async () => { await page.viewport(1440, 900); render(YearBand, { year: makeYear(1914, [makeEntry()]) }); const section = document.querySelector('section') as HTMLElement; const badge = document.querySelector('[data-testid="year-label"]') as HTMLElement; const s = section.getBoundingClientRect(); const b = badge.getBoundingClientRect(); const sectionCenter = s.left + s.width / 2; const badgeCenter = b.left + b.width / 2; expect(Math.abs(badgeCenter - sectionCenter)).toBeLessThan(8); }); it('left-aligns the year badge at phone width (REQ-003)', async () => { await page.viewport(375, 800); render(YearBand, { year: makeYear(1914, [makeEntry()]) }); const section = document.querySelector('section') as HTMLElement; const badge = document.querySelector('[data-testid="year-label"]') as HTMLElement; const s = section.getBoundingClientRect(); const b = badge.getBoundingClientRect(); // hugs the left spine — clearly not centered expect(b.left - s.left).toBeLessThan(s.width / 3); }); it('keeps the sticky year heading at top:4rem (REQ-003)', () => { render(YearBand, { year: makeYear(1914, [makeEntry()]) }); const h2 = document.querySelector('h2') as HTMLElement; const cs = getComputedStyle(h2); expect(cs.position).toBe('sticky'); expect(cs.top).toBe('64px'); }); it('renders a year-badge node marker that clears the badge text on phone (REQ-004)', async () => { await page.viewport(375, 800); render(YearBand, { year: makeYear(1914, [makeEntry()]) }); const node = document.querySelector('[data-testid="year-node"]') as HTMLElement; const label = document.querySelector('[data-testid="year-label"]') as HTMLElement; expect(node).not.toBeNull(); const n = node.getBoundingClientRect(); const l = label.getBoundingClientRect(); expect(n.right).toBeLessThanOrEqual(l.left + 0.5); // The badge must paint above the node so the centered desktop pill never // occludes the white year text (regression guard). expect(Number(getComputedStyle(label).zIndex)).toBeGreaterThan( Number(getComputedStyle(node).zIndex) ); }); it('renders one connector dot per letter row, each clearing its card on phone (REQ-005)', async () => { await page.viewport(375, 800); render(YearBand, { year: makeYear(1909, manyLetters(1909, 3)) }); const dots = document.querySelectorAll('[data-testid="letter-dot"]'); expect(dots).toHaveLength(3); const row = document.querySelector('.letter-row') as HTMLElement; const dot = row.querySelector('[data-testid="letter-dot"]') as HTMLElement; const card = row.querySelector('a') as HTMLElement; expect(dot.getBoundingClientRect().right).toBeLessThanOrEqual( card.getBoundingClientRect().left + 0.5 ); }); it('positions the year-node from the inherited --spine-x token (REQ-003/004)', async () => { await page.viewport(375, 800); // The spine X lives once on .timeline-axis as --spine-x; the markers must // track that token, not a hard-coded offset, so they never desync. document.documentElement.style.setProperty('--spine-x', '3rem'); try { render(YearBand, { year: makeYear(1914, [makeEntry()]) }); const node = document.querySelector('[data-testid="year-node"]') as HTMLElement; const section = document.querySelector('section') as HTMLElement; const n = node.getBoundingClientRect(); const s = section.getBoundingClientRect(); const nodeCenter = n.left + n.width / 2; // --spine-x:3rem = 48px from the band's left edge expect(Math.abs(nodeCenter - s.left - 48)).toBeLessThan(2); } finally { document.documentElement.style.removeProperty('--spine-x'); } }); }); describe('YearBand — grouping modes (#827)', () => { it('keeps individual letter cards and no buckets in Datum mode (default)', () => { render(YearBand, { year: makeYear(1915, manyLetters(1915, 3)) }); expect(document.querySelector('[data-testid="letter-bucket"]')).toBeNull(); expect(document.querySelectorAll('a')).toHaveLength(3); }); it('clusters loose letters under their linked event in Ereignis mode (REQ-002/003)', () => { const a = makeEntry({ documentId: 'a', linkedEventId: 'e1', eventDate: '1915-03-01' }); const b = makeEntry({ documentId: 'b', linkedEventId: 'e1', eventDate: '1915-04-01' }); render(YearBand, { year: makeYear(1915, [a, b]), groupingMode: 'event', eventLookup: new Map([['e1', 'Briefe von der Front']]) }); expect(document.querySelectorAll('[data-testid="letter-bucket"]')).toHaveLength(1); expect(document.body.textContent).toContain('Briefe von der Front'); // no alternating individual letter rows in grouped mode expect(document.querySelector('.letter-row')).toBeNull(); }); it('still renders the event world-band in Ereignis mode (REQ-001)', () => { const band = makeEntry({ kind: 'EVENT', type: 'HISTORICAL', precision: 'RANGE', eventDate: '1914-01-01', eventDateEnd: '1918-12-31', title: 'Erster Weltkrieg', senderName: '', receiverName: '', documentId: undefined }); const letter = makeEntry({ documentId: 'a', linkedEventId: 'e1', eventDate: '1914-05-01' }); render(YearBand, { year: makeYear(1914, [band, letter]), groupingMode: 'event', eventLookup: new Map([['e1', 'Front']]) }); expect(document.querySelector('[data-testid="world-range"]')).not.toBeNull(); expect(document.querySelector('[data-testid="letter-bucket"]')).not.toBeNull(); }); it('buckets loose letters under their root tag in Thema mode (REQ-004)', () => { const a = makeEntry({ documentId: 'a', rootTagId: 't1', rootTagName: 'Krieg', rootTagColor: 'sienna', eventDate: '1915-03-01' }); render(YearBand, { year: makeYear(1915, [a]), groupingMode: 'thema', eventLookup: new Map() }); const chip = document.querySelector('[data-testid="bucket-header-chip"]'); expect(chip?.textContent).toContain('Krieg'); }); });