mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
fix(update-center): cascade local and remote icon fallbacks
Keep update list icons from dropping straight to placeholders by retrying the remote store icon after local load failures. Align the update-center IPC and renderer types with the split local/remote icon contract.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/vue";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { defineComponent, nextTick, reactive, ref } from "vue";
|
||||
|
||||
import UpdateCenterItem from "@/components/update-center/UpdateCenterItem.vue";
|
||||
import type {
|
||||
@@ -33,10 +34,13 @@ const createTask = (
|
||||
});
|
||||
|
||||
describe("UpdateCenterItem", () => {
|
||||
it("renders an icon image when item.icon exists", () => {
|
||||
it("renders localIcon first when both icon sources exist", () => {
|
||||
render(UpdateCenterItem, {
|
||||
props: {
|
||||
item: createItem({ icon: "/usr/share/pixmaps/spark-weather.png" }),
|
||||
item: createItem({
|
||||
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||
remoteIcon: "https://example.com/spark-weather.png",
|
||||
}),
|
||||
task: createTask(),
|
||||
selected: false,
|
||||
},
|
||||
@@ -50,10 +54,13 @@ describe("UpdateCenterItem", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to a placeholder icon when the image fails", async () => {
|
||||
it("falls back to remoteIcon when localIcon fails", async () => {
|
||||
render(UpdateCenterItem, {
|
||||
props: {
|
||||
item: createItem({ icon: "https://example.com/spark-weather.png" }),
|
||||
item: createItem({
|
||||
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||
remoteIcon: "https://example.com/spark-weather.png",
|
||||
}),
|
||||
task: createTask(),
|
||||
selected: false,
|
||||
},
|
||||
@@ -63,16 +70,42 @@ describe("UpdateCenterItem", () => {
|
||||
|
||||
await fireEvent.error(icon);
|
||||
|
||||
expect(icon).toHaveAttribute(
|
||||
"src",
|
||||
"https://example.com/spark-weather.png",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the placeholder after localIcon and remoteIcon both fail", async () => {
|
||||
render(UpdateCenterItem, {
|
||||
props: {
|
||||
item: createItem({
|
||||
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||
remoteIcon: "https://example.com/spark-weather.png",
|
||||
}),
|
||||
task: createTask(),
|
||||
selected: false,
|
||||
},
|
||||
});
|
||||
|
||||
const icon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||
|
||||
await fireEvent.error(icon);
|
||||
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 () => {
|
||||
it("restarts from localIcon when a new item is rendered", async () => {
|
||||
const { rerender } = render(UpdateCenterItem, {
|
||||
props: {
|
||||
item: createItem({ icon: "https://example.com/spark-weather.png" }),
|
||||
item: createItem({
|
||||
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||
remoteIcon: "https://example.com/spark-weather.png",
|
||||
}),
|
||||
task: createTask(),
|
||||
selected: false,
|
||||
},
|
||||
@@ -82,12 +115,16 @@ describe("UpdateCenterItem", () => {
|
||||
|
||||
await fireEvent.error(firstIcon);
|
||||
|
||||
expect(firstIcon.getAttribute("src")).toContain("data:image/svg+xml");
|
||||
expect(firstIcon).toHaveAttribute(
|
||||
"src",
|
||||
"https://example.com/spark-weather.png",
|
||||
);
|
||||
|
||||
await rerender({
|
||||
item: createItem({
|
||||
displayName: "Spark Clock",
|
||||
icon: "/usr/share/pixmaps/spark-clock.png",
|
||||
localIcon: "/usr/share/pixmaps/spark-clock.png",
|
||||
remoteIcon: "https://example.com/spark-clock.png",
|
||||
}),
|
||||
task: createTask(),
|
||||
selected: false,
|
||||
@@ -101,11 +138,16 @@ describe("UpdateCenterItem", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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, {
|
||||
it("restarts from localIcon when icon sources change on the same item object", async () => {
|
||||
const item = reactive(
|
||||
createItem({
|
||||
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||
remoteIcon: "https://example.com/spark-weather.png",
|
||||
}),
|
||||
);
|
||||
render(UpdateCenterItem, {
|
||||
props: {
|
||||
item: createItem({ icon: brokenIcon }),
|
||||
item,
|
||||
task: createTask(),
|
||||
selected: false,
|
||||
},
|
||||
@@ -113,22 +155,63 @@ describe("UpdateCenterItem", () => {
|
||||
|
||||
const firstIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||
|
||||
await fireEvent.error(firstIcon);
|
||||
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,
|
||||
});
|
||||
item.localIcon = "/usr/share/pixmaps/spark-weather-refreshed.png";
|
||||
item.remoteIcon = "https://example.com/spark-weather-refreshed.png";
|
||||
|
||||
await nextTick();
|
||||
|
||||
const retriedIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||
|
||||
expect(retriedIcon).toHaveAttribute("src", brokenIcon);
|
||||
expect(retriedIcon).toHaveAttribute(
|
||||
"src",
|
||||
"file:///usr/share/pixmaps/spark-weather-refreshed.png",
|
||||
);
|
||||
});
|
||||
|
||||
it("restarts from localIcon for a fresh item object with the same icon sources", async () => {
|
||||
const item = ref(
|
||||
createItem({
|
||||
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||
remoteIcon: "https://example.com/spark-weather.png",
|
||||
}),
|
||||
);
|
||||
const Wrapper = defineComponent({
|
||||
components: { UpdateCenterItem },
|
||||
setup() {
|
||||
return {
|
||||
item,
|
||||
task: createTask(),
|
||||
};
|
||||
},
|
||||
template:
|
||||
'<UpdateCenterItem :item="item" :task="task" :selected="false" />',
|
||||
});
|
||||
|
||||
render(Wrapper);
|
||||
|
||||
const firstIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||
|
||||
await fireEvent.error(firstIcon);
|
||||
await fireEvent.error(firstIcon);
|
||||
|
||||
expect(firstIcon.getAttribute("src")).toContain("data:image/svg+xml");
|
||||
|
||||
item.value = createItem({
|
||||
localIcon: "/usr/share/pixmaps/spark-weather.png",
|
||||
remoteIcon: "https://example.com/spark-weather.png",
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
const retriedIcon = screen.getByRole("img", { name: "Spark Weather 图标" });
|
||||
|
||||
expect(retriedIcon).toHaveAttribute(
|
||||
"src",
|
||||
"file:///usr/share/pixmaps/spark-weather.png",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,12 +87,12 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("update-center icons", () => {
|
||||
it("prefers local desktop icon paths for aptss items", async () => {
|
||||
it("returns both localIcon and remoteIcon when an aptss desktop icon resolves", 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({
|
||||
const { resolveUpdateItemIcons } = await loadIconsModule({
|
||||
directories: {
|
||||
[applicationsDirectory]: ["weather-launcher.desktop"],
|
||||
},
|
||||
@@ -106,13 +106,19 @@ describe("update-center icons", () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveUpdateItemIcon({
|
||||
resolveUpdateItemIcons({
|
||||
pkgname,
|
||||
source: "aptss",
|
||||
currentVersion: "1.0.0",
|
||||
nextVersion: "2.0.0",
|
||||
category: "tools",
|
||||
arch: "amd64",
|
||||
}),
|
||||
).toBe(iconPath);
|
||||
).toEqual({
|
||||
localIcon: iconPath,
|
||||
remoteIcon:
|
||||
"https://erotica.spark-app.store/amd64-store/tools/spark-weather/icon.png",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves APM icon names from entries/icons when desktop icon is not absolute", async () => {
|
||||
@@ -120,7 +126,7 @@ describe("update-center icons", () => {
|
||||
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({
|
||||
const { resolveUpdateItemIcons } = await loadIconsModule({
|
||||
directories: {
|
||||
[desktopDirectory]: [`${pkgname}.desktop`],
|
||||
},
|
||||
@@ -131,13 +137,13 @@ describe("update-center icons", () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveUpdateItemIcon({
|
||||
resolveUpdateItemIcons({
|
||||
pkgname,
|
||||
source: "apm",
|
||||
currentVersion: "1.0.0",
|
||||
nextVersion: "2.0.0",
|
||||
}),
|
||||
).toBe(iconPath);
|
||||
).toEqual({ localIcon: iconPath });
|
||||
});
|
||||
|
||||
it("checks later APM desktop entries when the first one has no usable icon", async () => {
|
||||
@@ -197,11 +203,11 @@ describe("update-center icons", () => {
|
||||
expect(resolveApmIcon(pkgname)).toBe(sharedIconPath);
|
||||
});
|
||||
|
||||
it("builds a remote fallback URL when category and arch are available", async () => {
|
||||
const { resolveUpdateItemIcon } = await loadIconsModule({});
|
||||
it("returns only remoteIcon when no local icon resolves", async () => {
|
||||
const { resolveUpdateItemIcons } = await loadIconsModule({});
|
||||
|
||||
expect(
|
||||
resolveUpdateItemIcon({
|
||||
resolveUpdateItemIcons({
|
||||
pkgname: "spark-clock",
|
||||
source: "apm",
|
||||
currentVersion: "1.0.0",
|
||||
@@ -209,22 +215,51 @@ describe("update-center icons", () => {
|
||||
category: "utility",
|
||||
arch: "amd64",
|
||||
}),
|
||||
).toBe(
|
||||
"https://erotica.spark-app.store/amd64-apm/utility/spark-clock/icon.png",
|
||||
);
|
||||
).toEqual({
|
||||
remoteIcon:
|
||||
"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({});
|
||||
it("returns only localIcon when a remote fallback URL cannot be built", async () => {
|
||||
const pkgname = "spark-reader";
|
||||
const applicationsDirectory = "/usr/share/applications";
|
||||
const desktopPath = `${applicationsDirectory}/reader-launcher.desktop`;
|
||||
const iconPath = `/usr/share/pixmaps/${pkgname}.png`;
|
||||
const { resolveUpdateItemIcons } = await loadIconsModule({
|
||||
directories: {
|
||||
[applicationsDirectory]: ["reader-launcher.desktop"],
|
||||
},
|
||||
files: {
|
||||
[desktopPath]: `[Desktop Entry]\nName=Spark Reader\nIcon=${iconPath}\n`,
|
||||
[iconPath]: "png",
|
||||
},
|
||||
packageFiles: {
|
||||
[pkgname]: [desktopPath],
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveUpdateItemIcon({
|
||||
resolveUpdateItemIcons({
|
||||
pkgname,
|
||||
source: "aptss",
|
||||
currentVersion: "1.0.0",
|
||||
nextVersion: "2.0.0",
|
||||
}),
|
||||
).toEqual({ localIcon: iconPath });
|
||||
});
|
||||
|
||||
it("returns an empty object when neither local nor remote icons are available", async () => {
|
||||
const { resolveUpdateItemIcons } = await loadIconsModule({});
|
||||
|
||||
expect(
|
||||
resolveUpdateItemIcons({
|
||||
pkgname: "spark-empty",
|
||||
source: "aptss",
|
||||
currentVersion: "1.0.0",
|
||||
nextVersion: "2.0.0",
|
||||
}),
|
||||
).toBe("");
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
it("ignores unrelated desktop files while still resolving owned non-exact filenames", async () => {
|
||||
|
||||
@@ -175,7 +175,8 @@ describe("update-center load items", () => {
|
||||
nextVersion: "3.0.0",
|
||||
arch: "amd64",
|
||||
category: "tools",
|
||||
icon: "https://erotica.spark-app.store/amd64-apm/tools/spark-weather/icon.png",
|
||||
remoteIcon:
|
||||
"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,
|
||||
@@ -235,7 +236,8 @@ describe("update-center load items", () => {
|
||||
nextVersion: "2.0.0",
|
||||
arch: "amd64",
|
||||
category: "office",
|
||||
icon: "https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
||||
remoteIcon:
|
||||
"https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
||||
},
|
||||
]);
|
||||
expect(result.warnings).toEqual([
|
||||
@@ -308,7 +310,8 @@ describe("update-center load items", () => {
|
||||
nextVersion: "2.0.0",
|
||||
arch: "amd64",
|
||||
category: "office",
|
||||
icon: "https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
||||
remoteIcon:
|
||||
"https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -362,7 +365,8 @@ describe("update-center load items", () => {
|
||||
nextVersion: "2.0.0",
|
||||
arch: "amd64",
|
||||
category: "office",
|
||||
icon: "https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
||||
remoteIcon:
|
||||
"https://erotica.spark-app.store/amd64-store/office/spark-notes/icon.png",
|
||||
},
|
||||
]);
|
||||
expect(result.warnings).toEqual([
|
||||
|
||||
@@ -217,9 +217,16 @@ describe("update-center/ipc", () => {
|
||||
expect(snapshots.at(-1)?.items[0]).not.toHaveProperty("nextVersion");
|
||||
});
|
||||
|
||||
it("service task snapshots keep item icons for queued work", async () => {
|
||||
it("service task snapshots keep localIcon and remoteIcon for queued work", async () => {
|
||||
let releaseTask: (() => void) | undefined;
|
||||
const service = createUpdateCenterService({
|
||||
loadItems: async () => [{ ...createItem(), icon: "/icons/weather.png" }],
|
||||
loadItems: async () => [
|
||||
{
|
||||
...createItem(),
|
||||
localIcon: "/icons/weather.png",
|
||||
remoteIcon: "https://example.com/weather.png",
|
||||
},
|
||||
],
|
||||
createTaskRunner: (queue: UpdateCenterQueue) => ({
|
||||
cancelActiveTask: vi.fn(),
|
||||
runNextTask: async () => {
|
||||
@@ -228,6 +235,9 @@ describe("update-center/ipc", () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
releaseTask = resolve;
|
||||
});
|
||||
queue.markActiveTask(task.id, "installing");
|
||||
queue.finishTask(task.id, "completed");
|
||||
return task;
|
||||
@@ -236,15 +246,20 @@ describe("update-center/ipc", () => {
|
||||
});
|
||||
|
||||
await service.refresh();
|
||||
await service.start(["aptss:spark-weather"]);
|
||||
const startPromise = service.start(["aptss:spark-weather"]);
|
||||
await flushPromises();
|
||||
|
||||
expect(service.getState().tasks).toMatchObject([
|
||||
{
|
||||
taskKey: "aptss:spark-weather",
|
||||
icon: "/icons/weather.png",
|
||||
status: "completed",
|
||||
localIcon: "/icons/weather.png",
|
||||
remoteIcon: "https://example.com/weather.png",
|
||||
status: "queued",
|
||||
},
|
||||
]);
|
||||
|
||||
releaseTask?.();
|
||||
await startPromise;
|
||||
});
|
||||
|
||||
it("concurrent start calls still serialize through one processing pipeline", async () => {
|
||||
|
||||
Reference in New Issue
Block a user