diff --git a/web-panel/bridge/scripts/default/installed-apps-sync/installed-data.js b/web-panel/bridge/scripts/default/installed-apps-sync/installed-data.js index 9ed4cba..082de05 100644 --- a/web-panel/bridge/scripts/default/installed-apps-sync/installed-data.js +++ b/web-panel/bridge/scripts/default/installed-apps-sync/installed-data.js @@ -22,7 +22,10 @@ import { export function resolveInstalledData(state) { const storeState = getStoreState(state.storeRef) - if (isRecord(state.installedAppsService?.installedApps)) { + // A populated slice is required, not merely a present one: the service starts with + // an empty installedApps object and only fills it in refreshApps. Accepting {} here + // would take this branch with nothing to match and skip the store fallback below. + if (isNonEmptyRecord(state.installedAppsService?.installedApps)) { return { rawInstalledApps: state.installedAppsService.installedApps, catalog: isRecord(state.installedAppsService.catalog) @@ -351,6 +354,10 @@ export function getInstalledVersionsForGame(gameId, data) { ) } +function isNonEmptyRecord(value) { + return isRecord(value) && Object.keys(value).length > 0 +} + function getStoreState(storeRef) { const state = typeof storeRef?.state?.getValue === "function" @@ -561,17 +568,19 @@ function pickImageUrlForTitle(title, game, preferredApp, sidebarClientIconUrl, v : [title, game, preferredApp] return pickImageUrl( + // The Wand client icon CDN wins whenever a Steam AppID is resolvable, on any + // install platform. It yields null when no AppID is found, so the metadata + // candidates below still apply to non-Steam titles. + getSteamClientIconUrl( + findSteamAppId(...steamRoots), + getInstalledAppSteamAppId(preferredApp.platform, preferredApp.sku) + ), title?.imageUrl, title?.iconUrl, title?.coverUrl, title?.thumbnailUrl, title?.logoUrl, title?.headerImageUrl, - getSteamClientIconUrl( - findSteamAppId(...steamRoots), - getInstalledAppSteamAppId(preferredApp.platform, preferredApp.sku) - ), - sidebarClientIconUrl, title?.images, title?.assets, game.imageUrl, @@ -582,6 +591,9 @@ function pickImageUrlForTitle(title, game, preferredApp, sidebarClientIconUrl, v game.headerImageUrl, game.images, game.assets, - preferredApp.imageUrl + preferredApp.imageUrl, + // Scraped from the rendered sidebar, so it is the last resort once no + // metadata source exposed artwork. + sidebarClientIconUrl ) } diff --git a/web-panel/bridge/src/renderer-scripts.test.ts b/web-panel/bridge/src/renderer-scripts.test.ts index 75d4504..0291b02 100644 --- a/web-panel/bridge/src/renderer-scripts.test.ts +++ b/web-panel/bridge/src/renderer-scripts.test.ts @@ -5,8 +5,45 @@ import { getSteamClientIconUrl, normalizeImageUrl, } from '../scripts/default/installed-apps-sync/artwork.js'; +import { + buildSnapshot, + resolveInstalledData, +} from '../scripts/default/installed-apps-sync/installed-data.js'; import { resolveQrRenderer } from '../scripts/default/remote-popup-cleanup/qr-renderer.js'; +const CLIENT_ICON_URL = + 'https://api-cdn.wemod.com/steam_community/1245620/client_icon/96.webp'; + +function makeInstalledAppsState( + correlationId: string, + app: Record, + title: Record = {} +) { + return { + log: () => undefined, + storeRef: null, + unavailableTitlesById: {}, + installedAppsService: { + installedApps: { [correlationId]: app }, + catalog: { + games: { + '900': { + titleId: '500', + correlationIds: [correlationId], + displayName: 'Subnautica', + }, + }, + titles: { + '500': { name: 'Subnautica', ...title }, + }, + }, + installedVersions: { + '900': [{ correlationId }], + }, + }, + }; +} + describe('installed-apps renderer script models', () => { it('normalizes captured artwork shapes without a Wand runtime', () => { expect(normalizeImageUrl({ cover: { imageUrl: '//cdn.example/game.webp' } })) @@ -30,6 +67,56 @@ describe('installed-apps renderer script models', () => { .toBe('https://api-cdn.wemod.com/steam_community/1245620/client_icon/96.webp'); }); + it('prefers the Steam client icon over the title\'s own artwork fields', () => { + const snapshot = buildSnapshot( + makeInstalledAppsState( + 'steam:1245620', + { platform: 'steam', sku: '1245620', displayName: 'Subnautica' }, + { + imageUrl: 'https://cdn.example/subnautica-cover.webp', + coverUrl: 'https://cdn.example/subnautica-alt.webp', + } + ) + ); + + expect(snapshot?.apps).toHaveLength(1); + expect(snapshot?.apps[0].imageUrl).toBe(CLIENT_ICON_URL); + }); + + it('falls back to title artwork when no Steam AppID is resolvable', () => { + const snapshot = buildSnapshot( + makeInstalledAppsState( + 'epic:abc', + { platform: 'epic', sku: 'abc', displayName: 'Subnautica' }, + { imageUrl: 'https://cdn.example/epic-cover.webp' } + ) + ); + + expect(snapshot?.apps[0].imageUrl).toBe('https://cdn.example/epic-cover.webp'); + }); + + it('reads installed apps from the store while the service slice is still empty', () => { + const storeInstalledApps = { + 'steam:1245620': { platform: 'steam', sku: '1245620' }, + }; + const data = resolveInstalledData({ + unavailableTitlesById: {}, + installedAppsService: { installedApps: {} }, + storeRef: { + state: { + getValue: () => ({ + installedApps: storeInstalledApps, + catalog: {}, + installedGameVersions: {}, + }), + }, + }, + }); + + expect(data?.source).toBe('store'); + expect(data?.rawInstalledApps).toBe(storeInstalledApps); + }); + it('resolves the tree-shaken Wand QR renderer without a create export', () => { const renderer = () => undefined; const webpackRequire = {