feat: publish av-tools 0.1.0

This commit is contained in:
2026-07-24 14:58:03 +02:00
commit fcc537b024
254 changed files with 63270 additions and 0 deletions

View 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');
});
});