Files
TallyNote/tests/web-foundation.test.ts
T
Qiufeng 12495fb6a4
TallyNote release / linux-x64 (push) Successful in 6m24s
release: 1.1.0
2026-08-31 20:11:34 +08:00

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);
});
});