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( ); 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.000–00: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( ); 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( ); 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( 2.01 + index * 0.01), ]} onChange={vi.fn()} /> ); expect( screen.getByText(/Selected 00:00:00.000–00: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; }