/** * Derived normalization snapshot — three engine branches and a couple of * replaygain corner cases (no tag → fallback, neighbour-track context for * album-mode resolution). useAuthStore is mocked through a hoisted state * object so each test can flip flags without rebuilding the store. */ import type { Track } from './playerStoreTypes'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const { authState } = vi.hoisted(() => ({ authState: { normalizationEngine: 'off' as 'off' | 'replaygain' | 'loudness', loudnessTargetLufs: -14, replayGainEnabled: false, replayGainMode: 'track' as 'track' | 'album', replayGainPreGainDb: 0, replayGainFallbackDb: -6, }, })); vi.mock('./authStore', () => ({ useAuthStore: { getState: () => authState }, })); vi.mock('../utils/audio/resolveReplayGainDb', () => ({ resolveReplayGainDb: vi.fn( (track: Track) => (track as Track & { _testGain?: number | null })._testGain ?? null, ), })); import { deriveNormalizationSnapshot } from './normalizationSnapshot'; function track(id: string, gain: number | null = null): Track { return { id, title: id, artist: 'A', album: 'X', albumId: 'X', duration: 100, ...(gain !== null ? { _testGain: gain } : {}), } as Track; } beforeEach(() => { authState.normalizationEngine = 'off'; authState.loudnessTargetLufs = -14; authState.replayGainEnabled = false; authState.replayGainMode = 'track'; authState.replayGainPreGainDb = 0; authState.replayGainFallbackDb = -6; }); describe("engine='off'", () => { it('returns a fully-null snapshot', () => { expect(deriveNormalizationSnapshot(track('a'), [track('a')], 0)).toEqual({ normalizationNowDb: null, normalizationTargetLufs: null, normalizationEngineLive: 'off', }); }); }); describe("engine='loudness'", () => { it('returns the configured target LUFS and clears nowDb', () => { authState.normalizationEngine = 'loudness'; authState.loudnessTargetLufs = -10; expect(deriveNormalizationSnapshot(track('a'), [track('a')], 0)).toEqual({ normalizationNowDb: null, normalizationTargetLufs: -10, normalizationEngineLive: 'loudness', }); }); }); describe("engine='replaygain'", () => { it("falls through to 'off' when replayGainEnabled is false", () => { authState.normalizationEngine = 'replaygain'; authState.replayGainEnabled = false; expect(deriveNormalizationSnapshot(track('a'), [track('a')], 0).normalizationEngineLive).toBe('off'); }); it('uses the resolved gain + pre-gain when a tag is present', () => { authState.normalizationEngine = 'replaygain'; authState.replayGainEnabled = true; authState.replayGainPreGainDb = 2; const snap = deriveNormalizationSnapshot(track('a', -8), [track('a', -8)], 0); expect(snap.normalizationNowDb).toBe(-6); // -8 + 2 expect(snap.normalizationEngineLive).toBe('replaygain'); expect(snap.normalizationTargetLufs).toBeNull(); }); it('uses the fallback dB when no tag is resolvable', () => { authState.normalizationEngine = 'replaygain'; authState.replayGainEnabled = true; authState.replayGainFallbackDb = -3; const snap = deriveNormalizationSnapshot(track('a'), [track('a')], 0); expect(snap.normalizationNowDb).toBe(-3); expect(snap.normalizationEngineLive).toBe('replaygain'); }); it('passes neighbour-track context for album-mode resolution', async () => { const { resolveReplayGainDb } = await import('../utils/audio/resolveReplayGainDb'); const spy = vi.mocked(resolveReplayGainDb); spy.mockClear(); authState.normalizationEngine = 'replaygain'; authState.replayGainEnabled = true; authState.replayGainMode = 'album'; const queue = [track('prev'), track('current'), track('next')]; deriveNormalizationSnapshot(queue[1], queue, 1); expect(spy).toHaveBeenCalledWith(queue[1], queue[0], queue[2], true, 'album'); }); it('passes null for prev when queueIndex is 0', async () => { const { resolveReplayGainDb } = await import('../utils/audio/resolveReplayGainDb'); const spy = vi.mocked(resolveReplayGainDb); spy.mockClear(); authState.normalizationEngine = 'replaygain'; authState.replayGainEnabled = true; const queue = [track('a'), track('b')]; deriveNormalizationSnapshot(queue[0], queue, 0); expect(spy.mock.calls[0][1]).toBeNull(); expect(spy.mock.calls[0][2]).toEqual(queue[1]); }); it('passes null for next when queueIndex is the last slot', async () => { const { resolveReplayGainDb } = await import('../utils/audio/resolveReplayGainDb'); const spy = vi.mocked(resolveReplayGainDb); spy.mockClear(); authState.normalizationEngine = 'replaygain'; authState.replayGainEnabled = true; const queue = [track('a'), track('b')]; deriveNormalizationSnapshot(queue[1], queue, 1); expect(spy.mock.calls[0][1]).toEqual(queue[0]); expect(spy.mock.calls[0][2]).toBeNull(); }); });