mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
7a7a9f5e6b
111 of 122 top-level src/utils/ files move into 16 topic folders (audio, cache, cover, share, server, playback, playlist, deviceSync, waveform, mix, format, export, changelog, ui, perf, componentHelpers). True singletons with no cluster stay at the utils/ root. Pure file-move: a path-aware codemod rewrote 539 relative-import specifiers across 275 files; no logic touched. The hot-path coverage gate list (.github/frontend-hot-path-files.txt) is updated to the new paths for the 11 gated utils files — a mechanical consequence of the move, not a CI change. tsc is green.
100 lines
3.9 KiB
TypeScript
100 lines
3.9 KiB
TypeScript
/**
|
|
* `refreshWaveformForTrack` fetches an analysis row from Rust and applies
|
|
* it to the player store — but only if the refresh generation hasn't been
|
|
* bumped meanwhile and the track is still current. The tests pin both
|
|
* guards and the success / null-row / empty-bins / error branches.
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const hoisted = vi.hoisted(() => ({
|
|
invokeMock: vi.fn(async (_cmd: string, _args?: Record<string, unknown>) => null as unknown),
|
|
coerceWaveformBinsMock: vi.fn((bins: unknown) => {
|
|
if (bins == null) return null;
|
|
if (Array.isArray(bins) && bins.length === 0) return null;
|
|
return bins as number[];
|
|
}),
|
|
playerSnapshot: {
|
|
currentTrack: null as { id: string } | null,
|
|
},
|
|
playerSetStateMock: vi.fn(),
|
|
gen: 0,
|
|
getGenMock: vi.fn(() => hoisted.gen),
|
|
}));
|
|
|
|
vi.mock('@tauri-apps/api/core', () => ({ invoke: hoisted.invokeMock }));
|
|
vi.mock('../utils/waveform/waveformParse', () => ({ coerceWaveformBins: hoisted.coerceWaveformBinsMock }));
|
|
vi.mock('./playerStore', () => ({
|
|
usePlayerStore: {
|
|
getState: () => hoisted.playerSnapshot,
|
|
setState: hoisted.playerSetStateMock,
|
|
},
|
|
}));
|
|
vi.mock('./waveformRefreshGen', () => ({
|
|
getWaveformRefreshGen: hoisted.getGenMock,
|
|
}));
|
|
|
|
import { refreshWaveformForTrack } from './waveformRefresh';
|
|
|
|
beforeEach(() => {
|
|
hoisted.invokeMock.mockReset();
|
|
hoisted.invokeMock.mockResolvedValue(null);
|
|
hoisted.coerceWaveformBinsMock.mockClear();
|
|
hoisted.playerSetStateMock.mockClear();
|
|
hoisted.playerSnapshot.currentTrack = null;
|
|
hoisted.gen = 0;
|
|
});
|
|
|
|
describe('refreshWaveformForTrack', () => {
|
|
it('is a no-op for empty trackId', async () => {
|
|
await refreshWaveformForTrack('');
|
|
expect(hoisted.invokeMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('discards results when the gen has been bumped since the call started', async () => {
|
|
hoisted.playerSnapshot.currentTrack = { id: 't1' };
|
|
hoisted.invokeMock.mockImplementationOnce(async () => {
|
|
hoisted.gen = 99; // simulate concurrent bump
|
|
return { bins: [1, 2, 3] };
|
|
});
|
|
await refreshWaveformForTrack('t1');
|
|
expect(hoisted.playerSetStateMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('skips when the track is no longer current after the fetch', async () => {
|
|
hoisted.playerSnapshot.currentTrack = { id: 'other' };
|
|
hoisted.invokeMock.mockResolvedValueOnce({ bins: [1, 2, 3] });
|
|
await refreshWaveformForTrack('t1');
|
|
expect(hoisted.playerSetStateMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('blanks bins when the row is null', async () => {
|
|
hoisted.playerSnapshot.currentTrack = { id: 't1' };
|
|
hoisted.invokeMock.mockResolvedValueOnce(null);
|
|
await refreshWaveformForTrack('t1');
|
|
expect(hoisted.playerSetStateMock).toHaveBeenCalledWith({ waveformBins: null });
|
|
});
|
|
|
|
it('blanks bins when coerceWaveformBins returns null (invalid shape)', async () => {
|
|
hoisted.playerSnapshot.currentTrack = { id: 't1' };
|
|
hoisted.invokeMock.mockResolvedValueOnce({ bins: 'garbage' });
|
|
hoisted.coerceWaveformBinsMock.mockReturnValueOnce(null);
|
|
await refreshWaveformForTrack('t1');
|
|
expect(hoisted.playerSetStateMock).toHaveBeenCalledWith({ waveformBins: null });
|
|
});
|
|
|
|
it('applies the coerced bins on a clean fetch', async () => {
|
|
hoisted.playerSnapshot.currentTrack = { id: 't1' };
|
|
hoisted.invokeMock.mockResolvedValueOnce({ bins: [10, 20, 30] });
|
|
hoisted.coerceWaveformBinsMock.mockReturnValueOnce([10, 20, 30]);
|
|
await refreshWaveformForTrack('t1');
|
|
expect(hoisted.playerSetStateMock).toHaveBeenCalledWith({ waveformBins: [10, 20, 30] });
|
|
});
|
|
|
|
it('swallows fetch errors silently (placeholder waveform stays)', async () => {
|
|
hoisted.playerSnapshot.currentTrack = { id: 't1' };
|
|
hoisted.invokeMock.mockRejectedValueOnce(new Error('boom'));
|
|
await expect(refreshWaveformForTrack('t1')).resolves.toBeUndefined();
|
|
expect(hoisted.playerSetStateMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|