Files
familienarchiv/frontend/src/lib/timeline/event-note.svelte.spec.ts
Marcel ace9602f6e feat(timeline): add EventNote component with expand/collapse (REQ-002–008)
Handles XSS escaping, whitespace-pre-line, 3-line clamp via inline style,
and a toggle button that is only shown when content actually overflows.

Refs #844
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 15:04:24 +02:00

75 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 EventNote from './EventNote.svelte';
afterEach(() => cleanup());
const LONG_NOTE = Array.from({ length: 15 }, (_, i) => `Zeile ${i + 1}`).join('\n');
describe('EventNote (REQ-002008)', () => {
it('escapesHtml — renders XSS payload as inert text, no injected element (REQ-002)', () => {
render(EventNote, { description: '<script>alert(1)</script>' });
// The literal string should appear as text content
expect(document.body.textContent).toContain('<script>alert(1)</script>');
// No injected <script> with alert() should exist (Svelte's own scripts don't contain it)
const scripts = Array.from(document.querySelectorAll('script'));
expect(scripts.some((s) => s.textContent?.includes('alert(1)'))).toBe(false);
});
it('preservesLineBreaks — note element carries whitespace-pre-line class (REQ-003)', () => {
render(EventNote, { description: 'A\n\nB' });
const note = document.querySelector('[data-testid="event-note"]') as HTMLElement;
expect(note).not.toBeNull();
expect(note.className).toContain('whitespace-pre-line');
});
it('blankNoteRendersNothing — null description produces no note element (REQ-008)', () => {
render(EventNote, { description: null });
expect(document.querySelector('[data-testid="event-note"]')).toBeNull();
});
it('blankNoteRendersNothing — empty string produces no note element (REQ-008)', () => {
render(EventNote, { description: '' });
expect(document.querySelector('[data-testid="event-note"]')).toBeNull();
});
it('blankNoteRendersNothing — blank-only string produces no note element (REQ-008)', () => {
render(EventNote, { description: ' ' });
expect(document.querySelector('[data-testid="event-note"]')).toBeNull();
});
it('shortNoteNoToggle — a one-line note renders fully with no disclosure control (REQ-006)', async () => {
render(EventNote, { description: 'Kurze Notiz.' });
await tick();
expect(document.body.textContent).toContain('Kurze Notiz.');
expect(document.querySelector('[data-testid="note-toggle"]')).toBeNull();
});
it('clampsAndShowsToggle — long note shows "mehr anzeigen" with aria-expanded=false (REQ-005)', async () => {
render(EventNote, { description: LONG_NOTE });
await tick();
const btn = document.querySelector('[data-testid="note-toggle"]') as HTMLButtonElement;
expect(btn).not.toBeNull();
expect(btn.textContent?.trim()).toBe(m.timeline_note_show_more());
expect(btn.getAttribute('aria-expanded')).toBe('false');
});
it('toggleExpandsCollapses — click expands, re-click collapses (REQ-007)', async () => {
render(EventNote, { description: LONG_NOTE });
await tick();
const btn = document.querySelector('[data-testid="note-toggle"]') as HTMLButtonElement;
btn.click();
await tick();
expect(btn.getAttribute('aria-expanded')).toBe('true');
expect(btn.textContent?.trim()).toBe(m.timeline_note_show_less());
btn.click();
await tick();
expect(btn.getAttribute('aria-expanded')).toBe('false');
expect(btn.textContent?.trim()).toBe(m.timeline_note_show_more());
});
});