feat(journey-reader): match spec LR-2 — card layout, interlude, badge, actions; inline note in editor row
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m3s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 4m11s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m3s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 4m11s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
JourneyItemCard: restructure from full-<a> to div+card with meta line (date · von X an Y) and explicit "Brief öffnen →" link; note renders as mint-border annotation inside the card. JourneyInterlude: remove ❦ ornament; orange-400 left-border spec classes. JourneyReader: fix intro classes (dashed border-b); remove bottom author actions (moved to +page.svelte metabar). +page.svelte geschichten/[id]: badge above title with spec orange-50 classes; Bearbeiten/Löschen in metabar right side for isJourney + canBlogWrite. JourneyItemRow: items-center on main row; drag handle self-center; note textarea inline in content column (removes border-t section below). i18n: add journey_item_open, journey_item_meta_from_to to de/en/es. Tests: update JourneyItemCard + JourneyReader specs to match new structure; fix datePrecision 'FULL'→'DAY', add receiverCount: 0 to all test fixtures. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page, userEvent } from 'vitest/browser';
|
||||
import { page } from 'vitest/browser';
|
||||
import { createConfirmService, CONFIRM_KEY } from '$lib/shared/services/confirm.svelte.js';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
@@ -33,7 +33,13 @@ const baseGeschichte = (overrides: Partial<GeschichteView> = {}): GeschichteView
|
||||
const docItem = (id: string, title: string, position: number, note?: string): JourneyItemView => ({
|
||||
id,
|
||||
position,
|
||||
document: { id: `d${id}`, title, datePrecision: 'FULL', documentDate: '1923-05-15' },
|
||||
document: {
|
||||
id: `d${id}`,
|
||||
title,
|
||||
datePrecision: 'DAY',
|
||||
documentDate: '1923-05-15',
|
||||
receiverCount: 0
|
||||
},
|
||||
note
|
||||
});
|
||||
|
||||
@@ -50,10 +56,7 @@ describe('JourneyReader', () => {
|
||||
it('renders intro paragraph when body is non-empty', async () => {
|
||||
render(JourneyReader, {
|
||||
context: ctx(),
|
||||
props: {
|
||||
geschichte: baseGeschichte({ body: 'Eine Reise durch die Geschichte.' }),
|
||||
canBlogWrite: false
|
||||
}
|
||||
props: { geschichte: baseGeschichte({ body: 'Eine Reise durch die Geschichte.' }) }
|
||||
});
|
||||
|
||||
await expect.element(page.getByText('Eine Reise durch die Geschichte.')).toBeVisible();
|
||||
@@ -62,7 +65,7 @@ describe('JourneyReader', () => {
|
||||
it('omits intro paragraph when body is null', async () => {
|
||||
render(JourneyReader, {
|
||||
context: ctx(),
|
||||
props: { geschichte: baseGeschichte({ body: undefined }), canBlogWrite: false }
|
||||
props: { geschichte: baseGeschichte({ body: undefined }) }
|
||||
});
|
||||
|
||||
// Only empty state should render
|
||||
@@ -72,7 +75,7 @@ describe('JourneyReader', () => {
|
||||
it('omits intro paragraph when body is only whitespace', async () => {
|
||||
render(JourneyReader, {
|
||||
context: ctx(),
|
||||
props: { geschichte: baseGeschichte({ body: ' ' }), canBlogWrite: false }
|
||||
props: { geschichte: baseGeschichte({ body: ' ' }) }
|
||||
});
|
||||
|
||||
// Whitespace-only body must NOT produce a visible intro paragraph.
|
||||
@@ -85,7 +88,7 @@ describe('JourneyReader', () => {
|
||||
it('renders empty-state message when items array is empty', async () => {
|
||||
render(JourneyReader, {
|
||||
context: ctx(),
|
||||
props: { geschichte: baseGeschichte({ items: [] }), canBlogWrite: false }
|
||||
props: { geschichte: baseGeschichte({ items: [] }) }
|
||||
});
|
||||
|
||||
await expect.element(page.getByText('Diese Lesereise ist noch leer.')).toBeVisible();
|
||||
@@ -95,11 +98,7 @@ describe('JourneyReader', () => {
|
||||
render(JourneyReader, {
|
||||
context: ctx(),
|
||||
props: {
|
||||
geschichte: baseGeschichte({
|
||||
body: 'Eine Einleitung.',
|
||||
items: []
|
||||
}),
|
||||
canBlogWrite: false
|
||||
geschichte: baseGeschichte({ body: 'Eine Einleitung.', items: [] })
|
||||
}
|
||||
});
|
||||
|
||||
@@ -111,8 +110,7 @@ describe('JourneyReader', () => {
|
||||
render(JourneyReader, {
|
||||
context: ctx(),
|
||||
props: {
|
||||
geschichte: baseGeschichte({ items: [docItem('item1', 'Brief an Helene', 0)] }),
|
||||
canBlogWrite: false
|
||||
geschichte: baseGeschichte({ items: [docItem('item1', 'Brief an Helene', 0)] })
|
||||
}
|
||||
});
|
||||
|
||||
@@ -123,13 +121,11 @@ describe('JourneyReader', () => {
|
||||
render(JourneyReader, {
|
||||
context: ctx(),
|
||||
props: {
|
||||
geschichte: baseGeschichte({ items: [interludeItem('inter1', 'Eine Pause.', 0)] }),
|
||||
canBlogWrite: false
|
||||
geschichte: baseGeschichte({ items: [interludeItem('inter1', 'Eine Pause.', 0)] })
|
||||
}
|
||||
});
|
||||
|
||||
await expect.element(page.getByText('Eine Pause.')).toBeVisible();
|
||||
expect(document.body.textContent).toContain('❦');
|
||||
});
|
||||
|
||||
it('omits items where document is null AND note is blank (dangling-item rule)', async () => {
|
||||
@@ -141,8 +137,7 @@ describe('JourneyReader', () => {
|
||||
{ id: 'dangling', position: 0, document: undefined, note: ' ' },
|
||||
docItem('item2', 'Echter Brief', 1)
|
||||
]
|
||||
}),
|
||||
canBlogWrite: false
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -151,22 +146,6 @@ describe('JourneyReader', () => {
|
||||
await expect.element(page.getByText('Diese Lesereise ist noch leer.')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking delete button calls ondelete prop', async () => {
|
||||
const ondelete = vi.fn().mockResolvedValue(undefined);
|
||||
render(JourneyReader, {
|
||||
context: ctx(),
|
||||
props: {
|
||||
geschichte: baseGeschichte({ items: [docItem('i1', 'Brief', 0)] }),
|
||||
canBlogWrite: true,
|
||||
ondelete
|
||||
}
|
||||
});
|
||||
|
||||
await userEvent.click(page.getByRole('button', { name: /löschen/i }));
|
||||
|
||||
expect(ondelete).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('XSS: Journey body is rendered as plaintext — injected payload does not execute', async () => {
|
||||
// JourneyReader uses Svelte text interpolation, NOT {@html}.
|
||||
render(JourneyReader, {
|
||||
@@ -174,8 +153,7 @@ describe('JourneyReader', () => {
|
||||
props: {
|
||||
geschichte: baseGeschichte({
|
||||
body: '<img src=x onerror="window.__xss_journey=1">'
|
||||
}),
|
||||
canBlogWrite: false
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user