feat: publish av-tools 0.1.0
This commit is contained in:
199
tests/unit/project/project-validation.test.ts
Normal file
199
tests/unit/project/project-validation.test.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
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',
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user