8a0e6782ad
The Library showed a blocking error with the backend unreachable. Now it composes a read-only library from everything already in the RTK Query cache (Tier-2 rehydrated last-seen data + anything fetched this session), so it keeps rendering offline instead of erroring. - selectors: `selectLocalTracks/Albums/Artists` — memoized, union + dedupe across getTracks/getAlbums/getArtists, the per-album/artist list endpoints, and single-entity fetches; skips pending/rejected entries - LibraryPage: when offline, fall back to the composed lists (live data still wins online), filter client-side for search, show an offline banner, and never show the retry-only ErrorState - i18n: `library.offline.*` (en + ru) - test: selector composition / dedup / status filtering (3 cases) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
2.4 KiB
TypeScript
88 lines
2.4 KiB
TypeScript
import { expect, test } from '@rstest/core';
|
|
import {
|
|
selectLocalTracks,
|
|
selectLocalAlbums,
|
|
selectLocalArtists,
|
|
} from '../src/store/selectors/localLibrary';
|
|
import type { RootState } from '../src/store/index';
|
|
|
|
function stateWith(queries: Record<string, unknown>): RootState {
|
|
return { api: { queries } } as unknown as RootState;
|
|
}
|
|
|
|
const track = (id: string, over: Record<string, unknown> = {}) => ({
|
|
id,
|
|
title: `Track ${id}`,
|
|
artistName: 'A',
|
|
albumTitle: 'Alb',
|
|
...over,
|
|
});
|
|
|
|
test('selectLocalTracks unions getTracks pages, list endpoints and single tracks', () => {
|
|
const state = stateWith({
|
|
'getTracks(undefined)': {
|
|
status: 'fulfilled',
|
|
endpointName: 'getTracks',
|
|
data: { items: [track('1'), track('2')], total: 2 },
|
|
},
|
|
'getArtistTracks("x")': {
|
|
status: 'fulfilled',
|
|
endpointName: 'getArtistTracks',
|
|
data: [track('2'), track('3')], // 2 is a dupe
|
|
},
|
|
'getTrack("4")': {
|
|
status: 'fulfilled',
|
|
endpointName: 'getTrack',
|
|
data: track('4'),
|
|
},
|
|
});
|
|
|
|
const ids = selectLocalTracks(state)
|
|
.map((t) => t.id)
|
|
.sort();
|
|
expect(ids).toEqual(['1', '2', '3', '4']);
|
|
});
|
|
|
|
test('selectLocalTracks ignores pending/rejected and null-data entries', () => {
|
|
const state = stateWith({
|
|
'getTracks(a)': {
|
|
status: 'rejected',
|
|
endpointName: 'getTracks',
|
|
data: undefined,
|
|
},
|
|
'getTracks(b)': { status: 'pending', endpointName: 'getTracks' },
|
|
'getTracks(c)': {
|
|
status: 'fulfilled',
|
|
endpointName: 'getTracks',
|
|
data: { items: [track('9')], total: 1 },
|
|
},
|
|
});
|
|
expect(selectLocalTracks(state).map((t) => t.id)).toEqual(['9']);
|
|
});
|
|
|
|
test('selectLocalAlbums and selectLocalArtists compose and dedupe', () => {
|
|
const state = stateWith({
|
|
'getAlbums(undefined)': {
|
|
status: 'fulfilled',
|
|
endpointName: 'getAlbums',
|
|
data: { items: [{ id: 'al1' }, { id: 'al2' }], total: 2 },
|
|
},
|
|
'getArtistAlbums("x")': {
|
|
status: 'fulfilled',
|
|
endpointName: 'getArtistAlbums',
|
|
data: [{ id: 'al2' }], // dupe
|
|
},
|
|
'getArtists(undefined)': {
|
|
status: 'fulfilled',
|
|
endpointName: 'getArtists',
|
|
data: { items: [{ id: 'ar1' }], total: 1 },
|
|
},
|
|
});
|
|
expect(
|
|
selectLocalAlbums(state)
|
|
.map((a) => a.id)
|
|
.sort(),
|
|
).toEqual(['al1', 'al2']);
|
|
expect(selectLocalArtists(state).map((a) => a.id)).toEqual(['ar1']);
|
|
});
|