Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions cypress/e2e/webaudio.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,23 @@ describe('WebAudioPlayer', () => {
player.playbackRate = 2

return player.play().then(() => {
// currentPos should be 4 (2 * 2)
cy.get('@startStub').should('have.been.calledWith', 0, 4)
// currentPos should be 2 (playbackRate does not multiply start position)
cy.get('@startStub').should('have.been.calledWith', 0, 2)
expect(player.bufferNode.playbackRate.value).to.equal(2)
})
})
})

it('should NOT reset position when seeking to exactly the duration', () => {
cy.get('@player').then((player) => {
// Set position to exactly the duration (10)
player.currentTime = 10

return player.play().then(() => {
// Position should NOT be reset to 0, should start at the end
cy.get('@startStub').should('have.been.calledWith', 0, 10)
})
})
})
})
})
20 changes: 20 additions & 0 deletions src/__tests__/wavesurfer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,26 @@ describe('WaveSurfer public methods', () => {
expect((ws as any).stopAtPosition).toBe(4)
})

test('play waits for seek when media is ended', async () => {
const ws = createWs()
const media = ws.getMediaElement()
// Simulate media ended state
Object.defineProperty(media, 'ended', { configurable: true, value: true, writable: true })

const setTimeSpy = jest.spyOn(ws, 'setTime')

// Start play but don't await it yet
const playPromise = ws.play(10)

// Dispatch seeked event to simulate seek completion
media.dispatchEvent(new Event('seeked'))

await playPromise

expect(setTimeSpy).toHaveBeenCalledWith(10)
expect(media.play).toHaveBeenCalled()
})

test('playPause toggles play and pause', async () => {
const ws = createWs()
const media = ws.getMediaElement()
Expand Down
148 changes: 148 additions & 0 deletions src/__tests__/webaudio.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Mock AudioContext and related classes before importing
const mockBufferNode = {
buffer: null as AudioBuffer | null,
connect: jest.fn(),
disconnect: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
playbackRate: { value: 1 },
onended: null as (() => void) | null,
}

const mockGainNode = {
connect: jest.fn(),
disconnect: jest.fn(),
gain: { value: 1 },
}

let mockCurrentTime = 0

const mockAudioContext = {
get currentTime() {
return mockCurrentTime
},
createBufferSource: jest.fn(() => ({ ...mockBufferNode, onended: null })),
createGain: jest.fn(() => mockGainNode),
destination: {},
}

// Mock window.AudioContext
const mockAudioContextConstructor = jest.fn(() => mockAudioContext)
Object.defineProperty(global, 'AudioContext', { value: mockAudioContextConstructor })

import WebAudioPlayer from '../webaudio.js'

describe('WebAudioPlayer', () => {
let player: WebAudioPlayer

beforeEach(() => {
jest.clearAllMocks()
mockCurrentTime = 0
// Reset mockBufferNode state
mockBufferNode.onended = null

player = new WebAudioPlayer(mockAudioContext as unknown as AudioContext)
// Set up a mock buffer with 33 seconds duration
const playerWithBuffer = player as unknown as { buffer: { duration: number } }
playerWithBuffer.buffer = { duration: 33 }
})

describe('position reset behavior', () => {
it('should NOT reset position when seeking to exactly the duration', async () => {
// Set position to exactly the duration
player.currentTime = 33 // exactly at the end

await player.play()

// Position should remain at 33, not reset to 0
expect((player as any).playbackPosition).toBe(33)
})

it('should reset position when seeking past the duration', async () => {
// Set position past the duration
player.currentTime = 35 // past the end

await player.play()

// Position should be reset to 0
expect((player as any).playbackPosition).toBe(0)
})

it('should maintain position when seeking near but not at the duration', async () => {
// Set position near the end
player.currentTime = 32.5

await player.play()

// Position should be maintained
expect((player as any).playbackPosition).toBe(32.5)
})

it('should maintain position when seeking within valid range', async () => {
player.currentTime = 15

await player.play()

expect((player as any).playbackPosition).toBe(15)
})

it('should reset position when seeking to negative value', async () => {
player.currentTime = -5

await player.play()

expect((player as any).playbackPosition).toBe(0)
})
})

describe('seeking while playing', () => {
it('should correctly seek to new position while playing near the end', async () => {
// Start playing from position 0
mockCurrentTime = 0
await player.play()

// Simulate time passing - now at 31 seconds of playback
mockCurrentTime = 31
;(player as any).playStartTime = 0

// Seek to position 30 while playing
player.currentTime = 30

// Position should be 30
expect((player as any).playbackPosition).toBe(30)
expect(player.paused).toBe(false)
})

it('should correctly seek to a position close to duration while playing', async () => {
// Start playing from position 0
mockCurrentTime = 0
await player.play()

// Simulate time passing
mockCurrentTime = 31
;(player as any).playStartTime = 0

// Seek to position 32.9 (close to 33 second duration)
player.currentTime = 32.9

// Position should be maintained at 32.9, not reset
expect((player as any).playbackPosition).toBe(32.9)
})

it('should correctly seek to exactly duration while playing', async () => {
// Start playing from position 0
mockCurrentTime = 0
await player.play()

// Simulate time passing
mockCurrentTime = 31
;(player as any).playStartTime = 0

// Seek to exactly the duration
player.currentTime = 33

// Position should be maintained at 33, not reset to 0
expect((player as any).playbackPosition).toBe(33)
})
})
})
9 changes: 9 additions & 0 deletions src/wavesurfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,8 +655,17 @@ class WaveSurfer extends Player<WaveSurferEvents> {

/** Start playing the audio */
public async play(start?: number, end?: number): Promise<void> {
// If media is ended and we're setting a start position, wait for seek to complete
// before playing to avoid the browser restarting from the beginning
const isEnded = this.media instanceof WebAudioPlayer ? false : this.getMediaElement().ended
if (start != null) {
this.setTime(start)
// Wait for the seek to complete if the media was ended
if (isEnded) {
await new Promise<void>((resolve) => {
this.onMediaEvent('seeked', () => resolve(), { once: true })
})
}
}

const playResult = await super.play()
Expand Down
2 changes: 1 addition & 1 deletion src/webaudio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class WebAudioPlayer extends EventEmitter<WebAudioPlayerEvents> {
this.bufferNode.connect(this.gainNode)

let currentPos = this.playbackPosition
if (currentPos >= this.duration || currentPos < 0) {
if (currentPos > this.duration || currentPos < 0) {
currentPos = 0
this.playbackPosition = 0
}
Expand Down