Files
ds-galo/frontend/src/lib/curve-presets.test.ts
T
iu 0bfee5f2f8 feat(photos): добавить пресеты кривой и починить переключение фото
- PCHIP для предпросмотра и серверного рендера
- пресеты всех каналов в профиле, импорт и экспорт
- мерж настроек auth без потери разделов
- сохранение вкладки и правок текущего фото при переключении
2026-10-23 22:56:55 +03:00

167 lines
5.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
BUILTIN_CURVE_PRESETS,
MAX_CURVE_PRESETS,
exportCurvePresetFile,
findActiveCurvePreset,
normalizeCurvePreset,
normalizeCurvePresets,
parseCurvePresetFile,
presetPointsEqual,
upsertCurvePresets
} from './curve-presets';
import type { CurvePreset } from './types';
const linear = () => [
{ x: 0, y: 0 },
{ x: 1, y: 1 }
];
const medium = BUILTIN_CURVE_PRESETS.find((preset) => preset.name === 'Средний контраст')!;
const custom: CurvePreset = {
name: 'Мой',
channels: {
rgb: [
{ x: 0, y: 0 },
{ x: 0.5, y: 0.6 },
{ x: 1, y: 1 }
],
red: linear(),
green: linear(),
blue: linear()
}
};
describe('пресеты кривой', () => {
it('содержит встроенные пресеты со всеми четырьмя каналами', () => {
expect(BUILTIN_CURVE_PRESETS.map((preset) => preset.name)).toEqual([
'Линейный',
'Средний контраст',
'Сильный контраст'
]);
for (const preset of BUILTIN_CURVE_PRESETS) {
expect(Object.keys(preset.channels).sort()).toEqual(['blue', 'green', 'red', 'rgb']);
for (const channel of ['rgb', 'red', 'green', 'blue'] as const) {
expect(preset.channels[channel].length).toBeGreaterThanOrEqual(2);
}
}
// Контраст задаётся только мастер-кривой, остальные каналы линейные.
expect(medium.channels.rgb).not.toEqual(linear());
expect(medium.channels.red).toEqual(linear());
});
it('валидирует и нормализует пресет со всеми каналами', () => {
expect(normalizeCurvePreset(custom)).toEqual(custom);
expect(normalizeCurvePreset({ ...custom, name: ' Мой ' })).toEqual(custom);
expect(normalizeCurvePreset({ name: 'x', channels: { rgb: linear() } })).toBeNull();
expect(
normalizeCurvePreset({
...custom,
channels: { ...custom.channels, green: [{ x: 0, y: 0 }] }
})
).toBeNull();
expect(
normalizeCurvePreset({
...custom,
channels: { ...custom.channels, blue: [{ x: 0, y: 0 }, { x: 1.2, y: 1 }] }
})
).toBeNull();
expect(normalizeCurvePreset({ ...custom, name: 'a'.repeat(65) })).toBeNull();
});
it('конвертирует старый формат одного канала', () => {
const legacy = normalizeCurvePreset({
name: 'Старый',
channel: 'red',
points: [
{ x: 0, y: 0 },
{ x: 1, y: 0.4 }
]
});
expect(legacy?.channels.rgb).toEqual(linear());
expect(legacy?.channels.red).toEqual([
{ x: 0, y: 0 },
{ x: 1, y: 0.4 }
]);
expect(legacy?.channels.green).toEqual(linear());
expect(legacy?.channels.blue).toEqual(linear());
});
it('сортирует точки и отбрасывает дубликаты', () => {
const preset = normalizeCurvePreset({
...custom,
channels: {
...custom.channels,
rgb: [
{ x: 1, y: 1 },
{ x: 0, y: 0 },
{ x: 0.5, y: 0.6 }
]
}
});
expect(preset?.channels.rgb.map((point) => point.x)).toEqual([0, 0.5, 1]);
});
it('нормализует список из профиля и убирает повторы имён', () => {
expect(normalizeCurvePresets([custom, custom, { name: 'bad' }])).toEqual([custom]);
expect(normalizeCurvePresets('nope')).toEqual([]);
});
it('разбирает и сериализует файл экспорта', () => {
const text = exportCurvePresetFile([custom]);
expect(parseCurvePresetFile(text)).toEqual([custom]);
expect(() => parseCurvePresetFile('not json')).toThrow();
expect(() => parseCurvePresetFile(JSON.stringify({ version: 2, presets: [] }))).toThrow();
expect(() => parseCurvePresetFile(JSON.stringify({ version: 1, presets: [{ name: 'x' }] }))).toThrow();
expect(() =>
parseCurvePresetFile(JSON.stringify({ version: 1, presets: [custom, custom] }))
).toThrow();
});
it('добавляет и перезаписывает пресеты по имени', () => {
const changed: CurvePreset = {
name: 'Мой',
channels: { ...custom.channels, blue: [{ x: 0, y: 0 }, { x: 1, y: 0.5 }] }
};
const next = upsertCurvePresets([custom], [changed]);
expect(next).toHaveLength(1);
expect(next[0].channels.blue).toEqual(changed.channels.blue);
// Копии не должны делить массивы с исходным пресетом.
expect(next[0].channels.red).not.toBe(custom.channels.red);
const other: CurvePreset = { ...custom, name: 'Другой' };
expect(upsertCurvePresets([custom], [other]).map((preset) => preset.name)).toEqual([
'Мой',
'Другой'
]);
expect(() =>
upsertCurvePresets(
Array.from({ length: MAX_CURVE_PRESETS }, (_, i) => ({ ...custom, name: `p${i}` })),
[other]
)
).toThrow();
});
it('находит активный пресет по кривым всех каналов', () => {
expect(findActiveCurvePreset(medium.channels, [])?.name).toBe('Средний контраст');
expect(findActiveCurvePreset(custom.channels, [custom])?.name).toBe('Мой');
expect(
findActiveCurvePreset(
{ ...custom.channels, blue: [{ x: 0, y: 0 }, { x: 1, y: 0.5 }] },
[custom]
)
).toBeUndefined();
expect(
findActiveCurvePreset(
{ rgb: linear(), red: linear(), green: linear(), blue: linear() },
[]
)?.name
).toBe('Линейный');
});
it('сравнивает точки с допуском', () => {
expect(presetPointsEqual([{ x: 0, y: 0 }], [{ x: 1e-9, y: -1e-9 }])).toBe(true);
expect(presetPointsEqual([{ x: 0, y: 0 }], [{ x: 0.1, y: 0 }])).toBe(false);
});
});