test(geschichten): cover GeschichtenCard render, threshold, write-action gate

Browser-based component spec asserting:
- empty geschichten → no <section> rendered
- >= 1 story → heading + story link visible
- canWrite=false → no "+ Geschichte schreiben" link
- canWrite=true → link with /geschichten/new?personId pre-fill
- 0–2 stories → no footer link
- 3+ stories → "Alle Geschichten zu {name}" footer link to /geschichten?personId
- excerpt is plain text (no <strong>, no <script>)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-02 18:49:26 +02:00
parent da249369ee
commit 11c0d49907

View File

@@ -0,0 +1,120 @@
import { afterEach, describe, expect, it } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import GeschichtenCard from './GeschichtenCard.svelte';
const makeStory = (id: string, title: string, body: string | null = '<p>Body</p>') => ({
id,
title,
body,
status: 'PUBLISHED' as const,
publishedAt: '2024-04-01T12:00:00',
createdAt: '2024-03-01T12:00:00',
updatedAt: '2024-04-01T12:00:00',
persons: [],
documents: [],
author: {
id: 'u1',
email: 'marcel@example.com',
firstName: 'Marcel',
lastName: 'Raddatz',
enabled: true,
notifyOnReply: false,
notifyOnMention: false,
groups: [],
createdAt: '2024-01-01T00:00:00',
color: '#000'
}
});
afterEach(() => cleanup());
describe('GeschichtenCard', () => {
it('renders nothing when geschichten is empty', async () => {
render(GeschichtenCard, {
geschichten: [],
personId: 'p1',
personName: 'Franz',
canWrite: true
});
// No heading, no list — the entire <section> should not exist
expect(
document.querySelector('section[aria-labelledby="geschichten-card-heading"]')
).toBeNull();
});
it('renders the section heading and stories when geschichten is non-empty', async () => {
render(GeschichtenCard, {
geschichten: [makeStory('g1', 'Erinnerung an Franz')],
personId: 'p1',
personName: 'Franz',
canWrite: false
});
await expect.element(page.getByText('Geschichten')).toBeInTheDocument();
await expect
.element(page.getByRole('link', { name: 'Erinnerung an Franz' }))
.toBeInTheDocument();
});
it('hides the "+ Geschichte schreiben" link when canWrite is false', async () => {
render(GeschichtenCard, {
geschichten: [makeStory('g1', 'A story')],
personId: 'p1',
personName: 'Franz',
canWrite: false
});
const writeLinks = await page.getByText(/Geschichte schreiben/).all();
expect(writeLinks).toHaveLength(0);
});
it('shows the write-action link only when canWrite is true', async () => {
render(GeschichtenCard, {
geschichten: [makeStory('g1', 'A story')],
personId: 'p1',
personName: 'Franz',
canWrite: true
});
const link = await page.getByRole('link', { name: /Geschichte schreiben/ }).element();
expect(link.getAttribute('href')).toBe('/geschichten/new?personId=p1');
});
it('hides the "Alle Geschichten zu …" footer link below the 3-story threshold', async () => {
render(GeschichtenCard, {
geschichten: [makeStory('g1', 'A'), makeStory('g2', 'B')],
personId: 'p1',
personName: 'Franz',
canWrite: false
});
const overflow = await page.getByText(/Alle Geschichten zu/).all();
expect(overflow).toHaveLength(0);
});
it('shows the footer link at the 3-story threshold (>= 3)', async () => {
render(GeschichtenCard, {
geschichten: [makeStory('g1', 'A'), makeStory('g2', 'B'), makeStory('g3', 'C')],
personId: 'p1',
personName: 'Franz',
canWrite: false
});
const link = await page.getByRole('link', { name: /Alle Geschichten zu Franz/ }).element();
expect(link.getAttribute('href')).toBe('/geschichten?personId=p1');
});
it('renders a plain-text excerpt without HTML markup', async () => {
render(GeschichtenCard, {
geschichten: [
makeStory(
'g1',
'Mit HTML',
'<p>Plain <strong>bold</strong> story</p><script>alert(1)</script>'
)
],
personId: 'p1',
personName: 'Franz',
canWrite: false
});
// Body excerpt appears once as plain text — no <strong> rendered, no script
await expect.element(page.getByText(/Plain bold story/)).toBeInTheDocument();
expect(document.body.innerHTML).not.toContain('<script>');
});
});