import { describe, it, expect, vi, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; vi.mock('$lib/shared/services/confirm.svelte', () => ({ getConfirmService: () => ({ confirm: async () => false }) })); const { default: TranscriptionEditView } = await import('./TranscriptionEditView.svelte'); import type { TranscriptionBlockData } from '$lib/shared/types'; afterEach(cleanup); const baseBlock = (overrides: Partial = {}): TranscriptionBlockData => ({ id: 'b-1', annotationId: 'ann-1', text: 'Hello', sortOrder: 1, reviewed: false, mentionedPersons: [], label: null, ...overrides }) as TranscriptionBlockData; const baseProps = (overrides: Record = {}) => ({ documentId: 'doc-1', blocks: [] as TranscriptionBlockData[], canComment: false, currentUserId: null, onBlockFocus: () => {}, onSaveBlock: async () => {}, onDeleteBlock: async () => {}, onReviewToggle: async () => {}, ...overrides }); describe('TranscriptionEditView', () => { it('renders the empty-state coach when there are no blocks', async () => { render(TranscriptionEditView, { props: baseProps() }); // TranscribeCoachEmptyState renders some German text expect(document.body.textContent).toMatch(/markier|block|transkrip/i); }); it('renders the review progress counter when there are blocks', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock({ id: 'b1', reviewed: false }), baseBlock({ id: 'b2', reviewed: true })] }) }); expect(document.body.textContent).toMatch(/1\s*\/\s*2/); }); it('shows the "alle als fertig markieren" button when onMarkAllReviewed is provided', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock()], onMarkAllReviewed: async () => {} }) }); await expect.element(page.getByRole('button', { name: /alle als fertig/i })).toBeVisible(); }); it('disables the mark-all-reviewed button when all blocks are reviewed', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock({ reviewed: true })], onMarkAllReviewed: async () => {} }) }); const btn = (await page .getByRole('button', { name: /alle als fertig/i }) .element()) as HTMLButtonElement; expect(btn.disabled).toBe(true); }); it('enables the mark-all-reviewed button when not all blocks are reviewed', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock({ reviewed: false })], onMarkAllReviewed: async () => {} }) }); const btn = (await page .getByRole('button', { name: /alle als fertig/i }) .element()) as HTMLButtonElement; expect(btn.disabled).toBe(false); }); it('hides the mark-all-reviewed button when onMarkAllReviewed is not provided', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock()] }) }); await expect .element(page.getByRole('button', { name: /alle als fertig/i })) .not.toBeInTheDocument(); }); it('renders the OcrTrigger only when canRunOcr is true and onTriggerOcr is provided', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock()], canRunOcr: true, onTriggerOcr: () => {} }) }); // OcrTrigger renders a select with script-type options const select = document.querySelector('select'); expect(select).not.toBeNull(); }); it('hides the OcrTrigger when canRunOcr is false', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock()], canRunOcr: false, onTriggerOcr: () => {} }) }); const select = document.querySelector('select'); expect(select).toBeNull(); }); it('renders the training-label chips when canWrite=true and there are blocks', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock()], canWrite: true, trainingLabels: [], onToggleTrainingLabel: async () => {} }) }); // Training-label section caption expect(document.body.textContent).toMatch(/training/i); }); it('hides the training-label section when canWrite is false', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock()], canWrite: false }) }); expect(document.body.textContent).not.toMatch(/Für Training vormerken/i); }); it('toggles the training label chip when clicked', async () => { const onToggleTrainingLabel = vi.fn().mockResolvedValue(undefined); render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock()], canWrite: true, trainingLabels: [], onToggleTrainingLabel }) }); const chip = Array.from(document.querySelectorAll('button')).find((b) => /kurrent|segmentier/i.test(b.textContent ?? '') ); expect(chip).toBeDefined(); chip?.click(); await vi.waitFor(() => expect(onToggleTrainingLabel).toHaveBeenCalled()); }); it('renders blocks sorted by sortOrder', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [ baseBlock({ id: 'b3', sortOrder: 3, text: 'Third' }), baseBlock({ id: 'b1', sortOrder: 1, text: 'First' }), baseBlock({ id: 'b2', sortOrder: 2, text: 'Second' }) ] }) }); const text = document.body.textContent ?? ''; const idxFirst = text.indexOf('First'); const idxSecond = text.indexOf('Second'); const idxThird = text.indexOf('Third'); expect(idxFirst).toBeLessThan(idxSecond); expect(idxSecond).toBeLessThan(idxThird); }); it('renders both blocks with their text after rerender with a new activeAnnotationId', async () => { const { rerender } = render(TranscriptionEditView, { props: baseProps({ blocks: [ baseBlock({ id: 'b1', annotationId: 'ann-1', sortOrder: 1, text: 'First' }), baseBlock({ id: 'b2', annotationId: 'ann-2', sortOrder: 2, text: 'Second' }) ], activeAnnotationId: null }) }); // re-render with activeAnnotationId set to ann-2 — the activeBlockId $effect re-runs // and both blocks must still be present in the rendered list. await rerender({ ...baseProps({ blocks: [ baseBlock({ id: 'b1', annotationId: 'ann-1', sortOrder: 1, text: 'First' }), baseBlock({ id: 'b2', annotationId: 'ann-2', sortOrder: 2, text: 'Second' }) ], activeAnnotationId: 'ann-2' }) }); await vi.waitFor(() => { expect(document.body.textContent).toContain('First'); expect(document.body.textContent).toContain('Second'); }); }); it('handleMarkAllReviewed calls onMarkAllReviewed when clicked', async () => { const onMarkAllReviewed = vi.fn().mockResolvedValue(undefined); render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock({ reviewed: false })], onMarkAllReviewed }) }); const btn = (await page .getByRole('button', { name: /alle als fertig/i }) .element()) as HTMLButtonElement; btn.click(); await vi.waitFor(() => expect(onMarkAllReviewed).toHaveBeenCalledOnce()); }); it('renders all blocks with their text', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [ baseBlock({ id: 'b1', text: 'Erster Block' }), baseBlock({ id: 'b2', text: 'Zweiter Block' }) ] }) }); expect(document.body.textContent).toContain('Erster Block'); expect(document.body.textContent).toContain('Zweiter Block'); }); it('shows the next-block CTA when there are blocks', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock()] }) }); // CTA shows the number of the next block ("Nächster Block 2") expect(document.body.textContent).toMatch(/2/); }); it('shows the active training label highlighted when included in trainingLabels', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock()], canWrite: true, trainingLabels: ['KURRENT_RECOGNITION'], onToggleTrainingLabel: async () => {} }) }); // The chip for KURRENT_RECOGNITION should have the active class const chips = document.querySelectorAll('button'); const activeChip = Array.from(chips).find( (c) => c.className.includes('border-brand-mint') && c.className.includes('bg-brand-mint') ); expect(activeChip).toBeDefined(); }); it('renders the inactive training-label chip class when not in trainingLabels', async () => { render(TranscriptionEditView, { props: baseProps({ blocks: [baseBlock()], canWrite: true, trainingLabels: [], onToggleTrainingLabel: async () => {} }) }); // Inactive chip has border-line class, not bg-brand-mint const chips = Array.from(document.querySelectorAll('button')).filter((b) => /kurrent|segmentier/i.test(b.textContent ?? '') ); expect(chips.length).toBeGreaterThan(0); expect(chips[0].className).not.toContain('bg-brand-mint'); }); });