Files
psysonic/src/store/normalizationIpcDedupe.test.ts
T
Frank Stellmacher 86b13dd4d0 refactor(player): E.10 — extract normalization-IPC deduplicators (#573)
`invokeAudioSetNormalizationDeduped` (450 ms window for
`audio_set_normalization`) and `invokeAudioUpdateReplayGainDeduped`
(250 ms window for `audio_update_replay_gain`, with LUFS-target /
pre-trim implicitly contributing to the dedupe key) move into
`src/store/normalizationIpcDedupe.ts` along with their four
"last-invoked" mutables.

File-private throughout; three internal call sites become plain
imports. 11 focused tests cover the window-boundary behaviour, the
payload-field sensitivity, the engine-mode key contribution
(re-fires when LUFS target changes even with identical gain), and
the null / NaN serialization.

playerStore 3369 → 3302 LOC.
2026-05-12 14:07:44 +02:00

131 lines
4.7 KiB
TypeScript

/**
* IPC dedupers — each helper collapses repeat calls within a time-bounded
* window. The interesting behaviour is the engine-mode contribution to the
* replay-gain dedupe key (so changing the LUFS target re-fires even when
* the cached dB stays the same) and the null-aware number formatter.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const { authState, invokeMock } = vi.hoisted(() => ({
authState: {
normalizationEngine: 'off' as 'off' | 'replaygain' | 'loudness',
loudnessTargetLufs: -14,
loudnessPreAnalysisAttenuationDb: 0,
},
invokeMock: vi.fn(async (_cmd: string, _args?: Record<string, unknown>) => undefined),
}));
vi.mock('@tauri-apps/api/core', () => ({ invoke: invokeMock }));
vi.mock('./authStore', () => ({ useAuthStore: { getState: () => authState } }));
vi.mock('../utils/loudnessPreAnalysisSlider', () => ({
effectiveLoudnessPreAnalysisAttenuationDb: (attenuation: number) => attenuation,
}));
import {
_resetNormalizationIpcDedupeForTest,
invokeAudioSetNormalizationDeduped,
invokeAudioUpdateReplayGainDeduped,
} from './normalizationIpcDedupe';
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-05-12T12:00:00Z'));
invokeMock.mockClear();
authState.normalizationEngine = 'off';
authState.loudnessTargetLufs = -14;
authState.loudnessPreAnalysisAttenuationDb = 0;
});
afterEach(() => {
_resetNormalizationIpcDedupeForTest();
vi.useRealTimers();
});
describe('invokeAudioSetNormalizationDeduped', () => {
const payload = { engine: 'loudness', targetLufs: -14, preAnalysisAttenuationDb: 0 };
it('passes the first call through', () => {
invokeAudioSetNormalizationDeduped(payload);
expect(invokeMock).toHaveBeenCalledTimes(1);
expect(invokeMock).toHaveBeenCalledWith('audio_set_normalization', payload);
});
it('skips a repeat with the same payload inside the 450 ms window', () => {
invokeAudioSetNormalizationDeduped(payload);
vi.advanceTimersByTime(449);
invokeAudioSetNormalizationDeduped(payload);
expect(invokeMock).toHaveBeenCalledTimes(1);
});
it('fires again once the 450 ms window elapses', () => {
invokeAudioSetNormalizationDeduped(payload);
vi.advanceTimersByTime(450);
invokeAudioSetNormalizationDeduped(payload);
expect(invokeMock).toHaveBeenCalledTimes(2);
});
it('fires when any payload field changes within the window', () => {
invokeAudioSetNormalizationDeduped(payload);
invokeAudioSetNormalizationDeduped({ ...payload, targetLufs: -10 });
invokeAudioSetNormalizationDeduped({ ...payload, engine: 'replaygain' });
invokeAudioSetNormalizationDeduped({ ...payload, preAnalysisAttenuationDb: -2 });
expect(invokeMock).toHaveBeenCalledTimes(4);
});
});
describe('invokeAudioUpdateReplayGainDeduped', () => {
const payload = {
volume: 0.8,
replayGainDb: -6,
replayGainPeak: 0.9,
loudnessGainDb: -3,
preGainDb: 0,
fallbackDb: -6,
};
it('passes the first call through', () => {
invokeAudioUpdateReplayGainDeduped(payload);
expect(invokeMock).toHaveBeenCalledWith('audio_update_replay_gain', payload);
});
it('skips a repeat with the same payload inside the 250 ms window', () => {
invokeAudioUpdateReplayGainDeduped(payload);
vi.advanceTimersByTime(249);
invokeAudioUpdateReplayGainDeduped(payload);
expect(invokeMock).toHaveBeenCalledTimes(1);
});
it('fires again once the 250 ms window elapses', () => {
invokeAudioUpdateReplayGainDeduped(payload);
vi.advanceTimersByTime(250);
invokeAudioUpdateReplayGainDeduped(payload);
expect(invokeMock).toHaveBeenCalledTimes(2);
});
it('re-fires when the LUFS target changes even if the dB payload stays the same', () => {
authState.normalizationEngine = 'loudness';
authState.loudnessTargetLufs = -14;
invokeAudioUpdateReplayGainDeduped(payload);
authState.loudnessTargetLufs = -10;
invokeAudioUpdateReplayGainDeduped(payload);
expect(invokeMock).toHaveBeenCalledTimes(2);
});
it('treats null and non-finite gain values as the dedupe-string "null"', () => {
invokeAudioUpdateReplayGainDeduped({ ...payload, replayGainDb: null });
invokeAudioUpdateReplayGainDeduped({ ...payload, replayGainDb: Number.NaN });
// Same dedupe-key (both serialize to "null") + same window → second call dropped.
expect(invokeMock).toHaveBeenCalledTimes(1);
});
});
describe('_resetNormalizationIpcDedupeForTest', () => {
it('lets the next call through again after a reset', () => {
const payload = { engine: 'off', targetLufs: -14, preAnalysisAttenuationDb: 0 };
invokeAudioSetNormalizationDeduped(payload);
_resetNormalizationIpcDedupeForTest();
invokeAudioSetNormalizationDeduped(payload);
expect(invokeMock).toHaveBeenCalledTimes(2);
});
});