Skip to content

Commit f05e7c1

Browse files
authored
test(WaveSurfer): add unit tests for public API (#4115)
1 parent f9d7ce0 commit f05e7c1

File tree

1 file changed

+240
-0
lines changed

1 file changed

+240
-0
lines changed

src/__tests__/wavesurfer.test.ts

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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

Comments
 (0)