diff --git a/frontend/src/lib/timeline/LetterBucket.svelte b/frontend/src/lib/timeline/LetterBucket.svelte new file mode 100644 index 00000000..3b0f986c --- /dev/null +++ b/frontend/src/lib/timeline/LetterBucket.svelte @@ -0,0 +1,49 @@ + + +
+
+ {#if mode === 'thema' && bucket.kind === 'tag'} + + {:else if mode === 'event' && bucket.kind === 'event'} + + + {bucket.title} + + {:else} + {fallbackLabel} + {/if} + · {count} +
+ +
diff --git a/frontend/src/lib/timeline/LetterBucket.svelte.spec.ts b/frontend/src/lib/timeline/LetterBucket.svelte.spec.ts new file mode 100644 index 00000000..c024c9f5 --- /dev/null +++ b/frontend/src/lib/timeline/LetterBucket.svelte.spec.ts @@ -0,0 +1,74 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-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()); + }); +});