Files
fangke/sdk/test/context.test.ts
T
浅念 18537c5dba fix(sdk): 设备检测顺序修订(iPad/Firefox 平板先判)+ FxiOS 前移
- device_type:iPad/Tablet 先于 Mobile/Android/iPhone 判(真实 iPad UA 恒含 Mobile 串、Firefox Android 平板显式 '; Tablet;',旧顺序误判手机)
- browser:Firefox/FxiOS 前移至 Safari 之前(iOS Firefox UA 尾部恒含 Safari/,旧顺序为死代码)
- 测试 23 过(翻转 iPad 锁定测试 + 新增 Android 平板、iOS Firefox 两条)

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

179 lines
7.0 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 串——规格修订:iPad/Tablet 先判,归"平板"
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";
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 判", () => {
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");
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;
}
});
});