Five tests in usePdfRenderer.svelte.test.ts called createPdfRenderer() without a libLoader, causing init() to dynamically import pdfjs-dist in the browser. Every dynamic import goes through Playwright's route handler, which calls resolveManualMock via birpc to check for mocks. If the RPC closes during teardown while one of these imports is in flight, the birpc race fires — even though pdfjs-dist was never explicitly vi.mock()-ed. Replace all bare createPdfRenderer() calls that invoke init() with createPdfRenderer(makeFakeLibLoader()), identical to the pattern already used in PdfViewer.svelte.test.ts. No real module loads, no route-handler calls, no birpc exposure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
226 lines
6.7 KiB
TypeScript
226 lines
6.7 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import type { LibLoader } from './usePdfRenderer.svelte';
|
|
import { createPdfRenderer } from './usePdfRenderer.svelte';
|
|
|
|
function makeFakeLibLoader(): LibLoader {
|
|
return vi.fn().mockResolvedValue([
|
|
{
|
|
GlobalWorkerOptions: { workerSrc: '' },
|
|
getDocument: vi.fn().mockReturnValue({
|
|
promise: Promise.resolve({ numPages: 1, getPage: vi.fn() })
|
|
}),
|
|
TextLayer: class {
|
|
render() {
|
|
return Promise.resolve();
|
|
}
|
|
cancel() {}
|
|
}
|
|
} as unknown as typeof import('pdfjs-dist'),
|
|
{ default: '' }
|
|
] as const);
|
|
}
|
|
|
|
// Note: init() and loadDocument() require pdfjsLib (browser module).
|
|
// These tests cover pure state logic only — bounds clamping and zoom limits.
|
|
|
|
describe('createPdfRenderer', () => {
|
|
it('starts at page 1 with scale 1.5 and no error', () => {
|
|
const r = createPdfRenderer();
|
|
expect(r.currentPage).toBe(1);
|
|
expect(r.scale).toBe(1.5);
|
|
expect(r.totalPages).toBe(0);
|
|
expect(r.loading).toBe(false);
|
|
expect(r.error).toBeNull();
|
|
expect(r.isLoaded).toBe(false);
|
|
expect(r.pdfjsReady).toBe(false);
|
|
});
|
|
|
|
it('prevPage does not go below page 1', () => {
|
|
const r = createPdfRenderer();
|
|
r.prevPage();
|
|
expect(r.currentPage).toBe(1);
|
|
});
|
|
|
|
it('nextPage does not exceed totalPages', () => {
|
|
const r = createPdfRenderer();
|
|
// totalPages = 0, so 1 < 0 is false → stays at 1
|
|
r.nextPage();
|
|
expect(r.currentPage).toBe(1);
|
|
});
|
|
|
|
it('goToPage does not navigate when n > totalPages', () => {
|
|
const r = createPdfRenderer();
|
|
r.goToPage(5);
|
|
expect(r.currentPage).toBe(1);
|
|
});
|
|
|
|
it('goToPage does not navigate when n < 1', () => {
|
|
const r = createPdfRenderer();
|
|
r.goToPage(0);
|
|
expect(r.currentPage).toBe(1);
|
|
});
|
|
|
|
it('zoomIn increases scale by 0.25', () => {
|
|
const r = createPdfRenderer();
|
|
r.zoomIn();
|
|
expect(r.scale).toBeCloseTo(1.75);
|
|
});
|
|
|
|
it('zoomOut decreases scale by 0.25', () => {
|
|
const r = createPdfRenderer();
|
|
r.zoomOut();
|
|
expect(r.scale).toBeCloseTo(1.25);
|
|
});
|
|
|
|
it('zoomOut does not go below 0.5', () => {
|
|
const r = createPdfRenderer();
|
|
for (let i = 0; i < 20; i++) r.zoomOut();
|
|
expect(r.scale).toBeCloseTo(0.5);
|
|
});
|
|
|
|
it('loadDocument is a no-op when pdfjsLib not initialized', async () => {
|
|
const r = createPdfRenderer();
|
|
await r.loadDocument('/some/path');
|
|
// No-op because pdfjsLib is null (init not called)
|
|
expect(r.error).toBeNull();
|
|
expect(r.loading).toBe(false);
|
|
});
|
|
|
|
it('renderCurrentPage is a no-op when pdfjsLib is not initialized', async () => {
|
|
const r = createPdfRenderer();
|
|
// Should not throw — early-return branch
|
|
await expect(r.renderCurrentPage()).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('prerender is a no-op when pdfDoc is null', async () => {
|
|
const r = createPdfRenderer();
|
|
await expect(r.prerender()).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('destroy is safe to call when no document is loaded', () => {
|
|
const r = createPdfRenderer();
|
|
expect(() => r.destroy()).not.toThrow();
|
|
});
|
|
|
|
it('setElements stores canvas and text layer refs', () => {
|
|
const r = createPdfRenderer();
|
|
const canvas = document.createElement('canvas');
|
|
const textLayer = document.createElement('div');
|
|
expect(() => r.setElements(canvas, textLayer)).not.toThrow();
|
|
});
|
|
|
|
it('isLoaded reflects totalPages > 0', () => {
|
|
const r = createPdfRenderer();
|
|
// Initial state — totalPages=0 → not loaded
|
|
expect(r.isLoaded).toBe(false);
|
|
});
|
|
|
|
it('multiple zoomIn calls accumulate', () => {
|
|
const r = createPdfRenderer();
|
|
r.zoomIn();
|
|
r.zoomIn();
|
|
r.zoomIn();
|
|
expect(r.scale).toBeCloseTo(2.25);
|
|
});
|
|
|
|
it('mixed zoom in then zoom out lands back at start', () => {
|
|
const r = createPdfRenderer();
|
|
r.zoomIn();
|
|
r.zoomIn();
|
|
r.zoomOut();
|
|
r.zoomOut();
|
|
expect(r.scale).toBeCloseTo(1.5);
|
|
});
|
|
|
|
it('zoomOut at the floor does nothing', () => {
|
|
const r = createPdfRenderer();
|
|
// Force scale down to 0.5
|
|
for (let i = 0; i < 20; i++) r.zoomOut();
|
|
const before = r.scale;
|
|
r.zoomOut();
|
|
expect(r.scale).toBe(before);
|
|
});
|
|
|
|
it('init() sets pdfjsReady to true when loader resolves', async () => {
|
|
const r = createPdfRenderer(makeFakeLibLoader());
|
|
await expect(r.init()).resolves.toBeUndefined();
|
|
expect(r.pdfjsReady).toBe(true);
|
|
});
|
|
|
|
it('after init, loadDocument completes and loading returns to false', async () => {
|
|
const r = createPdfRenderer(makeFakeLibLoader());
|
|
await r.init();
|
|
|
|
await r.loadDocument('/some/path');
|
|
expect(r.loading).toBe(false);
|
|
});
|
|
|
|
it('renderCurrentPage is a no-op when canvasEl is null but pdfjsLib is initialized', async () => {
|
|
const r = createPdfRenderer(makeFakeLibLoader());
|
|
await r.init();
|
|
// Without setElements, canvasEl is null — early return
|
|
await expect(r.renderCurrentPage()).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('renderCurrentPage is a no-op when textLayerEl is null', async () => {
|
|
const r = createPdfRenderer(makeFakeLibLoader());
|
|
await r.init();
|
|
// Without setElements, textLayerEl is null — early return
|
|
await expect(r.renderCurrentPage()).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('init() can be called multiple times safely', async () => {
|
|
const r = createPdfRenderer(makeFakeLibLoader());
|
|
await r.init();
|
|
await r.init();
|
|
expect(r.pdfjsReady).toBe(true);
|
|
});
|
|
|
|
it('zoomIn after multiple zoomOuts lands at predictable scale', () => {
|
|
const r = createPdfRenderer();
|
|
// 1.5 -> 0.5 (floor) -> 0.75
|
|
for (let i = 0; i < 10; i++) r.zoomOut();
|
|
r.zoomIn();
|
|
expect(r.scale).toBeCloseTo(0.75);
|
|
});
|
|
|
|
it('goToPage(1) works when totalPages would be at least 1 (no-op currently)', () => {
|
|
const r = createPdfRenderer();
|
|
r.goToPage(1);
|
|
expect(r.currentPage).toBe(1);
|
|
});
|
|
|
|
it('calls injected libLoader during init and sets pdfjsReady', async () => {
|
|
const fakePdfjs = {
|
|
GlobalWorkerOptions: { workerSrc: '' },
|
|
getDocument: vi.fn(),
|
|
TextLayer: class {}
|
|
} as unknown as typeof import('pdfjs-dist');
|
|
const fakeLoader = vi.fn().mockResolvedValue([fakePdfjs, { default: '' }] as const);
|
|
const r = createPdfRenderer(fakeLoader);
|
|
await r.init();
|
|
expect(fakeLoader).toHaveBeenCalledOnce();
|
|
expect(r.pdfjsReady).toBe(true);
|
|
});
|
|
|
|
it('leaves pdfjsReady false when libLoader rejects', async () => {
|
|
const failingLoader = vi.fn().mockRejectedValue(new Error('load failed'));
|
|
const r = createPdfRenderer(failingLoader);
|
|
await expect(r.init()).rejects.toThrow('load failed');
|
|
expect(r.pdfjsReady).toBe(false);
|
|
});
|
|
|
|
it('init() is idempotent — libLoader called only once on repeated calls', async () => {
|
|
const fakePdfjs = {
|
|
GlobalWorkerOptions: { workerSrc: '' },
|
|
getDocument: vi.fn(),
|
|
TextLayer: class {}
|
|
} as unknown as typeof import('pdfjs-dist');
|
|
const fakeLoader = vi.fn().mockResolvedValue([fakePdfjs, { default: '' }] as const);
|
|
const r = createPdfRenderer(fakeLoader);
|
|
await r.init();
|
|
await r.init();
|
|
expect(fakeLoader).toHaveBeenCalledOnce();
|
|
});
|
|
});
|