Sag mir wo die Karabiner sind :)

This commit is contained in:
lovebird 2025-04-23 21:15:33 +02:00
parent 14189a7e7a
commit 712ac9b004
2 changed files with 135 additions and 224 deletions

View File

@ -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"
},

View File

@ -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<ConvertCommandConfig & { _: (string | number)[], $0: string, output?: string }>) {
// 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<ConvertCommandConfig & {output?: string}>;
// 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)
});