mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
Three corner cases fixed: 1. Removing the currently-playing song caused the next song in the array to be silently marked as "played" and skipped. Root cause: playedIndices was updated in next() by marking currentIndex, but after remove() shifts currentIndex, it pointed to the wrong song. Fix: mark songs as played at play-time (in play/playAt/next/prev) instead of at next-request-time. 2. Using prev() in Random mode could cause a song to play twice — the song navigated to via prev() was not recorded in playedIndices, so next() could randomly select it again. Fix: prev() now marks the returned song as played. 3. Switching to Random mode mid-playback could cause the current song to repeat because setMode() cleared playedIndices without preserving the currently-playing song. Fix: setMode() now re-adds currentIndex after clearing. https://claude.ai/code/session_01W3ZncxL5VfdZeB4qqYWDmY
265 lines
7.8 KiB
TypeScript
265 lines
7.8 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { PlayQueue, type QueuedSong, PlayMode } from "./queue.js";
|
|
|
|
function makeSong(id: string, name: string = id): QueuedSong {
|
|
return {
|
|
id,
|
|
name,
|
|
artist: "Artist",
|
|
album: "Album",
|
|
platform: "netease",
|
|
url: `https://example.com/${id}.mp3`,
|
|
coverUrl: `https://example.com/${id}.jpg`,
|
|
duration: 240,
|
|
};
|
|
}
|
|
|
|
describe("PlayQueue", () => {
|
|
let queue: PlayQueue;
|
|
|
|
beforeEach(() => {
|
|
queue = new PlayQueue();
|
|
});
|
|
|
|
it("starts empty", () => {
|
|
expect(queue.isEmpty()).toBe(true);
|
|
expect(queue.current()).toBeNull();
|
|
expect(queue.size()).toBe(0);
|
|
});
|
|
|
|
it("adds and retrieves songs", () => {
|
|
queue.add(makeSong("1", "Song A"));
|
|
queue.add(makeSong("2", "Song B"));
|
|
expect(queue.size()).toBe(2);
|
|
expect(queue.list()[0].name).toBe("Song A");
|
|
expect(queue.list()[1].name).toBe("Song B");
|
|
});
|
|
|
|
it("plays first song when starting", () => {
|
|
queue.add(makeSong("1"));
|
|
queue.add(makeSong("2"));
|
|
queue.play();
|
|
expect(queue.current()?.id).toBe("1");
|
|
});
|
|
|
|
it("advances to next song in sequential mode", () => {
|
|
queue.setMode(PlayMode.Sequential);
|
|
queue.add(makeSong("1"));
|
|
queue.add(makeSong("2"));
|
|
queue.add(makeSong("3"));
|
|
queue.play();
|
|
expect(queue.current()?.id).toBe("1");
|
|
const next = queue.next();
|
|
expect(next?.id).toBe("2");
|
|
expect(queue.current()?.id).toBe("2");
|
|
});
|
|
|
|
it("returns null at end in sequential mode", () => {
|
|
queue.setMode(PlayMode.Sequential);
|
|
queue.add(makeSong("1"));
|
|
queue.play();
|
|
const next = queue.next();
|
|
expect(next).toBeNull();
|
|
});
|
|
|
|
it("loops in loop mode", () => {
|
|
queue.setMode(PlayMode.Loop);
|
|
queue.add(makeSong("1"));
|
|
queue.play();
|
|
const next = queue.next();
|
|
expect(next?.id).toBe("1");
|
|
});
|
|
|
|
it("goes to previous song", () => {
|
|
queue.add(makeSong("1"));
|
|
queue.add(makeSong("2"));
|
|
queue.play();
|
|
queue.next();
|
|
expect(queue.current()?.id).toBe("2");
|
|
queue.prev();
|
|
expect(queue.current()?.id).toBe("1");
|
|
});
|
|
|
|
it("removes song by index", () => {
|
|
queue.add(makeSong("1"));
|
|
queue.add(makeSong("2"));
|
|
queue.add(makeSong("3"));
|
|
queue.remove(1);
|
|
expect(queue.size()).toBe(2);
|
|
expect(queue.list()[1].id).toBe("3");
|
|
});
|
|
|
|
it("removing a song before current shifts current index", () => {
|
|
queue.setMode(PlayMode.Sequential);
|
|
queue.add(makeSong("A"));
|
|
queue.add(makeSong("B"));
|
|
queue.add(makeSong("C"));
|
|
queue.playAt(2); // playing C at index 2
|
|
queue.remove(0); // remove A (before current)
|
|
expect(queue.current()?.id).toBe("C"); // still on C
|
|
expect(queue.getCurrentIndex()).toBe(1);
|
|
});
|
|
|
|
it("removing the currently-playing song lets next() advance to the shifted song", () => {
|
|
queue.setMode(PlayMode.Sequential);
|
|
queue.add(makeSong("A"));
|
|
queue.add(makeSong("B"));
|
|
queue.add(makeSong("C"));
|
|
queue.add(makeSong("D"));
|
|
queue.playAt(2); // playing C
|
|
queue.remove(2); // remove C — D shifts into slot 2
|
|
// Before the fix this returned null (D was silently skipped)
|
|
expect(queue.next()?.id).toBe("D");
|
|
});
|
|
|
|
it("removing the only song clears the queue", () => {
|
|
queue.add(makeSong("only"));
|
|
queue.playAt(0);
|
|
queue.remove(0);
|
|
expect(queue.size()).toBe(0);
|
|
expect(queue.current()).toBeNull();
|
|
expect(queue.next()).toBeNull();
|
|
});
|
|
|
|
it("removing the last song while playing it advances to null in sequential mode", () => {
|
|
queue.setMode(PlayMode.Sequential);
|
|
queue.add(makeSong("A"));
|
|
queue.add(makeSong("B"));
|
|
queue.playAt(1); // playing B (last)
|
|
queue.remove(1);
|
|
expect(queue.size()).toBe(1);
|
|
// currentIndex moved to 0, so next() should try to advance past the end
|
|
expect(queue.next()).toBeNull();
|
|
});
|
|
|
|
it("clears all songs", () => {
|
|
queue.add(makeSong("1"));
|
|
queue.add(makeSong("2"));
|
|
queue.clear();
|
|
expect(queue.isEmpty()).toBe(true);
|
|
expect(queue.current()).toBeNull();
|
|
});
|
|
|
|
it("random mode returns a song", () => {
|
|
queue.setMode(PlayMode.Random);
|
|
queue.add(makeSong("1"));
|
|
queue.add(makeSong("2"));
|
|
queue.add(makeSong("3"));
|
|
queue.play();
|
|
const next = queue.next();
|
|
expect(next).not.toBeNull();
|
|
});
|
|
|
|
it("random mode with single song returns null on next", () => {
|
|
queue.setMode(PlayMode.Random);
|
|
queue.add(makeSong("1"));
|
|
queue.play();
|
|
expect(queue.next()).toBeNull();
|
|
});
|
|
|
|
it("random mode plays each song exactly once then stops", () => {
|
|
queue.setMode(PlayMode.Random);
|
|
queue.add(makeSong("A"));
|
|
queue.add(makeSong("B"));
|
|
queue.add(makeSong("C"));
|
|
queue.play();
|
|
const played = new Set<string>();
|
|
played.add(queue.current()!.id);
|
|
for (let i = 0; i < 3; i++) {
|
|
const song = queue.next();
|
|
if (!song) break;
|
|
played.add(song.id);
|
|
}
|
|
// All 3 songs should have been played
|
|
expect(played).toEqual(new Set(["A", "B", "C"]));
|
|
// next() after all played should return null
|
|
expect(queue.next()).toBeNull();
|
|
});
|
|
|
|
it("random mode: removing currently-playing song does not skip others", () => {
|
|
queue.setMode(PlayMode.Random);
|
|
queue.add(makeSong("A"));
|
|
queue.add(makeSong("B"));
|
|
queue.add(makeSong("C"));
|
|
queue.add(makeSong("D"));
|
|
queue.play(); // plays A (index 0)
|
|
const second = queue.next()!; // plays some song
|
|
// Remove the currently-playing song
|
|
const curIdx = queue.getCurrentIndex();
|
|
queue.remove(curIdx);
|
|
// Remaining songs (excluding A and the removed song) should all be reachable
|
|
const played = new Set<string>();
|
|
played.add("A"); // already played via play()
|
|
played.add(second.id); // played and then removed
|
|
let song = queue.next();
|
|
while (song) {
|
|
played.add(song.id);
|
|
song = queue.next();
|
|
}
|
|
// All 4 original songs should have been played or accounted for
|
|
expect(played).toEqual(new Set(["A", "B", "C", "D"]));
|
|
});
|
|
|
|
it("random mode: prev does not cause duplicate plays", () => {
|
|
queue.setMode(PlayMode.Random);
|
|
queue.add(makeSong("A"));
|
|
queue.add(makeSong("B"));
|
|
queue.add(makeSong("C"));
|
|
queue.play(); // plays A
|
|
queue.next(); // plays B or C
|
|
queue.prev(); // go back — this song is now marked as played
|
|
// Exhaust remaining songs
|
|
const ids: string[] = [];
|
|
let song = queue.next();
|
|
while (song) {
|
|
ids.push(song.id);
|
|
song = queue.next();
|
|
}
|
|
// No song ID should appear more than once across the entire session
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
});
|
|
|
|
it("random mode: adding song mid-playback includes the new song", () => {
|
|
queue.setMode(PlayMode.Random);
|
|
queue.add(makeSong("A"));
|
|
queue.add(makeSong("B"));
|
|
queue.play(); // plays A
|
|
queue.next(); // plays B
|
|
// Add a new song while all existing songs have been played
|
|
queue.add(makeSong("C"));
|
|
const song = queue.next();
|
|
expect(song).not.toBeNull();
|
|
expect(song!.id).toBe("C");
|
|
// After C, should stop
|
|
expect(queue.next()).toBeNull();
|
|
});
|
|
|
|
it("random mode: setMode preserves current song as played", () => {
|
|
queue.add(makeSong("A"));
|
|
queue.add(makeSong("B"));
|
|
queue.play(); // plays A in sequential mode
|
|
queue.setMode(PlayMode.Random); // switch to random — A should be marked played
|
|
// next() should only return B, never A again
|
|
const song = queue.next();
|
|
expect(song?.id).toBe("B");
|
|
expect(queue.next()).toBeNull();
|
|
});
|
|
|
|
it("random-loop mode never returns null", () => {
|
|
queue.setMode(PlayMode.RandomLoop);
|
|
queue.add(makeSong("1"));
|
|
queue.play();
|
|
for (let i = 0; i < 10; i++) {
|
|
expect(queue.next()).not.toBeNull();
|
|
}
|
|
});
|
|
|
|
it("playAt jumps to specific index", () => {
|
|
queue.add(makeSong("1"));
|
|
queue.add(makeSong("2"));
|
|
queue.add(makeSong("3"));
|
|
queue.playAt(2);
|
|
expect(queue.current()?.id).toBe("3");
|
|
});
|
|
});
|