200 lines
5.2 KiB
TypeScript
200 lines
5.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
ProjectValidationError,
|
|
assertValidProject,
|
|
validateProject,
|
|
} from '../../../src/project/project.validation';
|
|
import { populatedProject } from './project-fixture';
|
|
|
|
describe('project validation', () => {
|
|
it('returns an isolated, typed copy for a valid version-1 project', () => {
|
|
const input = populatedProject();
|
|
const result = validateProject(input);
|
|
|
|
expect(result.valid).toBe(true);
|
|
if (result.valid) {
|
|
expect(result.project).toEqual(input);
|
|
expect(result.project).not.toBe(input);
|
|
}
|
|
});
|
|
|
|
it('reports duplicate identities, missing references and bad ranges', () => {
|
|
const project = populatedProject();
|
|
const invalid = {
|
|
...project,
|
|
assets: [...project.assets, { ...project.assets[0] }],
|
|
timeline: [
|
|
{
|
|
...project.timeline[0],
|
|
assetId: 'missing',
|
|
sourceInSeconds: 5,
|
|
sourceOutSeconds: 3,
|
|
crop: { x: -1, y: 0, width: 0, height: 100 },
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = validateProject(invalid);
|
|
expect(result.valid).toBe(false);
|
|
if (!result.valid) {
|
|
expect(result.issues.map((issue) => issue.code)).toEqual(
|
|
expect.arrayContaining([
|
|
'duplicate-id',
|
|
'missing-reference',
|
|
'invalid-range',
|
|
'invalid-value',
|
|
])
|
|
);
|
|
}
|
|
});
|
|
|
|
it('checks clip bounds, fade bounds, chapter bounds and output streams', () => {
|
|
const project = populatedProject();
|
|
const invalid = {
|
|
...project,
|
|
timeline: [
|
|
{
|
|
...project.timeline[0],
|
|
sourceOutSeconds: 11,
|
|
audio: {
|
|
enabled: true,
|
|
fadeInSeconds: 8,
|
|
fadeOutSeconds: 8,
|
|
},
|
|
},
|
|
],
|
|
chapters: [
|
|
{
|
|
id: 'chapter-1',
|
|
startSeconds: 0,
|
|
endSeconds: 20,
|
|
title: 'Too long',
|
|
},
|
|
],
|
|
output: {
|
|
...project.output,
|
|
videoEnabled: false,
|
|
audioEnabled: false,
|
|
},
|
|
};
|
|
const result = validateProject(invalid);
|
|
|
|
expect(result.valid).toBe(false);
|
|
if (!result.valid) {
|
|
expect(result.issues).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
code: 'invalid-range',
|
|
path: 'timeline[0]',
|
|
}),
|
|
expect.objectContaining({
|
|
code: 'out-of-bounds',
|
|
path: 'timeline[0].audio',
|
|
}),
|
|
expect.objectContaining({
|
|
code: 'out-of-bounds',
|
|
path: 'chapters[0].endSeconds',
|
|
}),
|
|
expect.objectContaining({
|
|
code: 'invalid-value',
|
|
path: 'output',
|
|
}),
|
|
])
|
|
);
|
|
}
|
|
});
|
|
|
|
it('validates persisted fade curves and chapter time-base resolution', () => {
|
|
const project = populatedProject();
|
|
const invalid = {
|
|
...project,
|
|
timeline: [
|
|
{
|
|
...project.timeline[0],
|
|
audio: {
|
|
enabled: true,
|
|
fadeInSeconds: 0.2,
|
|
fadeCurve: 'log',
|
|
},
|
|
},
|
|
],
|
|
chapters: [
|
|
{
|
|
id: 'chapter-1',
|
|
startSeconds: 0,
|
|
endSeconds: 0.1,
|
|
title: 'Too short for this base',
|
|
timeBase: '1/1',
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = validateProject(invalid);
|
|
expect(result.valid).toBe(false);
|
|
if (!result.valid) {
|
|
expect(result.issues).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
code: 'invalid-value',
|
|
path: 'timeline[0].audio.fadeCurve',
|
|
}),
|
|
expect.objectContaining({
|
|
code: 'invalid-value',
|
|
path: 'chapters[0].timeBase',
|
|
}),
|
|
])
|
|
);
|
|
}
|
|
});
|
|
|
|
it('rejects unsupported schema versions with a useful typed exception', () => {
|
|
const project = { ...populatedProject(), schemaVersion: 99 };
|
|
expect(() => assertValidProject(project)).toThrow(ProjectValidationError);
|
|
const result = validateProject(project);
|
|
expect(result.valid).toBe(false);
|
|
if (!result.valid) {
|
|
expect(result.issues[0]).toMatchObject({
|
|
code: 'unsupported-version',
|
|
path: 'schemaVersion',
|
|
});
|
|
}
|
|
});
|
|
|
|
it('does not trust malformed normalized probe fields from an import', () => {
|
|
const project = populatedProject();
|
|
const invalid = structuredClone(project) as unknown as {
|
|
assets: Array<{
|
|
probe?: {
|
|
streams: Array<{
|
|
width?: unknown;
|
|
rotationDegrees?: unknown;
|
|
}>;
|
|
};
|
|
}>;
|
|
};
|
|
const stream = invalid.assets[0]?.probe?.streams[0];
|
|
if (stream) {
|
|
stream.width = '1920';
|
|
stream.rotationDegrees = '90';
|
|
}
|
|
|
|
const result = validateProject(invalid);
|
|
expect(result.valid).toBe(false);
|
|
if (!result.valid) {
|
|
expect(result.issues).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'invalid-type',
|
|
path: 'assets[0].probe.streams[0].width',
|
|
})
|
|
);
|
|
expect(result.issues).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'invalid-type',
|
|
path: 'assets[0].probe.streams[0].rotationDegrees',
|
|
})
|
|
);
|
|
}
|
|
});
|
|
});
|