mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
8cc022581f
Move the 5 subsonic api modules that M3 had co-located into features (subsonicAlbumInfo/Artists/Playlists/Radio/Statistics) into src/lib/api/, the feature-free infra layer, and drop their feature-barrel re-exports. Consumers now import these protocol calls directly from @/lib/api/subsonic*; the feature barrels export only domain UI/hooks/state. This is the consistent api placement (plan §10.3, revised: ALL subsonic protocol clients are feature-free infra, not per-feature) and it kills the documented api-induced feature<->feature cycles: offline->artist/playlist (getArtist/ getPlaylist*), album->artist (getArtistInfo), deviceSync/nowPlaying/orbit->*. Remaining artist<->album refs are UI-only (OpenArtistRefInline/coerceOpenArtist Refs) and offline->playlist is store-level (usePlaylistStore) — both deeper, tracked for M5, not api placement. Pure move: import specifiers + vi.mock/spy targets repointed; no behaviour change. tsc 0, lint 0/0, full suite 319 files / 2353 tests green. Tooling note: barrel-imported and dynamic-import api symbols were split out of @/features/* to @/lib/api/* (tsc-driven); 12 vi.mock barrels retargeted to the deep lib module (mock-collapse sweep), AlbumHeader's UI mock left untouched.
83 lines
3.2 KiB
TypeScript
83 lines
3.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
makeInitialOrbitState,
|
|
ORBIT_DEFAULT_SETTINGS,
|
|
type OrbitSettings,
|
|
type OrbitState,
|
|
} from '@/features/orbit/api/orbit';
|
|
|
|
const { writeOrbitState } = vi.hoisted(() => ({ writeOrbitState: vi.fn(() => Promise.resolve()) }));
|
|
const { orbitStore } = vi.hoisted(() => ({
|
|
orbitStore: {
|
|
role: 'host' as 'host' | 'guest' | null,
|
|
state: null as OrbitState | null,
|
|
sessionPlaylistId: 'session-pl' as string | null,
|
|
setState: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/features/orbit/utils/remote', () => ({
|
|
writeOrbitState,
|
|
writeOrbitHeartbeat: vi.fn(() => Promise.resolve()),
|
|
}));
|
|
vi.mock('@/features/orbit/store/orbitStore', () => ({ useOrbitStore: { getState: () => orbitStore } }));
|
|
vi.mock('@/store/authStore', () => ({ useAuthStore: { getState: () => ({}) } }));
|
|
vi.mock('@/store/playerStore', () => ({ usePlayerStore: { getState: () => ({ enqueue: vi.fn() }) } }));
|
|
vi.mock('@/lib/api/subsonicPlaylists', () => ({ createPlaylist: vi.fn(), deletePlaylist: vi.fn() }));
|
|
vi.mock('@/lib/api/subsonicLibrary', () => ({ getSong: vi.fn() }));
|
|
vi.mock('@/utils/playback/songToTrack', () => ({ songToTrack: vi.fn() }));
|
|
|
|
import { updateOrbitSettings } from '@/features/orbit/utils/host';
|
|
|
|
function hostStateWith(settings: OrbitSettings | undefined): OrbitState {
|
|
const base = makeInitialOrbitState({ sid: 'aaaa1111', host: 'host', name: 'sesh' });
|
|
return { ...base, settings: settings as OrbitSettings };
|
|
}
|
|
|
|
/** The settings object that updateOrbitSettings persisted on its last call. */
|
|
function writtenSettings(): OrbitSettings {
|
|
const calls = writeOrbitState.mock.calls as unknown as Array<[string, OrbitState]>;
|
|
const lastCall = calls[calls.length - 1];
|
|
return lastCall[1].settings as OrbitSettings;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
writeOrbitState.mockClear();
|
|
orbitStore.setState.mockClear();
|
|
orbitStore.role = 'host';
|
|
orbitStore.sessionPlaylistId = 'session-pl';
|
|
});
|
|
|
|
describe('updateOrbitSettings', () => {
|
|
it('does not silently flip autoApprove on a legacy settings-less session', async () => {
|
|
orbitStore.state = hostStateWith(undefined);
|
|
await updateOrbitSettings({ autoShuffle: false });
|
|
|
|
const settings = writtenSettings();
|
|
// The patch only touched autoShuffle…
|
|
expect(settings.autoShuffle).toBe(false);
|
|
// …autoApprove must come from the canonical default (false), not flip true.
|
|
expect(settings.autoApprove).toBe(ORBIT_DEFAULT_SETTINGS.autoApprove);
|
|
expect(settings.autoApprove).toBe(false);
|
|
expect(settings.shuffleIntervalMin).toBe(ORBIT_DEFAULT_SETTINGS.shuffleIntervalMin);
|
|
});
|
|
|
|
it('preserves existing settings when patching one field', async () => {
|
|
orbitStore.state = hostStateWith({ autoApprove: true, autoShuffle: true, shuffleIntervalMin: 30 });
|
|
await updateOrbitSettings({ autoShuffle: false });
|
|
|
|
const settings = writtenSettings();
|
|
expect(settings.autoApprove).toBe(true);
|
|
expect(settings.autoShuffle).toBe(false);
|
|
expect(settings.shuffleIntervalMin).toBe(30);
|
|
});
|
|
|
|
it('is a no-op when not hosting', async () => {
|
|
orbitStore.role = 'guest';
|
|
orbitStore.state = hostStateWith(undefined);
|
|
await updateOrbitSettings({ autoShuffle: false });
|
|
expect(writeOrbitState).not.toHaveBeenCalled();
|
|
});
|
|
});
|