import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { getVisitorId, getSessionId, touchActivity } from "../src/identity"; describe("identity", () => { beforeEach(() => { localStorage.clear(); vi.useFakeTimers(); }); afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); }); it("visitor_id 持久化且格式正确", () => { const v = getVisitorId(); expect(v).toBe(getVisitorId()); expect(v).toMatch(/^[0-9a-f-]{36}$/); }); it("30 分钟无活动切新会话;窗口内 touch 续期", () => { const s1 = getSessionId(); vi.advanceTimersByTime(10 * 60_000); touchActivity(); vi.advanceTimersByTime(10 * 60_000); expect(getSessionId()).toBe(s1); // 20min 内有活动 → 同会话 vi.advanceTimersByTime(31 * 60_000); // 再过 31min 无活动 expect(getSessionId()).not.toBe(s1); }); it("localStorage 抛异常时内存降级不冒泡且功能可用", () => { // 探测确认:vitest happy-dom 环境下 window === globalThis,且 localStorage // 是 window 上的自有可配置 accessor——spy 其 getter 即可让模块内裸 // localStorage 引用在访问时抛异常(等效隐私模式/禁存储)。 const spy = vi.spyOn(window, "localStorage", "get").mockImplementation(() => { throw new DOMException("denied"); }); const v = getVisitorId(); expect(v).toMatch(/^[0-9a-f-]{36}$/); const s = getSessionId(); expect(s).toMatch(/^[0-9a-f-]{36}$/); expect(getSessionId()).toBe(s); // 纯内存降级下会话仍连续 expect(() => touchActivity()).not.toThrow(); // 还原后(内存态无法迁移到 localStorage 是预期)混用两套存储不抛 spy.mockRestore(); expect(() => getSessionId()).not.toThrow(); }); });