diff --git a/packages/content/ref/pdf-to-images/package.json b/packages/content/ref/pdf-to-images/package.json index 7e6060c4..70a7462f 100644 --- a/packages/content/ref/pdf-to-images/package.json +++ b/packages/content/ref/pdf-to-images/package.json @@ -10,7 +10,7 @@ "dev": "tsc -p . --watch", "build": "tsc", "start": "node dist/index.js", - "test:pdf": "node dist/index.js convert --input tests/e5dc.pdf -o tests/out/e5dc/ --startPage 3 --endPage 5", + "test:pdf": "node dist/index.js convert --input tests/e5dc.pdf --output tests/out/e5dc/ --startPage 3 --endPage 5", "test:basic": "vitest run", "test:variables": "vitest run tests/cli/variables.test.ts" }, diff --git a/packages/content/ref/pdf-to-images/tests/cli/convert.test.ts b/packages/content/ref/pdf-to-images/tests/cli/convert.test.ts index ff9d1c2f..cb238e70 100644 --- a/packages/content/ref/pdf-to-images/tests/cli/convert.test.ts +++ b/packages/content/ref/pdf-to-images/tests/cli/convert.test.ts @@ -3,67 +3,56 @@ import { describe, it, expect, vi, beforeEach, Mock, beforeAll } from 'vitest'; // Import types first import type { ConvertCommandConfig } from '../../src/types.js'; import type { Arguments } from 'yargs'; -import { Buffer } from 'node:buffer'; -import path from 'path'; +// Remove Buffer import if readFile is no longer mocked directly +// import { Buffer } from 'node:buffer'; +// Import path for constants only if needed, remove specific utils +import path from 'path'; // --- Define Mock Functions --- -const mockConvertPdfToImagesFn = vi.fn(); +// Keep only mocks needed for the simplified handler +const mockRunConversion = vi.fn(); // Mock the library function const mockExistsSync = vi.fn(); -const mockStatSync = vi.fn(); -const mockReadFile = vi.fn(); -const mockMkdir = vi.fn(); -const mockDirname = vi.fn(); -const mockBasename = vi.fn(); -const mockExtname = vi.fn(); -const mockResolve = vi.fn(); -const mockParse = vi.fn(); -const mockRelative = vi.fn(); const mockLoggerInfo = vi.fn(); const mockLoggerError = vi.fn(); const mockProcessExit = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any); -// Mocks for @polymech/commons -const mockResolveVariables = vi.fn(); -const mockPathInfoEx = vi.fn(); -const mockDEFAULT_ROOTS = { CWD: '/test/cwd', SCRIPT_DIR: '/test/script' }; -const mockDEFAULT_VARS = vi.fn().mockReturnValue({ SOME_DEFAULT: 'value' }); +// Remove mocks for functions no longer directly called by handler +// const mockConvertPdfToImagesFn = vi.fn(); +// const mockStatSync = vi.fn(); +// const mockReadFile = vi.fn(); +// const mockMkdir = vi.fn(); +// const mockDirname = vi.fn(); +// const mockBasename = vi.fn(); +// const mockExtname = vi.fn(); +// const mockResolve = vi.fn(); +// const mockParse = vi.fn(); +// const mockRelative = vi.fn(); +// const mockResolveVariables = vi.fn(); +// const mockPathInfoEx = vi.fn(); +// const mockDEFAULT_ROOTS = { CWD: '/test/cwd', SCRIPT_DIR: '/test/script' }; +// const mockDEFAULT_VARS = vi.fn().mockReturnValue({ SOME_DEFAULT: 'value' }); // Use beforeAll for mocks beforeAll(() => { // Mock dependencies using vi.doMock - vi.doMock('../../src/lib/pdf.js', () => ({ - convertPdfToImages: mockConvertPdfToImagesFn, + // Remove unused mocks + // vi.doMock('../../src/lib/pdf.js', ...) + vi.doMock('../../src/lib/convert.js', () => ({ + runConversion: mockRunConversion, // Mock the refactored library function })); vi.doMock('node:fs', () => ({ existsSync: mockExistsSync, - statSync: mockStatSync, - })); - vi.doMock('node:fs/promises', () => ({ - readFile: mockReadFile, - mkdir: mockMkdir, - })); - vi.doMock('node:path', () => ({ - dirname: mockDirname, - basename: mockBasename, - extname: mockExtname, - resolve: mockResolve, - parse: mockParse, - relative: mockRelative, - sep: '/', + // statSync: mockStatSync, })); + // vi.doMock('node:fs/promises', ...) + // vi.doMock('node:path', ...) vi.doMock('tslog', () => ({ Logger: vi.fn().mockImplementation(() => ({ info: mockLoggerInfo, error: mockLoggerError, })), })); - // Mock @polymech/commons - vi.doMock('@polymech/commons', () => ({ - resolveVariables: mockResolveVariables, - pathInfoEx: mockPathInfoEx, - DEFAULT_ROOTS: mockDEFAULT_ROOTS, - DEFAULT_VARS: mockDEFAULT_VARS, - })); + // vi.doMock('@polymech/commons', ...) }); // --- Test Suite --- @@ -78,7 +67,10 @@ describe('Convert Command CLI Handler', () => { }); // --- Helper Function to Run Handler --- + // Helper remains largely the same async function runHandlerHelper(args: Partial) { + // Add default values for required fields if not provided in args, + // reflecting what yargs + schema default would do. const fullArgs = { _: ['convert'], $0: 'test', @@ -86,199 +78,95 @@ describe('Convert Command CLI Handler', () => { format: 'png', ...args, } as Arguments; - // Make sure handler is loaded before calling + + // Need to simulate the full argv object including potential var-* args + // Zod schema parsing happens inside the handler now. + if (!convertHandler) throw new Error('Handler not loaded'); await convertHandler(fullArgs); } beforeEach(() => { vi.clearAllMocks(); - // Reset mocks - mockConvertPdfToImagesFn.mockResolvedValue(['output/img_1.png']); + // Reset only necessary mocks + mockRunConversion.mockResolvedValue(['path/to/image1.png']); // Default success mockExistsSync.mockReturnValue(true); - mockReadFile.mockResolvedValue(Buffer.from('fake-pdf-data')); - mockMkdir.mockResolvedValue(undefined); - - // Mock path functions more robustly - mockDirname.mockImplementation((p) => { - if (!p || p === '/') return '/'; - const lastSlash = p.lastIndexOf('/'); - if (lastSlash === -1) return '.'; // No slash, return current dir indicator - if (lastSlash === 0) return '/'; // Root directory - return p.substring(0, lastSlash); - }); - mockBasename.mockImplementation((p) => p.substring(p.lastIndexOf('/') > 0 ? p.lastIndexOf('/') + 1 : 0)); - mockExtname.mockImplementation((p) => { - const lastSlash = p.lastIndexOf('/'); - const dotIndex = p.lastIndexOf('.'); - return dotIndex > (lastSlash > -1 ? lastSlash : -1) ? p.substring(dotIndex) : ''; - }); - // Improved mockResolve to handle absolute/relative paths based on /test/cwd - mockResolve.mockImplementation((...paths) => { - let currentPath = '/test/cwd'; // Assume CWD - for (const p of paths) { - if (path.isAbsolute(p)) { // Use actual path.isAbsolute for check - currentPath = p; - } else { - currentPath = path.join(currentPath, p); // Use actual path.join - } - } - // Normalize (e.g., remove //, resolve ..) - return path.normalize(currentPath).replace(/\\/g, '/'); - }); - mockParse.mockImplementation((p) => ({ - root: '/', - dir: mockDirname(p), - base: mockBasename(p), - ext: mockExtname(p), - name: mockBasename(p, mockExtname(p)), - })); - mockRelative.mockImplementation((from, to) => to.startsWith(from) ? to.substring(from.length + 1) : to); - - // Reset @polymech/commons mocks - mockResolveVariables.mockImplementation(async (template, _bool, vars) => template.replace(/\${(.*?)}/g, (_, key) => vars[key] ?? 'UNDEFINED')); - mockPathInfoEx.mockImplementation((p) => ({ - ROOT: '/test/cwd', - SRC_DIR: mockDirname(p), - SRC_NAME: mockBasename(p, mockExtname(p)), - SRC_EXT: mockExtname(p), - })); - mockStatSync.mockImplementation((p) => { throw new Error('File not found'); }); - + // Removed resets for unused mocks mockProcessExit.mockClear(); }); - // --- Test cases --- - it('should call convertPdfToImages with correct default args when output is omitted', async () => { + // --- Updated Test cases --- + it('should call runConversion with config when output is omitted (uses default)', async () => { const args = { input: 'pdfs/document.pdf', + // output is omitted }; - // Setup mocks for this case - mockExistsSync.mockReturnValueOnce(true); // Explicitly mock for this input - mockResolve.mockImplementation((p) => p.startsWith('/') ? p : `/test/cwd/${p}`); - const expectedSrcDir = '/test/cwd/pdfs'; - const expectedSrcName = 'document'; - mockPathInfoEx.mockReturnValue({ - ROOT: '/test/cwd', - SRC_DIR: expectedSrcDir, - SRC_NAME: expectedSrcName, - SRC_EXT: '.pdf' - }); await runHandlerHelper(args); expect(mockExistsSync).toHaveBeenCalledWith(args.input); - expect(mockReadFile).toHaveBeenCalledWith(args.input); - expect(mockMkdir).toHaveBeenCalledWith(expectedSrcDir, { recursive: true }); - expect(mockConvertPdfToImagesFn).toHaveBeenCalledTimes(1); - expect(mockConvertPdfToImagesFn).toHaveBeenCalledWith(expect.any(Buffer), { - baseVariables: expect.objectContaining({ - SRC_DIR: expectedSrcDir, - SRC_NAME: expectedSrcName, - FORMAT: 'png', - DPI: 300, - SOME_DEFAULT: 'value', - CWD: mockDEFAULT_ROOTS.CWD + expect(mockRunConversion).toHaveBeenCalledTimes(1); + // Check the config object passed to runConversion + expect(mockRunConversion).toHaveBeenCalledWith( + expect.objectContaining({ + input: args.input, + output: undefined, // Output should be undefined in the config passed from handler + dpi: 300, // Default DPI + format: 'png', // Default format + // other args like startPage/endPage should be undefined }), - outputPathTemplate: `${'${SRC_DIR}'}/${'${SRC_NAME}'}_${'${PAGE}'}.${'${FORMAT}'}`, - dpi: 300, - format: 'png', - startPage: undefined, - endPage: undefined, - logger: expect.anything(), - }); + expect.anything() // Logger instance + ); + // Verify final success logs are called + expect(mockLoggerInfo).toHaveBeenCalledWith('Conversion completed successfully'); + expect(mockLoggerInfo).toHaveBeenCalledWith(expect.stringContaining('Generated')); // Check for generated count message expect(mockProcessExit).not.toHaveBeenCalled(); }); - it('should use custom output path template when provided', async () => { + it('should call runConversion with custom output path template when provided', async () => { + const customPattern = 'images/custom_${SRC_NAME}_page${PAGE}.${FORMAT}'; const args = { input: 'in.pdf', - output: 'images/custom_${SRC_NAME}_page${PAGE}.${FORMAT}', + output: customPattern, }; - mockExistsSync.mockReturnValueOnce(true); // Explicitly mock for this input - mockResolve.mockImplementation((p) => p.startsWith('/') ? p : `/test/cwd/${p}`); - mockPathInfoEx.mockReturnValue({ - ROOT: '/test/cwd', - SRC_DIR: '/test/cwd', - SRC_NAME: 'in', - SRC_EXT: '.pdf' - }); - const expectedPatternDir = '/test/cwd/images'; - // Ensure dirname mock works for the expected resolved path - // mockDirname.mockImplementation((p) => p === '/test/cwd/images/custom_in_pageUNDEFINED.png' ? expectedPatternDir : '/'); // Old complex mock removed, rely on general mock - + await runHandlerHelper(args); - expect(mockMkdir).toHaveBeenCalledWith(expectedPatternDir, { recursive: true }); - expect(mockConvertPdfToImagesFn).toHaveBeenCalledWith(expect.any(Buffer), expect.objectContaining({ - outputPathTemplate: args.output, - baseVariables: expect.objectContaining({ SRC_NAME: 'in' }), - })); + expect(mockExistsSync).toHaveBeenCalledWith(args.input); + expect(mockRunConversion).toHaveBeenCalledTimes(1); + expect(mockRunConversion).toHaveBeenCalledWith( + expect.objectContaining({ + input: args.input, + output: customPattern, // Expect the custom pattern string + }), + expect.anything() // Logger instance + ); expect(mockProcessExit).not.toHaveBeenCalled(); }); - it('should handle output path as a directory', async () => { + it('should call runConversion with output path when it is a directory', async () => { + const dirPath = 'output/images/'; const args = { input: 'some/path/doc.pdf', - output: 'output/images/', + output: dirPath, }; - const resolvedOutputDir = '/test/cwd/output/images'; - mockResolve.mockImplementation((p) => p === args.output ? resolvedOutputDir : p ); - mockStatSync.mockImplementation((p) => { - if (p === resolvedOutputDir) { - return { isDirectory: () => true }; - } - throw new Error('Not found'); - }); - mockPathInfoEx.mockReturnValue({ - ROOT: '/test/cwd', - SRC_DIR: '/test/cwd/some/path', - SRC_NAME: 'doc', - SRC_EXT: '.pdf' - }); await runHandlerHelper(args); - expect(mockMkdir).toHaveBeenCalledWith(resolvedOutputDir, { recursive: true }); - expect(mockConvertPdfToImagesFn).toHaveBeenCalledWith(expect.any(Buffer), expect.objectContaining({ - outputPathTemplate: `${'${OUT_DIR}'}/${'${SRC_NAME}'}_${'${PAGE}'}.${'${FORMAT}'}`, - baseVariables: expect.objectContaining({ - OUT_DIR: resolvedOutputDir, - SRC_NAME: 'doc', + expect(mockExistsSync).toHaveBeenCalledWith(args.input); + expect(mockRunConversion).toHaveBeenCalledTimes(1); + expect(mockRunConversion).toHaveBeenCalledWith( + expect.objectContaining({ + input: args.input, + output: dirPath, // Expect the directory path string }), - })); + expect.anything() + ); expect(mockProcessExit).not.toHaveBeenCalled(); }); - it('should handle output path that looks like a directory (ends with /) but doesnt exist yet', async () => { - const args = { - input: 'other.pdf', - output: 'new_dir/', - }; - const resolvedOutputDir = '/test/cwd/new_dir'; - mockResolve.mockImplementation((p) => p === args.output ? resolvedOutputDir : p ); - mockStatSync.mockImplementation((p) => { throw new Error('Not found'); }); - mockPathInfoEx.mockReturnValue({ - ROOT: '/test/cwd', - SRC_DIR: '/test/cwd', - SRC_NAME: 'other', - SRC_EXT: '.pdf' - }); - - await runHandlerHelper(args); - - expect(mockMkdir).toHaveBeenCalledWith(resolvedOutputDir, { recursive: true }); - expect(mockConvertPdfToImagesFn).toHaveBeenCalledWith(expect.any(Buffer), expect.objectContaining({ - outputPathTemplate: `${'${OUT_DIR}'}/${'${SRC_NAME}'}_${'${PAGE}'}.${'${FORMAT}'}`, - baseVariables: expect.objectContaining({ - OUT_DIR: resolvedOutputDir, - SRC_NAME: 'other', - }), - })); - expect(mockProcessExit).not.toHaveBeenCalled(); - }); - - it('should call convertPdfToImages with specific args', async () => { + // Test for specific args being passed through + it('should call runConversion with specific args', async () => { const args = { input: 'input.pdf', output: 'output/prefix', @@ -287,57 +175,80 @@ describe('Convert Command CLI Handler', () => { startPage: 2, endPage: 5, }; - mockResolve.mockImplementation((p) => p.startsWith('/') ? p : `/test/cwd/${p}`); - const expectedPatternDir = '/test/cwd/output'; - mockDirname.mockImplementation((p) => p.startsWith(expectedPatternDir) ? expectedPatternDir : '/'); - mockPathInfoEx.mockReturnValue({ ROOT: '/test/cwd', SRC_DIR: '/test/cwd', SRC_NAME: 'input', SRC_EXT: '.pdf' }); - + await runHandlerHelper(args); expect(mockExistsSync).toHaveBeenCalledWith(args.input); - expect(mockReadFile).toHaveBeenCalledWith(args.input); - expect(mockMkdir).toHaveBeenCalledWith(expectedPatternDir, { recursive: true }); - expect(mockConvertPdfToImagesFn).toHaveBeenCalledTimes(1); - expect(mockConvertPdfToImagesFn).toHaveBeenCalledWith(expect.any(Buffer), { - baseVariables: expect.objectContaining({ - SRC_NAME: 'input', - FORMAT: 'jpg', - DPI: 150 + expect(mockRunConversion).toHaveBeenCalledTimes(1); + expect(mockRunConversion).toHaveBeenCalledWith( + expect.objectContaining({ + input: args.input, + output: args.output, + dpi: args.dpi, + format: args.format, + startPage: args.startPage, + endPage: args.endPage, }), - outputPathTemplate: args.output, - dpi: args.dpi, - format: args.format, - startPage: args.startPage, - endPage: args.endPage, - logger: expect.anything(), - }); + expect.anything() + ); expect(mockProcessExit).not.toHaveBeenCalled(); }); + // Test for var-* args being passed through + it('should pass var-* arguments to runConversion', async () => { + const args = { + input: 'input.pdf', + 'var-MY_VAR': 'myValue', + 'var-OTHER': 123 + }; + + await runHandlerHelper(args); + + expect(mockRunConversion).toHaveBeenCalledTimes(1); + expect(mockRunConversion).toHaveBeenCalledWith( + expect.objectContaining({ + input: args.input, + 'var-MY_VAR': 'myValue', // Zod schema with catchall should preserve these + 'var-OTHER': 123, + }), + expect.anything() + ); + expect(mockProcessExit).not.toHaveBeenCalled(); + }); + + + // --- Error Handling Tests --- it('should handle missing input file', async () => { mockExistsSync.mockReturnValue(false); - const args = { input: 'nonexistent.pdf', output: 'out' }; + const args = { input: 'nonexistent.pdf' }; // Output is optional await runHandlerHelper(args); - expect(mockConvertPdfToImagesFn).not.toHaveBeenCalled(); + + expect(mockRunConversion).not.toHaveBeenCalled(); // Should not be called + // Check logger error message (updated) expect(mockLoggerError).toHaveBeenCalledWith( - expect.stringContaining('Error during conversion:'), + "Error during conversion command:", // Updated error context expect.stringContaining('Input file nonexistent.pdf does not exist'), expect.any(Error) ); expect(mockProcessExit).toHaveBeenCalledWith(1); }); - it('should handle conversion error', async () => { - const conversionError = new Error('Conversion failed'); - mockConvertPdfToImagesFn.mockRejectedValue(conversionError); + it('should handle conversion error from runConversion', async () => { + const conversionError = new Error('Conversion library failed'); + mockRunConversion.mockRejectedValue(conversionError); // Mock runConversion to throw const args = { input: 'in.pdf', output: 'out' }; + await runHandlerHelper(args); - expect(mockConvertPdfToImagesFn).toHaveBeenCalledTimes(1); + + expect(mockRunConversion).toHaveBeenCalledTimes(1); + // Check logger error message (updated) expect(mockLoggerError).toHaveBeenCalledWith( - 'Error during conversion:', - 'Conversion failed', + "Error during conversion command:", // Updated error context + conversionError.message, conversionError ); expect(mockProcessExit).toHaveBeenCalledWith(1); }); + + // Remove tests checking internal logic that was moved (e.g., mkdir calls) }); \ No newline at end of file