mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
126 lines
4.9 KiB
TypeScript
126 lines
4.9 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
|
|
const starMock = vi.fn();
|
|
const unstarMock = vi.fn();
|
|
const setRatingMock = vi.fn();
|
|
vi.mock('@/lib/api/subsonicStarRating', () => ({
|
|
star: (...a: unknown[]) => starMock(...a),
|
|
unstar: (...a: unknown[]) => unstarMock(...a),
|
|
setRating: (...a: unknown[]) => setRatingMock(...a),
|
|
}));
|
|
|
|
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
|
import type { Track } from '@/lib/media/trackTypes';
|
|
import {
|
|
resetActiveServerConnectionSnapshot,
|
|
setActiveServerReachable,
|
|
} from '@/lib/network/activeServerReachability';
|
|
import { queueSongStar, queueSongRating, _resetPendingStarSyncForTest } from '@/features/playback/store/pendingStarSync';
|
|
import {
|
|
getCachedTrack,
|
|
seedQueueResolver,
|
|
_resetQueueResolverForTest,
|
|
} from '@/features/playback/store/queueTrackResolver';
|
|
import { toQueueItemRefs } from '@/features/playback/store/queueItemRef';
|
|
|
|
const track = (id: string): Track => ({
|
|
id, title: id, artist: '', album: 'A', albumId: 'A', duration: 1,
|
|
});
|
|
|
|
describe('pendingStarSync', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
resetActiveServerConnectionSnapshot();
|
|
setActiveServerReachable(true);
|
|
starMock.mockReset().mockResolvedValue(undefined);
|
|
unstarMock.mockReset().mockResolvedValue(undefined);
|
|
setRatingMock.mockReset().mockResolvedValue(undefined);
|
|
_resetPendingStarSyncForTest();
|
|
_resetQueueResolverForTest();
|
|
// Thin-state: the queue's track copy lives in the resolver cache. Seed it so
|
|
// a star/rating success has a cached entry to patch in place.
|
|
seedQueueResolver('', [track('t1')]);
|
|
usePlayerStore.setState({
|
|
currentTrack: track('t1'),
|
|
queueItems: toQueueItemRefs('', [track('t1')]),
|
|
queueServerId: null,
|
|
starredOverrides: {},
|
|
userRatingOverrides: {},
|
|
});
|
|
});
|
|
afterEach(() => {
|
|
_resetPendingStarSyncForTest();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('stars optimistically, then keeps the override + patches the track on success', async () => {
|
|
queueSongStar('t1', true);
|
|
expect(usePlayerStore.getState().starredOverrides.t1).toBe(true); // optimistic, instant
|
|
|
|
await vi.runAllTimersAsync();
|
|
|
|
expect(starMock).toHaveBeenCalledWith('t1', 'song', undefined);
|
|
const s = usePlayerStore.getState();
|
|
expect(s.starredOverrides.t1).toBe(true); // kept on success so list views stay in sync
|
|
expect(s.currentTrack?.starred).toBeTruthy(); // in-memory track patched
|
|
// Thin-state: the resolver cache entry is patched in place (not dropped) so
|
|
// the visible queue row keeps its title and reflects the synced star —
|
|
// dropping it would blank the row to a "…" placeholder.
|
|
const cached = getCachedTrack({ serverId: '', trackId: 't1' });
|
|
expect(cached?.title).toBe('t1');
|
|
expect(cached?.starred).toBeTruthy();
|
|
});
|
|
|
|
it('does NOT roll back on a network failure and keeps retrying', async () => {
|
|
starMock.mockRejectedValue(new Error('offline'));
|
|
queueSongStar('t1', true);
|
|
|
|
await vi.advanceTimersByTimeAsync(4000); // 0ms + 1s + 2s backoff cycles
|
|
|
|
expect(starMock.mock.calls.length).toBeGreaterThanOrEqual(2); // retried
|
|
expect(usePlayerStore.getState().starredOverrides.t1).toBe(true); // override survives (no rollback)
|
|
});
|
|
|
|
it('flushes pending stars when the active server becomes reachable', async () => {
|
|
starMock.mockRejectedValue(new Error('offline'));
|
|
queueSongStar('t1', true);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
expect(starMock).toHaveBeenCalledTimes(1);
|
|
|
|
setActiveServerReachable(false);
|
|
setActiveServerReachable(true);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
|
|
expect(starMock.mock.calls.length).toBeGreaterThan(1);
|
|
});
|
|
|
|
it('passes serverId through to star/unstar for cross-server favorites', async () => {
|
|
queueSongStar('t1', true, 'srv-b');
|
|
await vi.runAllTimersAsync();
|
|
expect(starMock).toHaveBeenCalledWith('t1', 'song', { serverId: 'srv-b' });
|
|
});
|
|
|
|
it('latest toggle wins when re-queued before sync', async () => {
|
|
queueSongStar('t1', true);
|
|
queueSongStar('t1', false); // user toggled back off
|
|
await vi.runAllTimersAsync();
|
|
expect(unstarMock).toHaveBeenCalledWith('t1', 'song', undefined);
|
|
expect(usePlayerStore.getState().starredOverrides.t1).toBe(false); // kept as durable false
|
|
expect(usePlayerStore.getState().currentTrack?.starred).toBeFalsy();
|
|
});
|
|
|
|
it('rates optimistically (track patched), clears override on success', async () => {
|
|
queueSongRating('t1', 4);
|
|
// setUserRatingOverride patches the track immediately:
|
|
expect(usePlayerStore.getState().currentTrack?.userRating).toBe(4);
|
|
expect(usePlayerStore.getState().userRatingOverrides.t1).toBe(4);
|
|
|
|
await vi.runAllTimersAsync();
|
|
|
|
expect(setRatingMock).toHaveBeenCalledWith('t1', 4);
|
|
const s = usePlayerStore.getState();
|
|
expect('t1' in s.userRatingOverrides).toBe(false); // cleared
|
|
expect(s.currentTrack?.userRating).toBe(4); // track stays patched
|
|
});
|
|
});
|