fix(audio): re-attach waveform canvas on re-navigation to same song

When navigating away from SongPage and back to the same song, the container
div is a new DOM element but the URL is unchanged. The previous early-return
(currentUrl === url) would skip initialization entirely, leaving WaveSurfer
pointing at the detached old container — nothing rendered.

Fix: track currentContainer alongside currentUrl. When URL matches but container
has changed, call wavesurfer.setOptions({ container }) which moves the existing
canvas into the new container without reloading audio or interrupting playback.
WaveSurfer v7 renderer.setOptions() supports this: it calls
newParent.appendChild(this.container) to relocate the canvas div.

Three paths in initialize():
  1. Same URL + same container → no-op
  2. Same URL + new container  → setOptions re-attach (no reload)
  3. Different URL             → full teardown and reload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mistral Vibe
2026-04-08 21:31:08 +02:00
parent 8b7415954c
commit a0cc10ffca
2 changed files with 40 additions and 7 deletions

View File

@@ -23,6 +23,7 @@ function createMockWaveSurfer(duration = 120) {
isPlaying: vi.fn(() => false),
unAll: vi.fn(),
destroy: vi.fn(),
setOptions: vi.fn(),
// Helper to fire events from tests
_emit: (event: EventName, ...args: any[]) => handlers[event]?.(...args),
};
@@ -114,7 +115,24 @@ describe('AudioService', () => {
await expect(initPromise).rejects.toThrow('network error');
});
it('reuses the existing instance for the same URL', async () => {
it('does nothing when called with same URL and same container', async () => {
const WaveSurfer = (await import('wavesurfer.js')).default;
const mockWs = createMockWaveSurfer();
vi.mocked(WaveSurfer.create).mockReturnValue(mockWs as any);
const url = 'http://example.com/same.mp3';
const container = makeContainer();
const p1 = service.initialize(container, url);
mockWs._emit('ready');
await p1;
const createCount = vi.mocked(WaveSurfer.create).mock.calls.length;
await service.initialize(container, url); // exact same reference
expect(vi.mocked(WaveSurfer.create).mock.calls.length).toBe(createCount);
expect(mockWs.setOptions).not.toHaveBeenCalled();
});
it('re-attaches waveform to new container when same URL but container changed (re-navigation)', async () => {
const WaveSurfer = (await import('wavesurfer.js')).default;
const mockWs = createMockWaveSurfer();
vi.mocked(WaveSurfer.create).mockReturnValue(mockWs as any);
@@ -124,9 +142,13 @@ describe('AudioService', () => {
mockWs._emit('ready');
await p1;
const createCallCount = vi.mocked(WaveSurfer.create).mock.calls.length;
await service.initialize(makeContainer(), url); // same URL
expect(vi.mocked(WaveSurfer.create).mock.calls.length).toBe(createCallCount); // no new instance
const newContainer = makeContainer(); // different DOM element, same URL
const createCount = vi.mocked(WaveSurfer.create).mock.calls.length;
await service.initialize(newContainer, url);
// No new WaveSurfer instance — just a canvas re-attach
expect(vi.mocked(WaveSurfer.create).mock.calls.length).toBe(createCount);
expect(mockWs.setOptions).toHaveBeenCalledWith({ container: newContainer });
});
it('resets store state when URL changes', async () => {