Files
av-tools/tests/unit/export-results.test.ts

170 lines
5.2 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import {
collectCommandOutputs,
DEFAULT_RESULT_COLLECTION_LIMITS,
DEFAULT_RESULT_ZIP_LIMITS,
ExportResultCollection,
ExportResultLimitError,
MissingCommandOutputError,
saveCollectionResult,
type BlobSaver,
type ExportResultInput,
} from '../../src/export';
import type { FFmpegCommandPlan } from '../../src/commands';
function input(
id: string,
fileName = 'result.mp4',
contents = 'result'
): ExportResultInput {
return {
id,
planId: 'plan',
outputId: id,
fileName,
blob: new Blob([contents]),
mimeType: 'video/mp4',
role: 'media',
};
}
describe('export result collection', () => {
it('reserves one ZIP entry for the optional report', () => {
expect(DEFAULT_RESULT_COLLECTION_LIMITS.maxResults).toBe(255);
expect(DEFAULT_RESULT_ZIP_LIMITS.maxEntries).toBe(
DEFAULT_RESULT_COLLECTION_LIMITS.maxResults + 1
);
});
it('owns result Blobs and revokes every replaced or released object URL', () => {
let urlIndex = 0;
const revokeObjectURL = vi.fn();
const collection = new ExportResultCollection({
objectUrlApi: {
createObjectURL: () => `blob:result-${urlIndex++}`,
revokeObjectURL,
},
now: () => new Date('2026-07-24T10:00:00.000Z'),
});
const first = collection.add(input('one'));
expect(first).toMatchObject({
fileName: 'result.mp4',
mimeType: 'video/mp4',
size: 6,
createdAt: '2026-07-24T10:00:00.000Z',
});
expect(collection.objectUrl('one')).toBe('blob:result-0');
expect(collection.objectUrl('one')).toBe('blob:result-0');
collection.add(input('two', 'RESULT.mp4'));
expect(collection.get('two')?.fileName).toBe('RESULT-2.mp4');
collection.replace(input('one', 'replacement.mp4', 'new'));
expect(revokeObjectURL).toHaveBeenCalledWith('blob:result-0');
expect(collection.totalBytes).toBe(9);
collection.objectUrl('two');
expect(collection.remove('two')).toBe(true);
expect(revokeObjectURL).toHaveBeenCalledWith('blob:result-1');
collection.dispose();
expect(collection.totalBytes).toBe(0);
expect(() => collection.list()).toThrow(/disposed/iu);
expect(() => collection.dispose()).not.toThrow();
});
it('adds a batch atomically and enforces count and byte limits', () => {
const collection = new ExportResultCollection({
limits: {
maxResults: 2,
maxSingleResultBytes: 5,
maxTotalBytes: 8,
},
});
expect(() =>
collection.addMany([
input('one', 'one.bin', '1234'),
input('bad', 'bad.bin', '123456'),
])
).toThrowError(ExportResultLimitError);
expect(collection.size).toBe(0);
collection.add(input('one', 'one.bin', '1234'));
expect(() => collection.add(input('two', 'two.bin', '12345'))).toThrowError(
expect.objectContaining({ limit: 'total-result-bytes' })
);
expect(collection.size).toBe(1);
});
it('collects all planned outputs atomically from IDs or virtual paths', () => {
const collection = new ExportResultCollection();
const plan: Pick<FFmpegCommandPlan, 'id' | 'outputs'> = {
id: 'split-plan',
outputs: [
{
id: 'one',
path: '/work/job/one.mp4',
fileName: 'clip-001.mp4',
mediaType: 'video/mp4',
role: 'media',
timeRange: { startSeconds: 0, endSeconds: 5 },
},
{
id: 'two',
path: '/work/job/two.mp4',
fileName: 'clip-002.mp4',
mediaType: 'video/mp4',
role: 'media',
},
],
};
const results = collectCommandOutputs(
plan,
new Map<string, Blob | Uint8Array>([
['one', Uint8Array.of(1, 2)],
['/work/job/two.mp4', new Blob(['two'])],
]),
collection
);
expect(results.map((result) => result.outputId)).toEqual(['one', 'two']);
expect(results[0]?.blob.type).toBe('video/mp4');
expect(results[0]?.timeRange).toEqual({
startSeconds: 0,
endSeconds: 5,
});
const empty = new ExportResultCollection();
expect(() =>
collectCommandOutputs(plan, new Map([['one', new Blob(['one'])]]), empty)
).toThrowError(MissingCommandOutputError);
expect(empty.size).toBe(0);
});
});
describe('browser result saving', () => {
it('sanitizes the download name and releases a successfully saved result', async () => {
const collection = new ExportResultCollection();
collection.add(input('one', 'result.mp4'));
const saver = vi.fn<BlobSaver>(async () => 'downloaded');
await expect(
saveCollectionResult(collection, 'one', {
fileName: '../../My result.mp4',
saver,
})
).resolves.toBe('downloaded');
expect(saver).toHaveBeenCalledWith(
expect.any(Blob),
'My-result.mp4',
'video/mp4'
);
expect(collection.size).toBe(0);
});
it('retains the result when the save picker is cancelled', async () => {
const collection = new ExportResultCollection();
collection.add(input('one'));
const saver: BlobSaver = async () => 'cancelled';
await saveCollectionResult(collection, 'one', { saver });
expect(collection.size).toBe(1);
});
});