diff --git a/frontend/src/lib/components/GeschichtenCard.svelte.spec.ts b/frontend/src/lib/components/GeschichtenCard.svelte.spec.ts
new file mode 100644
index 00000000..0deeb17c
--- /dev/null
+++ b/frontend/src/lib/components/GeschichtenCard.svelte.spec.ts
@@ -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 = '
Body
') => ({
+ 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 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',
+ 'Plain bold story
'
+ )
+ ],
+ personId: 'p1',
+ personName: 'Franz',
+ canWrite: false
+ });
+ // Body excerpt appears once as plain text — no rendered, no script
+ await expect.element(page.getByText(/Plain bold story/)).toBeInTheDocument();
+ expect(document.body.innerHTML).not.toContain('