完善桌面端端到端回归收口

This commit is contained in:
Cheng Zhou
2026-07-02 10:04:31 +08:00
parent 360988de5e
commit 1c1527a224
8 changed files with 290 additions and 5 deletions
@@ -188,6 +188,7 @@ describe("desktop layout shell", () => {
expect(serverSettings).not.toContain("copyConnectionDiagnostic");
expect(serverSettings).toContain("确认切换服务器");
expect(serverSettings).toContain("切换服务器会退出当前会话并清除当前项目上下文");
expect(serverSettings).toContain("auth.logout({ rememberCurrentStudy: false })");
expect(serverSettings).not.toContain("服务器连接已确认");
expect(preferences).toContain("connectionDiagnostic");
expect(preferences).not.toContain("复制连接诊断");
@@ -0,0 +1,80 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const getSubscriptionMock = vi.hoisted(() => vi.fn());
const claimNotificationsMock = vi.hoisted(() => vi.fn());
const acknowledgeNotificationsMock = vi.hoisted(() => vi.fn());
const getTokenMock = vi.hoisted(() => vi.fn());
const getPermissionMock = vi.hoisted(() => vi.fn());
const showNotificationMock = vi.hoisted(() => vi.fn());
vi.mock("../api/desktopNotifications", () => ({
getDesktopNotificationSubscription: getSubscriptionMock,
claimDesktopNotifications: claimNotificationsMock,
acknowledgeDesktopNotifications: acknowledgeNotificationsMock,
}));
vi.mock("../utils/auth", () => ({
getToken: getTokenMock,
}));
vi.mock("../runtime", () => ({
getNotificationPermission: getPermissionMock,
isTauriRuntime: () => true,
showSystemNotification: showNotificationMock,
}));
describe("desktop notification manager", () => {
beforeEach(() => {
vi.useFakeTimers();
getTokenMock.mockReturnValue("session-token");
getPermissionMock.mockResolvedValue("granted");
getSubscriptionMock.mockResolvedValue({ data: { enabled: true } });
claimNotificationsMock.mockResolvedValue({
data: {
claim_token: "claim-token",
items: [
{ id: "notification-1" },
{ id: "notification-2" },
],
},
});
acknowledgeNotificationsMock.mockResolvedValue({ data: {} });
showNotificationMock.mockResolvedValue(undefined);
});
afterEach(async () => {
const { stopDesktopNotificationManager } = await import("./desktopNotificationManager");
stopDesktopNotificationManager();
vi.clearAllTimers();
vi.useRealTimers();
vi.resetModules();
vi.clearAllMocks();
});
it("acknowledges displayed notifications and leaves failed deliveries for retry", async () => {
showNotificationMock
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(new Error("notification failed"));
const { initDesktopNotificationManager, triggerDesktopNotificationPoll } = await import("./desktopNotificationManager");
initDesktopNotificationManager();
triggerDesktopNotificationPoll();
await vi.runOnlyPendingTimersAsync();
expect(showNotificationMock).toHaveBeenCalledTimes(2);
expect(acknowledgeNotificationsMock).toHaveBeenCalledWith("claim-token", ["notification-1"]);
expect(acknowledgeNotificationsMock).toHaveBeenCalledTimes(1);
});
it("does not claim notifications before operating system permission is granted", async () => {
getPermissionMock.mockResolvedValue("prompt");
const { initDesktopNotificationManager, triggerDesktopNotificationPoll } = await import("./desktopNotificationManager");
initDesktopNotificationManager();
triggerDesktopNotificationPoll();
await vi.runOnlyPendingTimersAsync();
expect(claimNotificationsMock).not.toHaveBeenCalled();
expect(acknowledgeNotificationsMock).not.toHaveBeenCalled();
});
});
@@ -41,13 +41,21 @@ const poll = async () => {
}
const { data } = await claimDesktopNotifications();
const deliveredIds: string[] = [];
let deliveryFailed = false;
for (const item of data.items) {
await showSystemNotification();
deliveredIds.push(item.id);
try {
await showSystemNotification();
deliveredIds.push(item.id);
} catch {
deliveryFailed = true;
}
}
if (data.claim_token && deliveredIds.length) {
await acknowledgeDesktopNotifications(data.claim_token, deliveredIds);
}
if (deliveryFailed) {
throw new Error("desktop notification delivery failed");
}
failureCount = 0;
schedule(POLL_INTERVAL_MS);
} catch {
@@ -0,0 +1,148 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const checkForDesktopUpdateMock = vi.hoisted(() => vi.fn());
const installPendingDesktopUpdateMock = vi.hoisted(() => vi.fn());
const isDesktopUpdaterAvailableMock = vi.hoisted(() => vi.fn());
const messageErrorMock = vi.hoisted(() => vi.fn());
const confirmMock = vi.hoisted(() => vi.fn());
vi.mock("../runtime", () => ({
checkForDesktopUpdate: checkForDesktopUpdateMock,
installPendingDesktopUpdate: installPendingDesktopUpdateMock,
isDesktopUpdaterAvailable: isDesktopUpdaterAvailableMock,
}));
vi.mock("element-plus", () => ({
ElMessage: {
error: messageErrorMock,
},
ElMessageBox: {
confirm: confirmMock,
},
}));
const createStorage = (): Storage => {
const data = new Map<string, string>();
return {
get length() {
return data.size;
},
clear: vi.fn(() => data.clear()),
getItem: vi.fn((key: string) => data.get(key) ?? null),
key: vi.fn((index: number) => Array.from(data.keys())[index] ?? null),
removeItem: vi.fn((key: string) => data.delete(key)),
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
};
};
const update = {
version: "0.1.1",
currentVersion: "0.1.0",
notes: "桌面端稳定化",
date: "2026-07-02",
};
describe("desktop update manager", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-07-02T00:00:00.000Z"));
vi.resetModules();
vi.clearAllMocks();
Object.defineProperty(window, "localStorage", {
value: createStorage(),
configurable: true,
});
isDesktopUpdaterAvailableMock.mockReturnValue(true);
checkForDesktopUpdateMock.mockResolvedValue(null);
installPendingDesktopUpdateMock.mockResolvedValue(undefined);
confirmMock.mockResolvedValue(undefined);
});
afterEach(async () => {
const { stopDesktopUpdateManager } = await import("./desktopUpdateManager");
stopDesktopUpdateManager();
vi.clearAllTimers();
vi.useRealTimers();
});
it("records up-to-date checks without prompting", async () => {
const { checkDesktopUpdateAndPrompt, getDesktopUpdateStatus } = await import("./desktopUpdateManager");
const status = await checkDesktopUpdateAndPrompt({ notifyWhenCurrent: true, promptWhenAvailable: true });
expect(status).toBe("up-to-date");
expect(confirmMock).not.toHaveBeenCalled();
expect(getDesktopUpdateStatus()).toMatchObject({
lastStatus: "up-to-date",
pendingUpdate: null,
lastError: "",
});
});
it("suppresses the same postponed version for 24 hours", async () => {
checkForDesktopUpdateMock.mockResolvedValue(update);
confirmMock.mockRejectedValueOnce("cancel");
const { checkDesktopUpdateAndPrompt, getDesktopUpdateStatus } = await import("./desktopUpdateManager");
const postponed = await checkDesktopUpdateAndPrompt({ promptWhenAvailable: true });
const postponedStatus = getDesktopUpdateStatus();
const suppressed = await checkDesktopUpdateAndPrompt({ promptWhenAvailable: true });
expect(postponed).toBe("postponed");
expect(suppressed).toBe("suppressed");
expect(confirmMock).toHaveBeenCalledTimes(1);
expect(installPendingDesktopUpdateMock).not.toHaveBeenCalled();
expect(postponedStatus.postponedUntil).toBe("2026-07-03T00:00:00.000Z");
expect(getDesktopUpdateStatus()).toMatchObject({
lastStatus: "suppressed",
pendingUpdate: update,
postponedUntil: "2026-07-03T00:00:00.000Z",
});
});
it("keeps a pending update retryable when installation fails", async () => {
checkForDesktopUpdateMock.mockResolvedValue(update);
installPendingDesktopUpdateMock.mockRejectedValueOnce(new Error("install failed"));
const { checkDesktopUpdateAndPrompt, getDesktopUpdateStatus } = await import("./desktopUpdateManager");
const status = await checkDesktopUpdateAndPrompt({ promptWhenAvailable: true });
expect(status).toBe("failed");
expect(messageErrorMock).toHaveBeenCalledWith("桌面端更新安装失败,请稍后重试或联系管理员。");
expect(getDesktopUpdateStatus()).toMatchObject({
installing: false,
lastStatus: "failed",
lastError: "桌面端更新安装失败",
pendingUpdate: update,
});
});
it("does not interrupt timed update checks when checking fails", async () => {
checkForDesktopUpdateMock.mockRejectedValueOnce(new Error("feed unavailable"));
const { checkDesktopUpdateAndPrompt, getDesktopUpdateStatus } = await import("./desktopUpdateManager");
const status = await checkDesktopUpdateAndPrompt();
expect(status).toBe("failed");
expect(messageErrorMock).not.toHaveBeenCalled();
expect(getDesktopUpdateStatus()).toMatchObject({
lastStatus: "failed",
lastError: "feed unavailable",
});
});
it("reports disabled updater builds explicitly", async () => {
isDesktopUpdaterAvailableMock.mockReturnValue(false);
const { checkDesktopUpdateAndPrompt, getDesktopUpdateStatus } = await import("./desktopUpdateManager");
const status = await checkDesktopUpdateAndPrompt({ notifyWhenCurrent: true });
expect(status).toBe("disabled");
expect(checkForDesktopUpdateMock).not.toHaveBeenCalled();
expect(getDesktopUpdateStatus()).toMatchObject({
available: false,
lastStatus: "disabled",
pendingUpdate: null,
});
});
});
+27
View File
@@ -99,6 +99,33 @@ describe("auth store logout", () => {
expect(session.timeoutWarningVisible).toBe(false);
});
it("can logout without remembering the current study during desktop server switch", async () => {
const { useAuthStore } = await import("./auth");
const { useStudyStore } = await import("./study");
const auth = useAuthStore();
const study = useStudyStore();
study.setCurrentStudy({
id: "study-old-server",
name: "旧服务器项目",
status: "ACTIVE",
is_locked: false,
} as any);
auth.user = {
id: "user-1",
email: "admin@test.com",
full_name: "Admin",
clinical_department: "Admin",
status: "ACTIVE",
is_admin: true,
};
await auth.logout({ rememberCurrentStudy: false });
expect(window.localStorage.getItem("ctms_last_study_by_user")).toBeNull();
expect(study.currentStudy).toBeNull();
});
it("uses encrypted login by default outside a secure browser context", async () => {
vi.stubGlobal("isSecureContext", false);
const { useAuthStore } = await import("./auth");
+4 -2
View File
@@ -62,11 +62,13 @@ export const useAuthStore = defineStore("auth", () => {
return data;
};
const logout = async () => {
const logout = async (options: { rememberCurrentStudy?: boolean } = {}) => {
const studyStore = useStudyStore();
const sessionStore = useSessionStore();
const userKey = user.value?.email || localStorage.getItem(LAST_LOGIN_EMAIL_KEY) || "";
studyStore.rememberCurrentStudyForUser(userKey);
if (options.rememberCurrentStudy !== false) {
studyStore.rememberCurrentStudyForUser(userKey);
}
token.value = null;
user.value = null;
forceLogin.value = false;
+1 -1
View File
@@ -144,7 +144,7 @@ const checkHealth = async (baseUrl: string) => {
};
const clearSessionForServerChange = async () => {
await auth.logout();
await auth.logout({ rememberCurrentStudy: false });
studyStore.clearCurrentStudy();
};