`showDate = !compact || !entry.title` dropped the date chip for ANY titled compact letter. But titles are free-form OCR/import text — a letter titled "Brief an Mutter" lost its month/day entirely, and inside an event card the band frames only the year. The chip now drops only when the formatted date actually appears in the title (e.g. "H-0023 – 6. Juli 1916"), so the row-height win holds where valid and no information is lost otherwise. The spec that asserted the date vanishes for any title is rewritten to the correct contract, plus an inverse test. Fixes review finding #4. Refs #850 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
185 lines
8.3 KiB
TypeScript
185 lines
8.3 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 LetterCard from './LetterCard.svelte';
|
||
import YearLetterStrip from './YearLetterStrip.svelte';
|
||
import { timelineDateLabel } from './dateLabel';
|
||
import { makeEntry } from './test-factories';
|
||
|
||
afterEach(() => cleanup());
|
||
|
||
const DOC_ID = '22222222-2222-2222-2222-222222222222';
|
||
|
||
describe('LetterCard', () => {
|
||
it('renders sender, receiver, and title', () => {
|
||
render(LetterCard, {
|
||
entry: makeEntry({ senderName: 'Karl', receiverName: 'Elfriede', title: 'Feldpost' })
|
||
});
|
||
expect(document.body.textContent).toContain('Karl');
|
||
expect(document.body.textContent).toContain('Elfriede');
|
||
expect(document.body.textContent).toContain('Feldpost');
|
||
});
|
||
|
||
it('renders the precision date exactly as timelineDateLabel returns (REQ-013)', () => {
|
||
const entry = makeEntry({ eventDate: '1915-06-15', precision: 'MONTH' });
|
||
const expected = timelineDateLabel(entry.eventDate, entry.precision, entry.eventDateEnd);
|
||
expect(expected).toBeTruthy();
|
||
render(LetterCard, { entry });
|
||
expect(document.body.textContent).toContain(expected as string);
|
||
});
|
||
|
||
it('renders no date chip when timelineDateLabel returns null (REQ-013)', () => {
|
||
const entry = makeEntry({ precision: 'UNKNOWN', eventDate: undefined });
|
||
render(LetterCard, { entry });
|
||
const chip = document.querySelector('[data-testid="letter-date"]');
|
||
expect(chip).toBeNull();
|
||
});
|
||
|
||
it('shows "Unbekannt" for an empty sender, never a bare arrow (REQ-014)', () => {
|
||
render(LetterCard, { entry: makeEntry({ senderName: '', receiverName: 'Elfriede' }) });
|
||
expect(document.body.textContent).toContain('Unbekannt');
|
||
});
|
||
|
||
it('shows "Unbekannt" for an empty receiver (REQ-014)', () => {
|
||
render(LetterCard, { entry: makeEntry({ senderName: 'Karl', receiverName: '' }) });
|
||
expect(document.body.textContent).toContain('Unbekannt');
|
||
});
|
||
|
||
it('links to exactly /documents/{documentId} with no target (REQ-023)', () => {
|
||
render(LetterCard, { entry: makeEntry({ documentId: DOC_ID }) });
|
||
const link = document.querySelector('a') as HTMLAnchorElement;
|
||
expect(link.getAttribute('href')).toBe(`/documents/${DOC_ID}`);
|
||
expect(link.hasAttribute('target')).toBe(false);
|
||
});
|
||
|
||
it('has a touch target of at least 44px (REQ-020)', () => {
|
||
render(LetterCard, { entry: makeEntry() });
|
||
const link = document.querySelector('a') as HTMLAnchorElement;
|
||
expect(link.getBoundingClientRect().height).toBeGreaterThanOrEqual(44);
|
||
});
|
||
|
||
it('prefixes a present title with an aria-hidden ✉ and an sr-only "Brief" label (REQ-008)', () => {
|
||
render(LetterCard, { entry: makeEntry({ title: 'Brief aus Stettin', documentId: DOC_ID }) });
|
||
const hidden = document.querySelector('[aria-hidden="true"]');
|
||
expect(hidden?.textContent).toContain('✉');
|
||
const srOnly = document.querySelector('.sr-only');
|
||
expect(srOnly?.textContent).toBe(m.timeline_letter_glyph_label());
|
||
// The glyph is decorative chrome — the document link is unchanged.
|
||
const link = document.querySelector('a') as HTMLAnchorElement;
|
||
expect(link.getAttribute('href')).toBe(`/documents/${DOC_ID}`);
|
||
});
|
||
|
||
it('renders no ✉ glyph and no "Brief" label when the title is empty (REQ-016)', () => {
|
||
render(LetterCard, {
|
||
entry: makeEntry({ title: '', senderName: 'Karl', receiverName: 'Elfriede' })
|
||
});
|
||
expect(document.body.textContent).not.toContain('✉');
|
||
expect(document.querySelector('.sr-only')).toBeNull();
|
||
// The row still shows sender → receiver and the date.
|
||
expect(document.body.textContent).toContain('Karl');
|
||
expect(document.body.textContent).toContain('Elfriede');
|
||
expect(document.querySelector('[data-testid="letter-date"]')).not.toBeNull();
|
||
});
|
||
|
||
it('renders an HTML-bearing title verbatim as text, never as markup (security, REQ-021)', () => {
|
||
const evil = '<script>alert(1)</script>';
|
||
render(LetterCard, { entry: makeEntry({ title: evil }) });
|
||
expect(document.body.textContent).toContain(evil);
|
||
expect(document.querySelector('a script')).toBeNull();
|
||
});
|
||
|
||
it('renders one root-tag chip beneath the meta line when rootTagName is present (REQ-008)', () => {
|
||
render(LetterCard, { entry: makeEntry({ rootTagName: 'Familie', rootTagColor: 'sage' }) });
|
||
const chips = document.querySelectorAll('[data-testid="tag-chip"]');
|
||
expect(chips).toHaveLength(1);
|
||
expect(chips[0].textContent).toContain('Familie');
|
||
});
|
||
|
||
it('renders no chip when the letter has no root tag (REQ-005/006)', () => {
|
||
render(LetterCard, { entry: makeEntry({ rootTagName: undefined, rootTagColor: undefined }) });
|
||
expect(document.querySelector('[data-testid="tag-chip"]')).toBeNull();
|
||
});
|
||
|
||
it('keeps a long tag name from overflowing the card at 320px, full name in the title (REQ-008a)', () => {
|
||
document.body.style.width = '320px';
|
||
render(LetterCard, {
|
||
entry: makeEntry({
|
||
rootTagName: 'Briefe von der Front und aus der Heimat',
|
||
rootTagColor: 'sienna'
|
||
})
|
||
});
|
||
const link = document.querySelector('a') as HTMLAnchorElement;
|
||
expect(link.scrollWidth).toBeLessThanOrEqual(link.clientWidth);
|
||
const chip = document.querySelector('[data-testid="tag-chip"]') as HTMLElement;
|
||
expect(chip.getAttribute('title')).toBe('Briefe von der Front und aus der Heimat');
|
||
document.body.style.width = '';
|
||
});
|
||
|
||
it('renders the chip inside an expanded YearLetterStrip too (REQ-012)', async () => {
|
||
render(YearLetterStrip, {
|
||
letters: [makeEntry({ rootTagName: 'Familie', rootTagColor: 'sage', documentId: 'doc-1' })],
|
||
year: 1909
|
||
});
|
||
(document.querySelector('[data-testid="strip-expand"]') as HTMLButtonElement).click();
|
||
await tick();
|
||
const chip = document.querySelector('[data-testid="tag-chip"]');
|
||
expect(chip?.textContent).toContain('Familie');
|
||
});
|
||
});
|
||
|
||
describe('LetterCard — event-cluster variants (#850, REQ-002)', () => {
|
||
it('carries the .lcard.ev class in the event variant (REQ-002)', () => {
|
||
render(LetterCard, { entry: makeEntry(), variant: 'event' });
|
||
expect(document.querySelector('a.lcard.ev')).not.toBeNull();
|
||
});
|
||
|
||
it('is a plain card with no .ev marker by default (REQ-006)', () => {
|
||
render(LetterCard, { entry: makeEntry() });
|
||
expect(document.querySelector('a.ev')).toBeNull();
|
||
});
|
||
|
||
it('suppresses the per-letter tag chip when asked, even with a root tag', () => {
|
||
render(LetterCard, {
|
||
entry: makeEntry({ rootTagName: 'Krieg', rootTagColor: 'sienna' }),
|
||
suppressTagChip: true
|
||
});
|
||
expect(document.querySelector('[data-testid="tag-chip"]')).toBeNull();
|
||
});
|
||
|
||
it('still shows the per-letter tag chip when not suppressed', () => {
|
||
render(LetterCard, { entry: makeEntry({ rootTagName: 'Krieg', rootTagColor: 'sienna' }) });
|
||
expect(document.querySelector('[data-testid="tag-chip"]')).not.toBeNull();
|
||
});
|
||
|
||
it('drops the compact date chip only when the title actually embeds the formatted date (#850)', () => {
|
||
// An archive title like "H-0023 – 6. Juli 1916" already carries the date, so inside an
|
||
// event card (where the band frames the time) the redundant chip is dropped.
|
||
const entry = makeEntry({ eventDate: '1916-07-06', precision: 'DAY' });
|
||
const dateLabel = timelineDateLabel(entry.eventDate, entry.precision, entry.eventDateEnd);
|
||
render(LetterCard, { entry: { ...entry, title: `H-0023 – ${dateLabel}` }, compact: true });
|
||
expect(document.querySelector('[data-testid="letter-date"]')).toBeNull();
|
||
expect(document.body.textContent).toContain('Karl Raddatz'); // sender still shown
|
||
});
|
||
|
||
it('keeps the compact date chip when the title does NOT embed the date (#850, finding #4)', () => {
|
||
// Titles are free-form OCR text — a titled letter whose title carries no date must keep
|
||
// its month/day, since inside an event card the band frames only the year.
|
||
render(LetterCard, {
|
||
entry: makeEntry({ eventDate: '1916-07-06', precision: 'DAY', title: 'Brief an Mutter' }),
|
||
compact: true
|
||
});
|
||
expect(document.querySelector('[data-testid="letter-date"]')).not.toBeNull();
|
||
});
|
||
|
||
it('keeps the date in the compact variant when the letter has no title (#850)', () => {
|
||
render(LetterCard, { entry: makeEntry({ title: undefined }), compact: true });
|
||
expect(document.querySelector('[data-testid="letter-date"]')).not.toBeNull();
|
||
});
|
||
|
||
it('renders the compact variant on a single tighter row (#850)', () => {
|
||
render(LetterCard, { entry: makeEntry(), compact: true });
|
||
expect(document.querySelector('a.lcard.compact')).not.toBeNull();
|
||
});
|
||
});
|