Five test files mocked $lib/shared/services/confirm.svelte under BOTH spellings (.svelte and .svelte.js) within the same file; two more mocked only the .svelte.js form. Both resolve to the same module URL but register two distinct Playwright route handlers in @vitest/browser-playwright. The cleanup logic only removes one, leaving an orphan that fires when the next session loads the module — crashing the run with "[birpc] rpc is closed, cannot call resolveManualMock". This is the exact trigger fixed upstream by vitest PR #10267 (issue #9957). Normalise every confirm.svelte mock to the no-extension form, matching production imports and the source file basename (confirm.svelte.ts). After this commit: 8 confirm.svelte mocks across 8 spec files, all under one canonical ID. A meta-test (next commit) prevents the duplicate-id pattern from reappearing. Refs: #553 · vitest-dev/vitest#9957 · vitest-dev/vitest#10267 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
300 lines
8.7 KiB
TypeScript
300 lines
8.7 KiB
TypeScript
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> = {}): TranscriptionBlockData =>
|
|
({
|
|
id: 'b-1',
|
|
annotationId: 'ann-1',
|
|
text: 'Hello',
|
|
sortOrder: 1,
|
|
reviewed: false,
|
|
mentionedPersons: [],
|
|
label: null,
|
|
...overrides
|
|
}) as TranscriptionBlockData;
|
|
|
|
const baseProps = (overrides: Record<string, unknown> = {}) => ({
|
|
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');
|
|
});
|
|
});
|