feat(test): add comprehensive test suite and testing infrastructure
- Add vitest configuration with coverage reporting - Create test setup file with DOM mocks - Add tests for state management, helpers, and tool configurations - Update tsconfig and package.json for testing support - Clean up unused comments and improve code style
This commit is contained in:
214
src/tests/helpers.test.ts
Normal file
214
src/tests/helpers.test.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
getStandardPageName,
|
||||
convertPoints,
|
||||
hexToRgb,
|
||||
formatBytes,
|
||||
parsePageRanges,
|
||||
} from '../js/utils/helpers';
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('getStandardPageName', () => {
|
||||
it('should identify A4 portrait', () => {
|
||||
expect(getStandardPageName(595.28, 841.89)).toBe('A4');
|
||||
});
|
||||
|
||||
it('should identify A4 landscape', () => {
|
||||
expect(getStandardPageName(841.89, 595.28)).toBe('A4');
|
||||
});
|
||||
|
||||
it('should identify Letter size', () => {
|
||||
expect(getStandardPageName(612, 792)).toBe('Letter');
|
||||
});
|
||||
|
||||
it('should identify Legal size', () => {
|
||||
expect(getStandardPageName(612, 1008)).toBe('Legal');
|
||||
});
|
||||
|
||||
it('should identify A3 size', () => {
|
||||
expect(getStandardPageName(841.89, 1190.55)).toBe('A3');
|
||||
});
|
||||
|
||||
it('should handle floating point variations within tolerance', () => {
|
||||
expect(getStandardPageName(595.5, 841.9)).toBe('A4');
|
||||
});
|
||||
|
||||
it('should return Custom for non-standard sizes', () => {
|
||||
expect(getStandardPageName(600, 800)).toBe('Custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertPoints', () => {
|
||||
it('should convert points to inches', () => {
|
||||
expect(convertPoints(72, 'in')).toBe('1.00');
|
||||
expect(convertPoints(144, 'in')).toBe('2.00');
|
||||
});
|
||||
|
||||
it('should convert points to millimeters', () => {
|
||||
expect(convertPoints(72, 'mm')).toBe('25.40');
|
||||
});
|
||||
|
||||
it('should convert points to pixels', () => {
|
||||
expect(convertPoints(72, 'px')).toBe('96.00');
|
||||
});
|
||||
|
||||
it('should return points as is for pt unit', () => {
|
||||
expect(convertPoints(100, 'pt')).toBe('100.00');
|
||||
});
|
||||
|
||||
it('should default to points for unknown unit', () => {
|
||||
expect(convertPoints(50, 'unknown')).toBe('50.00');
|
||||
});
|
||||
|
||||
it('should handle decimal values', () => {
|
||||
expect(convertPoints(36, 'in')).toBe('0.50');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hexToRgb', () => {
|
||||
it('should convert hex to RGB (with #)', () => {
|
||||
const result = hexToRgb('#ff0000');
|
||||
expect(result).toEqual({ r: 1, g: 0, b: 0 });
|
||||
});
|
||||
|
||||
it('should convert hex to RGB (without #)', () => {
|
||||
const result = hexToRgb('00ff00');
|
||||
expect(result).toEqual({ r: 0, g: 1, b: 0 });
|
||||
});
|
||||
|
||||
it('should handle blue color', () => {
|
||||
const result = hexToRgb('#0000ff');
|
||||
expect(result).toEqual({ r: 0, g: 0, b: 1 });
|
||||
});
|
||||
|
||||
it('should handle white color', () => {
|
||||
const result = hexToRgb('#ffffff');
|
||||
expect(result).toEqual({ r: 1, g: 1, b: 1 });
|
||||
});
|
||||
|
||||
it('should handle black color', () => {
|
||||
const result = hexToRgb('#000000');
|
||||
expect(result).toEqual({ r: 0, g: 0, b: 0 });
|
||||
});
|
||||
|
||||
it('should handle gray color', () => {
|
||||
const result = hexToRgb('#808080');
|
||||
expect(result.r).toBeCloseTo(0.502, 2);
|
||||
expect(result.g).toBeCloseTo(0.502, 2);
|
||||
expect(result.b).toBeCloseTo(0.502, 2);
|
||||
});
|
||||
|
||||
it('should return black for invalid hex', () => {
|
||||
const result = hexToRgb('invalid');
|
||||
expect(result).toEqual({ r: 0, g: 0, b: 0 });
|
||||
});
|
||||
|
||||
it('should be case insensitive', () => {
|
||||
const result = hexToRgb('#FF0000');
|
||||
expect(result).toEqual({ r: 1, g: 0, b: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatBytes', () => {
|
||||
it('should format 0 bytes', () => {
|
||||
expect(formatBytes(0)).toBe('0 Bytes');
|
||||
});
|
||||
|
||||
it('should format bytes', () => {
|
||||
expect(formatBytes(500)).toBe('500 Bytes');
|
||||
expect(formatBytes(1023)).toBe('1023 Bytes');
|
||||
});
|
||||
|
||||
it('should format kilobytes', () => {
|
||||
expect(formatBytes(1024)).toBe('1 KB');
|
||||
expect(formatBytes(2048)).toBe('2 KB');
|
||||
});
|
||||
|
||||
it('should format megabytes', () => {
|
||||
expect(formatBytes(1048576)).toBe('1 MB');
|
||||
expect(formatBytes(5242880)).toBe('5 MB');
|
||||
});
|
||||
|
||||
it('should format gigabytes', () => {
|
||||
expect(formatBytes(1073741824)).toBe('1 GB');
|
||||
});
|
||||
|
||||
it('should handle custom decimal places', () => {
|
||||
expect(formatBytes(1536, 2)).toBe('1.5 KB');
|
||||
expect(formatBytes(1536, 0)).toBe('2 KB');
|
||||
});
|
||||
|
||||
it('should handle decimal values', () => {
|
||||
expect(formatBytes(1536)).toBe('1.5 KB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parsePageRanges', () => {
|
||||
const totalPages = 10;
|
||||
|
||||
it('should return all pages for empty string', () => {
|
||||
const result = parsePageRanges('', totalPages);
|
||||
expect(result).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
});
|
||||
|
||||
it('should return all pages for whitespace', () => {
|
||||
const result = parsePageRanges(' ', totalPages);
|
||||
expect(result).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
});
|
||||
|
||||
it('should parse single page', () => {
|
||||
const result = parsePageRanges('5', totalPages);
|
||||
expect(result).toEqual([4]); // 0-indexed
|
||||
});
|
||||
|
||||
it('should parse multiple single pages', () => {
|
||||
const result = parsePageRanges('1,3,5', totalPages);
|
||||
expect(result).toEqual([0, 2, 4]);
|
||||
});
|
||||
|
||||
it('should parse page ranges', () => {
|
||||
const result = parsePageRanges('1-3', totalPages);
|
||||
expect(result).toEqual([0, 1, 2]);
|
||||
});
|
||||
|
||||
it('should parse mixed ranges and single pages', () => {
|
||||
const result = parsePageRanges('1,3-5,7', totalPages);
|
||||
expect(result).toEqual([0, 2, 3, 4, 6]);
|
||||
});
|
||||
|
||||
it('should handle spaces in input', () => {
|
||||
const result = parsePageRanges(' 1 , 3 - 5 , 7 ', totalPages);
|
||||
expect(result).toEqual([0, 2, 3, 4, 6]);
|
||||
});
|
||||
|
||||
it('should remove duplicates and sort', () => {
|
||||
const result = parsePageRanges('5,3,5,1-3', totalPages);
|
||||
expect(result).toEqual([0, 1, 2, 4]);
|
||||
});
|
||||
|
||||
it('should skip invalid page numbers', () => {
|
||||
const result = parsePageRanges('0,1,15,5', totalPages);
|
||||
expect(result).toEqual([0, 4]); // Only valid pages
|
||||
});
|
||||
|
||||
it('should skip invalid ranges', () => {
|
||||
const result = parsePageRanges('1-15,3-5', totalPages);
|
||||
expect(result).toEqual([2, 3, 4]); // Only 3-5 is valid
|
||||
});
|
||||
|
||||
it('should skip ranges where start > end', () => {
|
||||
const result = parsePageRanges('5-3,1-2', totalPages);
|
||||
expect(result).toEqual([0, 1]); // Only 1-2 is valid
|
||||
});
|
||||
|
||||
it('should handle all pages explicitly', () => {
|
||||
const result = parsePageRanges('1-10', totalPages);
|
||||
expect(result).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
});
|
||||
|
||||
it('should skip non-numeric values', () => {
|
||||
const result = parsePageRanges('1,abc,5', totalPages);
|
||||
expect(result).toEqual([0, 4]);
|
||||
});
|
||||
});
|
||||
});
|
||||
86
src/tests/pdf-tools.test.ts
Normal file
86
src/tests/pdf-tools.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { singlePdfLoadTools, simpleTools, multiFileTools } from '@/js/config/pdf-tools';
|
||||
|
||||
describe('Tool Configuration Arrays', () => {
|
||||
|
||||
// --- Tests for singlePdfLoadTools ---
|
||||
describe('singlePdfLoadTools', () => {
|
||||
it('should be an array of non-empty strings', () => {
|
||||
expect(Array.isArray(singlePdfLoadTools)).toBe(true);
|
||||
expect(singlePdfLoadTools.length).toBeGreaterThan(0);
|
||||
for (const tool of singlePdfLoadTools) {
|
||||
expect(typeof tool).toBe('string');
|
||||
expect(tool.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have the correct number of tools', () => {
|
||||
// This acts as a snapshot test to catch unexpected additions/removals.
|
||||
expect(singlePdfLoadTools).toHaveLength(37);
|
||||
});
|
||||
|
||||
it('should not contain any duplicate tools', () => {
|
||||
const uniqueTools = new Set(singlePdfLoadTools);
|
||||
expect(uniqueTools.size).toBe(singlePdfLoadTools.length);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Tests for simpleTools ---
|
||||
describe('simpleTools', () => {
|
||||
it('should be an array of non-empty strings', () => {
|
||||
expect(Array.isArray(simpleTools)).toBe(true);
|
||||
expect(simpleTools.length).toBeGreaterThan(0);
|
||||
simpleTools.forEach(tool => {
|
||||
expect(typeof tool).toBe('string');
|
||||
expect(tool.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have the correct number of tools', () => {
|
||||
expect(simpleTools).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should not contain any duplicate tools', () => {
|
||||
const uniqueTools = new Set(simpleTools);
|
||||
expect(uniqueTools.size).toBe(simpleTools.length);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Tests for multiFileTools ---
|
||||
describe('multiFileTools', () => {
|
||||
it('should be an array of non-empty strings', () => {
|
||||
expect(Array.isArray(multiFileTools)).toBe(true);
|
||||
expect(multiFileTools.length).toBeGreaterThan(0);
|
||||
multiFileTools.forEach(tool => {
|
||||
expect(typeof tool).toBe('string');
|
||||
expect(tool.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have the correct number of tools', () => {
|
||||
expect(multiFileTools).toHaveLength(10);
|
||||
});
|
||||
|
||||
it('should not contain any duplicate tools', () => {
|
||||
const uniqueTools = new Set(multiFileTools);
|
||||
expect(uniqueTools.size).toBe(multiFileTools.length);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Cross-Category Uniqueness Test ---
|
||||
describe('Uniqueness Across All Tool Categories', () => {
|
||||
it('should ensure no tool exists in more than one category', () => {
|
||||
const allTools = [
|
||||
...singlePdfLoadTools,
|
||||
...simpleTools,
|
||||
...multiFileTools,
|
||||
];
|
||||
const uniqueTools = new Set(allTools);
|
||||
|
||||
// If the size of the Set is different from the length of the combined array,
|
||||
// it means there was at least one duplicate tool across the categories.
|
||||
expect(uniqueTools.size).toBe(allTools.length);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
32
src/tests/setup.ts
Normal file
32
src/tests/setup.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { afterEach, vi } from 'vitest';
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
document.head.innerHTML = '';
|
||||
});
|
||||
|
||||
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
60
src/tests/state.test.ts
Normal file
60
src/tests/state.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { state, resetState } from '@/js/state';
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
describe('State Management', () => {
|
||||
|
||||
// Test the initial state on import
|
||||
describe('Initial State', () => {
|
||||
it('should have the correct initial values', () => {
|
||||
expect(state.activeTool).toBeNull();
|
||||
expect(state.files).toEqual([]); // Use toEqual for arrays/objects
|
||||
expect(state.pdfDoc).toBeNull();
|
||||
expect(state.pdfPages).toEqual([]);
|
||||
expect(state.currentPdfUrl).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// Test the resetState function
|
||||
describe('resetState function', () => {
|
||||
|
||||
// Before each test in this block, we'll "dirty" the state
|
||||
// to ensure the reset function is actually doing something.
|
||||
beforeEach(() => {
|
||||
// 1. Modify the state properties to non-default values
|
||||
state.activeTool = 'merge';
|
||||
state.files = [{ name: 'dummy.pdf', size: 1234 } as File];
|
||||
state.pdfDoc = { numPages: 5 }; // Mock PDF document object
|
||||
state.pdfPages = [{}, {}]; // Mock page objects
|
||||
state.currentPdfUrl = 'blob:http://localhost/some-uuid';
|
||||
|
||||
// 2. Create the DOM element that the function interacts with
|
||||
// The setup.ts file will clean this up automatically after each test.
|
||||
document.body.innerHTML = '<div id="tool-content">Some old tool content</div>';
|
||||
});
|
||||
|
||||
it('should reset all state properties to their initial values', () => {
|
||||
// Call the function to test
|
||||
resetState();
|
||||
|
||||
// Assert that all properties are back to their default state
|
||||
expect(state.activeTool).toBeNull();
|
||||
expect(state.files).toEqual([]);
|
||||
expect(state.pdfDoc).toBeNull();
|
||||
expect(state.pdfPages).toEqual([]);
|
||||
expect(state.currentPdfUrl).toBeNull();
|
||||
});
|
||||
|
||||
it('should clear the innerHTML of the #tool-content element', () => {
|
||||
const toolContentElement = document.getElementById('tool-content');
|
||||
|
||||
// Sanity check: ensure the content exists before resetting
|
||||
expect(toolContentElement?.innerHTML).toBe('Some old tool content');
|
||||
|
||||
// Call the function to test
|
||||
resetState();
|
||||
|
||||
// Assert that the element's content has been cleared
|
||||
expect(toolContentElement?.innerHTML).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
52
src/tests/tools.test.ts
Normal file
52
src/tests/tools.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// toolCategories.test.ts
|
||||
|
||||
import { categories } from '@/js/config/tools';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('Tool Categories Configuration', () => {
|
||||
|
||||
// 1. Basic Structure and Type Checking
|
||||
it('should be an array of category objects', () => {
|
||||
expect(Array.isArray(categories)).toBe(true);
|
||||
expect(categories.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// 2. Loop through each category to perform specific checks
|
||||
describe.each(categories)('Category: "$name"', (category) => {
|
||||
|
||||
// Check that the category object itself is well-formed
|
||||
it('should have a non-empty "name" string and a non-empty "tools" array', () => {
|
||||
expect(typeof category.name).toBe('string');
|
||||
expect(category.name.length).toBeGreaterThan(0);
|
||||
expect(Array.isArray(category.tools)).toBe(true);
|
||||
expect(category.tools.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// **KEY CHANGE**: This test now ensures IDs are unique only WITHIN this specific category.
|
||||
it('should not contain any duplicate tool IDs within its own list', () => {
|
||||
const toolIds = category.tools.map(tool => tool.id);
|
||||
const uniqueToolIds = new Set(toolIds);
|
||||
|
||||
// This assertion checks for duplicates inside THIS category only.
|
||||
expect(uniqueToolIds.size).toBe(toolIds.length);
|
||||
});
|
||||
|
||||
// 3. Loop through each tool inside the category to validate its schema
|
||||
describe.each(category.tools)('Tool: "$name"', (tool) => {
|
||||
it('should have the correct properties with non-empty string values', () => {
|
||||
// Check for property existence
|
||||
expect(tool).toHaveProperty('id');
|
||||
expect(tool).toHaveProperty('name');
|
||||
expect(tool).toHaveProperty('icon');
|
||||
expect(tool).toHaveProperty('subtitle');
|
||||
|
||||
// Check for non-empty string types for each property
|
||||
for (const key of ['id', 'name', 'icon', 'subtitle']) {
|
||||
const value = tool[key as keyof typeof tool];
|
||||
expect(typeof value).toBe('string');
|
||||
expect(value.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user