+ {#each sorted as block (block.id)}
+ onParagraphClick(block.annotationId)}
+ role="button"
+ tabindex="0"
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') onParagraphClick(block.annotationId); }}
+ >
+ {#each splitByMarkers(block.text) as segment, i (i)}
+ {#if segment.type === 'marker'}
+ {segment.text}
+ {:else}
+ {segment.text}
+ {/if}
+ {/each}
+
+ {/each}
+
+
+
diff --git a/frontend/src/lib/components/TranscriptionReadView.svelte.test.ts b/frontend/src/lib/components/TranscriptionReadView.svelte.test.ts
new file mode 100644
index 00000000..7c9b3d76
--- /dev/null
+++ b/frontend/src/lib/components/TranscriptionReadView.svelte.test.ts
@@ -0,0 +1,120 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import TranscriptionReadView from './TranscriptionReadView.svelte';
+import type { TranscriptionBlockData } from '$lib/types';
+
+const blocks: TranscriptionBlockData[] = [
+ {
+ id: 'b1',
+ annotationId: 'ann-1',
+ documentId: 'doc-1',
+ text: 'First paragraph text.',
+ label: null,
+ sortOrder: 1,
+ version: 1
+ },
+ {
+ id: 'b2',
+ annotationId: 'ann-2',
+ documentId: 'doc-1',
+ text: 'Second paragraph text.',
+ label: null,
+ sortOrder: 2,
+ version: 1
+ }
+];
+
+describe('TranscriptionReadView', () => {
+ it('should render one paragraph per block', async () => {
+ render(TranscriptionReadView, {
+ blocks,
+ onParagraphClick: () => {}
+ });
+
+ await expect.element(page.getByText('First paragraph text.')).toBeInTheDocument();
+ await expect.element(page.getByText('Second paragraph text.')).toBeInTheDocument();
+
+ const paragraphs = document.querySelectorAll('[data-block-id]');
+ expect(paragraphs.length).toBe(2);
+ });
+
+ it('should render [unleserlich] as italic muted text', async () => {
+ render(TranscriptionReadView, {
+ blocks: [
+ {
+ id: 'b1',
+ annotationId: 'ann-1',
+ documentId: 'doc-1',
+ text: 'Text before [unleserlich] text after',
+ label: null,
+ sortOrder: 1,
+ version: 1
+ }
+ ],
+ onParagraphClick: () => {}
+ });
+
+ const marker = document.querySelector('[data-marker]');
+ expect(marker).not.toBeNull();
+ expect(marker!.textContent).toBe('[unleserlich]');
+ expect(marker!.tagName.toLowerCase()).toBe('em');
+ });
+
+ it('should render [...] as italic muted text', async () => {
+ render(TranscriptionReadView, {
+ blocks: [
+ {
+ id: 'b1',
+ annotationId: 'ann-1',
+ documentId: 'doc-1',
+ text: 'Some [...] text',
+ label: null,
+ sortOrder: 1,
+ version: 1
+ }
+ ],
+ onParagraphClick: () => {}
+ });
+
+ const marker = document.querySelector('[data-marker]');
+ expect(marker).not.toBeNull();
+ expect(marker!.textContent).toBe('[...]');
+ });
+
+ it('should call onParagraphClick with annotationId when paragraph is clicked', async () => {
+ const onParagraphClick = vi.fn();
+ render(TranscriptionReadView, {
+ blocks,
+ onParagraphClick
+ });
+
+ const paragraph = document.querySelector('[data-block-id="b1"]')!;
+ paragraph.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+ expect(onParagraphClick).toHaveBeenCalledWith('ann-1');
+ });
+
+ it('should render blocks sorted by sortOrder', async () => {
+ render(TranscriptionReadView, {
+ blocks: [
+ { ...blocks[1], sortOrder: 1 },
+ { ...blocks[0], sortOrder: 2 }
+ ],
+ onParagraphClick: () => {}
+ });
+
+ const paragraphs = document.querySelectorAll('[data-block-id]');
+ expect(paragraphs[0].getAttribute('data-block-id')).toBe('b2');
+ expect(paragraphs[1].getAttribute('data-block-id')).toBe('b1');
+ });
+
+ it('should render empty state when no blocks', async () => {
+ render(TranscriptionReadView, {
+ blocks: [],
+ onParagraphClick: () => {}
+ });
+
+ const paragraphs = document.querySelectorAll('[data-block-id]');
+ expect(paragraphs.length).toBe(0);
+ });
+});