feat: publish av-tools 0.1.0
This commit is contained in:
259
tests/unit/components/media-preview.test.tsx
Normal file
259
tests/unit/components/media-preview.test.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { ImportedMediaAsset } from '../../../src/app/application-state';
|
||||
import {
|
||||
MediaPreview,
|
||||
type PreviewExportResult,
|
||||
} from '../../../src/components/MediaPreview';
|
||||
import { aggregatePcmPeaks } from '../../../src/waveform';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(HTMLMediaElement.prototype, 'canPlayType').mockReturnValue(
|
||||
'probably'
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('MediaPreview', () => {
|
||||
it('offers bounded configurable proxy settings after playback fails', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCreateProxy = vi.fn();
|
||||
const onProxySettingsChange = vi.fn();
|
||||
const { container } = render(
|
||||
<MediaPreview
|
||||
asset={videoAsset()}
|
||||
onCreateProxy={onCreateProxy}
|
||||
proxySettings={{ maxDurationSeconds: 30, maxWidth: 960 }}
|
||||
onProxySettingsChange={onProxySettingsChange}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.error(requiredElement(container.querySelector('video')));
|
||||
|
||||
expect(screen.getByText('Create a disposable preview proxy')).toBeVisible();
|
||||
fireEvent.change(
|
||||
screen.getByRole('spinbutton', { name: 'Maximum seconds' }),
|
||||
{ target: { value: '60' } }
|
||||
);
|
||||
expect(onProxySettingsChange).toHaveBeenLastCalledWith({
|
||||
maxDurationSeconds: 60,
|
||||
maxWidth: 960,
|
||||
});
|
||||
await user.selectOptions(
|
||||
screen.getByRole('combobox', { name: 'Maximum width' }),
|
||||
'1280'
|
||||
);
|
||||
expect(onProxySettingsChange).toHaveBeenLastCalledWith({
|
||||
maxDurationSeconds: 30,
|
||||
maxWidth: 1280,
|
||||
});
|
||||
await user.click(screen.getByRole('button', { name: 'Create proxy' }));
|
||||
expect(onCreateProxy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('seeks a source preview externally and reports its actual playback time', () => {
|
||||
const onCurrentTimeChange = vi.fn();
|
||||
const { container, rerender } = render(
|
||||
<MediaPreview
|
||||
asset={videoAsset()}
|
||||
currentTimeSeconds={3.25}
|
||||
onCurrentTimeChange={onCurrentTimeChange}
|
||||
/>
|
||||
);
|
||||
|
||||
const video = requiredElement(container.querySelector('video'));
|
||||
expect(video.currentTime).toBeCloseTo(3.25);
|
||||
|
||||
video.currentTime = 7.75;
|
||||
fireEvent.timeUpdate(video);
|
||||
expect(onCurrentTimeChange).toHaveBeenLastCalledWith(7.75);
|
||||
|
||||
rerender(
|
||||
<MediaPreview
|
||||
asset={videoAsset()}
|
||||
currentTimeSeconds={1.5}
|
||||
onCurrentTimeChange={onCurrentTimeChange}
|
||||
/>
|
||||
);
|
||||
expect(video.currentTime).toBeCloseTo(1.5);
|
||||
});
|
||||
|
||||
it('uses the result media kind, honors result seeks, and reports result time', () => {
|
||||
const onCurrentTimeChange = vi.fn();
|
||||
const result: PreviewExportResult = {
|
||||
name: 'audio-result.mp3',
|
||||
mimeType: 'audio/mpeg',
|
||||
objectUrl: 'blob:audio-result',
|
||||
};
|
||||
const { container, rerender } = render(
|
||||
<MediaPreview
|
||||
asset={videoAsset()}
|
||||
result={result}
|
||||
currentTimeSeconds={2}
|
||||
onCurrentTimeChange={onCurrentTimeChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.querySelector('video')).toBeNull();
|
||||
const audio = requiredElement(container.querySelector('audio'));
|
||||
expect(audio).toHaveAttribute('src', result.objectUrl);
|
||||
expect(audio.currentTime).toBeCloseTo(2);
|
||||
|
||||
rerender(
|
||||
<MediaPreview
|
||||
asset={videoAsset()}
|
||||
result={result}
|
||||
currentTimeSeconds={5.5}
|
||||
onCurrentTimeChange={onCurrentTimeChange}
|
||||
/>
|
||||
);
|
||||
expect(audio.currentTime).toBeCloseTo(5.5);
|
||||
|
||||
audio.currentTime = 8.125;
|
||||
fireEvent.timeUpdate(audio);
|
||||
expect(onCurrentTimeChange).toHaveBeenLastCalledWith(8.125);
|
||||
});
|
||||
|
||||
it('uses bounded peaks for an audio-only source and offers an AAC/M4A proxy', async () => {
|
||||
vi.mocked(HTMLMediaElement.prototype.canPlayType).mockReturnValue('');
|
||||
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
|
||||
canvasContext()
|
||||
);
|
||||
const peaks = aggregatePcmPeaks(
|
||||
{
|
||||
container: 'raw',
|
||||
data: new Int16Array([-32_768, 0, 16_384, 32_767]),
|
||||
encoding: 's16le',
|
||||
sampleRate: 2,
|
||||
},
|
||||
{ bucketCount: 4 }
|
||||
);
|
||||
const { container } = render(
|
||||
<MediaPreview
|
||||
asset={audioAsset()}
|
||||
waveformPeaks={peaks}
|
||||
currentTimeSeconds={1}
|
||||
onCreateProxy={vi.fn()}
|
||||
proxySettings={{ maxDurationSeconds: 30, maxWidth: 960 }}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(container.querySelector('canvas')).toHaveAttribute(
|
||||
'aria-label',
|
||||
'Bounded waveform preview for source.flac.'
|
||||
)
|
||||
);
|
||||
expect(container.querySelector('.audio-preview__disc')).toBeNull();
|
||||
expect(container.querySelector('audio')).toBeInTheDocument();
|
||||
expect(screen.getByText(/AAC\/M4A/iu)).toBeVisible();
|
||||
expect(
|
||||
screen.queryByRole('combobox', { name: 'Maximum width' })
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('shows an explicit download-only result instead of mounting unsupported playback', () => {
|
||||
vi.mocked(HTMLMediaElement.prototype.canPlayType).mockReturnValue('');
|
||||
const result: PreviewExportResult = {
|
||||
name: 'archive.flac',
|
||||
mimeType: 'audio/flac',
|
||||
objectUrl: 'blob:unsupported-result',
|
||||
};
|
||||
const { container } = render(<MediaPreview result={result} />);
|
||||
|
||||
expect(container.querySelector('audio')).toBeNull();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Download only: this browser cannot preview this result.'
|
||||
)
|
||||
).toBeVisible();
|
||||
expect(screen.getByText('Download only · archive.flac')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
function videoAsset(): ImportedMediaAsset {
|
||||
return {
|
||||
id: 'asset-video',
|
||||
file: new File(['video'], 'source.mp4', { type: 'video/mp4' }),
|
||||
objectUrl: 'blob:source-video',
|
||||
phase: 'ready',
|
||||
probe: {
|
||||
durationSeconds: 10,
|
||||
formatNames: ['mov', 'mp4'],
|
||||
tags: {},
|
||||
chapters: [],
|
||||
warnings: [],
|
||||
streams: [
|
||||
{
|
||||
index: 0,
|
||||
type: 'video',
|
||||
codecName: 'h264',
|
||||
width: 640,
|
||||
height: 360,
|
||||
disposition: {},
|
||||
tags: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function audioAsset(): ImportedMediaAsset {
|
||||
return {
|
||||
id: 'asset-audio',
|
||||
file: new File(['audio'], 'source.flac', { type: 'audio/flac' }),
|
||||
objectUrl: 'blob:source-audio',
|
||||
phase: 'ready',
|
||||
probe: {
|
||||
durationSeconds: 2,
|
||||
formatNames: ['flac'],
|
||||
tags: {},
|
||||
chapters: [],
|
||||
warnings: [],
|
||||
streams: [
|
||||
{
|
||||
index: 0,
|
||||
type: 'audio',
|
||||
codecName: 'flac',
|
||||
sampleRate: 2,
|
||||
channels: 1,
|
||||
disposition: {},
|
||||
tags: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function canvasContext(): CanvasRenderingContext2D {
|
||||
return {
|
||||
setTransform: vi.fn(),
|
||||
clearRect: vi.fn(),
|
||||
fillRect: vi.fn(),
|
||||
beginPath: vi.fn(),
|
||||
moveTo: vi.fn(),
|
||||
lineTo: vi.fn(),
|
||||
stroke: vi.fn(),
|
||||
fillStyle: '',
|
||||
strokeStyle: '',
|
||||
lineWidth: 1,
|
||||
} as unknown as CanvasRenderingContext2D;
|
||||
}
|
||||
|
||||
function requiredElement<T extends Element>(element: T | null): T {
|
||||
if (!element) {
|
||||
throw new Error('Expected media element to be rendered');
|
||||
}
|
||||
return element;
|
||||
}
|
||||
Reference in New Issue
Block a user