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 = { durationSeconds: 90, sourceExtension: 'mp4', hasAudio: true, hasVideo: true, selectedAudioStreamIndex: 1, }; const EXPORT_PRESETS: NonNullable = [ { 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( ); 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( ); 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 s–30 s')).toBeVisible(); expect(within(ranges).getByText('30 s–60 s')).toBeVisible(); expect(within(ranges).getByText('60 s–90 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( ); const status = screen.getByLabelText('Split segment status'); expect(within(status).getByText('source-split-001.mp4')).toBeVisible(); expect(within(status).getByText('0 s–30 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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(); }); });