import { describe, it, expect, vi } from 'vitest'; import { createPdfRenderer } from './usePdfRenderer.svelte'; // 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() is callable and resolves without throwing in browser env', async () => { const r = createPdfRenderer(); await expect(r.init()).resolves.toBeUndefined(); // pdfjsReady is now true expect(r.pdfjsReady).toBe(true); }); it('after init, loadDocument with a bogus URL sets error', async () => { const r = createPdfRenderer(); await r.init(); await r.loadDocument('about:invalid-pdf'); // Either error is set or loading flips back to false — both are acceptable expect(r.loading).toBe(false); }); it('renderCurrentPage is a no-op when canvasEl is null but pdfjsLib is initialized', async () => { const r = createPdfRenderer(); 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(); await r.init(); // Set only canvas, leave textLayer unset is not directly testable; // confirm calling without elements wired returns early. await expect(r.renderCurrentPage()).resolves.toBeUndefined(); }); it('init() can be called multiple times safely', async () => { const r = createPdfRenderer(); 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(); }); });