Files
av-tools/tests/unit/components/waveform-editor.test.tsx

217 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { WaveformEditor } from '../../../src/components/WaveformEditor';
import { aggregatePcmPeaks } from '../../../src/waveform';
describe('WaveformEditor', () => {
afterEach(cleanup);
beforeEach(() => {
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(
createCanvasContext()
);
});
it('renders a bounded canvas with accessible trim and marker controls', async () => {
render(
<WaveformEditor
peaks={createPeaks()}
inSeconds={1}
outSeconds={9}
markers={[3]}
currentTimeSeconds={5}
onChange={vi.fn()}
/>
);
const canvas = document.querySelector('canvas');
if (!(canvas instanceof HTMLCanvasElement)) {
throw new Error('Missing waveform canvas');
}
await waitFor(() =>
expect(canvas).toHaveAttribute(
'aria-label',
expect.stringContaining('Selected 00:00:01.00000:00:09.000')
)
);
expect(
screen.getByRole('slider', { name: 'In trim handle' })
).toHaveAttribute('aria-valuenow', '1');
expect(
screen.getByRole('slider', { name: 'Out trim handle' })
).toHaveAttribute('aria-valuenow', '9');
expect(
screen.getByRole('slider', { name: 'Split marker 1' })
).toHaveAttribute('aria-valuenow', '3');
expect(screen.getByText('Single sequential audio track')).toBeVisible();
});
it('emits validated keyboard edits and current-time actions', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
render(
<WaveformEditor
peaks={createPeaks()}
inSeconds={1}
outSeconds={9}
markers={[3]}
currentTimeSeconds={4}
onChange={onChange}
/>
);
fireEvent.keyDown(screen.getByRole('slider', { name: 'In trim handle' }), {
key: 'ArrowRight',
});
expect(onChange).toHaveBeenLastCalledWith({
inSeconds: 1.1,
outSeconds: 9,
markers: [3],
});
await user.click(
screen.getByRole('button', { name: 'Add split at current time' })
);
expect(onChange).toHaveBeenLastCalledWith({
inSeconds: 1,
outSeconds: 9,
markers: [3, 4],
});
fireEvent.keyDown(screen.getByRole('slider', { name: 'Split marker 1' }), {
key: 'Delete',
});
expect(onChange).toHaveBeenLastCalledWith({
inSeconds: 1,
outSeconds: 9,
markers: [],
});
});
it('drags split markers and seeks using the visible zoomed range', async () => {
const onChange = vi.fn();
const onSeek = vi.fn();
const { container } = render(
<WaveformEditor
peaks={createPeaks()}
inSeconds={0}
outSeconds={10}
markers={[2]}
onChange={onChange}
onSeek={onSeek}
/>
);
const stage = container.querySelector('.waveform-editor__stage');
if (!(stage instanceof HTMLElement)) {
throw new Error('Missing waveform stage');
}
vi.spyOn(stage, 'getBoundingClientRect').mockReturnValue({
x: 0,
y: 0,
left: 0,
top: 0,
right: 100,
bottom: 168,
width: 100,
height: 168,
toJSON: () => ({}),
});
const marker = screen.getByRole('slider', { name: 'Split marker 1' });
fireEvent.pointerDown(marker, {
pointerId: 11,
clientX: 20,
clientY: 20,
});
fireEvent.pointerMove(marker, {
pointerId: 11,
clientX: 50,
clientY: 20,
});
fireEvent.pointerUp(marker, { pointerId: 11 });
expect(onChange).toHaveBeenLastCalledWith({
inSeconds: 0,
outSeconds: 10,
markers: [5],
});
fireEvent.pointerDown(stage, {
button: 0,
clientX: 75,
clientY: 30,
});
expect(onSeek).toHaveBeenLastCalledWith(7.5);
fireEvent.change(screen.getByRole('slider', { name: 'Zoom waveform' }), {
target: { value: '2' },
});
expect(
screen.getByRole('slider', { name: 'Scroll waveform' })
).toBeEnabled();
});
it('normalizes untrusted selection values and caps marker count', () => {
render(
<WaveformEditor
peaks={createPeaks()}
inSeconds={Number.NaN}
outSeconds={999}
markers={[
Number.NaN,
-1,
2,
2.0004,
...Array.from({ length: 510 }, (_, index) => 2.01 + index * 0.01),
]}
onChange={vi.fn()}
/>
);
expect(
screen.getByText(/Selected 00:00:00.00000:00:10.000/u)
).toBeVisible();
expect(screen.getByText('254/254')).toBeVisible();
expect(
screen.getAllByRole('slider', { name: /Split marker/u })
).toHaveLength(254);
});
});
function createPeaks() {
return aggregatePcmPeaks(
{
container: 'raw',
data: new Int16Array(
Array.from({ length: 100 }, (_, index) =>
Math.round(Math.sin(index / 5) * 20_000)
)
),
encoding: 's16le',
sampleRate: 10,
},
{ bucketCount: 100 }
);
}
function createCanvasContext(): 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;
}