Files
fangke/sdk/test/context.test.ts
T

179 lines
7.0 KiB
TypeScript
Raw Normal View History

2026-09-15 15:48:17 +08:00
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 串——规格修订:iPad/Tablet 先判,归"平板"
2026-09-15 15:48:17 +08:00
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";
// Firefox Android 平板 UA:显式 "; Tablet;" 且含 Android 无 Mobile——旧顺序会误判手机
const UA_ANDROID_TABLET_FF =
"Mozilla/5.0 (Android 13; Tablet; rv:121.0) Gecko/121.0 Firefox/121.0";
// iOS Firefox UA:FxiOS + 尾部恒含 Safari/604.1——Firefox 检测须先于 Safari
const UA_IOS_FIREFOX =
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/121.0 Mobile/15E148 Safari/604.1";
2026-09-15 15:48:17 +08:00
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 串)→ 平板:规格修订 iPad/Tablet 先于 Mobile 判", () => {
2026-09-15 15:48:17 +08:00
stubUA(UA_IPAD_REAL);
const env = buildEnvContext();
expect(env.device_type).toBe("平板");
expect(env.os).toBe("iOS");
expect(env.browser).toBe("Safari");
});
it("Firefox Android 平板 UA(Android + '; Tablet;')→ 平板", () => {
stubUA(UA_ANDROID_TABLET_FF);
const env = buildEnvContext();
expect(env.device_type).toBe("平板");
expect(env.os).toBe("Android");
expect(env.browser).toBe("Firefox");
});
it("iOS Firefox UA(FxiOS + Safari 串)→ Firefox:FxiOS 先于 Safari 判", () => {
stubUA(UA_IOS_FIREFOX);
const env = buildEnvContext();
expect(env.browser).toBe("Firefox");
2026-09-15 15:48:17 +08:00
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;
}
});
});