68 lines
3.5 KiB
TypeScript
68 lines
3.5 KiB
TypeScript
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<ApiError>({ 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);
|
|
});
|
|
});
|