import { afterEach, describe, expect, it, vi } from "vitest"; type FsState = { directories?: Record; files?: Record; packageFiles?: Record; }; const loadIconsModule = async (state: FsState) => { vi.resetModules(); vi.doMock("node:fs", () => { const directories = state.directories ?? {}; const files = state.files ?? {}; const existsSync = (targetPath: string): boolean => { return targetPath in directories || targetPath in files; }; const readdirSync = (targetPath: string): string[] => { return directories[targetPath] ?? []; }; const readFileSync = (targetPath: string): string => { const content = files[targetPath]; if (content === undefined) { throw new Error(`Unexpected read for ${targetPath}`); } return content; }; return { default: { existsSync, readdirSync, readFileSync, }, existsSync, readdirSync, readFileSync, }; }); vi.doMock("node:child_process", () => { const packageFiles = state.packageFiles ?? {}; const spawnSync = (_command: string, args: string[]) => { const operation = args[0] ?? ""; const pkgname = args[1] ?? ""; const ownedFiles = packageFiles[pkgname]; if (operation !== "-L" || !ownedFiles) { return { status: 1, error: undefined, output: null, pid: 0, signal: null, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), }; } return { status: 0, error: undefined, output: null, pid: 0, signal: null, stdout: Buffer.from(`${ownedFiles.join("\n")}\n`), stderr: Buffer.alloc(0), }; }; return { default: { spawnSync }, spawnSync, }; }); return await import("../../../../electron/main/backend/update-center/icons"); }; afterEach(() => { vi.doUnmock("node:fs"); vi.doUnmock("node:child_process"); }); describe("update-center icons", () => { it("prefers local desktop icon paths for aptss items", async () => { const pkgname = "spark-weather"; const applicationsDirectory = "/usr/share/applications"; const desktopPath = `${applicationsDirectory}/weather-launcher.desktop`; const iconPath = `/usr/share/pixmaps/${pkgname}.png`; const { resolveUpdateItemIcon } = await loadIconsModule({ directories: { [applicationsDirectory]: ["weather-launcher.desktop"], }, files: { [desktopPath]: `[Desktop Entry]\nName=Spark Weather\nIcon=${iconPath}\n`, [iconPath]: "png", }, packageFiles: { [pkgname]: [desktopPath], }, }); expect( resolveUpdateItemIcon({ pkgname, source: "aptss", currentVersion: "1.0.0", nextVersion: "2.0.0", }), ).toBe(iconPath); }); it("resolves APM icon names from entries/icons when desktop icon is not absolute", async () => { const pkgname = "spark-music"; const desktopDirectory = `/var/lib/apm/apm/files/ace-env/var/lib/apm/${pkgname}/entries/applications`; const desktopPath = `${desktopDirectory}/${pkgname}.desktop`; const iconPath = `/var/lib/apm/apm/files/ace-env/var/lib/apm/${pkgname}/entries/icons/hicolor/48x48/apps/${pkgname}.png`; const { resolveUpdateItemIcon } = await loadIconsModule({ directories: { [desktopDirectory]: [`${pkgname}.desktop`], }, files: { [desktopPath]: `[Desktop Entry]\nName=Spark Music\nIcon=${pkgname}\n`, [iconPath]: "png", }, }); expect( resolveUpdateItemIcon({ pkgname, source: "apm", currentVersion: "1.0.0", nextVersion: "2.0.0", }), ).toBe(iconPath); }); it("checks later APM desktop entries when the first one has no usable icon", async () => { const pkgname = "spark-player"; const desktopDirectory = `/var/lib/apm/apm/files/ace-env/var/lib/apm/${pkgname}/entries/applications`; const invalidDesktopPath = `${desktopDirectory}/invalid.desktop`; const validDesktopPath = `${desktopDirectory}/valid.desktop`; const iconPath = `/var/lib/apm/apm/files/ace-env/var/lib/apm/${pkgname}/entries/icons/hicolor/48x48/apps/${pkgname}.png`; const { resolveApmIcon } = await loadIconsModule({ directories: { [desktopDirectory]: ["invalid.desktop", "valid.desktop"], }, files: { [invalidDesktopPath]: "[Desktop Entry]\nName=Invalid\nIcon=missing-icon\n", [validDesktopPath]: `[Desktop Entry]\nName=Spark Player\nIcon=${pkgname}\n`, [iconPath]: "png", }, }); expect(resolveApmIcon(pkgname)).toBe(iconPath); }); it("resolves APM icons from installed /opt/apps entries when package-path assets are absent", async () => { const pkgname = "spark-video"; const installedDesktopDirectory = `/opt/apps/${pkgname}/entries/applications`; const installedDesktopPath = `${installedDesktopDirectory}/${pkgname}.desktop`; const installedIconPath = `/opt/apps/${pkgname}/entries/icons/hicolor/48x48/apps/${pkgname}.png`; const { resolveApmIcon } = await loadIconsModule({ directories: { [installedDesktopDirectory]: [`${pkgname}.desktop`], }, files: { [installedDesktopPath]: `[Desktop Entry]\nName=Spark Video\nIcon=${pkgname}\n`, [installedIconPath]: "png", }, }); expect(resolveApmIcon(pkgname)).toBe(installedIconPath); }); it("resolves APM named icons from shared theme locations when local entries icons are absent", async () => { const pkgname = "spark-camera"; const desktopDirectory = `/var/lib/apm/apm/files/ace-env/var/lib/apm/${pkgname}/entries/applications`; const desktopPath = `${desktopDirectory}/${pkgname}.desktop`; const sharedIconPath = `/usr/share/icons/hicolor/48x48/apps/${pkgname}.png`; const { resolveApmIcon } = await loadIconsModule({ directories: { [desktopDirectory]: [`${pkgname}.desktop`], }, files: { [desktopPath]: `[Desktop Entry]\nName=Spark Camera\nIcon=${pkgname}\n`, [sharedIconPath]: "png", }, }); expect(resolveApmIcon(pkgname)).toBe(sharedIconPath); }); it("builds a remote fallback URL when category and arch are available", async () => { const { resolveUpdateItemIcon } = await loadIconsModule({}); expect( resolveUpdateItemIcon({ pkgname: "spark-clock", source: "apm", currentVersion: "1.0.0", nextVersion: "2.0.0", category: "utility", arch: "amd64", }), ).toBe( "https://erotica.spark-app.store/amd64-apm/utility/spark-clock/icon.png", ); }); it("returns empty string when neither local nor remote icon can be determined", async () => { const { resolveUpdateItemIcon } = await loadIconsModule({}); expect( resolveUpdateItemIcon({ pkgname: "spark-empty", source: "aptss", currentVersion: "1.0.0", nextVersion: "2.0.0", }), ).toBe(""); }); it("ignores unrelated desktop files while still resolving owned non-exact filenames", async () => { const pkgname = "spark-reader"; const applicationsDirectory = "/usr/share/applications"; const unrelatedDesktopPath = `${applicationsDirectory}/notes.desktop`; const ownedDesktopPath = `${applicationsDirectory}/reader-launcher.desktop`; const unrelatedIconPath = "/usr/share/pixmaps/notes.png"; const ownedIconPath = `/usr/share/pixmaps/${pkgname}.png`; const { resolveDesktopIcon } = await loadIconsModule({ directories: { [applicationsDirectory]: ["notes.desktop", "reader-launcher.desktop"], }, files: { [unrelatedDesktopPath]: `[Desktop Entry]\nName=Notes\nIcon=${unrelatedIconPath}\n`, [ownedDesktopPath]: `[Desktop Entry]\nName=Spark Reader\nIcon=${ownedIconPath}\n`, [unrelatedIconPath]: "png", [ownedIconPath]: "png", }, packageFiles: { [pkgname]: [ownedDesktopPath], }, }); expect(resolveDesktopIcon(pkgname)).toBe(ownedIconPath); }); });