The grouped view flooded: buckets had no visual containment (a tiny floating pill over cards identical to the ungrouped view) and the >12-letter density collapse was gone, so "Weitere Briefe · 325" / "Sonstiges · 10" dumped every card. LetterBucket now binds each cluster with a coloured left rail (tag colour in Thema, mint for an Ereignis cluster, neutral for the fallback), renders compact cards, and — above BUCKET_DENSE_THRESHOLD (6) — collapses to the existing month-density YearLetterStrip instead of a flood. Adds a `nested` mode (no header) for letters that sit under their event pill, and shares the tag-colour token allow-list via tagColorVar. Refs #827
137 lines
4.9 KiB
TypeScript
137 lines
4.9 KiB
TypeScript
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());
|
|
});
|
|
});
|
|
|
|
const manyLetters = (n: number) =>
|
|
Array.from({ length: n }, (_, i) =>
|
|
makeEntry({ documentId: `d${i}`, eventDate: `1916-0${(i % 9) + 1}-01` })
|
|
);
|
|
|
|
describe('LetterBucket — density + containment (#827)', () => {
|
|
it('collapses an oversized bucket to the density strip instead of flooding cards', () => {
|
|
const bucket: Bucket = {
|
|
key: 'tag:t1',
|
|
kind: 'tag',
|
|
title: 'Sonstiges',
|
|
color: null,
|
|
letters: manyLetters(10)
|
|
};
|
|
render(LetterBucket, { bucket, mode: 'thema', year: 1916 });
|
|
expect(document.querySelector('[data-testid="strip-expand"]')).not.toBeNull();
|
|
// not ten individual cards dumped into the timeline
|
|
expect(document.querySelectorAll('a.lcard')).toHaveLength(0);
|
|
});
|
|
|
|
it('renders compact cards for a small bucket (no strip)', () => {
|
|
const bucket: Bucket = {
|
|
key: 'tag:t1',
|
|
kind: 'tag',
|
|
title: 'Tod',
|
|
color: null,
|
|
letters: manyLetters(2)
|
|
};
|
|
render(LetterBucket, { bucket, mode: 'thema', year: 1916 });
|
|
expect(document.querySelector('[data-testid="strip-expand"]')).toBeNull();
|
|
expect(document.querySelectorAll('a.lcard.compact')).toHaveLength(2);
|
|
});
|
|
|
|
it('omits the header when nested — the event pill above is the header', () => {
|
|
const bucket: Bucket = {
|
|
key: 'event:e1',
|
|
kind: 'event',
|
|
title: 'Ein gewaltiger Stadtbrand',
|
|
color: null,
|
|
letters: manyLetters(1)
|
|
};
|
|
render(LetterBucket, { bucket, mode: 'event', nested: true, year: 1916 });
|
|
expect(document.querySelector('[data-testid="bucket-count"]')).toBeNull();
|
|
expect(document.body.textContent).not.toContain('Ein gewaltiger Stadtbrand');
|
|
// still the event-letter variant, just headerless under its pill
|
|
expect(document.querySelector('a.lcard.ev')).not.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)');
|
|
});
|
|
});
|