import { describe, expect, it, vi } from "vitest"; import { ApiError, api } from "../web-next/src/services/api"; import { DEFAULT_ROUTE_ID, routeIdFromPath, routePath } from "../web-next/src/router"; import sessionReducer, { bootstrapSession, isSessionBootstrapping, loginThunk } from "../web-next/src/store/sessionSlice"; import type { Admin } from "../web-next/src/types/auth"; describe("前端基础契约", () => { it("将页面路由映射为稳定的 URL,并容忍尾斜杠", () => { expect(routePath(DEFAULT_ROUTE_ID)).toBe("/"); expect(routeIdFromPath("/")).toBe("dashboard"); expect(routeIdFromPath("/dashboard")).toBe("dashboard"); expect(routeIdFromPath("/expenses/")).toBe("expenses"); expect(routeIdFromPath("/unknown")).toBeUndefined(); }); it("请求超时会中止并返回可识别的错误", async () => { vi.stubGlobal("fetch", vi.fn((_url: string, init?: RequestInit) => new Promise((_resolve, reject) => { init?.signal?.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")), { once: true }); }))); await expect(api("/api/slow", { timeoutMs: 10 })).rejects.toMatchObject({ status: 408, code: "REQUEST_TIMEOUT" }); vi.unstubAllGlobals(); }); it("忽略过期的会话初始化结果,避免覆盖较新的认证状态", () => { const admin: Admin = { id: "11111111-1111-4111-8111-111111111111", username: "admin", displayName: "管理员", status: "active", mustChangePassword: false, version: 1, createdAt: 1, lastLoginAt: null, disabledAt: null, }; let state = sessionReducer(undefined, bootstrapSession.pending("older", undefined)); state = sessionReducer(state, bootstrapSession.pending("newer", undefined)); state = sessionReducer(state, bootstrapSession.fulfilled({ initialized: true, timezone: "Asia/Shanghai", admin: null }, "older", undefined)); expect(state.status).toBe("loading"); expect(state.bootstrapRequestId).toBe("newer"); state = sessionReducer(state, loginThunk.fulfilled(admin, "login", { username: "admin", password: "test" })); expect(state.status).toBe("authenticated"); expect(state.admin?.id).toBe(admin.id); state = sessionReducer(state, bootstrapSession.fulfilled({ initialized: true, timezone: "Asia/Shanghai", admin: null }, "newer", undefined)); expect(state.status).toBe("authenticated"); expect(state.admin?.id).toBe(admin.id); }); it("登录提交时保留登录页面,不误显示启动连接占位", () => { let state = sessionReducer(undefined, bootstrapSession.pending("boot", undefined)); expect(isSessionBootstrapping(state)).toBe(true); state = sessionReducer(state, bootstrapSession.fulfilled({ initialized: true, timezone: "Asia/Shanghai", admin: null }, "boot", undefined)); state = sessionReducer(state, loginThunk.pending("login", { username: "admin", password: "test" })); expect(state.status).toBe("loading"); expect(state.initialized).toBe(true); expect(state.bootstrapRequestId).toBeNull(); expect(isSessionBootstrapping(state)).toBe(false); }); it("启动连接失败时显示可恢复的错误状态", () => { let state = sessionReducer(undefined, bootstrapSession.pending("boot", undefined)); state = sessionReducer(state, bootstrapSession.rejected(new Error("服务不可用"), "boot", undefined)); expect(state.status).toBe("error"); expect(state.initialized).toBeNull(); expect(isSessionBootstrapping(state)).toBe(false); }); });