|
| 1 | +jest.mock('../renderer.js', () => { |
| 2 | + let lastInstance: any |
| 3 | + class Renderer { |
| 4 | + options: any |
| 5 | + wrapper = document.createElement('div') |
| 6 | + renderProgress = jest.fn() |
| 7 | + on = jest.fn(() => () => undefined) |
| 8 | + setOptions = jest.fn() |
| 9 | + getWrapper = jest.fn(() => this.wrapper) |
| 10 | + getWidth = jest.fn(() => 100) |
| 11 | + getScroll = jest.fn(() => 0) |
| 12 | + setScroll = jest.fn() |
| 13 | + setScrollPercentage = jest.fn() |
| 14 | + render = jest.fn() |
| 15 | + zoom = jest.fn() |
| 16 | + exportImage = jest.fn(() => []) |
| 17 | + destroy = jest.fn() |
| 18 | + constructor(options: any) { |
| 19 | + this.options = options |
| 20 | + lastInstance = this |
| 21 | + } |
| 22 | + } |
| 23 | + return { __esModule: true, default: Renderer, getLastInstance: () => lastInstance } |
| 24 | +}) |
| 25 | + |
| 26 | +jest.mock('../timer.js', () => { |
| 27 | + let lastInstance: any |
| 28 | + class Timer { |
| 29 | + on = jest.fn(() => () => undefined) |
| 30 | + start = jest.fn() |
| 31 | + stop = jest.fn() |
| 32 | + destroy = jest.fn() |
| 33 | + } |
| 34 | + const ctor = jest.fn(() => { |
| 35 | + lastInstance = new Timer() |
| 36 | + return lastInstance |
| 37 | + }) |
| 38 | + return { __esModule: true, default: ctor, getLastInstance: () => lastInstance } |
| 39 | +}) |
| 40 | + |
| 41 | +jest.mock('../decoder.js', () => { |
| 42 | + const createBuffer = jest.fn((data: any[], duration: number) => ({ |
| 43 | + duration, |
| 44 | + numberOfChannels: data.length, |
| 45 | + getChannelData: (i: number) => Float32Array.from(data[i] as number[]), |
| 46 | + })) |
| 47 | + return { __esModule: true, default: { decode: jest.fn(), createBuffer } } |
| 48 | +}) |
| 49 | +import WaveSurfer from '../wavesurfer.js' |
| 50 | +import { BasePlugin } from '../base-plugin.js' |
| 51 | +import * as RendererModule from '../renderer.js' |
| 52 | +import * as TimerModule from '../timer.js' |
| 53 | +const getRenderer = (RendererModule as any).getLastInstance as () => any |
| 54 | +const getTimer = (TimerModule as any).getLastInstance as () => any |
| 55 | + |
| 56 | +const createMedia = () => { |
| 57 | + const media = document.createElement('audio') as HTMLMediaElement & { play: jest.Mock; pause: jest.Mock } |
| 58 | + media.play = jest.fn().mockResolvedValue(undefined) |
| 59 | + media.pause = jest.fn() |
| 60 | + Object.defineProperty(media, 'duration', { configurable: true, value: 100, writable: true }) |
| 61 | + return media |
| 62 | +} |
| 63 | + |
| 64 | +const createWs = (opts: any = {}) => { |
| 65 | + const container = document.createElement('div') |
| 66 | + return WaveSurfer.create({ container, media: createMedia(), ...opts }) |
| 67 | +} |
| 68 | + |
| 69 | +afterEach(() => { |
| 70 | + jest.clearAllMocks() |
| 71 | +}) |
| 72 | + |
| 73 | +describe('WaveSurfer public methods', () => { |
| 74 | + test('create returns instance', () => { |
| 75 | + const ws = createWs() |
| 76 | + expect(ws).toBeInstanceOf(WaveSurfer) |
| 77 | + }) |
| 78 | + |
| 79 | + test('setOptions merges options and updates renderer', () => { |
| 80 | + const ws = createWs() |
| 81 | + ws.setOptions({ height: 200, audioRate: 2, mediaControls: true }) |
| 82 | + const renderer = getRenderer() |
| 83 | + expect(ws.options.height).toBe(200) |
| 84 | + expect(renderer.setOptions).toHaveBeenCalledWith(ws.options) |
| 85 | + expect(ws.getPlaybackRate()).toBe(2) |
| 86 | + expect(ws.getMediaElement().controls).toBe(true) |
| 87 | + }) |
| 88 | + |
| 89 | + test('registerPlugin adds and removes plugin', () => { |
| 90 | + const ws = createWs() |
| 91 | + class TestPlugin extends BasePlugin<{ destroy: [] }, {}> {} |
| 92 | + const plugin = new TestPlugin({}) |
| 93 | + ws.registerPlugin(plugin) |
| 94 | + expect(ws.getActivePlugins()).toContain(plugin) |
| 95 | + plugin.destroy() |
| 96 | + expect(ws.getActivePlugins()).not.toContain(plugin) |
| 97 | + }) |
| 98 | + |
| 99 | + test('wrapper and scroll helpers call renderer', () => { |
| 100 | + const ws = createWs() |
| 101 | + const renderer = getRenderer() |
| 102 | + ws.getWrapper() |
| 103 | + expect(renderer.getWrapper).toHaveBeenCalled() |
| 104 | + ws.getWidth() |
| 105 | + expect(renderer.getWidth).toHaveBeenCalled() |
| 106 | + ws.getScroll() |
| 107 | + expect(renderer.getScroll).toHaveBeenCalled() |
| 108 | + ws.setScroll(42) |
| 109 | + expect(renderer.setScroll).toHaveBeenCalledWith(42) |
| 110 | + jest.spyOn(ws, 'getDuration').mockReturnValue(10) |
| 111 | + ws.setScrollTime(5) |
| 112 | + expect(renderer.setScrollPercentage).toHaveBeenCalledWith(0.5) |
| 113 | + }) |
| 114 | + |
| 115 | + test('load and loadBlob call loadAudio', async () => { |
| 116 | + const ws = createWs() |
| 117 | + const spy = jest.spyOn(ws as any, 'loadAudio').mockResolvedValue(undefined) |
| 118 | + await ws.load('url') |
| 119 | + expect(spy).toHaveBeenCalledWith('url', undefined, undefined, undefined) |
| 120 | + const blob = new Blob([]) |
| 121 | + await ws.loadBlob(blob) |
| 122 | + expect(spy).toHaveBeenCalledWith('', blob, undefined, undefined) |
| 123 | + }) |
| 124 | + |
| 125 | + test('zoom requires decoded data', () => { |
| 126 | + const ws = createWs() |
| 127 | + expect(() => ws.zoom(10)).toThrow() |
| 128 | + ;(ws as any).decodedData = { duration: 1 } |
| 129 | + ws.zoom(10) |
| 130 | + expect(getRenderer().zoom).toHaveBeenCalledWith(10) |
| 131 | + }) |
| 132 | + |
| 133 | + test('getDecodedData returns buffer', () => { |
| 134 | + const ws = createWs() |
| 135 | + ;(ws as any).decodedData = 123 as any |
| 136 | + expect(ws.getDecodedData()).toBe(123) |
| 137 | + }) |
| 138 | + |
| 139 | + test('exportPeaks reads data from buffer', () => { |
| 140 | + const ws = createWs() |
| 141 | + ;(ws as any).decodedData = { |
| 142 | + numberOfChannels: 1, |
| 143 | + getChannelData: () => new Float32Array([0, 1, -1]), |
| 144 | + duration: 3, |
| 145 | + } |
| 146 | + const peaks = ws.exportPeaks({ maxLength: 3, precision: 100 }) |
| 147 | + expect(peaks[0]).toEqual([0, 1, -1]) |
| 148 | + }) |
| 149 | + |
| 150 | + test('getDuration falls back to decoded data', () => { |
| 151 | + const ws = createWs() |
| 152 | + const media = ws.getMediaElement() |
| 153 | + Object.defineProperty(media, 'duration', { configurable: true, value: Infinity }) |
| 154 | + ;(ws as any).decodedData = { duration: 2 } |
| 155 | + expect(ws.getDuration()).toBe(2) |
| 156 | + }) |
| 157 | + |
| 158 | + test('toggleInteraction sets option', () => { |
| 159 | + const ws = createWs() |
| 160 | + ws.toggleInteraction(false) |
| 161 | + expect(ws.options.interact).toBe(false) |
| 162 | + }) |
| 163 | + |
| 164 | + test('setTime updates progress and emits event', () => { |
| 165 | + const ws = createWs() |
| 166 | + const spy = jest.fn() |
| 167 | + ws.on('timeupdate', spy) |
| 168 | + ws.setTime(1) |
| 169 | + expect(spy).toHaveBeenCalledWith(1) |
| 170 | + expect(getRenderer().renderProgress).toHaveBeenCalled() |
| 171 | + }) |
| 172 | + |
| 173 | + test('seekTo calculates correct time', () => { |
| 174 | + const ws = createWs() |
| 175 | + jest.spyOn(ws, 'getDuration').mockReturnValue(10) |
| 176 | + const setTimeSpy = jest.spyOn(ws, 'setTime') |
| 177 | + ws.seekTo(0.5) |
| 178 | + expect(setTimeSpy).toHaveBeenCalledWith(5) |
| 179 | + }) |
| 180 | + |
| 181 | + test('play sets start and end', async () => { |
| 182 | + const ws = createWs() |
| 183 | + const spy = jest.spyOn(ws, 'setTime') |
| 184 | + await ws.play(2, 4) |
| 185 | + expect(spy).toHaveBeenCalledWith(2) |
| 186 | + expect((ws as any).stopAtPosition).toBe(4) |
| 187 | + }) |
| 188 | + |
| 189 | + test('playPause toggles play and pause', async () => { |
| 190 | + const ws = createWs() |
| 191 | + const media = ws.getMediaElement() |
| 192 | + await ws.playPause() |
| 193 | + expect(media.play).toHaveBeenCalled() |
| 194 | + Object.defineProperty(media, 'paused', { configurable: true, value: false }) |
| 195 | + await ws.playPause() |
| 196 | + expect(media.pause).toHaveBeenCalled() |
| 197 | + }) |
| 198 | + |
| 199 | + test('stop resets time', () => { |
| 200 | + const ws = createWs() |
| 201 | + ws.setTime(5) |
| 202 | + ws.stop() |
| 203 | + expect(ws.getCurrentTime()).toBe(0) |
| 204 | + }) |
| 205 | + |
| 206 | + test('skip and empty', () => { |
| 207 | + const ws = createWs() |
| 208 | + ws.getMediaElement().currentTime = 1 |
| 209 | + const spy = jest.spyOn(ws, 'setTime') |
| 210 | + ws.skip(2) |
| 211 | + expect(spy).toHaveBeenCalledWith(3) |
| 212 | + const loadSpy = jest.spyOn(ws, 'load').mockResolvedValue(undefined) |
| 213 | + ws.empty() |
| 214 | + expect(loadSpy).toHaveBeenCalledWith('', [[0]], 0.001) |
| 215 | + }) |
| 216 | + |
| 217 | + test('setMediaElement reinitializes events', () => { |
| 218 | + const ws = createWs() |
| 219 | + const unsub = jest.spyOn(ws as any, 'unsubscribePlayerEvents') |
| 220 | + const init = jest.spyOn(ws as any, 'initPlayerEvents') |
| 221 | + const el = createMedia() |
| 222 | + ws.setMediaElement(el) |
| 223 | + expect(unsub).toHaveBeenCalled() |
| 224 | + expect(init).toHaveBeenCalled() |
| 225 | + expect(ws.getMediaElement()).toBe(el) |
| 226 | + }) |
| 227 | + |
| 228 | + test('exportImage uses renderer', async () => { |
| 229 | + const ws = createWs() |
| 230 | + await ws.exportImage('image/png', 1, 'dataURL') |
| 231 | + expect(getRenderer().exportImage).toHaveBeenCalled() |
| 232 | + }) |
| 233 | + |
| 234 | + test('destroy cleans up renderer and timer', () => { |
| 235 | + const ws = createWs() |
| 236 | + ws.destroy() |
| 237 | + expect(getRenderer().destroy).toHaveBeenCalled() |
| 238 | + expect(getTimer().destroy).toHaveBeenCalled() |
| 239 | + }) |
| 240 | +}) |
0 commit comments