import { describe, expect, it } from 'vitest'; import { CATEGORIES } from './categories'; import { defaultParams, getTool, outputOf, sanitizeParams, TOOLS, type ParamDef, type ToolEntry } from './registry'; const PLAN_TOOL_IDS = [ 'resize-png', 'crop-png', 'rotate-png', 'flip-png', 'grayscale-png', 'invert-colors-png', 'adjust-brightness-contrast-png', 'convert-png-to-jpg', 'convert-png-to-webp', 'remove-color-from-png', 'png-info' ]; describe('реестр инструментов', () => { it('содержит ровно 11 инструментов из плана MVP', () => { expect(TOOLS.map((t) => t.id).sort()).toEqual([...PLAN_TOOL_IDS].sort()); }); it('id уникальны и в kebab-case', () => { const ids = TOOLS.map((t) => t.id); expect(new Set(ids).size).toBe(ids.length); for (const id of ids) { expect(id).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/); } }); it.each(TOOLS.map((t) => [t.id, t] as const))('%s: категория валидна', (_, tool) => { expect(CATEGORIES.map((c) => c.id)).toContain(tool.category); }); it.each(TOOLS.map((t) => [t.id, t] as const))('%s: run определён', (_, tool) => { expect(typeof tool.run).toBe('function'); if (tool.preview) { expect(typeof tool.preview).toBe('function'); } expect(tool.title.length).toBeGreaterThan(0); expect(tool.description.length).toBeGreaterThan(0); }); it('у select дефолт входит в options, у number/slider дефолт в диапазоне', () => { for (const tool of TOOLS) { for (const param of tool.params) { if (param.type === 'select') { expect(param.options.map((o) => o.value)).toContain(param.default); expect(param.options.length).toBeGreaterThan(0); } if (param.type === 'number') { expect(param.min === undefined || param.default >= param.min).toBe(true); expect(param.max === undefined || param.default <= param.max).toBe(true); } if (param.type === 'slider') { expect(param.min).toBeLessThan(param.max); expect(param.default).toBeGreaterThanOrEqual(param.min); expect(param.default).toBeLessThanOrEqual(param.max); } if (param.type === 'color') { expect(param.default).toMatch(/^#[0-9a-f]{6}$/i); } } } }); it('id параметров уникальны внутри инструмента', () => { for (const tool of TOOLS) { const ids = tool.params.map((p) => p.id); expect(new Set(ids).size).toBe(ids.length); } }); it('qualityParamId ссылается на существующий числовой параметр', () => { for (const tool of TOOLS) { const output = outputOf(tool); if (output?.qualityParamId) { const param = tool.params.find( (p): p is Extract => p.id === output.qualityParamId ); expect(param).toBeDefined(); } } }); it('png-info не имеет формата вывода, конвертеры имеют jpeg/webp, остальные — png', () => { expect(outputOf(getToolOrThrow('png-info'))).toBeUndefined(); expect(outputOf(getToolOrThrow('convert-png-to-jpg'))?.mime).toBe('image/jpeg'); expect(outputOf(getToolOrThrow('convert-png-to-webp'))?.mime).toBe('image/webp'); expect(outputOf(getToolOrThrow('grayscale-png'))).toEqual({ mime: 'image/png', ext: 'png' }); }); it('defaultParams собирает значения по умолчанию', () => { const resize = getToolOrThrow('resize-png'); expect(defaultParams(resize)).toEqual({ width: 0, height: 0, keepAspect: true }); }); }); describe('sanitizeParams', () => { it('заменяет пустые и невалидные числа на дефолт', () => { const resize = getToolOrThrow('resize-png'); expect(sanitizeParams(resize, { width: undefined, height: null, keepAspect: true })).toEqual({ width: 0, height: 0, keepAspect: true }); }); it('клампит числа в диапазон параметра', () => { const bc = getToolOrThrow('adjust-brightness-contrast-png'); expect(sanitizeParams(bc, { brightness: 5000, contrast: -999 })).toEqual({ brightness: 100, contrast: -100 }); }); it('возвращает дефолт для невалидных select, checkbox и color', () => { const rotate = getToolOrThrow('rotate-png'); expect(sanitizeParams(rotate, { angle: '45' })).toEqual({ angle: '90' }); const resize = getToolOrThrow('resize-png'); expect(sanitizeParams(resize, { width: 10, height: 10, keepAspect: 'yes' })).toEqual({ width: 10, height: 10, keepAspect: true }); const jpg = getToolOrThrow('convert-png-to-jpg'); expect(sanitizeParams(jpg, { background: 'red', quality: 50 })).toEqual({ background: '#ffffff', quality: 50 }); }); }); describe('run инструмента resize-png', () => { const img = { width: 100, height: 50, data: new Uint8ClampedArray(100 * 50 * 4) }; const runResize = async (params: Record) => getToolOrThrow('resize-png').run(img, params); it('keepAspect + одна сторона — вторая считается по пропорции', async () => { const out = await runResize({ width: 200, height: 0, keepAspect: true }); expect(out.width).toBe(200); expect(out.height).toBe(100); }); it('keepAspect + обе стороны — вписывание в размеры без искажения', async () => { const out = await runResize({ width: 50, height: 50, keepAspect: true }); expect(out.width).toBe(50); expect(out.height).toBe(25); }); it('без keepAspect — обе стороны как задано', async () => { const out = await runResize({ width: 30, height: 40, keepAspect: false }); expect(out.width).toBe(30); expect(out.height).toBe(40); }); it('обе стороны 0 — человекочитаемая ошибка', async () => { await expect(runResize({ width: 0, height: 0, keepAspect: true })).rejects.toThrow( 'Укажите ширину' ); }); }); function getToolOrThrow(id: string): ToolEntry { const tool = getTool(id); if (!tool) throw new Error(`Инструмент "${id}" не найден`); return tool; }