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