Files
Psychotoxical-psysonic/src/store/loudnessReseed.test.ts
T
Frank Stellmacher 9606a99efb refactor(api): F.50 — extract 7 small domain modules (#615)
Seven domain-eng splits peel ~200 LOC of read endpoints out of
`api/subsonic.ts`:

- `subsonicStreamUrl.ts` — `buildStreamUrl`, `coverArtCacheKey`,
  `buildCoverArtUrl`, `buildDownloadUrl` (token-signed URL builders
  for the four /rest endpoints we hand to the browser).
- `subsonicStarRating.ts` — `getStarred`, `star`, `unstar`,
  `setRating`, `probeEntityRatingSupport`. `setRating` still triggers
  the lazy `navidromeBrowse` cache invalidation; the same-folder
  lazy import path is preserved.
- `subsonicSearch.ts` — `search`, `searchSongsPaged`.
- `subsonicScrobble.ts` — `scrobbleSong`, `reportNowPlaying`,
  `getNowPlaying`.
- `subsonicAlbumInfo.ts` — `getAlbumInfo2`.
- `subsonicLyrics.ts` — `getLyricsBySongId`.
- `subsonicGenres.ts` — `getGenres`, `getAlbumsByGenre`.

63 external call sites migrated to direct imports. Four `vi.mock`
targets in the store-level tests pointed at `../api/subsonic` and
were updated to the new module paths.

Pure code-move. subsonic.ts: 762 → 561 LOC (−201).
2026-05-13 00:46:13 +02:00

151 lines
5.7 KiB
TypeScript

/**
* `reseedLoudnessForTrackId` orchestrates a full analysis re-run for one
* track: invalidate waveform gen, clear loudness + backfill state, wipe
* server rows, kick a forced seed. The orchestration is what's worth
* pinning — each individual helper is already tested in its own module.
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
const hoisted = vi.hoisted(() => {
const authState = {
normalizationEngine: 'loudness' as 'off' | 'replaygain' | 'loudness',
loudnessTargetLufs: -14,
};
const playerSnapshot: {
currentTrack: { id: string } | null;
updateReplayGainForCurrentTrack: ReturnType<typeof vi.fn>;
} = {
currentTrack: null,
updateReplayGainForCurrentTrack: vi.fn(),
};
return {
authState,
playerSnapshot,
invokeMock: vi.fn(async (_cmd: string, _args?: Record<string, unknown>) => undefined),
buildStreamUrlMock: vi.fn((id: string) => `https://mock/stream/${id}`),
bumpWaveformRefreshGenMock: vi.fn(),
clearLoudnessCacheMock: vi.fn(),
resetBackfillStateMock: vi.fn(),
playerSetStateMock: vi.fn(),
};
});
vi.mock('@tauri-apps/api/core', () => ({ invoke: hoisted.invokeMock }));
vi.mock('../api/subsonicStreamUrl', () => ({ buildStreamUrl: hoisted.buildStreamUrlMock }));
vi.mock('./authStore', () => ({ useAuthStore: { getState: () => hoisted.authState } }));
vi.mock('./playerStore', () => ({
usePlayerStore: {
getState: () => hoisted.playerSnapshot,
setState: hoisted.playerSetStateMock,
},
}));
vi.mock('./waveformRefreshGen', () => ({
bumpWaveformRefreshGen: hoisted.bumpWaveformRefreshGenMock,
}));
vi.mock('./loudnessGainCache', () => ({
clearLoudnessCacheStateForTrackId: hoisted.clearLoudnessCacheMock,
}));
vi.mock('./loudnessBackfillState', () => ({
resetLoudnessBackfillStateForTrackId: hoisted.resetBackfillStateMock,
}));
import { reseedLoudnessForTrackId } from './loudnessReseed';
beforeEach(() => {
hoisted.authState.normalizationEngine = 'loudness';
hoisted.authState.loudnessTargetLufs = -14;
hoisted.playerSnapshot.currentTrack = null;
hoisted.invokeMock.mockReset();
hoisted.invokeMock.mockResolvedValue(undefined);
hoisted.buildStreamUrlMock.mockClear();
hoisted.bumpWaveformRefreshGenMock.mockClear();
hoisted.clearLoudnessCacheMock.mockClear();
hoisted.resetBackfillStateMock.mockClear();
hoisted.playerSetStateMock.mockClear();
hoisted.playerSnapshot.updateReplayGainForCurrentTrack = vi.fn();
});
describe('reseedLoudnessForTrackId', () => {
it('is a no-op for empty trackId', async () => {
await reseedLoudnessForTrackId('');
expect(hoisted.invokeMock).not.toHaveBeenCalled();
expect(hoisted.bumpWaveformRefreshGenMock).not.toHaveBeenCalled();
});
it("is a no-op when normalization engine isn't loudness", async () => {
hoisted.authState.normalizationEngine = 'off';
await reseedLoudnessForTrackId('t1');
expect(hoisted.invokeMock).not.toHaveBeenCalled();
expect(hoisted.bumpWaveformRefreshGenMock).not.toHaveBeenCalled();
});
it('runs the full reseed pipeline in order', async () => {
await reseedLoudnessForTrackId('t1');
expect(hoisted.bumpWaveformRefreshGenMock).toHaveBeenCalledWith('t1');
expect(hoisted.clearLoudnessCacheMock).toHaveBeenCalledWith('t1');
expect(hoisted.resetBackfillStateMock).toHaveBeenCalledWith('t1');
const invokeCalls = hoisted.invokeMock.mock.calls.map(c => c[0]);
expect(invokeCalls).toEqual([
'analysis_delete_waveform_for_track',
'analysis_delete_loudness_for_track',
'analysis_enqueue_seed_from_url',
]);
expect(hoisted.invokeMock.mock.calls[2][1]).toEqual({
trackId: 't1',
url: 'https://mock/stream/t1',
force: true,
});
});
it('blanks the seekbar only when the reseed target is the current track', async () => {
hoisted.playerSnapshot.currentTrack = { id: 't1' };
await reseedLoudnessForTrackId('t1');
const setStateCalls = hoisted.playerSetStateMock.mock.calls.map(c => c[0]);
expect(setStateCalls).toContainEqual({ waveformBins: null });
});
it('does NOT blank the seekbar when reseeding a different track', async () => {
hoisted.playerSnapshot.currentTrack = { id: 'other' };
await reseedLoudnessForTrackId('t1');
const setStateCalls = hoisted.playerSetStateMock.mock.calls.map(c => c[0]);
expect(setStateCalls).not.toContainEqual({ waveformBins: null });
});
it('resets live normalization-state to placeholder values', async () => {
hoisted.authState.loudnessTargetLufs = -10;
await reseedLoudnessForTrackId('t1');
const setStateCalls = hoisted.playerSetStateMock.mock.calls.map(c => c[0]);
expect(setStateCalls).toContainEqual({
normalizationNowDb: null,
normalizationTargetLufs: -10,
normalizationEngineLive: 'loudness',
});
});
it('continues past errors in delete-waveform', async () => {
hoisted.invokeMock
.mockRejectedValueOnce(new Error('waveform delete failed'))
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined);
await reseedLoudnessForTrackId('t1');
expect(hoisted.invokeMock).toHaveBeenCalledTimes(3);
});
it('continues past errors in delete-loudness', async () => {
hoisted.invokeMock
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(new Error('loudness delete failed'))
.mockResolvedValueOnce(undefined);
await reseedLoudnessForTrackId('t1');
expect(hoisted.invokeMock).toHaveBeenCalledTimes(3);
});
it('swallows the final enqueue-seed error', async () => {
hoisted.invokeMock
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(new Error('enqueue failed'));
await expect(reseedLoudnessForTrackId('t1')).resolves.toBeUndefined();
});
});