Files
easy-png-tools/web/src/lib/registry.test.ts
T
2026-08-23 05:18:10 +05:00

179 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ParamDef, { type: 'number' | 'slider' }> =>
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<string, unknown>) =>
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;
}