From 753f91e837d353c9dda01c187ddfbc11cab6cc7c Mon Sep 17 00:00:00 2001 From: momen Date: Tue, 19 May 2026 01:50:34 +0800 Subject: [PATCH] fix(sync): resolve restore item edge cases --- src/App.vue | 9 +-- .../unit/App.account-placeholders.test.ts | 59 ++++++++++++++++++- .../unit/AppListRestoreModal.test.ts | 18 ++++++ src/components/AppListRestoreModal.vue | 18 +++++- 4 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src/App.vue b/src/App.vue index 170cd6ec..e9d49cb9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1583,12 +1583,13 @@ const openRestoreFromAccount = async (): Promise => { const installCloudItems = (items: SyncedAppListItem[]): void => { for (const item of items) { - const app = apps.value.find( + const candidates = apps.value.filter( (candidate) => - candidate.pkgname === item.pkgname && - candidate.origin === item.origin && - candidate.category === item.category, + candidate.pkgname === item.pkgname && candidate.origin === item.origin, ); + const app = + candidates.find((candidate) => candidate.category === item.category) ?? + candidates[0]; if (!app) continue; void onDetailInstall(app); } diff --git a/src/__tests__/unit/App.account-placeholders.test.ts b/src/__tests__/unit/App.account-placeholders.test.ts index bd683dcc..932d6d61 100644 --- a/src/__tests__/unit/App.account-placeholders.test.ts +++ b/src/__tests__/unit/App.account-placeholders.test.ts @@ -2,7 +2,11 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/vue"; import { beforeEach, describe, expect, it, vi } from "vitest"; import App from "@/App.vue"; -import { listDownloadedApps, listFavoriteFolders } from "@/modules/backendApi"; +import { + fetchSyncedAppList, + listDownloadedApps, + listFavoriteFolders, +} from "@/modules/backendApi"; import { setAuthSession } from "@/global/authState"; import type { DownloadedAppList, @@ -97,10 +101,11 @@ vi.mock("axios", () => { } return { data: [] }; }); + const post = vi.fn(async () => ({ data: { ok: true } })); return { default: { - create: () => ({ get }), + create: () => ({ get, post }), }, }; }); @@ -138,9 +143,11 @@ vi.mock("@/modules/backendApi", () => ({ bulkDeleteFavoriteItems: vi.fn(), createFavoriteFolder: vi.fn(), exchangeFlarumToken: vi.fn(), + fetchSyncedAppList: vi.fn(async () => null), listDownloadedApps: vi.fn(async () => downloadedList([])), listFavoriteFolders: vi.fn(async () => favoriteFolders), listFavoriteItems: vi.fn(async () => favoriteItems), + uploadSyncedAppList: vi.fn(), setBackendToken: vi.fn(), })); @@ -437,4 +444,52 @@ describe("App account placeholders", () => { expect(screen.queryByText("旧账号应用")).toBeNull(); expect(screen.getByText("暂无下载记录。")).toBeTruthy(); }); + + it("restores cloud apps by origin and package when category changed", async () => { + vi.mocked(fetchSyncedAppList).mockResolvedValueOnce({ + snapshotName: "默认列表", + clientArch: "amd64", + distro: "deepin 25", + updatedAt: "2026-05-18T00:00:00Z", + items: [ + { + pkgname: "wps", + origin: "apm", + category: "legacy-office", + version: "1.0.0", + packageArch: "amd64", + appName: "WPS Cloud", + iconUrl: "", + }, + ], + }); + invoke.mockImplementation(async (channel: string, payload?: unknown) => { + if (channel === "get-store-filter") return "apm"; + if (channel === "check-spark-available") return false; + if (channel === "check-apm-available") return true; + if (channel === "get-app-version") return "5.0.0"; + if (channel === "get-system-info") return { distro: "deepin 25" }; + if (channel === "check-installed") return false; + if (channel === "list-installed") { + const request = payload as { origin?: string }; + if (request.origin === "apm") return { success: true, apps: [] }; + } + return []; + }); + render(App); + + await fireEvent.click(await screen.findByText("应用管理")); + await fireEvent.click( + await screen.findByRole("button", { name: /从账号恢复/ }), + ); + await fireEvent.click(await screen.findByLabelText("WPS Cloud")); + await fireEvent.click(screen.getByRole("button", { name: "加入安装队列" })); + + await waitFor(() => { + expect(window.ipcRenderer.send).toHaveBeenCalledWith( + "queue-install", + expect.stringContaining('"pkgname":"wps"'), + ); + }); + }); }); diff --git a/src/__tests__/unit/AppListRestoreModal.test.ts b/src/__tests__/unit/AppListRestoreModal.test.ts index dbe26fc5..b65098b2 100644 --- a/src/__tests__/unit/AppListRestoreModal.test.ts +++ b/src/__tests__/unit/AppListRestoreModal.test.ts @@ -54,4 +54,22 @@ describe("AppListRestoreModal", () => { expect(screen.getByLabelText("Spark Notes")).toBeDisabled(); expect(screen.getByText("已安装")).toBeTruthy(); }); + + it("removes selected items when they become installed", async () => { + const rendered = render(AppListRestoreModal, { + props: { + show: true, + loading: false, + error: "", + items: [createItem()], + installedKeys: new Set(), + }, + }); + + await fireEvent.click(screen.getByLabelText("Spark Notes")); + await rendered.rerender({ installedKeys: new Set(["spark:spark-notes"]) }); + + expect(screen.getByLabelText("Spark Notes")).toBeDisabled(); + expect(screen.getByRole("button", { name: "加入安装队列" })).toBeDisabled(); + }); }); diff --git a/src/components/AppListRestoreModal.vue b/src/components/AppListRestoreModal.vue index 3b577c90..06feaab8 100644 --- a/src/components/AppListRestoreModal.vue +++ b/src/components/AppListRestoreModal.vue @@ -150,9 +150,17 @@ const isInstalled = (item: SyncedAppListItem): boolean => props.installedKeys.has(cloudItemKey(item)); const selectedItems = computed(() => - props.items.filter((item) => selectedKeys.value.has(cloudItemKey(item))), + props.items.filter( + (item) => selectedKeys.value.has(cloudItemKey(item)) && !isInstalled(item), + ), ); +const pruneSelectedKeys = (): void => { + selectedKeys.value = new Set( + [...selectedKeys.value].filter((key) => !props.installedKeys.has(key)), + ); +}; + const toggleSelection = (item: SyncedAppListItem): void => { if (isInstalled(item)) return; const key = cloudItemKey(item); @@ -169,4 +177,12 @@ watch( }, { deep: true }, ); + +watch( + () => props.installedKeys, + () => { + pruneSelectedKeys(); + }, + { deep: true }, +);