diff --git a/src/App.vue b/src/App.vue index 02d49c85..7991c251 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1274,7 +1274,7 @@ const refreshFavorites = async (): Promise => { favoriteLoading.value = true; favoriteError.value = ""; try { - await loadFavoriteFolders(); + await Promise.all([refreshInstalledApps(), loadFavoriteFolders()]); await loadActiveFavoriteItems(); } catch (error: unknown) { favoriteError.value = (error as Error)?.message || "读取收藏夹失败"; diff --git a/src/__tests__/unit/App.account-placeholders.test.ts b/src/__tests__/unit/App.account-placeholders.test.ts index f99b08a9..2a5fb6c8 100644 --- a/src/__tests__/unit/App.account-placeholders.test.ts +++ b/src/__tests__/unit/App.account-placeholders.test.ts @@ -3,12 +3,59 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import App from "@/App.vue"; import { setAuthSession } from "@/global/authState"; +import type { FavoriteFolder, FavoriteItem } from "@/global/typedefinition"; const invoke = vi.fn(); +const favoriteFolders: FavoriteFolder[] = [ + { + id: 7, + name: "默认收藏夹", + itemCount: 1, + createdAt: "2026-05-18T00:00:00Z", + updatedAt: "2026-05-18T00:00:00Z", + }, +]; + +const favoriteItems: FavoriteItem[] = [ + { + id: 11, + appKey: "app:office:wps", + pkgname: "wps", + name: "WPS", + category: "office", + iconUrl: "", + createdAt: "2026-05-18T00:00:00Z", + }, +]; + vi.mock("axios", () => { const get = vi.fn(async (url: string) => { - if (url.includes("categories.json")) return { data: {} }; + if (url.includes("categories.json")) { + return { data: { office: { zh: "办公" } } }; + } + if (url.includes("/office/applist.json")) { + return { + data: [ + { + Name: "WPS", + Pkgname: "wps", + Version: "1.0.0", + Filename: "wps_1.0.0_amd64.deb", + Torrent_address: "", + Author: "", + Contributor: "", + Website: "", + Update: "", + Size: "", + More: "", + Tags: "", + img_urls: [], + icons: "", + }, + ], + }; + } return { data: [] }; }); @@ -47,6 +94,16 @@ vi.mock("@/modules/updateCenter", () => ({ }), })); +vi.mock("@/modules/backendApi", () => ({ + addFavoriteItem: vi.fn(), + bulkDeleteFavoriteItems: vi.fn(), + createFavoriteFolder: vi.fn(), + exchangeFlarumToken: vi.fn(), + listFavoriteFolders: vi.fn(async () => favoriteFolders), + listFavoriteItems: vi.fn(async () => favoriteItems), + setBackendToken: vi.fn(), +})); + describe("App account placeholders", () => { beforeEach(() => { invoke.mockReset(); @@ -114,4 +171,42 @@ describe("App account placeholders", () => { ).toBeTruthy(); expect(screen.queryByText("请登录后查看我的收藏。")).toBeNull(); }); + + it("refreshes installed apps before resolving favorite management state", async () => { + invoke.mockImplementation(async (channel: string) => { + if (channel === "get-store-filter") return "both"; + if (channel === "check-spark-available") return true; + if (channel === "check-apm-available") return true; + if (channel === "get-app-version") return "5.0.0"; + if (channel === "list-installed") { + return { + success: true, + apps: [ + { + pkgname: "wps", + name: "WPS", + version: "1.0.0", + arch: "amd64", + flags: "installed", + origin: "apm", + }, + ], + }; + } + return []; + }); + render(App); + + await fireEvent.click(await screen.findByRole("button", { name: /Momen/ })); + await fireEvent.click(screen.getByText("我的收藏")); + + expect( + await screen.findByRole("heading", { name: "我的收藏" }), + ).toBeTruthy(); + expect(await screen.findByText("已安装")).toBeTruthy(); + expect(invoke).toHaveBeenCalledWith("list-installed", { + origin: "apm", + pkgnameList: undefined, + }); + }); }); diff --git a/src/__tests__/unit/favoriteAvailability.test.ts b/src/__tests__/unit/favoriteAvailability.test.ts index 4b7e5c6b..5519c0c1 100644 --- a/src/__tests__/unit/favoriteAvailability.test.ts +++ b/src/__tests__/unit/favoriteAvailability.test.ts @@ -1,8 +1,11 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { resolveFavoriteItems } from "@/modules/favoriteAvailability"; +import { loadPriorityConfig } from "@/global/storeConfig"; import type { App, FavoriteItem } from "@/global/typedefinition"; +const originalFetch = globalThis.fetch; + const app = (origin: "spark" | "apm", overrides: Partial = {}): App => ({ name: "WPS", pkgname: "wps", @@ -36,6 +39,14 @@ const favorite: FavoriteItem = { }; describe("favoriteAvailability", () => { + afterEach(async () => { + vi.restoreAllMocks(); + globalThis.fetch = originalFetch; + vi.spyOn(globalThis, "fetch").mockResolvedValue({ ok: false } as Response); + await loadPriorityConfig("amd64"); + vi.restoreAllMocks(); + }); + it("marks downlisted favorites", () => { expect( resolveFavoriteItems( @@ -48,7 +59,16 @@ describe("favoriteAvailability", () => { ).toBe("downlisted"); }); - it("selects preferred installable variant", () => { + it("selects preferred installable variant", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue({ + ok: true, + json: async () => ({ + sparkPriority: { pkgnames: [], categories: [], tags: [] }, + apmPriority: { pkgnames: [], categories: [], tags: [] }, + }), + } as Response); + await loadPriorityConfig("amd64"); + const resolved = resolveFavoriteItems( [favorite], [app("spark"), app("apm")], @@ -60,6 +80,28 @@ describe("favoriteAvailability", () => { expect(resolved.selectedApp?.origin).toBe("apm"); }); + it("selects Spark when hybrid priority config prefers Spark", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue({ + ok: true, + json: async () => ({ + sparkPriority: { pkgnames: ["wps"], categories: [], tags: [] }, + apmPriority: { pkgnames: [], categories: [], tags: [] }, + }), + } as Response); + await loadPriorityConfig("amd64"); + + const resolved = resolveFavoriteItems( + [favorite], + [app("spark"), app("apm")], + [], + { spark: true, apm: true }, + "both", + )[0]; + + expect(resolved.status).toBe("installable"); + expect(resolved.selectedApp?.origin).toBe("spark"); + }); + it("marks installed favorites", () => { const resolved = resolveFavoriteItems( [favorite], diff --git a/src/modules/favoriteAvailability.ts b/src/modules/favoriteAvailability.ts index 0da82e81..4323f855 100644 --- a/src/modules/favoriteAvailability.ts +++ b/src/modules/favoriteAvailability.ts @@ -1,7 +1,4 @@ -import { - HYBRID_DEFAULT_PRIORITY, - getHybridDefaultOrigin, -} from "@/global/storeConfig"; +import { getHybridDefaultOrigin } from "@/global/storeConfig"; import type { App, FavoriteItem, @@ -20,6 +17,10 @@ const normalizeArch = (arch: string): string => const appMatchesFavorite = (app: App, item: FavoriteItem): boolean => app.pkgname === item.pkgname && app.category === item.category; +const installedAppMatchesFavorite = (app: App, item: FavoriteItem): boolean => + app.pkgname === item.pkgname && + (app.category === item.category || app.category === "unknown"); + const appMatchesClientArch = (app: App, clientArch: string): boolean => { if (!app.arch) return true; return normalizeArch(app.arch) === normalizeArch(clientArch); @@ -39,10 +40,7 @@ const choosePreferredApp = (apps: App[]): App => { if (apps.length === 1) return apps[0]; const referenceApp = apps.find((app) => app.origin === "spark") ?? apps[0]; - const preferredOrigin = - getHybridDefaultOrigin(referenceApp) === "spark" - ? HYBRID_DEFAULT_PRIORITY - : getHybridDefaultOrigin(referenceApp); + const preferredOrigin = getHybridDefaultOrigin(referenceApp); return apps.find((app) => app.origin === preferredOrigin) ?? apps[0]; }; @@ -69,7 +67,7 @@ export const resolveFavoriteItems = ( } const installedMatch = installedApps.find((app) => - appMatchesFavorite(app, item), + installedAppMatchesFavorite(app, item), ); if (installedMatch) { return {