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,555 @@
import {
cleanup,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
AdvancedOperations,
type AdvancedCapabilityKey,
type AdvancedCapabilityStatus,
type AdvancedOperationsProps,
} from '../../src/components/AdvancedOperations';
const AVAILABLE: AdvancedCapabilityStatus = { available: true };
function capabilities(
...keys: readonly AdvancedCapabilityKey[]
): AdvancedOperationsProps['availability'] {
return Object.fromEntries(keys.map((key) => [key, AVAILABLE]));
}
const SOURCE: NonNullable<AdvancedOperationsProps['source']> = {
durationSeconds: 90,
sourceExtension: 'mp4',
hasAudio: true,
hasVideo: true,
selectedAudioStreamIndex: 1,
};
const EXPORT_PRESETS: NonNullable<AdvancedOperationsProps['exportPresets']> = [
{
id: 'mp4-h264-balanced',
name: 'MP4 · H.264 + AAC',
fileExtension: 'mp4',
supportsAudio: true,
supportsVideo: true,
},
{
id: 'audio-mp3',
name: 'MP3',
fileExtension: 'mp3',
supportsAudio: true,
supportsVideo: false,
},
];
afterEach(cleanup);
describe('AdvancedOperations', () => {
it('fails closed when a runtime capability was not explicitly verified', () => {
render(
<AdvancedOperations
source={SOURCE}
splitMarkers={[30]}
onSplit={vi.fn()}
/>
);
const action = screen.getByRole('button', { name: 'Queue 2 segments' });
expect(action).toBeDisabled();
expect(
screen.getByText(
'The required browser and FFmpeg capabilities have not been verified.'
)
).toBeInTheDocument();
});
it('builds a validated marker request and explains fast versus accurate mode', async () => {
const user = userEvent.setup();
const onSplit = vi.fn();
render(
<AdvancedOperations
source={SOURCE}
splitMarkers={[60, 30]}
exportPresets={EXPORT_PRESETS}
availability={capabilities('split-fast', 'split-accurate')}
onSplit={onSplit}
/>
);
expect(
screen.getByText(/boundaries may move to nearby keyframes/iu)
).toBeInTheDocument();
expect(screen.getByText(/practical frame accuracy/iu)).toBeInTheDocument();
const ranges = screen.getByLabelText('Exact planned segment ranges');
expect(within(ranges).getByText('0 s30 s')).toBeVisible();
expect(within(ranges).getByText('30 s60 s')).toBeVisible();
expect(within(ranges).getByText('60 s90 s')).toBeVisible();
await user.click(screen.getByRole('button', { name: 'Queue 3 segments' }));
await waitFor(() =>
expect(onSplit).toHaveBeenCalledWith({
mode: 'fast',
definition: { type: 'markers', markers: [30, 60] },
targetExtension: 'mp4',
})
);
await user.click(
screen.getByRole('radio', { name: /Accurate re-encode/iu })
);
await user.click(screen.getByRole('button', { name: 'Queue 3 segments' }));
await waitFor(() =>
expect(onSplit).toHaveBeenLastCalledWith({
mode: 'accurate',
definition: { type: 'markers', markers: [30, 60] },
targetExtension: 'mp4',
presetId: 'mp4-h264-balanced',
})
);
});
it('shows exact latest-batch status and individual segment failure details', () => {
render(
<AdvancedOperations
source={SOURCE}
splitMarkers={[30]}
splitBatch={{
planId: 'split-plan',
segments: [
{
outputId: 'segment-1',
fileName: 'source-split-001.mp4',
startSeconds: 0,
endSeconds: 30,
state: 'verified',
},
{
outputId: 'segment-2',
fileName: 'source-split-002.mp4',
startSeconds: 30,
endSeconds: 90,
state: 'failed',
message: 'FFmpeg did not return this expected output.',
},
],
}}
/>
);
const status = screen.getByLabelText('Split segment status');
expect(within(status).getByText('source-split-001.mp4')).toBeVisible();
expect(within(status).getByText('0 s30 s')).toBeVisible();
expect(within(status).getByText('verified')).toBeVisible();
expect(
within(status).getByText('FFmpeg did not return this expected output.')
).toBeVisible();
});
it('supports keyboard tab navigation and emits typed loudness targets', async () => {
const user = userEvent.setup();
const onAudioAction = vi.fn();
render(
<AdvancedOperations
source={SOURCE}
exportPresets={EXPORT_PRESETS}
availability={capabilities(
'loudness-measure',
'loudness-normalize',
'peak-normalize',
'waveform-static',
'waveform-peaks'
)}
onAudioAction={onAudioAction}
/>
);
const splitTab = screen.getByRole('tab', { name: 'Split media' });
splitTab.focus();
await user.keyboard('{ArrowRight}');
expect(
screen.getByRole('tab', { name: 'Waveform and loudness' })
).toHaveAttribute('aria-selected', 'true');
await user.selectOptions(
screen.getByLabelText(/^Target profile/iu),
'podcast'
);
await user.click(screen.getByRole('button', { name: 'Measure EBU R128' }));
await waitFor(() =>
expect(onAudioAction).toHaveBeenCalledWith({
type: 'loudness-measurement',
audioStreamIndex: 1,
targets: {
integratedLufs: -16,
truePeakDbtp: -1,
loudnessRangeLu: 11,
},
})
);
const normalize = screen.getByRole('button', {
name: 'Apply measured normalization',
});
expect(normalize).toBeDisabled();
expect(
screen.getByText(
'Run the first-pass loudness measurement before normalization.'
)
).toBeInTheDocument();
});
it('uses an audio-only preset for measured normalization', async () => {
const user = userEvent.setup();
const onAudioAction = vi.fn();
const measurement = {
inputIntegratedLufs: -19.7,
inputTruePeakDbtp: -2.1,
inputLoudnessRangeLu: 4.2,
inputThresholdLufs: -30,
targetOffsetLu: -0.3,
};
render(
<AdvancedOperations
source={SOURCE}
exportPresets={EXPORT_PRESETS}
loudnessMeasurement={measurement}
availability={capabilities('loudness-normalize')}
onAudioAction={onAudioAction}
/>
);
await user.click(
screen.getByRole('tab', { name: 'Waveform and loudness' })
);
await user.click(
screen.getByRole('button', { name: 'Apply measured normalization' })
);
await waitFor(() =>
expect(onAudioAction).toHaveBeenCalledWith({
type: 'loudness-normalization',
audioStreamIndex: 1,
targets: {
integratedLufs: -14,
truePeakDbtp: -1,
loudnessRangeLu: 11,
},
measurement,
presetId: 'audio-mp3',
})
);
});
it('keeps unverified contact-sheet labels off and emits a bounded series request', async () => {
const user = userEvent.setup();
const onThumbnailSeries = vi.fn();
render(
<AdvancedOperations
source={SOURCE}
availability={capabilities('thumbnail-series', 'contact-sheet')}
imageFormatAvailability={{ jpeg: AVAILABLE }}
onThumbnailSeries={onThumbnailSeries}
onContactSheet={vi.fn()}
/>
);
await user.click(
screen.getByRole('tab', {
name: 'Thumbnails and contact sheets',
})
);
expect(screen.getByLabelText('Timestamp labels')).toBeDisabled();
expect(screen.getByLabelText('Source filename label')).toBeDisabled();
expect(
screen.getByText(
'Labels stay off until drawtext and a safe local font are verified.'
)
).toBeInTheDocument();
await user.click(
screen.getByRole('button', { name: 'Generate thumbnail series' })
);
await waitFor(() =>
expect(onThumbnailSeries).toHaveBeenCalledWith({
distribution: { type: 'evenly-spaced', count: 12 },
width: 480,
format: 'jpeg',
quality: 85,
})
);
});
it('classifies bitmap subtitles conservatively before enabling extraction', async () => {
const user = userEvent.setup();
const onSubtitleAction = vi.fn();
render(
<AdvancedOperations
source={SOURCE}
subtitleStreams={[
{ index: 2, codec: 'dvd_subtitle', language: 'deu' },
{ index: 3, codec: 'subrip', language: 'eng' },
]}
availability={capabilities('subtitle-extract')}
onSubtitleAction={onSubtitleAction}
/>
);
await user.click(screen.getByRole('tab', { name: 'Subtitle operations' }));
expect(
screen.getByRole('button', { name: 'Extract subtitle' })
).toBeDisabled();
expect(
screen.getByText(/bitmap subtitle extraction is not text conversion/iu)
).toBeInTheDocument();
await user.selectOptions(screen.getByLabelText('Subtitle stream'), '3');
await user.click(screen.getByRole('button', { name: 'Extract subtitle' }));
await waitFor(() =>
expect(onSubtitleAction).toHaveBeenCalledWith({
type: 'extract',
stream: { index: 3, codec: 'subrip', language: 'eng' },
outputFormat: 'srt',
})
);
});
it('retains validated stream metadata JSON for apply and export', async () => {
const user = userEvent.setup();
const onMetadataAction = vi.fn();
render(
<AdvancedOperations
source={SOURCE}
availability={capabilities('metadata-write')}
onMetadataAction={onMetadataAction}
/>
);
await user.click(screen.getByRole('tab', { name: 'Metadata policies' }));
await user.upload(
screen.getByLabelText('Import metadata JSON'),
new File(
[
JSON.stringify({
format: { title: 'Imported title', genre: 'Synthetic' },
streams: {
1: { language: 'deu', title: 'German dialogue' },
},
}),
],
'metadata.json',
{ type: 'application/json' }
)
);
await waitFor(() =>
expect(onMetadataAction).toHaveBeenCalledWith({
type: 'import',
edits: {
format: { title: 'Imported title', genre: 'Synthetic' },
streams: {
1: { language: 'deu', title: 'German dialogue' },
},
},
})
);
expect(screen.getByText(/1 stream metadata edit loaded/iu)).toBeVisible();
await user.click(
screen.getByRole('button', { name: 'Apply metadata policies' })
);
await waitFor(() =>
expect(onMetadataAction).toHaveBeenLastCalledWith({
type: 'apply',
edits: {
format: { genre: 'Synthetic', title: 'Imported title' },
streams: {
1: { language: 'deu', title: 'German dialogue' },
},
},
sourceMetadataPolicy: 'copy',
chapterPolicy: 'keep',
coverArtPolicy: 'keep',
})
);
});
it('edits probed stream language, title, and typed dispositions directly', async () => {
const user = userEvent.setup();
const onMetadataAction = vi.fn();
render(
<AdvancedOperations
source={{ ...SOURCE, sourceId: 'source-a' }}
metadataStreams={[
{
index: 1,
type: 'audio',
codec: 'aac',
language: 'eng',
title: 'Original',
disposition: { default: false, forced: false },
},
]}
availability={capabilities('metadata-write')}
onMetadataAction={onMetadataAction}
/>
);
await user.click(screen.getByRole('tab', { name: 'Metadata policies' }));
const language = screen.getByLabelText('Stream #1 language');
await user.clear(language);
await user.type(language, 'deu');
const title = screen.getByLabelText('Stream #1 title');
await user.clear(title);
await user.type(title, 'German dialogue');
await user.click(screen.getByLabelText('Default stream #1'));
await user.click(
screen.getByRole('button', { name: 'Apply metadata policies' })
);
await waitFor(() =>
expect(onMetadataAction).toHaveBeenCalledWith({
type: 'apply',
edits: {
format: {},
streams: {
1: { language: 'deu', title: 'German dialogue' },
},
dispositions: { 1: ['default'] },
},
sourceMetadataPolicy: 'copy',
chapterPolicy: 'keep',
coverArtPolicy: 'keep',
})
);
});
it('does not clear untouched source container tags', async () => {
const user = userEvent.setup();
const onMetadataAction = vi.fn();
render(
<AdvancedOperations
source={{ ...SOURCE, sourceId: 'source-tags' }}
metadataFormatTags={{
title: 'Original title',
artist: 'Original artist',
album: 'Original album',
}}
availability={capabilities('metadata-write')}
onMetadataAction={onMetadataAction}
/>
);
await user.click(screen.getByRole('tab', { name: 'Metadata policies' }));
expect(screen.getByLabelText('Artist')).toHaveValue('Original artist');
const title = screen.getByLabelText('Title');
await user.clear(title);
await user.type(title, 'Replacement title');
await user.click(
screen.getByRole('button', { name: 'Apply metadata policies' })
);
await waitFor(() =>
expect(onMetadataAction).toHaveBeenCalledWith({
type: 'apply',
edits: { format: { title: 'Replacement title' } },
sourceMetadataPolicy: 'copy',
chapterPolicy: 'keep',
coverArtPolicy: 'keep',
})
);
});
it('creates only an allowlisted typed user preset', async () => {
const user = userEvent.setup();
const onUserPresetAction = vi.fn();
render(
<AdvancedOperations
availability={capabilities('preset-storage')}
onUserPresetAction={onUserPresetAction}
/>
);
await user.click(screen.getByRole('tab', { name: 'User export presets' }));
const editor = screen
.getByRole('heading', { name: 'Create typed preset' })
.closest('section');
expect(editor).not.toBeNull();
const editorQueries = within(editor as HTMLElement);
await user.type(editorQueries.getByLabelText('Safe ID'), 'my-web-export');
await user.type(editorQueries.getByLabelText('Name'), 'My web export');
await user.click(
editorQueries.getByRole('button', { name: 'Create preset' })
);
await waitFor(() =>
expect(onUserPresetAction).toHaveBeenCalledWith({
type: 'create',
preset: {
schemaVersion: 1,
id: 'my-web-export',
name: 'My web export',
kind: 'video',
container: 'mp4',
fileExtension: 'mp4',
video: {
codec: 'libx264',
qualityMode: 'balanced',
crf: 23,
},
audio: {
codec: 'aac',
bitrateKbps: 160,
sampleRate: 48000,
channels: 2,
},
metadataPolicy: 'copy',
chapterPolicy: 'keep',
subtitlePolicy: 'none',
fastStart: true,
},
})
);
expect(JSON.stringify(onUserPresetAction.mock.calls)).not.toMatch(
/(?:args|filter|protocol|inputPath)/u
);
});
it('does not offer preset policies that require explicit advanced inputs', async () => {
const user = userEvent.setup();
render(
<AdvancedOperations
availability={capabilities('preset-storage')}
onUserPresetAction={vi.fn()}
/>
);
await user.click(screen.getByRole('tab', { name: 'User export presets' }));
const editor = screen
.getByRole('heading', { name: 'Create typed preset' })
.closest('section');
expect(editor).not.toBeNull();
const editorQueries = within(editor as HTMLElement);
expect(
within(editorQueries.getByLabelText('Metadata')).queryByRole('option', {
name: 'Apply edits',
})
).not.toBeInTheDocument();
expect(
within(editorQueries.getByLabelText('Chapters')).queryByRole('option', {
name: 'Replace',
})
).not.toBeInTheDocument();
expect(
within(editorQueries.getByLabelText('Subtitles')).queryByRole('option', {
name: 'Burn in',
})
).not.toBeInTheDocument();
expect(
editorQueries.getByText(/remain explicit operations/iu)
).toBeVisible();
});
});