- make listening status prefetch optional so /stats still renders on status failures - normalize no-stats-yet cache responses into the existing empty stats state - restore streaming fallback when library track resolution errors - add route and API regression coverage for the review fixes
143 lines
4.1 KiB
TypeScript
143 lines
4.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { HttpResponse, http, server } from '@/test/msw';
|
|
|
|
import {
|
|
fetchListeningStatsStatus,
|
|
fetchStatsCached,
|
|
fetchStatsDbStorage,
|
|
fetchStatsLibraryDiskUsage,
|
|
resolveStatsTrack,
|
|
streamStatsTrack,
|
|
triggerListeningStatsSync,
|
|
} from './-stats.api';
|
|
|
|
describe('stats api', () => {
|
|
it('fetches the cached stats payload for a range', async () => {
|
|
server.use(
|
|
http.get('/api/stats/cached', ({ request }) => {
|
|
const url = new URL(request.url);
|
|
expect(url.searchParams.get('range')).toBe('30d');
|
|
|
|
return HttpResponse.json({
|
|
success: true,
|
|
overview: { total_plays: 12 },
|
|
top_artists: [],
|
|
top_albums: [],
|
|
top_tracks: [],
|
|
timeline: [],
|
|
genres: [],
|
|
recent: [],
|
|
health: {},
|
|
});
|
|
}),
|
|
);
|
|
|
|
await expect(fetchStatsCached('30d')).resolves.toMatchObject({
|
|
overview: { total_plays: 12 },
|
|
});
|
|
});
|
|
|
|
it('returns an empty payload when cached stats are not available yet', async () => {
|
|
server.use(
|
|
http.get('/api/stats/cached', () =>
|
|
HttpResponse.json({ success: false, error: 'Listening stats not synced yet' }),
|
|
),
|
|
);
|
|
|
|
await expect(fetchStatsCached('7d')).resolves.toMatchObject({
|
|
success: true,
|
|
overview: { total_plays: 0 },
|
|
top_artists: [],
|
|
top_albums: [],
|
|
top_tracks: [],
|
|
timeline: [],
|
|
genres: [],
|
|
recent: [],
|
|
health: {},
|
|
});
|
|
});
|
|
|
|
it('returns an empty payload when the server reports a cache miss as an HTTP error', async () => {
|
|
server.use(
|
|
http.get('/api/stats/cached', () =>
|
|
HttpResponse.json({ error: 'No cached stats available yet' }, { status: 500 }),
|
|
),
|
|
);
|
|
|
|
await expect(fetchStatsCached('7d')).resolves.toMatchObject({
|
|
success: true,
|
|
overview: { total_plays: 0 },
|
|
top_artists: [],
|
|
top_albums: [],
|
|
top_tracks: [],
|
|
timeline: [],
|
|
genres: [],
|
|
recent: [],
|
|
health: {},
|
|
});
|
|
});
|
|
|
|
it('surfaces db storage and disk usage errors', async () => {
|
|
server.use(
|
|
http.get('/api/stats/db-storage', () =>
|
|
HttpResponse.json({ error: 'db unavailable' }, { status: 500 }),
|
|
),
|
|
http.get('/api/stats/library-disk-usage', () =>
|
|
HttpResponse.json({ error: 'disk unavailable' }, { status: 500 }),
|
|
),
|
|
);
|
|
|
|
await expect(fetchStatsDbStorage()).rejects.toThrow('db unavailable');
|
|
await expect(fetchStatsLibraryDiskUsage()).rejects.toThrow('disk unavailable');
|
|
});
|
|
|
|
it('reads listening status and triggers manual sync', async () => {
|
|
server.use(
|
|
http.get('/api/listening-stats/status', () =>
|
|
HttpResponse.json({ stats: { last_poll: '2026-05-14 10:00:00' } }),
|
|
),
|
|
http.post('/api/listening-stats/sync', () => HttpResponse.json({ success: true })),
|
|
);
|
|
|
|
await expect(fetchListeningStatsStatus()).resolves.toEqual({
|
|
stats: { last_poll: '2026-05-14 10:00:00' },
|
|
});
|
|
await expect(triggerListeningStatsSync()).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('resolves and streams tracks through the stats playback helpers', async () => {
|
|
server.use(
|
|
http.post('/api/stats/resolve-track', async ({ request }) => {
|
|
await expect(request.json()).resolves.toEqual({
|
|
title: 'Track',
|
|
artist: 'Artist',
|
|
});
|
|
return HttpResponse.json({
|
|
success: true,
|
|
track: { id: 1, title: 'Track', file_path: '/music/track.flac' },
|
|
});
|
|
}),
|
|
http.post('/api/enhanced-search/stream-track', async ({ request }) => {
|
|
await expect(request.json()).resolves.toEqual({
|
|
track_name: 'Track',
|
|
artist_name: 'Artist',
|
|
album_name: 'Album',
|
|
duration_ms: 0,
|
|
});
|
|
return HttpResponse.json({
|
|
success: true,
|
|
result: { stream_url: '/api/stream/1' },
|
|
});
|
|
}),
|
|
);
|
|
|
|
await expect(resolveStatsTrack('Track', 'Artist')).resolves.toMatchObject({
|
|
id: 1,
|
|
title: 'Track',
|
|
});
|
|
await expect(streamStatsTrack('Track', 'Artist', 'Album')).resolves.toEqual({
|
|
stream_url: '/api/stream/1',
|
|
});
|
|
});
|
|
});
|