diff --git a/src/App.vue b/src/App.vue index 8a5ad7e8..447e24f0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1541,7 +1541,9 @@ const loadDownloadedHistory = async (): Promise => { } }; -const refreshInstalledSyncCandidates = async (): Promise => { +const refreshInstalledSyncCandidates = async ( + isCurrentRequest: () => boolean, +): Promise => { const origins: Array<"spark" | "apm"> = []; if (isOriginEnabled(storeFilter.value, "spark") && sparkAvailable.value) { origins.push("spark"); @@ -1572,11 +1574,16 @@ const refreshInstalledSyncCandidates = async (): Promise => { }), ); + if (!isCurrentRequest()) { + return false; + } + syncCandidateApps.value = mergeInstalledApps( syncCandidateApps.value, refreshedApps, origins, ); + return true; }; const syncInstalledAppsToAccount = async (): Promise => { @@ -1588,13 +1595,12 @@ const syncInstalledAppsToAccount = async (): Promise => { syncRequestGeneration.value = generation; syncLoading.value = true; try { - await refreshInstalledSyncCandidates(); - if ( - syncRequestGeneration.value !== generation || - currentUser.value?.id !== userId - ) { - return; - } + const refreshed = await refreshInstalledSyncCandidates( + () => + syncRequestGeneration.value === generation && + currentUser.value?.id === userId, + ); + if (!refreshed) return; const items = buildSyncItems(syncCandidateApps.value); await uploadSyncedAppList({ clientArch: window.apm_store.arch || "amd64", @@ -1641,7 +1647,10 @@ const openRestoreFromAccount = async (): Promise => { restoreError.value = ""; restoreItems.value = []; try { - await refreshInstalledSyncCandidates(); + const refreshed = await refreshInstalledSyncCandidates(() => + isCurrentRestoreRequest(generation, userId), + ); + if (!refreshed) return; const result = await fetchSyncedAppList(); if (!isCurrentRestoreRequest(generation, userId)) return; restoreItems.value = result?.items || []; diff --git a/src/__tests__/unit/App.account-placeholders.test.ts b/src/__tests__/unit/App.account-placeholders.test.ts index bcea8572..a3052621 100644 --- a/src/__tests__/unit/App.account-placeholders.test.ts +++ b/src/__tests__/unit/App.account-placeholders.test.ts @@ -583,4 +583,83 @@ describe("App account placeholders", () => { }); syncUpload.resolve(syncedList([])); }); + + it("does not upload stale sync candidates after logout", async () => { + const slowInstalled = createDeferred<{ + success: true; + apps: Array<{ + pkgname: string; + name: string; + version: string; + arch: string; + flags: string; + origin: "apm"; + }>; + }>(); + let listInstalledCalls = 0; + invoke.mockImplementation(async (channel: string) => { + 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 === "list-installed") { + listInstalledCalls += 1; + if (listInstalledCalls === 1) return slowInstalled.promise; + return { success: true, apps: [] }; + } + return []; + }); + vi.mocked(uploadSyncedAppList).mockResolvedValue(syncedList([])); + render(App); + + await fireEvent.click(await screen.findByRole("button", { name: /Momen/ })); + await fireEvent.click(screen.getByText("用户管理")); + await fireEvent.click( + await screen.findByRole("button", { name: "立即同步" }), + ); + await fireEvent.click( + await screen.findByRole("button", { name: /^Momen$/ }), + ); + if (!screen.queryByText("退出登录")) { + await fireEvent.click( + await screen.findByRole("button", { name: /^Momen$/ }), + ); + } + await fireEvent.click(screen.getByText("退出登录")); + + slowInstalled.resolve({ + success: true, + apps: [ + { + pkgname: "wps", + name: "WPS", + version: "1.0.0", + arch: "amd64", + flags: "installed", + origin: "apm", + }, + ], + }); + await slowInstalled.promise; + await Promise.resolve(); + + setSecondUserSession(); + const secondUserButton = await screen.findByRole("button", { + name: /^Second User$/, + }); + if (!screen.queryByText("用户管理")) { + await fireEvent.click(secondUserButton); + } + await fireEvent.click(await screen.findByText("用户管理")); + await fireEvent.click( + await screen.findByRole("button", { name: "立即同步" }), + ); + + await waitFor(() => { + expect(uploadSyncedAppList).toHaveBeenCalledWith( + expect.objectContaining({ items: [] }), + ); + }); + }); }); diff --git a/src/components/InstalledAppsModal.vue b/src/components/InstalledAppsModal.vue index 280968e7..b2717d0d 100644 --- a/src/components/InstalledAppsModal.vue +++ b/src/components/InstalledAppsModal.vue @@ -254,11 +254,21 @@ const emit = defineEmits<{ }>(); const handleSyncClick = () => { - emit(props.loggedIn ? "sync-to-account" : "request-login"); + if (props.loggedIn) { + emit("sync-to-account"); + return; + } + + emit("request-login"); }; const handleRestoreClick = () => { - emit(props.loggedIn ? "restore-from-account" : "request-login"); + if (props.loggedIn) { + emit("restore-from-account"); + return; + } + + emit("request-login"); }; const onOverlayWheel = (e: WheelEvent) => {