Grouped-mode buckets stack many letters; the full two-line card with its own date chip floods the view. The compact variant tightens the padding and, when the letter has a title, drops the redundant date chip (the per-year bucket already frames the time and these archive titles embed the date). Datum mode is untouched — compact defaults to false. Refs #827
173 lines
7.7 KiB
TypeScript
173 lines
7.7 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 — grouping variants (#827, REQ-014/017)', () => {
|
||
it('carries the .lcard.ev class in the event variant (REQ-014)', () => {
|
||
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-014)', () => {
|
||
render(LetterCard, { entry: makeEntry() });
|
||
expect(document.querySelector('a.ev')).toBeNull();
|
||
});
|
||
|
||
it('suppresses the per-letter tag chip when asked, even with a root tag (REQ-017)', () => {
|
||
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 — Datum/Ereignis (REQ-017)', () => {
|
||
render(LetterCard, { entry: makeEntry({ rootTagName: 'Krieg', rootTagColor: 'sienna' }) });
|
||
expect(document.querySelector('[data-testid="tag-chip"]')).not.toBeNull();
|
||
});
|
||
|
||
it('drops the redundant date line in the compact variant when a title is present (#827)', () => {
|
||
// Inside a per-year bucket the year already frames the time, and these archive
|
||
// titles embed the date — so the compact in-bucket card omits the date chip.
|
||
render(LetterCard, { entry: makeEntry({ title: 'H-0023 – 6. Juli 1916' }), compact: true });
|
||
expect(document.querySelector('[data-testid="letter-date"]')).toBeNull();
|
||
expect(document.body.textContent).toContain('Karl Raddatz'); // sender still shown
|
||
});
|
||
|
||
it('keeps the date in the compact variant when the letter has no title (#827)', () => {
|
||
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 (#827)', () => {
|
||
render(LetterCard, { entry: makeEntry(), compact: true });
|
||
expect(document.querySelector('a.lcard.compact')).not.toBeNull();
|
||
});
|
||
});
|