From 2e2c634b41a1457f6b483406969f47aaa6d645de Mon Sep 17 00:00:00 2001 From: divy-11 Date: Sun, 19 Oct 2025 18:36:57 +0530 Subject: [PATCH 1/3] Feat: Reverse-pages for mulit-pdf - Added new state pdfDocs: [ ] - Made tweaks for new state in fileHandler.ts --- src/js/config/pdf-tools.ts | 1 + src/js/handlers/fileHandler.ts | 11 ++++++----- src/js/logic/reverse-pages.ts | 34 ++++++++++++++++++++-------------- src/js/state.ts | 2 ++ src/js/ui.ts | 2 +- 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/js/config/pdf-tools.ts b/src/js/config/pdf-tools.ts index 17ba38e..6dcd8e2 100644 --- a/src/js/config/pdf-tools.ts +++ b/src/js/config/pdf-tools.ts @@ -62,4 +62,5 @@ export const multiFileTools = [ 'tiff-to-pdf', 'alternate-merge', 'linearize', + 'reverse-pages', ]; diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index dd558ff..5515926 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -25,9 +25,9 @@ async function handleSinglePdfUpload(toolId, file) { showLoader('Loading PDF...'); try { const pdfBytes = await readFileAsArrayBuffer(file); - state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { - ignoreEncryption: true, - }); + const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { ignoreEncryption: true }); + state.pdfDocs = [pdfDoc]; + state.pdfDoc = pdfDoc; hideLoader(); if ( @@ -324,7 +324,8 @@ async function handleSinglePdfUpload(toolId, file) { } async function handleMultiFileUpload(toolId) { - if (toolId === 'merge' || toolId === 'alternate-merge') { + console.log(toolId); + if (toolId === 'merge' || toolId === 'alternate-merge' || toolId === 'reverse-pages') { const pdfFilesUnloaded: File[] = []; state.files.forEach((file) => { @@ -346,7 +347,7 @@ async function handleMultiFileUpload(toolId) { }; }) ); - + state.pdfDocs = pdfFilesLoaded.map(p => p.pdfDoc); const foundEncryptedPDFs = pdfFilesLoaded.filter( (pdf) => pdf.pdfDoc.isEncrypted ); diff --git a/src/js/logic/reverse-pages.ts b/src/js/logic/reverse-pages.ts index 95a2aca..08bed23 100644 --- a/src/js/logic/reverse-pages.ts +++ b/src/js/logic/reverse-pages.ts @@ -3,29 +3,35 @@ import { downloadFile } from '../utils/helpers.js'; import { state } from '../state.js'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; +import JSZip from 'jszip'; export async function reversePages() { - if (!state.pdfDoc) { + const pdfDocs = Array.isArray(state.pdfDocs) ? state.pdfDocs : state.pdfDoc ? [state.pdfDoc] : []; + if (!pdfDocs.length) { showAlert('Error', 'PDF not loaded.'); return; } showLoader('Reversing page order...'); try { - const newPdf = await PDFLibDocument.create(); - const pageCount = state.pdfDoc.getPageCount(); - const reversedIndices = Array.from( - { length: pageCount }, - (_, i) => pageCount - 1 - i - ); + const zip = new JSZip(); + for (let j = 0; j < pdfDocs.length; j++) { + const pdfDoc = pdfDocs[j]; + const newPdf = await PDFLibDocument.create(); + const pageCount = pdfDoc.getPageCount(); + const reversedIndices = Array.from( + { length: pageCount }, + (_, i) => pageCount - 1 - i + ); - const copiedPages = await newPdf.copyPages(state.pdfDoc, reversedIndices); - copiedPages.forEach((page: any) => newPdf.addPage(page)); + const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices); + copiedPages.forEach((page: any) => newPdf.addPage(page)); - const newPdfBytes = await newPdf.save(); - downloadFile( - new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), - 'reversed.pdf' - ); + const newPdfBytes = await newPdf.save(); + const fileName = pdfDocs.length > 1 ? `reversed_${j + 1}.pdf` : 'reversed.pdf'; + zip.file(fileName, newPdfBytes); + } + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, 'reversed_pdfs.zip'); } catch (e) { console.error(e); showAlert('Error', 'Could not reverse the PDF pages.'); diff --git a/src/js/state.ts b/src/js/state.ts index 4950b43..9b07460 100644 --- a/src/js/state.ts +++ b/src/js/state.ts @@ -1,6 +1,7 @@ export const state = { activeTool: null, files: [], + pdfDocs: [], pdfDoc: null, pdfPages: [], currentPdfUrl: null, @@ -10,6 +11,7 @@ export const state = { export function resetState() { state.activeTool = null; state.files = []; + state.pdfDocs = []; state.pdfDoc = null; state.pdfPages = []; state.currentPdfUrl = null; diff --git a/src/js/ui.ts b/src/js/ui.ts index bdd9f39..ffad5d8 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -969,7 +969,7 @@ export const toolTemplates = { 'reverse-pages': () => `

Reverse PDF Pages

Flip the order of all pages in your document, making the last page the first.

- ${createFileInputHTML()} + ${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}
`, From a605e56834a661ca765eafd7f5076a64c514239e Mon Sep 17 00:00:00 2001 From: divy-11 Date: Sun, 19 Oct 2025 19:50:19 +0530 Subject: [PATCH 2/3] test-case added. --- src/js/config/pdf-tools.ts | 1 - src/tests/pdf-tools.test.ts | 4 +- src/tests/reversePages.multi.test.ts | 111 +++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 src/tests/reversePages.multi.test.ts diff --git a/src/js/config/pdf-tools.ts b/src/js/config/pdf-tools.ts index 6dcd8e2..574b64f 100644 --- a/src/js/config/pdf-tools.ts +++ b/src/js/config/pdf-tools.ts @@ -18,7 +18,6 @@ export const singlePdfLoadTools = [ 'add-header-footer', 'invert-colors', 'view-metadata', - 'reverse-pages', 'crop', 'redact', 'pdf-to-bmp', diff --git a/src/tests/pdf-tools.test.ts b/src/tests/pdf-tools.test.ts index a5436d8..6370e75 100644 --- a/src/tests/pdf-tools.test.ts +++ b/src/tests/pdf-tools.test.ts @@ -19,7 +19,7 @@ describe('Tool Configuration Arrays', () => { it('should have the correct number of tools', () => { // This acts as a snapshot test to catch unexpected additions/removals. - expect(singlePdfLoadTools).toHaveLength(40); + expect(singlePdfLoadTools).toHaveLength(39); }); it('should not contain any duplicate tools', () => { @@ -61,7 +61,7 @@ describe('Tool Configuration Arrays', () => { }); it('should have the correct number of tools', () => { - expect(multiFileTools).toHaveLength(12); + expect(multiFileTools).toHaveLength(13); }); it('should not contain any duplicate tools', () => { diff --git a/src/tests/reversePages.multi.test.ts b/src/tests/reversePages.multi.test.ts new file mode 100644 index 0000000..fcee27b --- /dev/null +++ b/src/tests/reversePages.multi.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { PDFDocument as PDFLibDocument } from 'pdf-lib'; +import { reversePages } from '../js/logic/reverse-pages'; +import { state } from '../js/state'; +import * as helpers from '../js/utils/helpers'; +import * as ui from '../js/ui'; +import JSZip from 'jszip'; + +// -------------------- Mock Modules -------------------- +vi.mock('../js/ui', () => ({ + showLoader: vi.fn(), + hideLoader: vi.fn(), + showAlert: vi.fn(), +})); + +vi.mock('../js/utils/helpers', () => ({ + downloadFile: vi.fn(), +})); + +vi.mock('pdf-lib', () => ({ + PDFDocument: { + create: vi.fn(), + }, +})); + +// -------------------- Test Suite -------------------- +describe('reversePages - multi PDF support', () => { + let mockNewDoc: any; + + beforeEach(() => { + // Reset state + state.pdfDocs = []; + + // Mock PDFDocument.create + mockNewDoc = { + copyPages: vi.fn((doc: any, indices: number[]) => + Promise.resolve(indices.map((i: number) => ({ page: `page-${i}` }))) + ), + addPage: vi.fn(), + save: vi.fn(() => Promise.resolve(new Uint8Array([1, 2, 3]))), + }; + vi.mocked(PDFLibDocument.create).mockResolvedValue(mockNewDoc); + + // Mock helpers + vi.mocked(helpers.downloadFile).mockImplementation(() => {}); + vi.mocked(ui.showLoader).mockImplementation(() => {}); + vi.mocked(ui.hideLoader).mockImplementation(() => {}); + vi.mocked(ui.showAlert).mockImplementation(() => {}); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should reverse pages for multiple PDFs and create a zip', async () => { + // Mock 2 PDFs + const pdf1 = { getPageCount: () => 2 }; + const pdf2 = { getPageCount: () => 3 }; + state.pdfDocs = [pdf1, pdf2]; + + await reversePages(); + + // downloadFile called + expect(helpers.downloadFile).toHaveBeenCalledWith(expect.any(Blob), 'reversed_pdfs.zip'); + + // copyPages called for each PDF + expect(mockNewDoc.copyPages).toHaveBeenCalledTimes(2); + + // addPage called correct number of times + expect(mockNewDoc.addPage).toHaveBeenCalled(); + + // save called for each PDF + expect(mockNewDoc.save).toHaveBeenCalledTimes(2); + }); + + it('should handle empty PDF list gracefully', async () => { + state.pdfDocs = []; + await reversePages(); + expect(ui.showAlert).toHaveBeenCalledWith('Error', 'PDF not loaded.'); + }); + + it('should handle PDF creation errors', async () => { + vi.mocked(PDFLibDocument.create).mockRejectedValue(new Error('Create failed')); + state.pdfDocs = [{ getPageCount: () => 2 }]; + + await reversePages(); + + expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.'); + expect(ui.hideLoader).toHaveBeenCalled(); + }); + + it('should handle PDF processing errors', async () => { + mockNewDoc.copyPages.mockRejectedValue(new Error('Copy failed')); + state.pdfDocs = [{ getPageCount: () => 2 }]; + + await reversePages(); + + expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.'); + expect(ui.hideLoader).toHaveBeenCalled(); + }); + + it('should handle save errors', async () => { + mockNewDoc.save.mockRejectedValue(new Error('Save failed')); + state.pdfDocs = [{ getPageCount: () => 2 }]; + + await reversePages(); + + expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.'); + expect(ui.hideLoader).toHaveBeenCalled(); + }); +}); From 4e6cc77a286c1e307a8ccf9285b415d09267303c Mon Sep 17 00:00:00 2001 From: divy-11 Date: Mon, 20 Oct 2025 16:39:45 +0530 Subject: [PATCH 3/3] Removed new state pdfDocs - update reverse-pages function - updated test-case --- src/js/handlers/fileHandler.ts | 9 +++--- src/js/logic/reverse-pages.ts | 9 ++++-- src/js/state.ts | 2 -- src/tests/reversePages.multi.test.ts | 47 +++++++++++++++------------- 4 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index 5515926..b5cfc68 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -25,9 +25,9 @@ async function handleSinglePdfUpload(toolId, file) { showLoader('Loading PDF...'); try { const pdfBytes = await readFileAsArrayBuffer(file); - const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { ignoreEncryption: true }); - state.pdfDocs = [pdfDoc]; - state.pdfDoc = pdfDoc; + state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { + ignoreEncryption: true + }); hideLoader(); if ( @@ -324,7 +324,6 @@ async function handleSinglePdfUpload(toolId, file) { } async function handleMultiFileUpload(toolId) { - console.log(toolId); if (toolId === 'merge' || toolId === 'alternate-merge' || toolId === 'reverse-pages') { const pdfFilesUnloaded: File[] = []; @@ -347,7 +346,7 @@ async function handleMultiFileUpload(toolId) { }; }) ); - state.pdfDocs = pdfFilesLoaded.map(p => p.pdfDoc); + const foundEncryptedPDFs = pdfFilesLoaded.filter( (pdf) => pdf.pdfDoc.isEncrypted ); diff --git a/src/js/logic/reverse-pages.ts b/src/js/logic/reverse-pages.ts index 08bed23..2b47278 100644 --- a/src/js/logic/reverse-pages.ts +++ b/src/js/logic/reverse-pages.ts @@ -6,7 +6,7 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import JSZip from 'jszip'; export async function reversePages() { - const pdfDocs = Array.isArray(state.pdfDocs) ? state.pdfDocs : state.pdfDoc ? [state.pdfDoc] : []; + const pdfDocs = state.files.filter((file: File) => file.type === 'application/pdf'); if (!pdfDocs.length) { showAlert('Error', 'PDF not loaded.'); return; @@ -15,7 +15,9 @@ export async function reversePages() { try { const zip = new JSZip(); for (let j = 0; j < pdfDocs.length; j++) { - const pdfDoc = pdfDocs[j]; + const file = pdfDocs[j]; + const arrayBuffer = await file.arrayBuffer(); + const pdfDoc = await PDFLibDocument.load(arrayBuffer); const newPdf = await PDFLibDocument.create(); const pageCount = pdfDoc.getPageCount(); const reversedIndices = Array.from( @@ -27,7 +29,8 @@ export async function reversePages() { copiedPages.forEach((page: any) => newPdf.addPage(page)); const newPdfBytes = await newPdf.save(); - const fileName = pdfDocs.length > 1 ? `reversed_${j + 1}.pdf` : 'reversed.pdf'; + const originalName = file.name.replace(/\.pdf$/i, ''); + const fileName = `${originalName}_reversed.pdf`; zip.file(fileName, newPdfBytes); } const zipBlob = await zip.generateAsync({ type: 'blob' }); diff --git a/src/js/state.ts b/src/js/state.ts index 9b07460..4950b43 100644 --- a/src/js/state.ts +++ b/src/js/state.ts @@ -1,7 +1,6 @@ export const state = { activeTool: null, files: [], - pdfDocs: [], pdfDoc: null, pdfPages: [], currentPdfUrl: null, @@ -11,7 +10,6 @@ export const state = { export function resetState() { state.activeTool = null; state.files = []; - state.pdfDocs = []; state.pdfDoc = null; state.pdfPages = []; state.currentPdfUrl = null; diff --git a/src/tests/reversePages.multi.test.ts b/src/tests/reversePages.multi.test.ts index fcee27b..c25bce1 100644 --- a/src/tests/reversePages.multi.test.ts +++ b/src/tests/reversePages.multi.test.ts @@ -6,7 +6,6 @@ import * as helpers from '../js/utils/helpers'; import * as ui from '../js/ui'; import JSZip from 'jszip'; -// -------------------- Mock Modules -------------------- vi.mock('../js/ui', () => ({ showLoader: vi.fn(), hideLoader: vi.fn(), @@ -20,18 +19,21 @@ vi.mock('../js/utils/helpers', () => ({ vi.mock('pdf-lib', () => ({ PDFDocument: { create: vi.fn(), + load: vi.fn().mockResolvedValue({ + getPageCount: vi.fn(() => 2), + copyPages: vi.fn((_, indices) => + Promise.resolve(indices.map((i) => ({ page: `page-${i}` }))) + ), + }), }, })); -// -------------------- Test Suite -------------------- describe('reversePages - multi PDF support', () => { let mockNewDoc: any; beforeEach(() => { - // Reset state - state.pdfDocs = []; + state.files = []; // ✅ now using files, not pdfDocs - // Mock PDFDocument.create mockNewDoc = { copyPages: vi.fn((doc: any, indices: number[]) => Promise.resolve(indices.map((i: number) => ({ page: `page-${i}` }))) @@ -41,7 +43,6 @@ describe('reversePages - multi PDF support', () => { }; vi.mocked(PDFLibDocument.create).mockResolvedValue(mockNewDoc); - // Mock helpers vi.mocked(helpers.downloadFile).mockImplementation(() => {}); vi.mocked(ui.showLoader).mockImplementation(() => {}); vi.mocked(ui.hideLoader).mockImplementation(() => {}); @@ -53,35 +54,33 @@ describe('reversePages - multi PDF support', () => { }); it('should reverse pages for multiple PDFs and create a zip', async () => { - // Mock 2 PDFs - const pdf1 = { getPageCount: () => 2 }; - const pdf2 = { getPageCount: () => 3 }; - state.pdfDocs = [pdf1, pdf2]; + const mockFile = (name: string) => ({ + name, + type: 'application/pdf', + arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)), + }); + + state.files = [mockFile('a.pdf'), mockFile('b.pdf')]; // ✅ now matches function await reversePages(); - // downloadFile called expect(helpers.downloadFile).toHaveBeenCalledWith(expect.any(Blob), 'reversed_pdfs.zip'); - - // copyPages called for each PDF expect(mockNewDoc.copyPages).toHaveBeenCalledTimes(2); - - // addPage called correct number of times expect(mockNewDoc.addPage).toHaveBeenCalled(); - - // save called for each PDF expect(mockNewDoc.save).toHaveBeenCalledTimes(2); }); it('should handle empty PDF list gracefully', async () => { - state.pdfDocs = []; + state.files = []; await reversePages(); expect(ui.showAlert).toHaveBeenCalledWith('Error', 'PDF not loaded.'); }); it('should handle PDF creation errors', async () => { vi.mocked(PDFLibDocument.create).mockRejectedValue(new Error('Create failed')); - state.pdfDocs = [{ getPageCount: () => 2 }]; + state.files = [ + { name: 'x.pdf', type: 'application/pdf', arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)) }, + ]; await reversePages(); @@ -91,7 +90,9 @@ describe('reversePages - multi PDF support', () => { it('should handle PDF processing errors', async () => { mockNewDoc.copyPages.mockRejectedValue(new Error('Copy failed')); - state.pdfDocs = [{ getPageCount: () => 2 }]; + state.files = [ + { name: 'y.pdf', type: 'application/pdf', arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)) }, + ]; await reversePages(); @@ -101,11 +102,13 @@ describe('reversePages - multi PDF support', () => { it('should handle save errors', async () => { mockNewDoc.save.mockRejectedValue(new Error('Save failed')); - state.pdfDocs = [{ getPageCount: () => 2 }]; + state.files = [ + { name: 'z.pdf', type: 'application/pdf', arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)) }, + ]; await reversePages(); expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.'); expect(ui.hideLoader).toHaveBeenCalled(); }); -}); +}); \ No newline at end of file