import { describe, it, expect, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { tick } from 'svelte'; import * as m from '$lib/paraglide/messages.js'; import LetterBucket from './LetterBucket.svelte'; import { makeEntry } from './test-factories'; import type { LetterBucket as Bucket } from './timelineGrouping'; afterEach(() => cleanup()); const eventBucket: Bucket = { key: 'event:e1', kind: 'event', title: 'Briefe von der Front', color: null, letters: [makeEntry({ documentId: 'a' }), makeEntry({ documentId: 'b' })] }; const tagBucket: Bucket = { key: 'tag:t1', kind: 'tag', title: 'Krieg', color: 'sienna', letters: [makeEntry({ documentId: 'c', rootTagName: 'Krieg', rootTagColor: 'sienna' })] }; describe('LetterBucket — Ereignis mode (REQ-003/006/014)', () => { it('shows the event title and the cluster count', () => { render(LetterBucket, { bucket: eventBucket, mode: 'event' }); expect(document.body.textContent).toContain('Briefe von der Front'); expect(document.querySelector('[data-testid="bucket-count"]')?.textContent).toContain('2'); }); it('renders its letters as .lcard.ev event cards (REQ-014)', () => { render(LetterBucket, { bucket: eventBucket, mode: 'event' }); expect(document.querySelectorAll('a.lcard.ev')).toHaveLength(2); }); it('uses the localized "Weitere Briefe" label and plain cards for the fallback bucket (REQ-006)', () => { const fb: Bucket = { key: '__fallback__', kind: 'fallback', color: null, letters: [makeEntry({ documentId: 'x' })] }; render(LetterBucket, { bucket: fb, mode: 'event' }); expect(document.body.textContent).toContain(m.timeline_bucket_other_letters()); // fallback letters are not clustered under a curated event → plain card, never .lcard.ev expect(document.querySelector('a.ev')).toBeNull(); }); }); describe('LetterBucket — Thema mode (REQ-004/007/015/017)', () => { it('renders a tinted bucket-header chip carrying the root-tag name (REQ-015)', () => { render(LetterBucket, { bucket: tagBucket, mode: 'thema' }); const chip = document.querySelector('[data-testid="bucket-header-chip"]'); expect(chip?.textContent).toContain('Krieg'); }); it('suppresses the per-letter tag chip inside its own root-tag bucket (REQ-017)', () => { render(LetterBucket, { bucket: tagBucket, mode: 'thema' }); expect(document.querySelector('[data-testid="tag-chip"]')).toBeNull(); }); it('uses the localized "Ohne Thema" label for the untagged fallback bucket (REQ-007)', () => { const fb: Bucket = { key: '__fallback__', kind: 'fallback', color: null, letters: [makeEntry({ documentId: 'y', rootTagName: undefined })] }; render(LetterBucket, { bucket: fb, mode: 'thema' }); expect(document.body.textContent).toContain(m.timeline_bucket_no_topic()); }); }); const manyLetters = (n: number) => Array.from({ length: n }, (_, i) => makeEntry({ documentId: `d${i}`, eventDate: `1916-0${(i % 9) + 1}-01` }) ); describe('LetterBucket — preview cap + show-more (#827 redesign)', () => { it('shows only the first 5 letters with a show-more toggle when the cluster is larger', () => { const bucket: Bucket = { key: 'tag:t1', kind: 'tag', title: 'Krieg', color: 'sienna', letters: manyLetters(8) }; render(LetterBucket, { bucket, mode: 'thema', year: 1916 }); expect(document.querySelectorAll('a.lcard')).toHaveLength(5); expect(document.querySelector('[data-testid="bucket-show-more"]')).not.toBeNull(); expect(document.querySelector('[data-testid="strip-expand"]')).toBeNull(); // sparkline gone }); it('expands to all letters and collapses back on toggle', async () => { const bucket: Bucket = { key: 'tag:t1', kind: 'tag', title: 'Krieg', color: 'sienna', letters: manyLetters(8) }; render(LetterBucket, { bucket, mode: 'thema', year: 1916 }); (document.querySelector('[data-testid="bucket-show-more"]') as HTMLButtonElement).click(); await tick(); expect(document.querySelectorAll('a.lcard')).toHaveLength(8); (document.querySelector('[data-testid="bucket-show-more"]') as HTMLButtonElement).click(); await tick(); expect(document.querySelectorAll('a.lcard')).toHaveLength(5); }); it('shows all letters and no toggle for a small cluster (<= 5)', () => { const bucket: Bucket = { key: 'tag:t1', kind: 'tag', title: 'Tod', color: null, letters: manyLetters(3) }; render(LetterBucket, { bucket, mode: 'thema', year: 1916 }); expect(document.querySelectorAll('a.lcard')).toHaveLength(3); expect(document.querySelector('[data-testid="bucket-show-more"]')).toBeNull(); }); it('binds a tag bucket together with a coloured left rail from its token', () => { const bucket: Bucket = { key: 'tag:t1', kind: 'tag', title: 'Krieg', color: 'sienna', letters: manyLetters(1) }; render(LetterBucket, { bucket, mode: 'thema', year: 1916 }); const section = document.querySelector('[data-testid="letter-bucket"]') as HTMLElement; expect(section.getAttribute('style') ?? '').toContain('var(--c-tag-sienna)'); }); }); describe('LetterBucket — leftover drawer (#827 redesign)', () => { const fb = (n: number): Bucket => ({ key: '__fallback__', kind: 'fallback', color: null, letters: Array.from({ length: n }, (_, i) => makeEntry({ documentId: `f${i}`, eventDate: `1916-01-0${(i % 9) + 1}` }) ) }); it('renders collapsed — count + reveal, no letter cards — until opened', () => { render(LetterBucket, { bucket: fb(20), mode: 'event', year: 1916 }); expect(document.querySelector('a.lcard')).toBeNull(); expect(document.body.textContent).toContain(m.timeline_bucket_other_letters()); expect(document.querySelector('[data-testid="bucket-reveal"]')).not.toBeNull(); }); it('reveals the first 5 letters when opened', async () => { render(LetterBucket, { bucket: fb(20), mode: 'event', year: 1916 }); (document.querySelector('[data-testid="bucket-reveal"]') as HTMLButtonElement).click(); await tick(); expect(document.querySelectorAll('a.lcard')).toHaveLength(5); expect(document.querySelector('[data-testid="bucket-show-more"]')).not.toBeNull(); }); }); describe('LetterBucket — card chrome (#827 redesign)', () => { it('renders the cluster as a contained card (bordered, rounded, surface)', () => { const bucket: Bucket = { key: 'tag:t1', kind: 'tag', title: 'Krieg', color: 'sienna', letters: [makeEntry({ documentId: 'a' })] }; render(LetterBucket, { bucket, mode: 'thema', year: 1916 }); const card = document.querySelector('[data-testid="letter-bucket"]') as HTMLElement; expect(card.className).toMatch(/\brounded\b|rounded-/); expect(card.className).toContain('border'); expect(card.className).toContain('bg-surface'); }); }); describe('LetterBucket — event-as-header (#827 redesign)', () => { it('renders the curated event as the card header when given an `event` (no separate pill)', () => { const event = makeEntry({ kind: 'EVENT', type: 'PERSONAL', derived: false, eventId: 'e1', title: 'Ein gewaltiger Stadtbrand', eventDate: '1916-07-06', senderName: '', receiverName: '', documentId: undefined }); const bucket: Bucket = { key: 'event:e1', kind: 'event', title: 'Ein gewaltiger Stadtbrand', color: null, letters: [makeEntry({ documentId: 'a', linkedEventId: 'e1' })] }; render(LetterBucket, { bucket, mode: 'event', year: 1916, event, canWrite: true }); const header = document.querySelector('[data-testid="bucket-event-header"]') as HTMLElement; expect(header.textContent).toContain('Ein gewaltiger Stadtbrand'); expect(header.textContent).toContain(m.timeline_provenance_curated()); expect(document.querySelector('[data-testid="event-edit"]')?.getAttribute('href')).toBe( '/zeitstrahl/events/e1/edit' ); }); it('shows no edit affordance in the header when canWrite is false', () => { const event = makeEntry({ kind: 'EVENT', type: 'PERSONAL', derived: false, eventId: 'e1', title: 'X', senderName: '', receiverName: '', documentId: undefined }); const bucket: Bucket = { key: 'event:e1', kind: 'event', title: 'X', color: null, letters: [makeEntry({ documentId: 'a' })] }; render(LetterBucket, { bucket, mode: 'event', year: 1916, event, canWrite: false }); expect(document.querySelector('[data-testid="event-edit"]')).toBeNull(); }); });