fix(sync): resolve restore item edge cases

This commit is contained in:
2026-05-19 01:50:34 +08:00
parent acffb6c5ee
commit 753f91e837
4 changed files with 97 additions and 7 deletions
+5 -4
View File
@@ -1583,12 +1583,13 @@ const openRestoreFromAccount = async (): Promise<void> => {
const installCloudItems = (items: SyncedAppListItem[]): void => { const installCloudItems = (items: SyncedAppListItem[]): void => {
for (const item of items) { for (const item of items) {
const app = apps.value.find( const candidates = apps.value.filter(
(candidate) => (candidate) =>
candidate.pkgname === item.pkgname && candidate.pkgname === item.pkgname && candidate.origin === item.origin,
candidate.origin === item.origin &&
candidate.category === item.category,
); );
const app =
candidates.find((candidate) => candidate.category === item.category) ??
candidates[0];
if (!app) continue; if (!app) continue;
void onDetailInstall(app); void onDetailInstall(app);
} }
@@ -2,7 +2,11 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/vue";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import App from "@/App.vue"; import App from "@/App.vue";
import { listDownloadedApps, listFavoriteFolders } from "@/modules/backendApi"; import {
fetchSyncedAppList,
listDownloadedApps,
listFavoriteFolders,
} from "@/modules/backendApi";
import { setAuthSession } from "@/global/authState"; import { setAuthSession } from "@/global/authState";
import type { import type {
DownloadedAppList, DownloadedAppList,
@@ -97,10 +101,11 @@ vi.mock("axios", () => {
} }
return { data: [] }; return { data: [] };
}); });
const post = vi.fn(async () => ({ data: { ok: true } }));
return { return {
default: { default: {
create: () => ({ get }), create: () => ({ get, post }),
}, },
}; };
}); });
@@ -138,9 +143,11 @@ vi.mock("@/modules/backendApi", () => ({
bulkDeleteFavoriteItems: vi.fn(), bulkDeleteFavoriteItems: vi.fn(),
createFavoriteFolder: vi.fn(), createFavoriteFolder: vi.fn(),
exchangeFlarumToken: vi.fn(), exchangeFlarumToken: vi.fn(),
fetchSyncedAppList: vi.fn(async () => null),
listDownloadedApps: vi.fn(async () => downloadedList([])), listDownloadedApps: vi.fn(async () => downloadedList([])),
listFavoriteFolders: vi.fn(async () => favoriteFolders), listFavoriteFolders: vi.fn(async () => favoriteFolders),
listFavoriteItems: vi.fn(async () => favoriteItems), listFavoriteItems: vi.fn(async () => favoriteItems),
uploadSyncedAppList: vi.fn(),
setBackendToken: vi.fn(), setBackendToken: vi.fn(),
})); }));
@@ -437,4 +444,52 @@ describe("App account placeholders", () => {
expect(screen.queryByText("旧账号应用")).toBeNull(); expect(screen.queryByText("旧账号应用")).toBeNull();
expect(screen.getByText("暂无下载记录。")).toBeTruthy(); 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"'),
);
});
});
}); });
@@ -54,4 +54,22 @@ describe("AppListRestoreModal", () => {
expect(screen.getByLabelText("Spark Notes")).toBeDisabled(); expect(screen.getByLabelText("Spark Notes")).toBeDisabled();
expect(screen.getByText("已安装")).toBeTruthy(); 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<string>(),
},
});
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();
});
}); });
+17 -1
View File
@@ -150,9 +150,17 @@ const isInstalled = (item: SyncedAppListItem): boolean =>
props.installedKeys.has(cloudItemKey(item)); props.installedKeys.has(cloudItemKey(item));
const selectedItems = computed(() => 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 => { const toggleSelection = (item: SyncedAppListItem): void => {
if (isInstalled(item)) return; if (isInstalled(item)) return;
const key = cloudItemKey(item); const key = cloudItemKey(item);
@@ -169,4 +177,12 @@ watch(
}, },
{ deep: true }, { deep: true },
); );
watch(
() => props.installedKeys,
() => {
pruneSelectedKeys();
},
{ deep: true },
);
</script> </script>