Files
fangke/sdk/test/context.test.ts
T
浅念 c58e49bf5b feat(sdk): 身份与环境上下文模块
- identity.ts: 访客/会话 ID(30min 空闲轮换),localStorage 异常统一内存降级不冒泡(spec §4.5 红线)
- context.ts: buildEnvContext/parseUtm/sanitizeUrl/pageContext;参数黑名单与 server URL_PARAM_DROP_PATTERNS 同源(整键精确匹配、大小写不敏感、保留空值)
- 测试 21 过(含隐私模式降级、Edg 先于 Chrome、微信 UA 判定、iPad-Mobile 规格顺序锁定)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 15:48:17 +08:00

157 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, afterEach } from "vitest";
import { buildEnvContext, parseUtm, sanitizeUrl, pageContext } from "../src/context";
// userAgent 是 Navigator.prototype 上的 accessor;实例上 defineProperty 覆盖,delete 还原
function stubUA(ua: string) {
Object.defineProperty(navigator, "userAgent", { value: ua, configurable: true });
}
afterEach(() => {
delete (navigator as { userAgent?: string }).userAgent;
});
const UA_IPHONE_SAFARI =
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (Version/17.0 Mobile/15E148 Safari/604.1";
const UA_TABLET =
"Mozilla/5.0 (X11; Tablet; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36";
// 真实 iPad Safari UA 恒含 Mobile 串——按任务规格的判定顺序(Mobile 先于 iPad)
// 会落入"手机",此处锁定该规格行为,避免后人无意"修复"
const UA_IPAD_REAL =
"Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (Version/16.0 Mobile/15E148 Safari/604.1";
const UA_WIN_CHROME =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36";
const UA_WIN_EDGE =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0";
const UA_ANDROID_WECHAT =
"Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36 MicroMessenger/8.0.49";
describe("parseUtm", () => {
it("只保留 utm_ 前缀参数", () => {
expect(parseUtm("?utm_source=google&utm_medium=cpc&other=1&q=中文")).toEqual({
utm_source: "google",
utm_medium: "cpc",
});
});
it("键值各截 100 字符", () => {
const long = "x".repeat(150);
const out = parseUtm(`?utm_${long}=${long}`);
expect(Object.keys(out)).toHaveLength(1);
const key = Object.keys(out)[0]!;
expect(key).toHaveLength(100);
expect(out[key]).toHaveLength(100);
});
it("无 utm 参数时返回空对象", () => {
expect(parseUtm("?a=1&b=2")).toEqual({});
});
});
describe("sanitizeUrl", () => {
it("删除黑名单参数,保留其余(含空值),不误伤 monkey", () => {
expect(sanitizeUrl("/p?email=a@b.com&token=x&id=3&utm_source=g&monkey=1")).toBe(
"/p?id=3&utm_source=g&monkey=1",
);
});
it("空值参数保留", () => {
expect(sanitizeUrl("/p?empty=&id=1")).toBe("/p?empty=&id=1");
});
it("黑名单整键精确匹配(大小写不敏感),子串参数不删", () => {
expect(sanitizeUrl("/p?Token=1&email=x@y.z&monkey=1&utoken=2")).toBe("/p?monkey=1&utoken=2");
});
it("全部被删时不残留 ?,无查询串时原样返回", () => {
expect(sanitizeUrl("/p?token=1&password=2")).toBe("/p");
expect(sanitizeUrl("/p/plain")).toBe("/p/plain");
});
});
describe("buildEnvContext", () => {
it("happy-dom 默认 UA:含全部必需键且 device_type 合法", () => {
// 默认 UA: Mozilla/5.0 (X11; Win32 x64) AppleWebKit/537.36 ... HappyDOM/0.0.0
const env = buildEnvContext();
expect(env.device_type).toBe("PC"); // 默认 UA 无 Mobile/Android/iPhone/iPad/Tablet
expect(["PC", "手机", "平板"]).toContain(env.device_type);
expect(typeof env.os).toBe("string");
expect(typeof env.browser).toBe("string");
expect(typeof env.language).toBe("string");
expect(env.language).toBe(navigator.language);
expect(typeof env.timezone).toBe("string");
expect(env.screen).toBe(`${screen.width}x${screen.height}`);
expect(typeof env.hardware_concurrency).toBe("number"); // happy-dom 提供
expect("connection" in env).toBe(false); // happy-dom 无 connection
});
it("iPhone UA → 手机 + iOS + Safari", () => {
stubUA(UA_IPHONE_SAFARI);
const env = buildEnvContext();
expect(env.device_type).toBe("手机");
expect(env.os).toBe("iOS");
expect(env.browser).toBe("Safari");
});
it("Tablet UA(无 Mobile/Android/iPhone 串)→ 平板", () => {
stubUA(UA_TABLET);
expect(buildEnvContext().device_type).toBe("平板");
});
it("真实 iPad UA(含 Mobile 串)→ 手机:规格判定顺序 Mobile 先于 iPad", () => {
stubUA(UA_IPAD_REAL);
const env = buildEnvContext();
expect(env.device_type).toBe("手机");
expect(env.os).toBe("iOS");
});
it("Windows Chrome UA → PC + Windows + Chrome", () => {
stubUA(UA_WIN_CHROME);
const env = buildEnvContext();
expect(env.device_type).toBe("PC");
expect(env.os).toBe("Windows");
expect(env.browser).toBe("Chrome");
});
it("Edg UA(内嵌 Chrome 串)→ browser 为 Edge,验证 Edg 先于 Chrome", () => {
stubUA(UA_WIN_EDGE);
const env = buildEnvContext();
expect(env.browser).toBe("Edge");
expect(env.os).toBe("Windows");
});
it("微信 UA(内嵌 Chrome/Safari 串)→ browser 为 WeChat", () => {
stubUA(UA_ANDROID_WECHAT);
const env = buildEnvContext();
expect(env.browser).toBe("WeChat");
expect(env.device_type).toBe("手机");
expect(env.os).toBe("Android");
});
});
describe("pageContext", () => {
it("path 清洗 + utm 提取 + title 读取", async () => {
await (window as any).happyDOM.setURL("http://localhost/p?token=1&utm_source=g");
document.title = "测试页";
const ctx = pageContext();
expect(ctx.path).toBe("/p?utm_source=g"); // token 已删,utm_source 保留
expect(ctx.title).toBe("测试页");
expect(ctx.utm).toEqual({ utm_source: "g" });
expect(ctx.referrer).toBe("");
expect(ctx.referrer_domain).toBe("");
});
it("referrer_domain 提取 hostname,非法/空 referrer 降级空串", () => {
const stub = (v: string) =>
Object.defineProperty(document, "referrer", { value: v, configurable: true });
try {
stub("https://www.google.com/search?q=x");
expect(pageContext().referrer_domain).toBe("www.google.com");
stub("not a url");
expect(pageContext().referrer_domain).toBe("");
stub("");
expect(pageContext().referrer_domain).toBe("");
} finally {
delete (document as { referrer?: string }).referrer;
}
});
});