180 lines
5.2 KiB
TypeScript
180 lines
5.2 KiB
TypeScript
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');
|
|
});
|
|
});
|