diff --git a/web/src/lib/player/store.svelte.ts b/web/src/lib/player/store.svelte.ts index ff738ad3..1da64ceb 100644 --- a/web/src/lib/player/store.svelte.ts +++ b/web/src/lib/player/store.svelte.ts @@ -283,8 +283,18 @@ export function removeFromQueue(idx: number): void { _error = null; return; } - // _index stays — now points at what was the next track. - if (_index >= next.length) _index = next.length - 1; + if (_index >= next.length) { + // Removed the last track. Mirror skipNext end-of-queue behaviour: + // pause on the now-last remaining track at position 0 — don't + // auto-play a track the user already heard. + _index = next.length - 1; + _state = 'paused'; + _position = 0; + _duration = 0; + _error = null; + return; + } + // _index stays — now points at what was the next track. Advance into it. _position = 0; _duration = 0; _state = 'loading'; diff --git a/web/src/lib/player/store.test.ts b/web/src/lib/player/store.test.ts index 961b44d1..e621953b 100644 --- a/web/src/lib/player/store.test.ts +++ b/web/src/lib/player/store.test.ts @@ -33,6 +33,7 @@ beforeEach(() => { localStorage.clear(); // Reset state by starting with an empty queue (playQueue([]) leaves queue=[], index=0). playQueue([]); + closeQueueDrawer(); setVolume(1); registerAudioEl(null); // Reset shuffle and repeat to defaults @@ -460,6 +461,21 @@ describe('queue mutations', () => { expect(player.index).toBe(0); }); + test('moveQueueItem with from === to is a no-op', () => { + playQueue([track('a'), track('b'), track('c')], 1); + moveQueueItem(1, 1); + expect(player.queue.map((x) => x.id)).toEqual(['a', 'b', 'c']); + expect(player.index).toBe(1); + }); + + test('moveQueueItem with out-of-range from or to is a no-op', () => { + playQueue([track('a'), track('b'), track('c')], 0); + moveQueueItem(-1, 1); + expect(player.queue.map((x) => x.id)).toEqual(['a', 'b', 'c']); + moveQueueItem(0, 5); + expect(player.queue.map((x) => x.id)).toEqual(['a', 'b', 'c']); + }); + test('removeFromQueue with idx < _index decrements _index', () => { playQueue([track('a'), track('b'), track('c')], 2); removeFromQueue(0); @@ -489,6 +505,15 @@ describe('queue mutations', () => { expect(player.state).toBe('idle'); }); + test('removeFromQueue of the currently-playing last track pauses on the previous track', () => { + playQueue([track('a'), track('b'), track('c')], 2); + removeFromQueue(2); + expect(player.queue.map((x) => x.id)).toEqual(['a', 'b']); + expect(player.index).toBe(1); + expect(player.state).toBe('paused'); + expect(player.position).toBe(0); + }); + test('playFromQueueIndex sets index, resets position, sets state to loading', () => { playQueue([track('a'), track('b'), track('c')], 0); playFromQueueIndex(2); @@ -499,8 +524,10 @@ describe('queue mutations', () => { test('playFromQueueIndex with out-of-range idx is a no-op', () => { playQueue([track('a'), track('b')], 0); + const stateBefore = player.state; playFromQueueIndex(5); expect(player.index).toBe(0); + expect(player.state).toBe(stateBefore); }); test('toggleQueueDrawer flips _queueDrawerOpen', () => {