feat(update-center): 实现集中式软件更新中心功能

新增更新中心模块,支持管理 APM 和传统 deb 软件更新任务
- 添加更新任务队列管理、状态跟踪和日志记录功能
- 实现更新项忽略配置持久化存储
- 新增更新确认对话框和迁移提示
- 优化主窗口关闭时的任务保护机制
- 添加单元测试覆盖核心逻辑
This commit is contained in:
2026-04-09 08:19:51 +08:00
parent 97bb8e5f59
commit 0b17ada45a
37 changed files with 6389 additions and 342 deletions
@@ -0,0 +1,182 @@
import { computed, ref } from "vue";
import { fireEvent, render, screen } from "@testing-library/vue";
import { describe, expect, it, vi } from "vitest";
import UpdateCenterModal from "@/components/UpdateCenterModal.vue";
import type {
UpdateCenterItem,
UpdateCenterSnapshot,
UpdateCenterTaskState,
} from "@/global/typedefinition";
import type { UpdateCenterStore } from "@/modules/updateCenter";
const createItem = (
overrides: Partial<UpdateCenterItem> = {},
): UpdateCenterItem => ({
taskKey: "aptss:spark-weather",
packageName: "spark-weather",
displayName: "Spark Weather",
currentVersion: "1.0.0",
newVersion: "2.0.0",
source: "aptss",
...overrides,
});
const createTask = (
overrides: Partial<UpdateCenterTaskState> = {},
): UpdateCenterTaskState => ({
taskKey: "aptss:spark-weather",
packageName: "spark-weather",
source: "aptss",
status: "downloading",
progress: 42,
logs: [],
errorMessage: "",
...overrides,
});
const createStore = (
overrides: Partial<UpdateCenterSnapshot> = {},
): UpdateCenterStore => {
const snapshot = ref<UpdateCenterSnapshot>({
items: [
createItem({
taskKey: "aptss:spark-weather",
source: "aptss",
}),
createItem({
taskKey: "apm:spark-clock",
packageName: "spark-clock",
displayName: "Spark Clock",
source: "apm",
isMigration: true,
migrationTarget: "apm",
}),
],
tasks: [createTask()],
warnings: ["更新过程中请勿关闭商店"],
hasRunningTasks: true,
...overrides,
});
const selectedTaskKeys = ref(new Set<string>(["aptss:spark-weather"]));
return {
isOpen: ref(true),
showCloseConfirm: ref(true),
showMigrationConfirm: ref(false),
searchQuery: ref(""),
selectedTaskKeys,
snapshot,
filteredItems: computed(() => snapshot.value.items),
bind: vi.fn(),
unbind: vi.fn(),
open: vi.fn(),
refresh: vi.fn(),
toggleSelection: vi.fn(),
getSelectedItems: vi.fn(() =>
snapshot.value.items.filter(
(item) =>
selectedTaskKeys.value.has(item.taskKey) && item.ignored !== true,
),
),
closeNow: vi.fn(),
startSelected: vi.fn(),
requestClose: vi.fn(),
};
};
describe("UpdateCenterModal", () => {
it("renders source tags, running state, warnings, migration marker, and close confirmation", () => {
const store = createStore();
render(UpdateCenterModal, {
props: {
show: true,
store,
},
});
expect(screen.getByText("软件更新")).toBeTruthy();
expect(screen.getByText("传统deb")).toBeTruthy();
expect(screen.getByText("APM")).toBeTruthy();
expect(screen.getByText("将迁移到 APM")).toBeTruthy();
expect(screen.getByText("更新过程中请勿关闭商店")).toBeTruthy();
expect(screen.getByText("下载中")).toBeTruthy();
expect(screen.getByText("42%")).toBeTruthy();
expect(screen.getByText(/确定关闭/)).toBeTruthy();
});
it("close confirmation exposes a confirm-close path", async () => {
const onConfirmClose = vi.fn();
const store = createStore();
render(UpdateCenterModal, {
props: {
show: true,
store,
onConfirmClose,
},
});
await fireEvent.click(screen.getByRole("button", { name: "确认关闭" }));
expect(onConfirmClose).toHaveBeenCalledTimes(1);
});
it("renders ignored items as disabled instead of normal selectable actions", () => {
const store = createStore({
items: [
createItem({
taskKey: "aptss:spark-weather",
packageName: "spark-weather",
displayName: "Spark Weather",
source: "aptss",
ignored: true,
}),
],
tasks: [],
warnings: [],
hasRunningTasks: false,
});
render(UpdateCenterModal, {
props: {
show: true,
store,
},
});
expect(screen.getByText("已忽略")).toBeTruthy();
expect(screen.getByRole("checkbox")).toBeDisabled();
});
it("renders migration confirmation when requested", () => {
const store = createStore({ hasRunningTasks: false });
store.showMigrationConfirm.value = true;
render(UpdateCenterModal, {
props: {
show: true,
store,
},
});
expect(screen.getByText("迁移确认")).toBeTruthy();
});
it("close button triggers request-close flow", async () => {
const store = createStore({ hasRunningTasks: false });
render(UpdateCenterModal, {
props: {
show: true,
store,
},
});
await fireEvent.click(screen.getByRole("button", { name: "关闭" }));
expect(store.requestClose).toHaveBeenCalledTimes(1);
});
});