A curated event with letters in its own band now becomes the contained card header (glyph, title, date, provenance, edit pencil) instead of a separate floating pill — the title reads once. Derived life-events, world-bands, and letterless event pills are unchanged (REQ-001 amended for curated-with-letters; the identity fixture now links its letter to the curated event so the letterless world band stays a band). Refs #827
233 lines
8.2 KiB
TypeScript
233 lines
8.2 KiB
TypeScript
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();
|
|
});
|
|
});
|