GetTracks answered with one JSON object per track: 20.5 MB at 26k tracks, ~350 bytes a row of repeated key names, four cover URLs that are identical across an album, and the album, artist and genre strings repeated on every track. Encoding it was ~170 MB of transient Go allocation, and parsing it the WebView's memory peak. GetTrackTable replaces it: one array per column, every repeated string stored once and sent as an index, genre lists interned, and only the fields the Tracks view reads. LastPlayed and the three larger cover tiers are left to the details dialog, which now fetches whole tracks by path. Each row still goes through trackFromRow, so this is an encoding of the one projection, not a second one. 167 bytes a track against 929 in the test library; a Go test holds it under a quarter of the object encoding, and a Vitest test reads the Go columns and the generated Track interface and fails if they drift. An empty library is now an empty table rather than the "no tracks in library" error the old binding returned. Closes #281
179 lines
5.7 KiB
TypeScript
179 lines
5.7 KiB
TypeScript
/**
|
|
* Every custom element in the tree, mounted against an empty backend.
|
|
*
|
|
* This is deliberately shallow: it asserts each component renders
|
|
* *something* and logs no error, which is the state an agent's change
|
|
* most often breaks and which nothing else here would notice. Depth
|
|
* belongs in the per-component specs and in e2e; breadth belongs here,
|
|
* because 46 elements is more than anyone will write specs for.
|
|
*/
|
|
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
|
|
|
|
// One import per component module, so a component that fails to even
|
|
// load is a failure here rather than a silent absence.
|
|
import '@components/artist-details/artist-details';
|
|
import '@components/artists-view/artists-view';
|
|
import '@components/audio-player/audio-player';
|
|
import '@components/audio-player/controls/player-controls';
|
|
import '@components/audio-player/seekbar/seek-bar';
|
|
import '@components/audio-player/volume-control/volume-control';
|
|
import '@components/autotag-view/autotag-view';
|
|
import '@components/combobox/combobox';
|
|
import '@components/config-page/config-page';
|
|
import '@components/config-page/config-field';
|
|
import '@components/config-page/config-section';
|
|
import '@components/config-page/download-clients';
|
|
import '@components/config-page/shortcut-capture';
|
|
import '@components/cover-grid/cover-grid';
|
|
import '@components/cover-grid/album-dropdown';
|
|
import '@components/download-picker/download-picker';
|
|
import '@components/download-picker/candidate-row';
|
|
import '@components/downloads-view/downloads-view';
|
|
import '@components/duplicate-tracks-dialog/duplicate-tracks-dialog';
|
|
import '@components/explore-album-details/explore-album-details';
|
|
import '@components/explore-artist-details/explore-artist-details';
|
|
import '@components/explore-view/explore-view';
|
|
import '@components/first-run-wizard/first-run-wizard';
|
|
import '@components/genre-details/genre-details';
|
|
import '@components/genres-view/genres-view';
|
|
import '@components/jobs/job-details-drawer';
|
|
import '@components/jobs/job-indicator';
|
|
import '@components/jobs/job-log-view';
|
|
import '@components/jobs/job-row';
|
|
import '@components/jobs/job-panel';
|
|
import '@components/library-filter/library-filter';
|
|
import '@components/library-status-indicator/library-status-indicator';
|
|
import '@components/now-playing/now-playing';
|
|
import '@components/phantom-resolver/phantom-resolver';
|
|
import '@components/playlist-details/playlist-details';
|
|
import '@components/playlist-picker/playlist-picker';
|
|
import '@components/playlist-view/playlist-view';
|
|
import '@components/queue-panel/queue-panel';
|
|
import '@components/search-bar/search-bar';
|
|
import '@components/sidebar/app-sidebar';
|
|
import '@components/smart-playlist-details/smart-playlist-details';
|
|
import '@components/smart-playlist-editor/smart-playlist-editor';
|
|
import '@components/top-results-row/top-results-row';
|
|
import '@components/track-details/track-details';
|
|
import '@components/track-info/track-info';
|
|
import '@components/track-list/track-list';
|
|
|
|
import { flush, stub } from '@test/support/harness';
|
|
import { fixture } from '@test/support/render';
|
|
|
|
/** Every element the app registers, in registration order. */
|
|
const TAGS = [
|
|
'album-dropdown',
|
|
'app-sidebar',
|
|
'artist-details',
|
|
'artists-view',
|
|
'audio-player',
|
|
'autotag-view',
|
|
'candidate-row',
|
|
'config-field',
|
|
'config-page',
|
|
'config-section',
|
|
'cover-grid',
|
|
'download-clients',
|
|
'download-picker',
|
|
'downloads-view',
|
|
'duplicate-tracks-dialog',
|
|
'explore-album-details',
|
|
'explore-artist-details',
|
|
'explore-view',
|
|
'first-run-wizard',
|
|
'genre-details',
|
|
'genres-view',
|
|
'job-details-drawer',
|
|
'job-indicator',
|
|
'job-log-view',
|
|
'job-panel',
|
|
'job-row',
|
|
'library-filter',
|
|
'library-status-indicator',
|
|
'now-playing',
|
|
'phantom-resolver',
|
|
'player-controls',
|
|
'playlist-details',
|
|
'playlist-picker',
|
|
'playlist-view',
|
|
'queue-panel',
|
|
'search-bar',
|
|
'seek-bar',
|
|
'shortcut-capture',
|
|
'smart-playlist-details',
|
|
'smart-playlist-editor',
|
|
'top-results-row',
|
|
'track-details',
|
|
'track-info',
|
|
'track-list',
|
|
'volume-control',
|
|
'yj-combobox',
|
|
];
|
|
|
|
/**
|
|
* An empty-but-valid backend. Unstubbed bindings resolve undefined,
|
|
* which is not what Go sends — an empty list is.
|
|
*/
|
|
function stubEmptyBackend(): void {
|
|
const emptyLists = [
|
|
'library.Library.GetTrackTable',
|
|
'library.Library.GetAlbums',
|
|
'library.Library.GetArtists',
|
|
'library.Library.GetGenres',
|
|
'library.Library.GetAllLibrariesWithTrackCounts',
|
|
'playlist.Service.GetAllPlaylists',
|
|
'playlist.Service.GetAllPlaylistsWithTracks',
|
|
'playlist.Service.GetDefaultPlaylistTrackPaths',
|
|
'jobs.Service.GetJobs',
|
|
'download.Service.ListProviders',
|
|
'download.Service.ListDownloads',
|
|
'download.Service.ListRequests',
|
|
'download.Service.ProviderKinds',
|
|
];
|
|
|
|
for (const path of emptyLists) stub(path, []);
|
|
|
|
stub('config.Config.GetShortcuts', {});
|
|
stub('config.Config.GetDownloadPreferences', {});
|
|
stub('config.Config.GetThemeAccentColor', '#ffd43b');
|
|
stub('config.Config.GetThemeBackgroundShade', 'dark');
|
|
}
|
|
|
|
describe('every component mounts on an empty library', () => {
|
|
let errors: unknown[][] = [];
|
|
|
|
beforeEach(() => {
|
|
stubEmptyBackend();
|
|
errors = [];
|
|
vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => {
|
|
errors.push(args);
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('registers every element it defines', () => {
|
|
const missing = TAGS.filter((tag) => !customElements.get(tag));
|
|
|
|
expect(missing).toEqual([]);
|
|
});
|
|
|
|
for (const tag of TAGS) {
|
|
it(`<${tag}> renders without logging an error`, async () => {
|
|
const el = await fixture(tag);
|
|
|
|
await flush();
|
|
await el.updateComplete;
|
|
|
|
expect({ tag, root: el.shadowRoot !== null, errors }).toEqual({
|
|
tag,
|
|
root: true,
|
|
errors: [],
|
|
});
|
|
});
|
|
}
|
|
});
|