Compare commits
6 Commits
feat/issue
...
8dd9e58fa4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dd9e58fa4 | ||
|
|
f39f16d96c | ||
|
|
f938971292 | ||
|
|
c1e5732fad | ||
|
|
32d02b79f7 | ||
|
|
1f7f6bde15 |
@@ -36,6 +36,14 @@ jobs:
|
|||||||
run: npm run lint
|
run: npm run lint
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
|
|
||||||
|
- name: Assert no banned vi.mock patterns
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if grep -rF "vi.mock('pdfjs-dist'" frontend/src/; then
|
||||||
|
echo "FAIL: banned vi.mock('pdfjs-dist') pattern found — see ADR 012. Use the libLoader prop injection pattern instead."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Run unit and component tests with coverage
|
- name: Run unit and component tests with coverage
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -86,5 +86,5 @@ These modules are resolved at static import time (before any test runs). Their `
|
|||||||
- New browser-mode specs that need to stub an external library **must not** use `vi.mock(externalLib, factory)`. Add a loader/factory parameter to the underlying hook or service instead.
|
- New browser-mode specs that need to stub an external library **must not** use `vi.mock(externalLib, factory)`. Add a loader/factory parameter to the underlying hook or service instead.
|
||||||
- The CI `unit-tests` job includes a permanent grep guard that fails the build if `rpc is closed` appears in any coverage run log. This catches regressions before they reach the acceptance criterion.
|
- The CI `unit-tests` job includes a permanent grep guard that fails the build if `rpc is closed` appears in any coverage run log. This catches regressions before they reach the acceptance criterion.
|
||||||
- Acceptance criterion for #535: 60 consecutive green `workflow_dispatch` CI runs against `main` after the fix is merged, with zero `rpc is closed` lines in any log.
|
- Acceptance criterion for #535: 60 consecutive green `workflow_dispatch` CI runs against `main` after the fix is merged, with zero `rpc is closed` lines in any log.
|
||||||
- **Enforcement:** No automated lint rule is planned; the CI coverage guard is the regression backstop. If a lint rule is added later (e.g. an ESLint rule flagging `vi.mock` of non-virtual modules in browser-mode spec files), update this ADR.
|
- **Enforcement:** An ESLint `no-restricted-syntax` rule in `eslint.config.js` (scoped to `**/*.spec.ts` and `**/*.test.ts`) flags any `vi.mock('pdfjs-dist', ...)` call with a message referencing this ADR. This turns a ~2-minute CI wait into an immediate lint error on save. The CI birpc grep guard remains as belt-and-suspenders for the coverage run. A CI static grep step (added in issue #546) also catches the pattern before the test suite launches.
|
||||||
- **When to revisit the LibLoader home:** If three or more components adopt this pattern, consider extracting a shared `$lib/types/lib-loader.ts` or a generic `DynamicImportLoader<T>` type to avoid parallel type definitions across modules.
|
- **When to revisit the LibLoader home:** If three or more components adopt this pattern, consider extracting a shared `$lib/types/lib-loader.ts` or a generic `DynamicImportLoader<T>` type to avoid parallel type definitions across modules.
|
||||||
|
|||||||
@@ -72,6 +72,20 @@ export default defineConfig(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.spec.ts', '**/*.test.ts'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-syntax': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
selector:
|
||||||
|
"CallExpression[callee.object.name='vi'][callee.property.name='mock'] > Literal[value=/^pdfjs-dist/]",
|
||||||
|
message:
|
||||||
|
"Banned: vi.mock('pdfjs-dist', factory) causes a birpc teardown race in browser-mode specs — see ADR 012. Use the libLoader prop injection pattern instead."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
plugins: { boundaries },
|
plugins: { boundaries },
|
||||||
settings: {
|
settings: {
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { vi, describe, it, expect, afterEach } from 'vitest';
|
|
||||||
import { cleanup, render } from 'vitest-browser-svelte';
|
|
||||||
import { page } from 'vitest/browser';
|
|
||||||
import type { LibLoader } from '$lib/document/viewer/usePdfRenderer.svelte';
|
|
||||||
import PdfViewer from './PdfViewer.svelte';
|
|
||||||
|
|
||||||
afterEach(cleanup);
|
|
||||||
|
|
||||||
function makeFakePdfjsLib() {
|
|
||||||
class TextLayerMock {
|
|
||||||
render() {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
cancel() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
GlobalWorkerOptions: { workerSrc: '' },
|
|
||||||
getDocument: vi.fn().mockReturnValue({
|
|
||||||
promise: Promise.resolve({
|
|
||||||
numPages: 2,
|
|
||||||
getPage: vi.fn().mockResolvedValue({
|
|
||||||
getViewport: vi.fn().mockReturnValue({ width: 595, height: 842 }),
|
|
||||||
render: vi.fn().mockReturnValue({ promise: Promise.resolve() }),
|
|
||||||
streamTextContent: vi.fn().mockReturnValue(new ReadableStream())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
TextLayer: TextLayerMock
|
|
||||||
} as unknown as typeof import('pdfjs-dist');
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeFakeLibLoader(): LibLoader {
|
|
||||||
const fakePdfjs = makeFakePdfjsLib();
|
|
||||||
return vi.fn().mockResolvedValue([fakePdfjs, { default: '' }] as const);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('PdfViewer', () => {
|
|
||||||
it('shows previous and next page navigation buttons', async () => {
|
|
||||||
render(PdfViewer, { url: '/api/documents/test-id/file', libLoader: makeFakeLibLoader() });
|
|
||||||
await expect.element(page.getByRole('button', { name: /zurück/i })).toBeInTheDocument();
|
|
||||||
await expect.element(page.getByRole('button', { name: /weiter/i })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows zoom controls', async () => {
|
|
||||||
render(PdfViewer, { url: '/api/documents/test-id/file', libLoader: makeFakeLibLoader() });
|
|
||||||
await expect.element(page.getByRole('button', { name: /vergrößern/i })).toBeInTheDocument();
|
|
||||||
await expect.element(page.getByRole('button', { name: /verkleinern/i })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays the page counter once the PDF has loaded', async () => {
|
|
||||||
render(PdfViewer, { url: '/api/documents/test-id/file', libLoader: makeFakeLibLoader() });
|
|
||||||
// Fake loader resolves synchronously, so "1 / 2" should appear quickly
|
|
||||||
await expect.element(page.getByText(/1\s*\/\s*2/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,43 +1,20 @@
|
|||||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
import { cleanup, render } from 'vitest-browser-svelte';
|
import { cleanup, render } from 'vitest-browser-svelte';
|
||||||
import { page } from 'vitest/browser';
|
import { page } from 'vitest/browser';
|
||||||
|
import PdfViewer from './PdfViewer.svelte';
|
||||||
vi.mock('pdfjs-dist', () => {
|
import { makeFakeLibLoader } from './testHelpers';
|
||||||
function TextLayerMock() {}
|
|
||||||
TextLayerMock.prototype.render = () => Promise.resolve();
|
|
||||||
TextLayerMock.prototype.cancel = () => {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
GlobalWorkerOptions: { workerSrc: '' },
|
|
||||||
getDocument: vi.fn().mockReturnValue({
|
|
||||||
promise: Promise.resolve({
|
|
||||||
numPages: 2,
|
|
||||||
getPage: vi.fn().mockResolvedValue({
|
|
||||||
getViewport: vi.fn().mockReturnValue({ width: 595, height: 842 }),
|
|
||||||
render: vi.fn().mockReturnValue({ promise: Promise.resolve() }),
|
|
||||||
streamTextContent: vi.fn().mockReturnValue(new ReadableStream())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
TextLayer: TextLayerMock
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock('pdfjs-dist/build/pdf.worker.min.mjs?url', () => ({ default: '' }));
|
|
||||||
|
|
||||||
const { default: PdfViewer } = await import('./PdfViewer.svelte');
|
|
||||||
|
|
||||||
afterEach(cleanup);
|
afterEach(cleanup);
|
||||||
|
|
||||||
describe('PdfViewer — empty / error states', () => {
|
describe('PdfViewer — empty / error states', () => {
|
||||||
it('renders the no-file placeholder when url is empty', async () => {
|
it('renders the no-file placeholder when url is empty', async () => {
|
||||||
render(PdfViewer, { url: '' });
|
render(PdfViewer, { url: '', libLoader: makeFakeLibLoader() });
|
||||||
|
|
||||||
await expect.element(page.getByText('Keine Datei vorhanden')).toBeVisible();
|
await expect.element(page.getByText('Keine Datei vorhanden')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render the controls when url is empty', async () => {
|
it('does not render the controls when url is empty', async () => {
|
||||||
render(PdfViewer, { url: '' });
|
render(PdfViewer, { url: '', libLoader: makeFakeLibLoader() });
|
||||||
|
|
||||||
const buttons = document.querySelectorAll('button');
|
const buttons = document.querySelectorAll('button');
|
||||||
expect(buttons.length).toBe(0);
|
expect(buttons.length).toBe(0);
|
||||||
@@ -49,10 +26,10 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
render(PdfViewer, {
|
render(PdfViewer, {
|
||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test',
|
documentId: 'test',
|
||||||
annotationReloadKey: 0
|
annotationReloadKey: 0,
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
// PdfControls renders its nav + zoom buttons once the document.promise resolves.
|
|
||||||
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
||||||
await expect.element(page.getByRole('button', { name: 'Weiter' })).toBeVisible();
|
await expect.element(page.getByRole('button', { name: 'Weiter' })).toBeVisible();
|
||||||
await expect.element(page.getByRole('button', { name: 'Vergrößern' })).toBeVisible();
|
await expect.element(page.getByRole('button', { name: 'Vergrößern' })).toBeVisible();
|
||||||
@@ -63,7 +40,8 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
render(PdfViewer, {
|
render(PdfViewer, {
|
||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test',
|
documentId: 'test',
|
||||||
annotationsDimmed: true
|
annotationsDimmed: true,
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
@@ -96,11 +74,10 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test',
|
documentId: 'test',
|
||||||
transcribeMode: true,
|
transcribeMode: true,
|
||||||
documentFileHash: 'match'
|
documentFileHash: 'match',
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
// transcribeMode forces showAnnotations=true; toggle button surfaces with "hide" label
|
|
||||||
// (only when annotationCount > 0).
|
|
||||||
await expect
|
await expect
|
||||||
.element(page.getByRole('button', { name: /annotierungen verbergen/i }))
|
.element(page.getByRole('button', { name: /annotierungen verbergen/i }))
|
||||||
.toBeVisible();
|
.toBeVisible();
|
||||||
@@ -113,7 +90,8 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
render(PdfViewer, {
|
render(PdfViewer, {
|
||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test',
|
documentId: 'test',
|
||||||
documentFileHash: 'abc123'
|
documentFileHash: 'abc123',
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
@@ -125,7 +103,8 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
render(PdfViewer, {
|
render(PdfViewer, {
|
||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test',
|
documentId: 'test',
|
||||||
flashAnnotationId: 'ann-flashing'
|
flashAnnotationId: 'ann-flashing',
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
||||||
@@ -135,7 +114,8 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
render(PdfViewer, {
|
render(PdfViewer, {
|
||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test',
|
documentId: 'test',
|
||||||
blockNumbers: { 'ann-1': 1, 'ann-2': 2 }
|
blockNumbers: { 'ann-1': 1, 'ann-2': 2 },
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
||||||
@@ -145,7 +125,8 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
render(PdfViewer, {
|
render(PdfViewer, {
|
||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test',
|
documentId: 'test',
|
||||||
activeAnnotationId: 'ann-1'
|
activeAnnotationId: 'ann-1',
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
||||||
@@ -156,10 +137,10 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test',
|
documentId: 'test',
|
||||||
transcribeMode: true,
|
transcribeMode: true,
|
||||||
activeAnnotationId: 'ann-1'
|
activeAnnotationId: 'ann-1',
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Without an annotations fetch, the visibility toggle is hidden — just assert the always-on nav.
|
|
||||||
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
||||||
await expect.element(page.getByRole('button', { name: 'Weiter' })).toBeVisible();
|
await expect.element(page.getByRole('button', { name: 'Weiter' })).toBeVisible();
|
||||||
});
|
});
|
||||||
@@ -169,7 +150,8 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
render(PdfViewer, {
|
render(PdfViewer, {
|
||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test',
|
documentId: 'test',
|
||||||
onAnnotationClick
|
onAnnotationClick,
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
||||||
@@ -199,7 +181,8 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
render(PdfViewer, {
|
render(PdfViewer, {
|
||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test',
|
documentId: 'test',
|
||||||
documentFileHash: 'new-hash'
|
documentFileHash: 'new-hash',
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
@@ -234,10 +217,10 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
render(PdfViewer, {
|
render(PdfViewer, {
|
||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test',
|
documentId: 'test',
|
||||||
documentFileHash: 'matching-hash'
|
documentFileHash: 'matching-hash',
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Controls finish mounting, and the outdated notice stays absent.
|
|
||||||
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
||||||
expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull();
|
expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -250,10 +233,10 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
try {
|
try {
|
||||||
render(PdfViewer, {
|
render(PdfViewer, {
|
||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test'
|
documentId: 'test',
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
// PDF rendering does not depend on the annotations fetch — controls still appear.
|
|
||||||
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
||||||
expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull();
|
expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -268,7 +251,8 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
try {
|
try {
|
||||||
render(PdfViewer, {
|
render(PdfViewer, {
|
||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
documentId: 'test'
|
documentId: 'test',
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
|
||||||
@@ -277,4 +261,21 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
fetchSpy.mockRestore();
|
fetchSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows previous and next page navigation buttons', async () => {
|
||||||
|
render(PdfViewer, { url: '/api/documents/test-id/file', libLoader: makeFakeLibLoader() });
|
||||||
|
await expect.element(page.getByRole('button', { name: /zurück/i })).toBeVisible();
|
||||||
|
await expect.element(page.getByRole('button', { name: /weiter/i })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows zoom controls', async () => {
|
||||||
|
render(PdfViewer, { url: '/api/documents/test-id/file', libLoader: makeFakeLibLoader() });
|
||||||
|
await expect.element(page.getByRole('button', { name: /vergrößern/i })).toBeVisible();
|
||||||
|
await expect.element(page.getByRole('button', { name: /verkleinern/i })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the page counter once the PDF has loaded', async () => {
|
||||||
|
render(PdfViewer, { url: '/api/documents/test-id/file', libLoader: makeFakeLibLoader() });
|
||||||
|
await expect.element(page.getByText(/1\s*\/\s*2/)).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
31
frontend/src/lib/document/viewer/testHelpers.ts
Normal file
31
frontend/src/lib/document/viewer/testHelpers.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { vi } from 'vitest';
|
||||||
|
import type { LibLoader } from './usePdfRenderer.svelte';
|
||||||
|
|
||||||
|
export function makeFakePdfjsLib() {
|
||||||
|
class TextLayerMock {
|
||||||
|
render() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
cancel() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
GlobalWorkerOptions: { workerSrc: '' },
|
||||||
|
getDocument: vi.fn().mockReturnValue({
|
||||||
|
promise: Promise.resolve({
|
||||||
|
numPages: 2,
|
||||||
|
getPage: vi.fn().mockResolvedValue({
|
||||||
|
getViewport: vi.fn().mockReturnValue({ width: 595, height: 842 }),
|
||||||
|
render: vi.fn().mockReturnValue({ promise: Promise.resolve() }),
|
||||||
|
streamTextContent: vi.fn().mockReturnValue(new ReadableStream())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
TextLayer: TextLayerMock
|
||||||
|
} as unknown as typeof import('pdfjs-dist');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeFakeLibLoader(): LibLoader {
|
||||||
|
const fakePdfjs = makeFakePdfjsLib();
|
||||||
|
return vi.fn().mockResolvedValue([fakePdfjs, { default: '' }] as const);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { createPdfRenderer } from './usePdfRenderer.svelte';
|
import { createPdfRenderer } from './usePdfRenderer.svelte';
|
||||||
|
import { makeFakeLibLoader } from './testHelpers';
|
||||||
|
|
||||||
// Note: init() and loadDocument() require pdfjsLib (browser module).
|
// Note: init() and loadDocument() require pdfjsLib (browser module).
|
||||||
// These tests cover pure state logic only — bounds clamping and zoom limits.
|
// These tests cover pure state logic only — bounds clamping and zoom limits.
|
||||||
@@ -122,39 +123,36 @@ describe('createPdfRenderer', () => {
|
|||||||
expect(r.scale).toBe(before);
|
expect(r.scale).toBe(before);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('init() is callable and resolves without throwing in browser env', async () => {
|
it('init() sets pdfjsReady to true when loader resolves', async () => {
|
||||||
const r = createPdfRenderer();
|
const r = createPdfRenderer(makeFakeLibLoader());
|
||||||
await expect(r.init()).resolves.toBeUndefined();
|
await expect(r.init()).resolves.toBeUndefined();
|
||||||
// pdfjsReady is now true
|
|
||||||
expect(r.pdfjsReady).toBe(true);
|
expect(r.pdfjsReady).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('after init, loadDocument with a bogus URL sets error', async () => {
|
it('after init, loadDocument completes and loading returns to false', async () => {
|
||||||
const r = createPdfRenderer();
|
const r = createPdfRenderer(makeFakeLibLoader());
|
||||||
await r.init();
|
await r.init();
|
||||||
|
|
||||||
await r.loadDocument('about:invalid-pdf');
|
await r.loadDocument('/some/path');
|
||||||
// Either error is set or loading flips back to false — both are acceptable
|
|
||||||
expect(r.loading).toBe(false);
|
expect(r.loading).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renderCurrentPage is a no-op when canvasEl is null but pdfjsLib is initialized', async () => {
|
it('renderCurrentPage is a no-op when canvasEl is null but pdfjsLib is initialized', async () => {
|
||||||
const r = createPdfRenderer();
|
const r = createPdfRenderer(makeFakeLibLoader());
|
||||||
await r.init();
|
await r.init();
|
||||||
// Without setElements, canvasEl is null — early return
|
// Without setElements, canvasEl is null — early return
|
||||||
await expect(r.renderCurrentPage()).resolves.toBeUndefined();
|
await expect(r.renderCurrentPage()).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renderCurrentPage is a no-op when textLayerEl is null', async () => {
|
it('renderCurrentPage is a no-op when textLayerEl is null', async () => {
|
||||||
const r = createPdfRenderer();
|
const r = createPdfRenderer(makeFakeLibLoader());
|
||||||
await r.init();
|
await r.init();
|
||||||
// Set only canvas, leave textLayer unset is not directly testable;
|
// Without setElements, textLayerEl is null — early return
|
||||||
// confirm calling without elements wired returns early.
|
|
||||||
await expect(r.renderCurrentPage()).resolves.toBeUndefined();
|
await expect(r.renderCurrentPage()).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('init() can be called multiple times safely', async () => {
|
it('init() can be called multiple times safely', async () => {
|
||||||
const r = createPdfRenderer();
|
const r = createPdfRenderer(makeFakeLibLoader());
|
||||||
await r.init();
|
await r.init();
|
||||||
await r.init();
|
await r.init();
|
||||||
expect(r.pdfjsReady).toBe(true);
|
expect(r.pdfjsReady).toBe(true);
|
||||||
@@ -206,4 +204,24 @@ describe('createPdfRenderer', () => {
|
|||||||
await r.init();
|
await r.init();
|
||||||
expect(fakeLoader).toHaveBeenCalledOnce();
|
expect(fakeLoader).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('loadDocument sets error and loading=false when getDocument().promise rejects', async () => {
|
||||||
|
const failingLib = {
|
||||||
|
GlobalWorkerOptions: { workerSrc: '' },
|
||||||
|
getDocument: vi.fn().mockReturnValue({
|
||||||
|
promise: Promise.reject(new Error('PDF not found'))
|
||||||
|
}),
|
||||||
|
TextLayer: class {
|
||||||
|
render() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
cancel() {}
|
||||||
|
}
|
||||||
|
} as unknown as typeof import('pdfjs-dist');
|
||||||
|
const r = createPdfRenderer(vi.fn().mockResolvedValue([failingLib, { default: '' }] as const));
|
||||||
|
await r.init();
|
||||||
|
await r.loadDocument('/bad/path');
|
||||||
|
expect(r.loading).toBe(false);
|
||||||
|
expect(r.error).toBe('PDF not found');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user