fix(favorites): honor source priority and installed state

This commit is contained in:
2026-05-18 23:40:28 +08:00
parent e116dcee63
commit 58789ecd1f
4 changed files with 148 additions and 13 deletions
+1 -1
View File
@@ -1274,7 +1274,7 @@ const refreshFavorites = async (): Promise<void> => {
favoriteLoading.value = true; favoriteLoading.value = true;
favoriteError.value = ""; favoriteError.value = "";
try { try {
await loadFavoriteFolders(); await Promise.all([refreshInstalledApps(), loadFavoriteFolders()]);
await loadActiveFavoriteItems(); await loadActiveFavoriteItems();
} catch (error: unknown) { } catch (error: unknown) {
favoriteError.value = (error as Error)?.message || "读取收藏夹失败"; favoriteError.value = (error as Error)?.message || "读取收藏夹失败";
@@ -3,12 +3,59 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import App from "@/App.vue"; import App from "@/App.vue";
import { setAuthSession } from "@/global/authState"; import { setAuthSession } from "@/global/authState";
import type { FavoriteFolder, FavoriteItem } from "@/global/typedefinition";
const invoke = vi.fn(); 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", () => { vi.mock("axios", () => {
const get = vi.fn(async (url: string) => { 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: [] }; 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", () => { describe("App account placeholders", () => {
beforeEach(() => { beforeEach(() => {
invoke.mockReset(); invoke.mockReset();
@@ -114,4 +171,42 @@ describe("App account placeholders", () => {
).toBeTruthy(); ).toBeTruthy();
expect(screen.queryByText("请登录后查看我的收藏。")).toBeNull(); 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,
});
});
}); });
@@ -1,8 +1,11 @@
import { describe, expect, it } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveFavoriteItems } from "@/modules/favoriteAvailability"; import { resolveFavoriteItems } from "@/modules/favoriteAvailability";
import { loadPriorityConfig } from "@/global/storeConfig";
import type { App, FavoriteItem } from "@/global/typedefinition"; import type { App, FavoriteItem } from "@/global/typedefinition";
const originalFetch = globalThis.fetch;
const app = (origin: "spark" | "apm", overrides: Partial<App> = {}): App => ({ const app = (origin: "spark" | "apm", overrides: Partial<App> = {}): App => ({
name: "WPS", name: "WPS",
pkgname: "wps", pkgname: "wps",
@@ -36,6 +39,14 @@ const favorite: FavoriteItem = {
}; };
describe("favoriteAvailability", () => { 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", () => { it("marks downlisted favorites", () => {
expect( expect(
resolveFavoriteItems( resolveFavoriteItems(
@@ -48,7 +59,16 @@ describe("favoriteAvailability", () => {
).toBe("downlisted"); ).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( const resolved = resolveFavoriteItems(
[favorite], [favorite],
[app("spark"), app("apm")], [app("spark"), app("apm")],
@@ -60,6 +80,28 @@ describe("favoriteAvailability", () => {
expect(resolved.selectedApp?.origin).toBe("apm"); 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", () => { it("marks installed favorites", () => {
const resolved = resolveFavoriteItems( const resolved = resolveFavoriteItems(
[favorite], [favorite],
+7 -9
View File
@@ -1,7 +1,4 @@
import { import { getHybridDefaultOrigin } from "@/global/storeConfig";
HYBRID_DEFAULT_PRIORITY,
getHybridDefaultOrigin,
} from "@/global/storeConfig";
import type { import type {
App, App,
FavoriteItem, FavoriteItem,
@@ -20,6 +17,10 @@ const normalizeArch = (arch: string): string =>
const appMatchesFavorite = (app: App, item: FavoriteItem): boolean => const appMatchesFavorite = (app: App, item: FavoriteItem): boolean =>
app.pkgname === item.pkgname && app.category === item.category; 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 => { const appMatchesClientArch = (app: App, clientArch: string): boolean => {
if (!app.arch) return true; if (!app.arch) return true;
return normalizeArch(app.arch) === normalizeArch(clientArch); return normalizeArch(app.arch) === normalizeArch(clientArch);
@@ -39,10 +40,7 @@ const choosePreferredApp = (apps: App[]): App => {
if (apps.length === 1) return apps[0]; if (apps.length === 1) return apps[0];
const referenceApp = apps.find((app) => app.origin === "spark") ?? apps[0]; const referenceApp = apps.find((app) => app.origin === "spark") ?? apps[0];
const preferredOrigin = const preferredOrigin = getHybridDefaultOrigin(referenceApp);
getHybridDefaultOrigin(referenceApp) === "spark"
? HYBRID_DEFAULT_PRIORITY
: getHybridDefaultOrigin(referenceApp);
return apps.find((app) => app.origin === preferredOrigin) ?? apps[0]; return apps.find((app) => app.origin === preferredOrigin) ?? apps[0];
}; };
@@ -69,7 +67,7 @@ export const resolveFavoriteItems = (
} }
const installedMatch = installedApps.find((app) => const installedMatch = installedApps.find((app) =>
appMatchesFavorite(app, item), installedAppMatchesFavorite(app, item),
); );
if (installedMatch) { if (installedMatch) {
return { return {