mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
feat(update-center): add update list icons
This commit is contained in:
134
src/__tests__/unit/update-center/UpdateCenterItem.test.ts
Normal file
134
src/__tests__/unit/update-center/UpdateCenterItem.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/vue";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import UpdateCenterItem from "@/components/update-center/UpdateCenterItem.vue";
|
||||
import type {
|
||||
UpdateCenterItem as UpdateCenterItemData,
|
||||
UpdateCenterTaskState,
|
||||
} from "@/global/typedefinition";
|
||||
|
||||
const createItem = (
|
||||
overrides: Partial<UpdateCenterItemData> = {},
|
||||
): UpdateCenterItemData => ({
|
||||
taskKey: "aptss:spark-weather",
|
||||
packageName: "spark-weather",
|
||||
displayName: "Spark Weather",
|
||||
currentVersion: "1.0.0",
|
||||
newVersion: "2.0.0",
|
||||
source: "aptss",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createTask = (
|
||||
overrides: Partial<UpdateCenterTaskState> = {},
|
||||
): UpdateCenterTaskState => ({
|
||||
taskKey: "aptss:spark-weather",
|
||||
packageName: "spark-weather",
|
||||
source: "aptss",
|
||||
status: "downloading",
|
||||
progress: 42,
|
||||
logs: [],
|
||||
errorMessage: "",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("UpdateCenterItem", () => {
|
||||
it("renders an icon image when item.icon exists", () => {
|
||||
render(UpdateCenterItem, {
|
||||
props: {
|
||||
item: createItem({ icon: "/usr/share/pixmaps/spark-weather.png" }),
|
||||
task: createTask(),
|
||||
selected: false,
|
||||
},
|
||||
});
|
||||
|
||||
const icon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||
|
||||
expect(icon).toHaveAttribute(
|
||||
"src",
|
||||
"file:///usr/share/pixmaps/spark-weather.png",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to a placeholder icon when the image fails", async () => {
|
||||
render(UpdateCenterItem, {
|
||||
props: {
|
||||
item: createItem({ icon: "https://example.com/spark-weather.png" }),
|
||||
task: createTask(),
|
||||
selected: false,
|
||||
},
|
||||
});
|
||||
|
||||
const icon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||
|
||||
await fireEvent.error(icon);
|
||||
|
||||
expect(icon.getAttribute("src")).toContain("data:image/svg+xml");
|
||||
expect(icon.getAttribute("src")).not.toContain(
|
||||
"https://example.com/spark-weather.png",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a new item icon again after a previous icon failure", async () => {
|
||||
const { rerender } = render(UpdateCenterItem, {
|
||||
props: {
|
||||
item: createItem({ icon: "https://example.com/spark-weather.png" }),
|
||||
task: createTask(),
|
||||
selected: false,
|
||||
},
|
||||
});
|
||||
|
||||
const firstIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||
|
||||
await fireEvent.error(firstIcon);
|
||||
|
||||
expect(firstIcon.getAttribute("src")).toContain("data:image/svg+xml");
|
||||
|
||||
await rerender({
|
||||
item: createItem({
|
||||
displayName: "Spark Clock",
|
||||
icon: "/usr/share/pixmaps/spark-clock.png",
|
||||
}),
|
||||
task: createTask(),
|
||||
selected: false,
|
||||
});
|
||||
|
||||
const nextIcon = screen.getByRole("img", { name: "Spark Clock 图标" });
|
||||
|
||||
expect(nextIcon).toHaveAttribute(
|
||||
"src",
|
||||
"file:///usr/share/pixmaps/spark-clock.png",
|
||||
);
|
||||
});
|
||||
|
||||
it("retries the same icon string when a fresh item object is rendered", async () => {
|
||||
const brokenIcon = "https://example.com/spark-weather.png";
|
||||
const { rerender } = render(UpdateCenterItem, {
|
||||
props: {
|
||||
item: createItem({ icon: brokenIcon }),
|
||||
task: createTask(),
|
||||
selected: false,
|
||||
},
|
||||
});
|
||||
|
||||
const firstIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||
|
||||
await fireEvent.error(firstIcon);
|
||||
|
||||
expect(firstIcon.getAttribute("src")).toContain("data:image/svg+xml");
|
||||
|
||||
await rerender({
|
||||
item: createItem({
|
||||
currentVersion: "1.1.0",
|
||||
newVersion: "2.1.0",
|
||||
icon: brokenIcon,
|
||||
}),
|
||||
task: createTask({ progress: 75 }),
|
||||
selected: false,
|
||||
});
|
||||
|
||||
const retriedIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||
|
||||
expect(retriedIcon).toHaveAttribute("src", brokenIcon);
|
||||
});
|
||||
});
|
||||
254
src/__tests__/unit/update-center/icons.test.ts
Normal file
254
src/__tests__/unit/update-center/icons.test.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type FsState = {
|
||||
directories?: Record<string, string[]>;
|
||||
files?: Record<string, string>;
|
||||
packageFiles?: Record<string, string[]>;
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { loadUpdateCenterItems } from "../../../../electron/main/backend/update-center";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
interface CommandResult {
|
||||
code: number;
|
||||
@@ -8,6 +6,10 @@ interface CommandResult {
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
type RemoteStoreResponse =
|
||||
| Record<string, unknown>
|
||||
| Array<Record<string, unknown>>;
|
||||
|
||||
const APTSS_LIST_UPGRADABLE_KEY =
|
||||
"bash -lc env LANGUAGE=en_US /usr/bin/apt -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf list --upgradable -o Dir::Etc::sourcelist=/opt/durapps/spark-store/bin/apt-fast-conf/sources.list.d/aptss.list -o Dir::Etc::sourceparts=/dev/null -o APT::Get::List-Cleanup=0";
|
||||
|
||||
@@ -17,8 +19,86 @@ const DPKG_QUERY_INSTALLED_KEY =
|
||||
const APM_PRINT_URIS_KEY =
|
||||
"bash -lc amber-pm-debug /usr/bin/apt -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf download spark-weather --print-uris";
|
||||
|
||||
const loadUpdateCenterModule = async (
|
||||
remoteStore: Record<string, RemoteStoreResponse>,
|
||||
) => {
|
||||
vi.resetModules();
|
||||
|
||||
vi.doMock("node:fs", () => {
|
||||
const existsSync = () => false;
|
||||
const readdirSync = () => [] as string[];
|
||||
const readFileSync = () => {
|
||||
throw new Error("Unexpected icon file read");
|
||||
};
|
||||
|
||||
return {
|
||||
default: {
|
||||
existsSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
},
|
||||
existsSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
};
|
||||
});
|
||||
|
||||
vi.doMock("node:child_process", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("node:child_process")>(
|
||||
"node:child_process",
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
spawnSync: (command: string, args: string[]) => {
|
||||
if (command === "dpkg" && args[0] === "-L") {
|
||||
return {
|
||||
status: 1,
|
||||
error: undefined,
|
||||
output: null,
|
||||
pid: 0,
|
||||
signal: null,
|
||||
stdout: Buffer.alloc(0),
|
||||
stderr: Buffer.alloc(0),
|
||||
};
|
||||
}
|
||||
|
||||
return actual.spawnSync(command, args);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: string | URL) => {
|
||||
const url = String(input);
|
||||
const body = remoteStore[url];
|
||||
|
||||
return {
|
||||
ok: body !== undefined,
|
||||
async json() {
|
||||
if (body === undefined) {
|
||||
throw new Error(`Unexpected fetch for ${url}`);
|
||||
}
|
||||
|
||||
return body;
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return await import("../../../../electron/main/backend/update-center/index");
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vi.doUnmock("node:fs");
|
||||
vi.doUnmock("node:child_process");
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("update-center load items", () => {
|
||||
it("enriches apm and migration items with download metadata needed by the runner", async () => {
|
||||
it("enriches apm migration items with download metadata and remote fallback icons", async () => {
|
||||
const commandResults = new Map<string, CommandResult>([
|
||||
[
|
||||
APTSS_LIST_UPGRADABLE_KEY,
|
||||
@@ -62,6 +142,20 @@ describe("update-center load items", () => {
|
||||
},
|
||||
],
|
||||
]);
|
||||
const { loadUpdateCenterItems } = await loadUpdateCenterModule({
|
||||
"https://erotica.spark-app.store/amd64-store/categories.json": {
|
||||
tools: { zh: "Tools" },
|
||||
},
|
||||
"https://erotica.spark-app.store/amd64-store/tools/applist.json": [
|
||||
{ Pkgname: "spark-weather" },
|
||||
],
|
||||
"https://erotica.spark-app.store/amd64-apm/categories.json": {
|
||||
tools: { zh: "Tools" },
|
||||
},
|
||||
"https://erotica.spark-app.store/amd64-apm/tools/applist.json": [
|
||||
{ Pkgname: "spark-weather" },
|
||||
],
|
||||
});
|
||||
|
||||
const result = await loadUpdateCenterItems(async (command, args) => {
|
||||
const key = `${command} ${args.join(" ")}`;
|
||||
@@ -79,6 +173,9 @@ describe("update-center load items", () => {
|
||||
source: "apm",
|
||||
currentVersion: "1.5.0",
|
||||
nextVersion: "3.0.0",
|
||||
arch: "amd64",
|
||||
category: "tools",
|
||||
icon: "https://erotica.spark-app.store/amd64-apm/tools/spark-weather/icon.png",
|
||||
downloadUrl: "https://example.invalid/spark-weather_3.0.0_amd64.deb",
|
||||
fileName: "spark-weather_3.0.0_amd64.deb",
|
||||
size: 123456,
|
||||
@@ -91,6 +188,15 @@ describe("update-center load items", () => {
|
||||
});
|
||||
|
||||
it("degrades to aptss-only results when apm commands fail", async () => {
|
||||
const { loadUpdateCenterItems } = await loadUpdateCenterModule({
|
||||
"https://erotica.spark-app.store/amd64-store/categories.json": {
|
||||
office: { zh: "Office" },
|
||||
},
|
||||
"https://erotica.spark-app.store/amd64-store/office/applist.json": [
|
||||
{ Pkgname: "spark-notes" },
|
||||
],
|
||||
});
|
||||
|
||||
const result = await loadUpdateCenterItems(async (command, args) => {
|
||||
const key = `${command} ${args.join(" ")}`;
|
||||
|
||||
@@ -127,6 +233,136 @@ describe("update-center load items", () => {
|
||||
source: "aptss",
|
||||
currentVersion: "1.0.0",
|
||||
nextVersion: "2.0.0",
|
||||
arch: "amd64",
|
||||
category: "office",
|
||||
icon: "https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
||||
},
|
||||
]);
|
||||
expect(result.warnings).toEqual([
|
||||
"apm upgradable query failed: apm: command not found",
|
||||
"apm installed query failed: apm: command not found",
|
||||
]);
|
||||
});
|
||||
|
||||
it("retries category lookup after an earlier fetch failure in the same process", async () => {
|
||||
const remoteStore: Record<string, RemoteStoreResponse> = {};
|
||||
const { loadUpdateCenterItems } = await loadUpdateCenterModule(remoteStore);
|
||||
|
||||
const runCommand = async (command: string, args: string[]) => {
|
||||
const key = `${command} ${args.join(" ")}`;
|
||||
|
||||
if (key === APTSS_LIST_UPGRADABLE_KEY) {
|
||||
return {
|
||||
code: 0,
|
||||
stdout: "spark-notes/stable 2.0.0 amd64 [upgradable from: 1.0.0]",
|
||||
stderr: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (key === DPKG_QUERY_INSTALLED_KEY) {
|
||||
return {
|
||||
code: 0,
|
||||
stdout: "spark-notes\tinstall ok installed\n",
|
||||
stderr: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (key === "apm list --upgradable" || key === "apm list --installed") {
|
||||
return {
|
||||
code: 127,
|
||||
stdout: "",
|
||||
stderr: "apm: command not found",
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected command ${key}`);
|
||||
};
|
||||
|
||||
const firstResult = await loadUpdateCenterItems(runCommand);
|
||||
|
||||
expect(firstResult.items).toEqual([
|
||||
{
|
||||
pkgname: "spark-notes",
|
||||
source: "aptss",
|
||||
currentVersion: "1.0.0",
|
||||
nextVersion: "2.0.0",
|
||||
arch: "amd64",
|
||||
},
|
||||
]);
|
||||
|
||||
remoteStore["https://erotica.spark-app.store/amd64-store/categories.json"] =
|
||||
{
|
||||
office: { zh: "Office" },
|
||||
};
|
||||
remoteStore[
|
||||
"https://erotica.spark-app.store/amd64-store/office/applist.json"
|
||||
] = [{ Pkgname: "spark-notes" }];
|
||||
|
||||
const secondResult = await loadUpdateCenterItems(runCommand);
|
||||
|
||||
expect(secondResult.items).toEqual([
|
||||
{
|
||||
pkgname: "spark-notes",
|
||||
source: "aptss",
|
||||
currentVersion: "1.0.0",
|
||||
nextVersion: "2.0.0",
|
||||
arch: "amd64",
|
||||
category: "office",
|
||||
icon: "https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps successfully loaded categories when another category applist fetch fails", async () => {
|
||||
const { loadUpdateCenterItems } = await loadUpdateCenterModule({
|
||||
"https://erotica.spark-app.store/amd64-store/categories.json": {
|
||||
office: { zh: "Office" },
|
||||
tools: { zh: "Tools" },
|
||||
},
|
||||
"https://erotica.spark-app.store/amd64-store/office/applist.json": [
|
||||
{ Pkgname: "spark-notes" },
|
||||
],
|
||||
});
|
||||
|
||||
const result = await loadUpdateCenterItems(async (command, args) => {
|
||||
const key = `${command} ${args.join(" ")}`;
|
||||
|
||||
if (key === APTSS_LIST_UPGRADABLE_KEY) {
|
||||
return {
|
||||
code: 0,
|
||||
stdout: "spark-notes/stable 2.0.0 amd64 [upgradable from: 1.0.0]",
|
||||
stderr: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (key === DPKG_QUERY_INSTALLED_KEY) {
|
||||
return {
|
||||
code: 0,
|
||||
stdout: "spark-notes\tinstall ok installed\n",
|
||||
stderr: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (key === "apm list --upgradable" || key === "apm list --installed") {
|
||||
return {
|
||||
code: 127,
|
||||
stdout: "",
|
||||
stderr: "apm: command not found",
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected command ${key}`);
|
||||
});
|
||||
|
||||
expect(result.items).toEqual([
|
||||
{
|
||||
pkgname: "spark-notes",
|
||||
source: "aptss",
|
||||
currentVersion: "1.0.0",
|
||||
nextVersion: "2.0.0",
|
||||
arch: "amd64",
|
||||
category: "office",
|
||||
icon: "https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
||||
},
|
||||
]);
|
||||
expect(result.warnings).toEqual([
|
||||
|
||||
@@ -23,6 +23,7 @@ describe("update-center query", () => {
|
||||
source: "aptss",
|
||||
currentVersion: "1.9.0",
|
||||
nextVersion: "2.0.0",
|
||||
arch: "amd64",
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -37,6 +38,7 @@ describe("update-center query", () => {
|
||||
source: "aptss",
|
||||
currentVersion: "1.1.0",
|
||||
nextVersion: "1.2.0",
|
||||
arch: "amd64",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -46,6 +48,7 @@ describe("update-center query", () => {
|
||||
source: "apm",
|
||||
currentVersion: "1.5.0",
|
||||
nextVersion: "2.0.0",
|
||||
arch: "amd64",
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -262,6 +265,7 @@ describe("update-center query", () => {
|
||||
source: "apm",
|
||||
currentVersion: "4.5.0",
|
||||
nextVersion: "5.0.0",
|
||||
arch: "arm64",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -217,6 +217,36 @@ describe("update-center/ipc", () => {
|
||||
expect(snapshots.at(-1)?.items[0]).not.toHaveProperty("nextVersion");
|
||||
});
|
||||
|
||||
it("service task snapshots keep item icons for queued work", async () => {
|
||||
const service = createUpdateCenterService({
|
||||
loadItems: async () => [{ ...createItem(), icon: "/icons/weather.png" }],
|
||||
createTaskRunner: (queue: UpdateCenterQueue) => ({
|
||||
cancelActiveTask: vi.fn(),
|
||||
runNextTask: async () => {
|
||||
const task = queue.getNextQueuedTask();
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
|
||||
queue.markActiveTask(task.id, "installing");
|
||||
queue.finishTask(task.id, "completed");
|
||||
return task;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await service.refresh();
|
||||
await service.start(["aptss:spark-weather"]);
|
||||
|
||||
expect(service.getState().tasks).toMatchObject([
|
||||
{
|
||||
taskKey: "aptss:spark-weather",
|
||||
icon: "/icons/weather.png",
|
||||
status: "completed",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("concurrent start calls still serialize through one processing pipeline", async () => {
|
||||
const startedTaskIds: number[] = [];
|
||||
const releases: Array<() => void> = [];
|
||||
|
||||
Reference in New Issue
Block a user