refactor: move geschichte domain to lib/geschichte/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
140
frontend/src/lib/geschichte/GeschichtenCard.svelte.spec.ts
Normal file
140
frontend/src/lib/geschichte/GeschichtenCard.svelte.spec.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
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();
|
||||
// The whole row is one link to the story; matching on the title text via
|
||||
// a partial regex tolerates trailing author/date metadata in the
|
||||
// accessible name.
|
||||
const link = await page
|
||||
.getByRole('link', { name: /Erinnerung an Franz/ })
|
||||
.first()
|
||||
.element();
|
||||
expect(link.getAttribute('href')).toBe('/geschichten/g1');
|
||||
});
|
||||
|
||||
it('makes the entire story row a single clickable link', async () => {
|
||||
render(GeschichtenCard, {
|
||||
geschichten: [makeStory('g1', 'A title', '<p>Some body excerpt text</p>')],
|
||||
personId: 'p1',
|
||||
personName: 'Franz',
|
||||
canWrite: false
|
||||
});
|
||||
// The body-excerpt text is inside the same <a> as the title.
|
||||
const links = await page.getByRole('link', { name: /A title/ }).all();
|
||||
expect(links.length).toBeGreaterThan(0);
|
||||
const linkEl = await links[0].element();
|
||||
expect(linkEl.tagName).toBe('A');
|
||||
expect(linkEl.textContent).toContain('Some body excerpt text');
|
||||
});
|
||||
|
||||
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>');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user