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,216 @@
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;
}