Files
fangke/sdk/test/business.test.ts
T
2026-09-18 23:47:44 +08:00

576 lines
24 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, beforeEach, afterEach, vi } from "vitest";
import { Transport } from "../src/transport";
import { mergeConfig } from "../src/config";
import { startPageSession, getPageSession, onSegmentChange } from "../src/page";
import type { PageSession } from "../src/page";
import { startBusinessTracking } from "../src/business";
// business.ts(spec §1.1-§1.3 / §1.5):contact_click(链接协议前缀 + beacon flush)、
// CF7 全过程(form_start 去重 + _ctk_sid 空占位 / submit capture 生成 submission_id /
// wpcf7statuschanged 终态映射)、product_view(路径段匹配 + onSegmentChange 切段)。
// 事件属性恰合服务端白名单(constants.py EVENT_PROP_WHITELIST + _biz_props_ok 值域:
// channel 枚举 / form_id cf7-N / submission_id uuid4 小写 / product_id slug≤100)。
// 注意:startPageSession/startBusinessTracking 安装的监听与 history 包装跨用例存留
// (模块级 hooksInstalled),旧用例监听把事件推入旧 transport(信封 site 不同——按
// envelope site_id 过滤免疫);每用例独立 siteId(沿 page.test.ts / stay.test.ts 模式)。
// isTrusted:happy-dom 合成事件默认 false——dispatch 前 defineProperty 绕过(同
// page.test.ts persisted 模式),并保留 isTrusted=false 不触发的守护断言。
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const UUID4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
let fetchMock: ReturnType<typeof vi.fn>;
let beacon: ReturnType<typeof vi.fn>;
function stubNavigator() {
beacon = vi.fn().mockReturnValue(true);
vi.stubGlobal("navigator", {
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
language: "zh-CN",
sendBeacon: beacon,
});
}
function mkTransport(siteId: string) {
// flushMs 拉大到 24h:业务事件只入队,flush 全部由用例显式驱动(beacon 除外)
return new Transport(
mergeConfig({ siteId, endpoint: "/api/collect/batch", batchSize: 50, flushMs: 86_400_000 })
);
}
/** fetch 路径(显式 t.flush())事件汇总(按信封 site_id 过滤,免疫旧 transport 串扰) */
function fetchedEvents(siteId: string) {
const out: Record<string, any>[] = [];
for (const call of fetchMock.mock.calls as unknown as [string, RequestInit][]) {
const body = JSON.parse(call[1].body as string);
if (body.site_id === siteId) out.push(...body.events);
}
return out;
}
/** beacon 路径(contact_click 的 flush(true))事件汇总 */
async function beaconEvents(siteId: string) {
const out: Record<string, any>[] = [];
for (const [, blob] of beacon.mock.calls as unknown as [string, Blob][]) {
const body = JSON.parse(await blob.text());
if (body.site_id === siteId) out.push(...body.events);
}
return out;
}
/** URL 重置:走原型方法绕开 page.ts 的 pushState 包装——不触发切段杂事件 */
function setUrl(url: string) {
const proto = Object.getPrototypeOf(window.history) as {
replaceState: (data: unknown, unused: string, url?: string | URL | null) => void;
};
proto.replaceState.call(window.history, {}, "", url);
}
function mkLink(href: string) {
const a = document.createElement("a");
a.href = href;
document.body.appendChild(a);
return a;
}
function mkForm(opts: { action?: string; id?: string } = {}) {
const form = document.createElement("form");
if (opts.action !== undefined) form.setAttribute("action", opts.action);
if (opts.id !== undefined) form.id = opts.id;
form.className = "wpcf7-form init"; // CF7 真实形态(class 含 wpcf7-form)
const input = document.createElement("input");
input.name = "your-name";
form.appendChild(input);
document.body.appendChild(form);
return form;
}
function ctkSidField(form: HTMLFormElement) {
return form.querySelector('input[name="_ctk_sid"]') as HTMLInputElement | null;
}
function click(el: Element) {
el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
}
/** isTrusted=true 的合成事件(happy-dom 默认 false) */
function trusted(type: string, el: Element) {
const ev = new Event(type, { bubbles: true });
Object.defineProperty(ev, "isTrusted", { value: true });
el.dispatchEvent(ev);
}
function submit(form: HTMLFormElement) {
form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
}
/** CF7 6.x 状态事件(bubbles:true + detail.status——线上 6.1.6 源码实测形态) */
function cf7status(form: HTMLFormElement, status: string) {
form.dispatchEvent(
new CustomEvent("wpcf7statuschanged", { detail: { status }, bubbles: true })
);
}
describe("business / startBusinessTracking", () => {
beforeEach(() => {
localStorage.clear();
vi.useFakeTimers();
fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200 } as Response);
vi.stubGlobal("fetch", fetchMock);
stubNavigator();
});
afterEach(() => {
vi.unstubAllGlobals();
vi.useRealTimers();
document.body.innerHTML = ""; // 清掉本用例挂的链接/表单
});
// 必须为本文件首例:startBusinessTracking 先于 startPageSession(异常接线顺序)
// ——getPageSession() 为 null 时无从归属,静默不发。依赖模块级 current 尚为 null。
it("未启动 PageSession 时静默不发(接线顺序守卫)", () => {
const t = mkTransport("biz-nops");
startBusinessTracking(t);
click(mkLink("https://wa.me/8613800138000"));
const form = mkForm({ action: "#wpcf7-f156-p20-o1" });
trusted("focusin", form.querySelector('input[name="your-name"]')!);
submit(form);
cf7status(form, "mail_sent");
t.flush();
expect(fetchedEvents("biz-nops")).toHaveLength(0);
});
// ---- page.ts onSegmentChange(spec §1.3:触发点钉死在 onRouteChange 新段创建后) ----
it("onSegmentChange:切段后以新段回调;初始 startPageSession/同 path 不触发;回调 throw 不冒泡不阻断后续", () => {
setUrl("/seg-a");
const t = mkTransport("biz-seg");
startPageSession(t);
const seen: PageSession[] = [];
onSegmentChange(() => {
throw new Error("listener boom"); // 先注册:异常不得阻断后续回调
});
onSegmentChange((ps) => seen.push(ps));
startBusinessTracking(t); // business 也注册段回调(产品页判定)
expect(seen).toEqual([]); // 初始 startPageSession 不触发——init 判定由消费方自理
window.history.pushState({}, "", "/seg-a?x=1"); // path 未变(仅 query)→ 不切
expect(seen).toEqual([]);
expect(() => window.history.pushState({}, "", "/seg-b")).not.toThrow(); // thrower 不冒泡
expect(seen).toHaveLength(1);
expect(seen[0]).toBe(getPageSession()); // 回调收到的是新段(创建之后)
expect(seen[0]!.path).toBe("/seg-b");
});
// ---- contact_click(spec §1.1) ----
it("contact_click:wa.me/api.whatsapp.com/whatsapp:// 三形态(含 http)→ whatsapp + flush(true) beacon", async () => {
setUrl("/cc-wa");
const t = mkTransport("biz-cc-wa");
startPageSession(t);
startBusinessTracking(t);
click(mkLink("https://wa.me/8613800138000"));
click(mkLink("https://api.whatsapp.com/send?phone=8613800138000"));
click(mkLink("whatsapp://send?phone=8613800138000"));
click(mkLink("http://wa.me/8613800138000")); // https? 两形态
const ccs = (await beaconEvents("biz-cc-wa")).filter((e) => e.event_name === "contact_click");
expect(ccs).toHaveLength(4); // flush(true) 被调——beacon 路径送达(非 fetch)
expect(ccs.map((e) => e.properties)).toEqual(
Array(4).fill({ channel: "whatsapp" }) // 属性恰为白名单单键
);
expect(ccs[0].event_id).toMatch(UUID_RE);
expect(ccs[0].occurred_at).toMatch(/^\d{4}-\d{2}-\d{2}T/);
expect(ccs[0].path).toBe("/cc-wa");
expect(ccs[0].title).toBe(document.title);
// 顶层归属完整(events 形态 + Attribution 展开,同 stay.ts)
expect(ccs[0].site_id).toBe("biz-cc-wa");
expect(ccs[0].visitor_id).toMatch(UUID_RE);
expect(ccs[0].session_id).toMatch(UUID_RE);
expect(ccs[0].page_id).toBe(getPageSession()!.pageId);
expect(typeof ccs[0].seq).toBe("number");
expect(ccs[0].purpose).toBe(null);
expect(ccs[0].epoch).toBe(null);
expect(ccs[0].grant_ref).toBe(null);
});
it("contact_click:tel:/mailto:(大小写不敏感)→ phone/email", async () => {
setUrl("/cc-tel");
const t = mkTransport("biz-cc-tel");
startPageSession(t);
startBusinessTracking(t);
click(mkLink("tel:+8613800138000"));
click(mkLink("mailto:sales@example.com"));
click(mkLink("MAILTO:sales@example.com")); // 大小写不敏感
const ccs = (await beaconEvents("biz-cc-tel")).filter((e) => e.event_name === "contact_click");
expect(ccs.map((e) => e.properties)).toEqual([
{ channel: "phone" },
{ channel: "email" },
{ channel: "email" },
]);
});
it("contact_click:非链接点击/普通 http 链接/相对链接不触发", async () => {
setUrl("/cc-none");
const t = mkTransport("biz-cc-none");
startPageSession(t);
startBusinessTracking(t);
t.flush(); // 先排空 page_view(此后 beacon 只可能由 contact_click 的 flush 触发)
const div = document.createElement("div");
document.body.appendChild(div);
click(div);
click(mkLink("https://example.com/some-page"));
click(mkLink("/relative/page"));
t.flush();
expect(fetchedEvents("biz-cc-none").some((e) => e.event_name === "contact_click")).toBe(false);
expect((await beaconEvents("biz-cc-none")).some((e) => e.event_name === "contact_click")).toBe(false);
});
it("contact_click:链接内子元素点击经 closest 委托命中", async () => {
setUrl("/cc-child");
const t = mkTransport("biz-cc-child");
startPageSession(t);
startBusinessTracking(t);
const a = mkLink("https://wa.me/8613800138000");
const span = document.createElement("span");
a.appendChild(span);
click(span); // e.target 是 span——closest("a[href]") 委托
const ccs = (await beaconEvents("biz-cc-child")).filter((e) => e.event_name === "contact_click");
expect(ccs).toHaveLength(1);
expect(ccs[0].properties).toEqual({ channel: "whatsapp" });
});
// ---- form_*(spec §1.2 / §1.5,CF7 6.x 状态机) ----
it("form_start:isTrusted focusin → 事件 + _ctk_sid 空占位;合成 focus 不触发;同表单第二次不重发", () => {
setUrl("/fs");
const t = mkTransport("biz-fs");
startPageSession(t);
startBusinessTracking(t);
const form = mkForm({ action: "#wpcf7-f156-p20-o1" });
const input = form.querySelector('input[name="your-name"]')!;
const plain = document.createElement("form"); // 非 CF7 表单:不触发
const plainInput = document.createElement("input");
plain.appendChild(plainInput);
document.body.appendChild(plain);
trusted("focusin", plainInput); // 非 .wpcf7-form:无事件
input.dispatchEvent(new Event("focusin", { bubbles: true })); // isTrusted=false:不触发
t.flush();
expect(fetchedEvents("biz-fs").some((e) => e.event_name === "form_start")).toBe(false);
trusted("focusin", input); // 首次真实聚焦
t.flush();
const fss = fetchedEvents("biz-fs").filter((e) => e.event_name === "form_start");
expect(fss).toHaveLength(1);
expect(fss[0].properties).toEqual({ form_id: "cf7-156" }); // 属性恰为白名单单键
expect(fss[0].page_id).toBe(getPageSession()!.pageId);
expect(fss[0].site_id).toBe("biz-fs");
const field = ctkSidField(form);
expect(field).toBeTruthy(); // _ctk_sid 空占位已注入(DOM API)
expect(field!.type).toBe("hidden");
expect(field!.value).toBe("");
trusted("focusin", input); // 同表单第二次:WeakSet 去重
const input2 = document.createElement("input");
input2.name = "your-email";
form.appendChild(input2);
trusted("focusin", input2); // 同表单另一字段:仍是同一 form 实例
t.flush();
expect(fetchedEvents("biz-fs").filter((e) => e.event_name === "form_start")).toHaveLength(1);
});
it("form_start:isTrusted input 事件同触发(focusin 缺席路径)", () => {
setUrl("/fs-input");
const t = mkTransport("biz-fs-input");
startPageSession(t);
startBusinessTracking(t);
const form = mkForm({ action: "#wpcf7-f156-p20-o1" });
trusted("input", form.querySelector('input[name="your-name"]')!);
t.flush();
const fss = fetchedEvents("biz-fs-input").filter((e) => e.event_name === "form_start");
expect(fss).toHaveLength(1);
expect(fss[0].properties).toEqual({ form_id: "cf7-156" });
expect(ctkSidField(form)).toBeTruthy();
});
it("form_submit:原生 submit → submission_id 生成 + _ctk_sid 值更新 + 事件引用同 ID;两次提交两个不同 ID", () => {
setUrl("/fsub");
const t = mkTransport("biz-fsub");
startPageSession(t);
startBusinessTracking(t);
const form = mkForm({ action: "#wpcf7-f156-p20-o1" });
trusted("focusin", form.querySelector('input[name="your-name"]')!); // 先 start 注入占位
submit(form);
t.flush();
let subs = fetchedEvents("biz-fsub").filter((e) => e.event_name === "form_submit");
expect(subs).toHaveLength(1);
expect(subs[0].properties.form_id).toBe("cf7-156");
const sid1 = subs[0].properties.submission_id;
expect(sid1).toMatch(UUID4_RE); // uuid4 小写(服务端 _SUBMIT_RE)
expect(ctkSidField(form)!.value).toBe(sid1); // 隐藏字段值 === 事件引用的 ID
expect(Object.keys(subs[0].properties).sort()).toEqual(["form_id", "submission_id"]);
const form2 = mkForm({ action: "#wpcf7-f156-p20-o1" }); // 未经 form_start 直接 submit:找/建字段
submit(form2);
submit(form); // 同表单第二次提交:新 submission_id
t.flush();
subs = fetchedEvents("biz-fsub").filter((e) => e.event_name === "form_submit");
expect(subs).toHaveLength(3);
const sid2 = subs[2].properties.submission_id;
expect(sid2).toMatch(UUID4_RE);
expect(sid2).not.toBe(sid1); // 每次尝试独立
expect(ctkSidField(form)!.value).toBe(sid2); // 字段随最近一次提交更新
expect(ctkSidField(form2)!.value).toMatch(UUID4_RE); // submit 时自建字段并填值
});
it("form_error:invalid/unaccepted→invalid、spam→spam;field_count 数 .wpcf7-not-valid-tip;引用最近 submission_id;中间态无事件", () => {
setUrl("/ferr");
const t = mkTransport("biz-ferr");
startPageSession(t);
startBusinessTracking(t);
const form = mkForm({ action: "#wpcf7-f156-p20-o1" });
cf7status(form, "invalid"); // 无前置 submit → submission_id 空串(服务端允许空)
t.flush();
let errs = fetchedEvents("biz-ferr").filter((e) => e.event_name === "form_error");
expect(errs).toHaveLength(1);
expect(errs[0].properties).toEqual({
form_id: "cf7-156",
submission_id: "",
error_code: "invalid",
field_count: 0,
});
submit(form); // 生成当次 submission_id
form.insertAdjacentHTML(
"beforeend",
'<span class="wpcf7-not-valid-tip">e1</span><span class="wpcf7-not-valid-tip">e2</span>'
);
cf7status(form, "unaccepted"); // spec §1.2 映射 → invalid(勿漏)
t.flush();
errs = fetchedEvents("biz-ferr").filter((e) => e.event_name === "form_error");
expect(errs).toHaveLength(2);
expect(errs[1].properties.error_code).toBe("invalid");
expect(errs[1].properties.field_count).toBe(2); // 从 DOM .wpcf7-not-valid-tip 计数
expect(errs[1].properties.submission_id).toBe(ctkSidField(form)!.value); // 引用最近 submission_id
expect(errs[1].properties.submission_id).toMatch(UUID4_RE);
cf7status(form, "spam");
t.flush();
errs = fetchedEvents("biz-ferr").filter((e) => e.event_name === "form_error");
expect(errs).toHaveLength(3);
expect(errs[2].properties).toEqual({
form_id: "cf7-156",
submission_id: errs[1].properties.submission_id,
error_code: "spam",
field_count: 2,
});
for (const s of ["init", "validating", "sending", "resetting"]) cf7status(form, s);
t.flush();
const all = fetchedEvents("biz-ferr");
expect(all.filter((e) => e.event_name === "form_error")).toHaveLength(3); // 中间态不产生事件
expect(all.filter((e) => e.event_name === "form_submit_result")).toHaveLength(0);
});
it("form_submit_result:mail_sent/mail_failed → result_code;引用最近 submission_id", () => {
setUrl("/fres");
const t = mkTransport("biz-fres");
startPageSession(t);
startBusinessTracking(t);
const form = mkForm({ action: "#wpcf7-f156-p20-o1" });
submit(form);
const sid1 = ctkSidField(form)!.value;
cf7status(form, "mail_sent");
submit(form); // 第二次尝试 → 新 ID
const sid2 = ctkSidField(form)!.value;
expect(sid2).not.toBe(sid1);
cf7status(form, "mail_failed");
t.flush();
const rs = fetchedEvents("biz-fres").filter((e) => e.event_name === "form_submit_result");
expect(rs).toHaveLength(2);
expect(rs[0].properties).toEqual({
form_id: "cf7-156",
submission_id: sid1,
result_code: "mail_sent",
});
expect(rs[1].properties).toEqual({
form_id: "cf7-156",
submission_id: sid2,
result_code: "mail_failed",
});
});
it("form_id 规整:action #wpcf7-fN-pM-oK → cf7-N;action 解析不出走 form.id;都失败 → cf7-0", () => {
setUrl("/fid");
const t = mkTransport("biz-fid");
startPageSession(t);
startBusinessTracking(t);
const f1 = mkForm({ action: "#wpcf7-f156-p20-o1" });
trusted("focusin", f1.querySelector("input")!);
const f2 = mkForm({ action: "/contact-us/", id: "wpcf7-f42-o1" }); // id 兜底路径
trusted("focusin", f2.querySelector("input")!);
const f3 = mkForm({ action: "/contact-us/" }); // 无 id:解析不出
trusted("focusin", f3.querySelector("input")!);
t.flush();
const ids = fetchedEvents("biz-fid")
.filter((e) => e.event_name === "form_start")
.map((e) => e.properties.form_id);
expect(ids).toEqual(["cf7-156", "cf7-42", "cf7-0"]);
});
it("form_id 回归(T4 审查遗留):11 位 ID 不截断匹配 → cf7-0(服务端 cf7-N 上限 10 位)", () => {
setUrl("/fid11");
const t = mkTransport("biz-fid11");
startPageSession(t);
startBusinessTracking(t);
const form = mkForm({ action: "#wpcf7-f12345678901" });
trusted("focusin", form.querySelector("input")!);
t.flush();
const fss = fetchedEvents("biz-fid11").filter((e) => e.event_name === "form_start");
expect(fss).toHaveLength(1);
expect(fss[0].properties.form_id).toBe("cf7-0"); // (?!\d) 拒绝截断——按解析失败归 cf7-0
});
// ---- product_view(spec §1.3:非锚定段匹配 + onSegmentChange 切段) ----
it("product_view:init 时 /product/slug/ 判定 → inferred=true rule_version=v1 + 顶层归属", () => {
setUrl("/product/hydraulic-press-100t/");
const t = mkTransport("biz-pv-init");
const ps = startPageSession(t);
startBusinessTracking(t);
t.flush();
const pvs = fetchedEvents("biz-pv-init").filter((e) => e.event_name === "product_view");
expect(pvs).toHaveLength(1);
const pv = pvs[0];
expect(pv.properties).toEqual({
product_id: "hydraulic-press-100t",
inferred: true,
rule_version: "v1",
});
expect(pv.event_id).toMatch(UUID_RE);
expect(pv.occurred_at).toMatch(/^\d{4}-\d{2}-\d{2}T/);
expect(pv.path).toBe("/product/hydraulic-press-100t/");
expect(pv.title).toBe(document.title);
expect(pv.site_id).toBe("biz-pv-init");
expect(pv.visitor_id).toMatch(UUID_RE);
expect(pv.session_id).toMatch(UUID_RE);
expect(pv.page_id).toBe(ps.pageId);
expect(typeof pv.seq).toBe("number");
expect(pv.purpose).toBe(null); // V1.1 预留从快照展开
expect(pv.epoch).toBe(null);
expect(pv.grant_ref).toBe(null);
});
it("product_view:/de/product/slug/ 多语言前缀命中(非锚定段匹配);slug 截断 ≤100", () => {
setUrl("/de/product/flachbett-drucker/");
let t = mkTransport("biz-pv-de");
startPageSession(t);
startBusinessTracking(t);
t.flush();
let pvs = fetchedEvents("biz-pv-de").filter((e) => e.event_name === "product_view");
expect(pvs).toHaveLength(1);
expect(pvs[0].properties.product_id).toBe("flachbett-drucker");
setUrl("/product/" + "a".repeat(150) + "/"); // 超 100 截断(对齐服务端 _PROD_RE 上限)
t = mkTransport("biz-pv-long");
startPageSession(t);
startBusinessTracking(t);
t.flush();
pvs = fetchedEvents("biz-pv-long").filter((e) => e.event_name === "product_view");
expect(pvs).toHaveLength(1);
expect(pvs[0].properties.product_id).toHaveLength(100);
});
it("product_view:/product-category/suv/、/products/x/、裸 /product/ 不触发(段匹配精确性)", () => {
setUrl("/product-category/suv/");
let t = mkTransport("biz-pv-cat");
startPageSession(t);
startBusinessTracking(t);
setUrl("/products/x/"); // 复数段 ≠ "product"(精确段匹配)
t = mkTransport("biz-pv-plural");
startPageSession(t);
startBusinessTracking(t);
setUrl("/product/"); // "product" 后无 slug 段
t = mkTransport("biz-pv-bare");
startPageSession(t);
startBusinessTracking(t);
setUrl("/product/%E4%B8%AD%E6%96%87/"); // 百分号编码 slug:服务端 _PROD_RE 必拒——客户端即不发
t = mkTransport("biz-pv-enc");
startPageSession(t);
startBusinessTracking(t);
t.flush();
expect(fetchedEvents("biz-pv-cat").some((e) => e.event_name === "product_view")).toBe(false);
expect(fetchedEvents("biz-pv-plural").some((e) => e.event_name === "product_view")).toBe(false);
expect(fetchedEvents("biz-pv-bare").some((e) => e.event_name === "product_view")).toBe(false);
expect(fetchedEvents("biz-pv-enc").some((e) => e.event_name === "product_view")).toBe(false);
});
it("form_error:field_count 钳制上限 99(服务端 0-99 契约)", () => {
setUrl("/ferr-cap");
const t = mkTransport("biz-ferr-cap");
startPageSession(t);
startBusinessTracking(t);
const form = mkForm({ action: "#wpcf7-f156-p20-o1" });
for (let i = 0; i < 105; i++) {
const tip = document.createElement("span");
tip.className = "wpcf7-not-valid-tip";
form.appendChild(tip);
}
cf7status(form, "invalid");
t.flush();
const errs = fetchedEvents("biz-ferr-cap").filter((e) => e.event_name === "form_error");
expect(errs).toHaveLength(1);
expect(errs[0].properties.field_count).toBe(99); // 105 → 钳 99,不落服务端拒绝区
});
it("product_view:onSegmentChange 切段到产品页 → 事件带新 page_id;切段到非产品页不发", () => {
setUrl("/pv-spa-home");
const t = mkTransport("biz-pv-spa");
const ps1 = startPageSession(t);
startBusinessTracking(t);
t.flush();
expect(fetchedEvents("biz-pv-spa").some((e) => e.event_name === "product_view")).toBe(false);
window.history.pushState({}, "", "/pv-spa-mid/"); // 切段到非产品页
t.flush();
expect(fetchedEvents("biz-pv-spa").some((e) => e.event_name === "product_view")).toBe(false);
window.history.pushState({}, "", "/ar/product/suv-ar/"); // 切段到产品页(阿语前缀)
const ps2 = getPageSession()!;
expect(ps2).not.toBe(ps1);
t.flush();
const pvs = fetchedEvents("biz-pv-spa").filter((e) => e.event_name === "product_view");
expect(pvs).toHaveLength(1);
expect(pvs[0].properties).toEqual({ product_id: "suv-ar", inferred: true, rule_version: "v1" });
expect(pvs[0].page_id).toBe(ps2.pageId); // 事件带新 page_id
expect(pvs[0].page_id).not.toBe(ps1.pageId);
});
// ---- 红线(spec §4.5):监听器内异常不冒泡宿主 ----
it("红线:emit 内部异常(t.push 抛错)不冒泡——click/focusin/submit/status 各路径", () => {
setUrl("/red");
const t = mkTransport("biz-red");
startPageSession(t);
startBusinessTracking(t);
vi.spyOn(t, "push").mockImplementation(() => {
throw new Error("push boom");
});
const form = mkForm({ action: "#wpcf7-f156-p20-o1" });
expect(() => click(mkLink("https://wa.me/8613800138000"))).not.toThrow();
expect(() => trusted("focusin", form.querySelector("input")!)).not.toThrow();
expect(() => submit(form)).not.toThrow();
expect(() => cf7status(form, "mail_sent")).not.toThrow();
expect(() => window.history.pushState({}, "", "/red-next")).not.toThrow(); // 段回调 emit 异常同静默
});
});