mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 13:02:49 +08:00
`store.elapsed` is a Pinia getter (a cached Vue computed) that interpolates with `Date.now()`. Because `Date.now()` is not a reactive dependency, the computed only re-ran on WebSocket pushes / the 3s server poll, so the bottom progress bar jumped ~3s at a time and lyric highlighting lagged ~half a line — even though the consumers read it from a 60fps requestAnimationFrame loop. Add a pure `interpolateElapsed()` helper and a non-cached `liveElapsed()` store action. The per-frame consumers now call `liveElapsed()` so the value advances every frame instead of returning a frozen cache: - web/src/components/Player.vue (desktop progress bar, rAF) - web/src/App.vue (mobile progress bar, rAF) - web/src/views/Lyrics.vue (lyric highlight, 500ms interval) pause() now freezes at the live value rather than a possibly-stale cached one. The `elapsed` getter is refactored onto the same helper (behaviour unchanged). Adds web/src/stores/elapsed.test.ts covering the time-advancing interpolation, paused freeze, no-anchor, and duration-clamp cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
45 lines
1.8 KiB
TypeScript
45 lines
1.8 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
import { interpolateElapsed, type TimingState } from "./player.js";
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function timing(partial: Partial<TimingState>): TimingState {
|
|
return { serverElapsed: 0, serverSyncTime: 0, wasPlaying: false, ...partial };
|
|
}
|
|
|
|
describe("interpolateElapsed", () => {
|
|
it("returns serverElapsed before playback has a sync anchor", () => {
|
|
expect(interpolateElapsed(timing({ serverElapsed: 12, wasPlaying: false }), false, Infinity)).toBe(12);
|
|
// wasPlaying but no sync time yet
|
|
expect(interpolateElapsed(timing({ serverElapsed: 5, wasPlaying: true, serverSyncTime: 0 }), false, Infinity)).toBe(5);
|
|
});
|
|
|
|
it("advances with wall-clock time while playing (regression: must not be frozen)", () => {
|
|
const spy = vi.spyOn(Date, "now");
|
|
const t = timing({ serverElapsed: 30, serverSyncTime: 10_000, wasPlaying: true });
|
|
|
|
spy.mockReturnValue(10_000);
|
|
expect(interpolateElapsed(t, false, Infinity)).toBeCloseTo(30, 5);
|
|
|
|
spy.mockReturnValue(11_000); // +1s
|
|
expect(interpolateElapsed(t, false, Infinity)).toBeCloseTo(31, 5);
|
|
|
|
spy.mockReturnValue(13_500); // +3.5s — distinct from the 1s reading
|
|
expect(interpolateElapsed(t, false, Infinity)).toBeCloseTo(33.5, 5);
|
|
});
|
|
|
|
it("freezes at serverElapsed while paused", () => {
|
|
vi.spyOn(Date, "now").mockReturnValue(99_000);
|
|
const t = timing({ serverElapsed: 42, serverSyncTime: 10_000, wasPlaying: true });
|
|
expect(interpolateElapsed(t, true, Infinity)).toBe(42);
|
|
});
|
|
|
|
it("clamps to maxDuration", () => {
|
|
vi.spyOn(Date, "now").mockReturnValue(1_000_000);
|
|
const t = timing({ serverElapsed: 100, serverSyncTime: 1_000, wasPlaying: true });
|
|
expect(interpolateElapsed(t, false, 180)).toBe(180);
|
|
});
|
|
});
|