diff --git a/frontend/src/lib/document/transcription/TranscriptionEditView.svelte.test.ts b/frontend/src/lib/document/transcription/TranscriptionEditView.svelte.test.ts new file mode 100644 index 00000000..32a8ad3e --- /dev/null +++ b/frontend/src/lib/document/transcription/TranscriptionEditView.svelte.test.ts @@ -0,0 +1,198 @@ +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 }) +})); +vi.mock('$lib/shared/services/confirm.svelte.js', () => ({ + 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 new Promise((r) => setTimeout(r, 30)); + 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' }) + ] + }) + }); + + // All three texts should appear in document order: First, Second, Third + 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); + }); +});