Some checks failed
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / OCR Service Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Failing after 2m3s
CI / OCR Service Tests (push) Successful in 16s
CI / Backend Unit Tests (push) Successful in 4m15s
CI / fail2ban Regex (push) Successful in 38s
CI / Compose Bucket Idempotency (push) Failing after 11s
- Extract makeFakePdfjsLib / makeFakeLibLoader to testHelpers.ts — single source of truth used by both PdfViewer.svelte.test.ts and usePdfRenderer.svelte.test.ts; removes the diverging-fidelity DRY violation flagged by @felixbrandt and @saraholt in the PR review - Add 'loadDocument sets error and loading=false when getDocument().promise rejects' test to usePdfRenderer.svelte.test.ts — closes the error-path gap flagged by @felixbrandt and @saraholt - Replace toBeInTheDocument() with toBeVisible() in the three absorbed spec-file tests — uniform assertion style across the loaded-state describe block, as flagged by @felixbrandt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
228 lines
6.9 KiB
TypeScript
228 lines
6.9 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { createPdfRenderer } from './usePdfRenderer.svelte';
|
|
import { makeFakeLibLoader } from './testHelpers';
|
|
|
|
// 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();
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|