feat: publish av-tools 0.1.0
This commit is contained in:
179
tests/unit/project/project-serialization.test.ts
Normal file
179
tests/unit/project/project-serialization.test.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
ProjectImportError,
|
||||
importProjectDocument,
|
||||
projectDocumentFileName,
|
||||
serializeProject,
|
||||
} from '../../../src/project/project.serialization';
|
||||
import { ProjectValidationError } from '../../../src/project/project.validation';
|
||||
import { populatedProject } from './project-fixture';
|
||||
|
||||
describe('project serialization', () => {
|
||||
it('serializes readable, versioned JSON and can omit transient UI state', () => {
|
||||
const project = populatedProject();
|
||||
const json = serializeProject(project, { includeUiState: false });
|
||||
const parsed = JSON.parse(json) as Record<string, unknown>;
|
||||
|
||||
expect(json.endsWith('\n')).toBe(true);
|
||||
expect(parsed.schemaVersion).toBe(1);
|
||||
expect(parsed.ui).toBeUndefined();
|
||||
expect(project.ui).toBeDefined();
|
||||
});
|
||||
|
||||
it('validates before serializing', () => {
|
||||
const project = {
|
||||
...populatedProject(),
|
||||
output: { ...populatedProject().output, fileName: '' },
|
||||
};
|
||||
expect(() => serializeProject(project)).toThrow(ProjectValidationError);
|
||||
});
|
||||
|
||||
it('imports a round trip but correctly marks browser sources missing', () => {
|
||||
const project = populatedProject();
|
||||
const result = importProjectDocument(serializeProject(project));
|
||||
|
||||
expect(result.fromVersion).toBe(1);
|
||||
expect(result.project.assets[0]?.sourceStatus).toBe('missing');
|
||||
expect(result.warnings).toContainEqual(
|
||||
expect.objectContaining({ code: 'source-reattachment-required' })
|
||||
);
|
||||
});
|
||||
|
||||
it('can retain runtime source status only when explicitly requested', () => {
|
||||
const project = populatedProject();
|
||||
const result = importProjectDocument(serializeProject(project), {
|
||||
markSourcesMissing: false,
|
||||
});
|
||||
expect(result.project).toEqual(project);
|
||||
expect(result.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it('round-trips fade curves and editable chapter time bases', () => {
|
||||
const project = populatedProject();
|
||||
const persisted = {
|
||||
...project,
|
||||
timeline: [
|
||||
{
|
||||
...project.timeline[0]!,
|
||||
audio: {
|
||||
enabled: true,
|
||||
fadeInSeconds: 0.2,
|
||||
fadeCurve: 'qsin' as const,
|
||||
},
|
||||
video: {
|
||||
enabled: true,
|
||||
fadeOutSeconds: 0.2,
|
||||
fadeCurve: 'exp' as const,
|
||||
},
|
||||
},
|
||||
],
|
||||
chapters: [
|
||||
{
|
||||
id: 'chapter-1',
|
||||
title: 'Intro',
|
||||
startSeconds: 0,
|
||||
endSeconds: 1,
|
||||
timeBase: '1/48000',
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = importProjectDocument(serializeProject(persisted), {
|
||||
markSourcesMissing: false,
|
||||
});
|
||||
|
||||
expect(result.project.timeline[0]?.audio?.fadeCurve).toBe('qsin');
|
||||
expect(result.project.timeline[0]?.video?.fadeCurve).toBe('exp');
|
||||
expect(result.project.chapters[0]?.timeBase).toBe('1/48000');
|
||||
});
|
||||
|
||||
it('still reports reattachment when an exported project was already missing sources', () => {
|
||||
const project = {
|
||||
...populatedProject(),
|
||||
assets: [
|
||||
{ ...populatedProject().assets[0]!, sourceStatus: 'missing' as const },
|
||||
],
|
||||
};
|
||||
const result = importProjectDocument(serializeProject(project));
|
||||
expect(result.warnings).toContainEqual(
|
||||
expect.objectContaining({ code: 'source-reattachment-required' })
|
||||
);
|
||||
});
|
||||
|
||||
it('migrates the documented legacy version zero shape', () => {
|
||||
const legacy = {
|
||||
schemaVersion: 0,
|
||||
id: 'legacy-project',
|
||||
title: 'Legacy project',
|
||||
createdAt: '2025-01-01T00:00:00.000Z',
|
||||
updatedAt: '2025-01-01T00:00:00.000Z',
|
||||
media: [
|
||||
{
|
||||
id: 'legacy-source',
|
||||
fileName: 'legacy.mp4',
|
||||
size: 42,
|
||||
lastModified: 100,
|
||||
type: 'video/mp4',
|
||||
sourceStatus: 'attached',
|
||||
},
|
||||
],
|
||||
clips: [
|
||||
{
|
||||
id: 'legacy-clip',
|
||||
assetId: 'legacy-source',
|
||||
inSeconds: 0,
|
||||
outSeconds: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = importProjectDocument(JSON.stringify(legacy));
|
||||
|
||||
expect(result.fromVersion).toBe(0);
|
||||
expect(result.project).toMatchObject({
|
||||
schemaVersion: 1,
|
||||
name: 'Legacy project',
|
||||
assets: [
|
||||
{
|
||||
originalName: 'legacy.mp4',
|
||||
mimeType: 'video/mp4',
|
||||
sourceStatus: 'missing',
|
||||
},
|
||||
],
|
||||
timeline: [
|
||||
{
|
||||
sourceInSeconds: 0,
|
||||
sourceOutSeconds: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.warnings.map((warning) => warning.code)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'renamed-field',
|
||||
'defaulted-field',
|
||||
'source-reattachment-required',
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects malformed, unversioned, future and oversized documents', () => {
|
||||
expect(() => importProjectDocument('{')).toThrow(ProjectImportError);
|
||||
expect(() => importProjectDocument('{}')).toThrow(
|
||||
'does not declare a schema version'
|
||||
);
|
||||
expect(() => importProjectDocument('{"schemaVersion":999}')).toThrow(
|
||||
'is not supported'
|
||||
);
|
||||
expect(() =>
|
||||
importProjectDocument('{"schemaVersion":1}', { maximumBytes: 5 })
|
||||
).toThrow('exceeds');
|
||||
});
|
||||
|
||||
it('creates a safe project-document filename', () => {
|
||||
expect(
|
||||
projectDocumentFileName({
|
||||
...populatedProject(),
|
||||
name: '../../My Project',
|
||||
})
|
||||
).toBe('My-Project.avproject.json');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user