diff --git a/apps/desktop/src/components/layout/UpdateDialog.spec.ts b/apps/desktop/src/components/layout/UpdateDialog.spec.ts index aa97c51e59..76360ddd42 100644 --- a/apps/desktop/src/components/layout/UpdateDialog.spec.ts +++ b/apps/desktop/src/components/layout/UpdateDialog.spec.ts @@ -1,16 +1,34 @@ // @vitest-environment happy-dom import { createApp, defineComponent, h, nextTick, reactive, type App } from "vue"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import i18n from "@/i18n"; import UpdateDialog from "@/components/layout/UpdateDialog.vue"; const runtimeState = vi.hoisted(() => ({ tauri: true })); +const updateMocks = vi.hoisted(() => ({ + fetchChangelog: vi.fn(), + getAppVersion: vi.fn(), + openExternal: vi.fn(), +})); + vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => runtimeState.tauri, })); +vi.mock("@/lib/app/changelog", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, fetchChangelog: updateMocks.fetchChangelog }; +}); + +vi.mock("@/lib/backend/api", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, getAppVersion: updateMocks.getAppVersion }; +}); + +vi.mock("@tauri-apps/plugin-shell", () => ({ open: updateMocks.openExternal })); + const mountedApps: App[] = []; interface DialogState { @@ -136,6 +154,13 @@ async function clickOutside() { await flushDialog(); } +beforeEach(() => { + // 默认没有历史版本:只有专门的历史版本用例才提供发布记录 + updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "", releases: [] }); + updateMocks.getAppVersion.mockResolvedValue("0.5.60"); + updateMocks.openExternal.mockClear(); +}); + afterEach(() => { runtimeState.tauri = true; for (const app of mountedApps.splice(0)) app.unmount(); @@ -379,8 +404,105 @@ describe("UpdateDialog release notes safety", () => { }); }); +describe("UpdateDialog older versions", () => { + const release = (tag: string, date: string) => ({ tag, name: `DBX ${tag}`, date, sections: [] }); + const upToDateInfo = { + current_version: "0.5.60", + latest_version: "0.5.60", + update_available: false, + portable_mode: false, + manual_update_only: false, + release_name: "", + release_url: "", + release_notes: "", + }; + const historyRegion = () => document.body.querySelector("[data-update-history]"); + const historyButton = (text: string) => Array.from(historyRegion()?.querySelectorAll("button") ?? []).find((button) => button.textContent?.includes(text)); + + it("lists only versions older than the current build and opens their release page", async () => { + updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "2026-09-10", releases: [release("v0.5.61", "2026-09-10"), release("v0.5.60", "2026-09-01"), release("v0.5.59", "2026-08-20")] }); + + await mountDialog(0, {}, undefined, { updateInfo: upToDateInfo, updateCheckMessage: "DBX is up to date (0.5.60)." }); + await flushDialog(); + + // 默认折叠:即使没有可用更新也先收起,且列表只保留低于当前版本的版本 + const region = historyRegion(); + expect(region?.textContent).toContain("Older versions"); + expect(region?.textContent).not.toContain("v0.5.59"); + + historyButton("Older versions")?.click(); + await flushDialog(); + + expect(region?.textContent).toContain("Back up your data before rolling back"); + expect(region?.textContent).toContain("v0.5.59"); + expect(region?.textContent).not.toContain("v0.5.61"); + + historyButton("Open Release")?.click(); + await flushDialog(); + + expect(updateMocks.openExternal).toHaveBeenCalledWith("https://github.com/t8y2/dbx/releases/tag/v0.5.59"); + }); + + it("keeps the history collapsed until the user expands it", async () => { + updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "2026-09-10", releases: [release("v0.5.61", "2026-09-10"), release("v0.5.59", "2026-08-20")] }); + + await mountDialog(0); + await flushDialog(); + + expect(historyRegion()?.textContent).toContain("Older versions"); + expect(historyRegion()?.textContent).not.toContain("v0.5.59"); + + historyButton("Older versions")?.click(); + await flushDialog(); + + expect(historyRegion()?.textContent).toContain("v0.5.59"); + }); + + it("starts collapsed again after the dialog is reopened", async () => { + updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "2026-09-10", releases: [release("v0.5.61", "2026-09-10"), release("v0.5.59", "2026-08-20")] }); + + const { state } = await mountDialog(0, {}, undefined, { updateInfo: upToDateInfo, updateCheckMessage: "DBX is up to date (0.5.60)." }); + await flushDialog(); + + historyButton("Older versions")?.click(); + await flushDialog(); + expect(historyRegion()?.textContent).toContain("v0.5.59"); + + state.open = false; + await flushDialog(); + state.open = true; + await flushDialog(); + + expect(historyRegion()?.textContent).toContain("Older versions"); + expect(historyRegion()?.textContent).not.toContain("v0.5.59"); + }); + + it("hides the history section when no release is older than the current build", async () => { + updateMocks.fetchChangelog.mockResolvedValue({ updatedAt: "2026-09-10", releases: [release("v0.5.61", "2026-09-10"), release("v0.5.60", "2026-09-01")] }); + + await mountDialog(0); + await flushDialog(); + + expect(historyRegion()).toBeNull(); + }); + + it("surfaces a changelog load failure instead of hiding it silently", async () => { + updateMocks.fetchChangelog.mockRejectedValue(new Error("offline")); + + await mountDialog(0, {}, undefined, { updateInfo: upToDateInfo, updateCheckMessage: "DBX is up to date (0.5.60)." }); + await flushDialog(); + + expect(historyRegion()?.textContent).toContain("Older versions"); + + historyButton("Older versions")?.click(); + await flushDialog(); + + expect(historyRegion()?.textContent).toContain("Failed to load older versions"); + }); +}); + describe("UpdateDialog aggregate update center", () => { - it("hides the tab bar when no updates are available", async () => { + it("keeps only the client tab when nothing is updatable", async () => { await mountDialog(0, {}, undefined, { updateInfo: { current_version: "0.5.60", @@ -395,8 +517,9 @@ describe("UpdateDialog aggregate update center", () => { updateCheckMessage: "DBX is up to date (0.5.60).", }); - expect(document.body.querySelector('[role="tablist"]')).toBeNull(); - expect(document.body.querySelector('[data-update-tab="app"]')).toBeNull(); + // 客户端页签常驻(承载回退入口),但没有可更新项时不再出现其他页签 + expect(document.body.querySelector('[data-update-tab="app"]')).not.toBeNull(); + expect(document.body.querySelector('[data-update-tab="drivers"]')).toBeNull(); expect(document.body.textContent).toContain("DBX is up to date (0.5.60)."); }); diff --git a/apps/desktop/src/components/layout/UpdateDialog.vue b/apps/desktop/src/components/layout/UpdateDialog.vue index 2e9c12bb2c..6d696df25b 100644 --- a/apps/desktop/src/components/layout/UpdateDialog.vue +++ b/apps/desktop/src/components/layout/UpdateDialog.vue @@ -1,19 +1,21 @@