样式修改

This commit is contained in:
谢宇宁
2026-10-16 14:10:10 +07:00
parent d2ddb1c321
commit 4afd5f9143
42 changed files with 89 additions and 1765 deletions
+3 -15
View File
@@ -26,7 +26,7 @@ try {
await context.route("**/*", async (route) => {
const request = route.request();
const local = new URL(request.url()).origin === origin;
// Exercise UI without recording synthetic analytics, purchases or user actions.
// Exercise UI without recording synthetic purchases or user actions.
if (
!local &&
(request.method() !== "GET" || request.resourceType() === "script")
@@ -55,12 +55,6 @@ try {
timeout: 40000,
});
await page.waitForTimeout(500);
if (path !== "/discover/search/")
await page.waitForFunction(
() => typeof (window as unknown as Record<string, unknown>).tracker === "object",
null,
{ timeout: 15000 },
);
for (let index = 0; index < 12; index++) {
const close = page.locator(
"[data-dialog-ad].hk-is-open [data-dialog-ad-item]:not([hidden]) [data-dialog-ad-close]",
@@ -75,9 +69,6 @@ try {
legacyScripts: Array.from(document.scripts).some((script) =>
/\/static\/js\//.test(script.src),
),
tracker:
typeof (window as unknown as Record<string, unknown>).tracker ===
"object",
}));
results.push({
device: name,
@@ -91,11 +82,8 @@ try {
throw new Error(
`${name} ${path} overflows horizontally: ${layout.document}`,
);
if (
layout.legacyScripts ||
(path !== "/discover/search/" && !layout.tracker)
)
throw new Error(`${name} ${path} missing shared client runtime`);
if (layout.legacyScripts)
throw new Error(`${name} ${path} loaded legacy business scripts`);
if (path === "/") {
const accountEntries = await page.locator(
"[data-login-open], [data-register-open], .hk-user-menu, a[href^='/account']",
+1 -3
View File
@@ -146,10 +146,8 @@ try {
page.on("pageerror", (error) => errors.push(error.message));
await context.addInitScript({
content: `
window.__copies=[];window.__events=[];window.__navigation='';
window.__copies=[];window.__navigation='';
Object.defineProperty(navigator,'clipboard',{configurable:true,value:{writeText:async function(value){window.__copies.push(value)}}});
window.tracker={getSDKCommonHeader:function(){return 'mock-common'},getCommonFields:function(){return {device_id:'mock-device',device:'OTHER',sid:'mock-sid'}}};
window.WebSDK={track:new Proxy({},{get:function(_,method){return function(payload){window.__events.push({method:String(method),payload:payload})}}})};
if(!localStorage.getItem('hgdj_search_history'))localStorage.setItem('hgdj_search_history',JSON.stringify(['历史词']));
`,
});
+5 -56
View File
@@ -12,7 +12,7 @@ import { createStyleFixture } from "./style-fixtures";
// Bundle the actual React provider, dialogs, header/footer and encrypted media
// hooks. The ephemeral server and browser request policy isolate all writes,
// destinations and analytics; no original business JS or template runtime runs.
// and destinations; no original business JS or template runtime runs.
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const temporary = await mkdtemp(join(tmpdir(), "huakui-native-site-")),
bundle = join(temporary, "site.js");
@@ -23,7 +23,6 @@ await build({
contents: `
import {createRoot} from 'react-dom/client';
import {SiteProvider,useSite} from '@/components/providers/SiteProvider';
import {TrackerProvider} from '@/components/analytics/TrackerProvider';
import {SiteHeader} from '@/components/layout/SiteHeader';
import {SiteFooter} from '@/components/layout/SiteFooter';
import {SiteShell} from '@/components/layout/SiteShell';
@@ -32,7 +31,7 @@ import {AppDownloadLink} from '@/components/catalog/Interactions';
import {setAuthSession} from '@/lib/api';
window.__setAuthSession=setAuthSession;
const app=createRoot(document.getElementById('root'));function UserProbe(){const site=useSite();return <div style={{position:'absolute',top:150,left:10}}><output id='display-avatar' data-value={site.user?.avatar||''} data-name={site.user?.nickName||''} data-balance={site.user?.recharge||0} data-registered={String(!!site.user?.isRegister)}/></div>}
function render(data){app.render(data.requestPath==='/navigation/'?<SiteShell view='nav' data={data}><SiteNavigationPage data={data}/></SiteShell>:<SiteProvider data={data}><TrackerProvider data={data}><SiteHeader data={data}/><UserProbe/><main style={{minHeight:900,paddingTop:100}}><h1>真实 React 共享组件隔离检查</h1><AppDownloadLink className="hk-app-cta__download" href={data.installGuide.fallbackURL}>点击下载</AppDownloadLink><button id="outside">页面按钮</button></main><SiteFooter data={data}/></TrackerProvider></SiteProvider>);}window.__rerender=render;render(window.__fixture);
function render(data){app.render(data.requestPath==='/navigation/'?<SiteShell view='nav' data={data}><SiteNavigationPage data={data}/></SiteShell>:<SiteProvider data={data}><SiteHeader data={data}/><UserProbe/><main style={{minHeight:900,paddingTop:100}}><h1>真实 React 共享组件隔离检查</h1><AppDownloadLink className="hk-app-cta__download" href={data.installGuide.fallbackURL}>点击下载</AppDownloadLink><button id="outside">页面按钮</button></main><SiteFooter data={data}/></SiteProvider>);}window.__rerender=render;render(window.__fixture);
`,
},
bundle: true,
@@ -72,13 +71,6 @@ const server = createServer(async (request, response) => {
response.end(await readFile(bundle));
return;
}
if (path === "/static/common/web-sdk-v1.2.0.js") {
response.setHeader("Content-Type", "application/javascript");
response.end(
"/* SDK mocked by the browser fixture before React mounts. */",
);
return;
}
if (path.startsWith("/static/")) {
const file = resolve(root, "public", "." + path);
if (!file.startsWith(join(root, "public") + "/")) {
@@ -151,7 +143,6 @@ const server = createServer(async (request, response) => {
requestPath: path,
pageKey: "home",
pageName: "首页",
trackerAppId: "isolated",
isLoggedIn: user,
isRegistered: false,
userName: "隔离游客",
@@ -203,9 +194,7 @@ const image = Buffer.from(
key = Buffer.from("2019ysapp7527");
for (let i = 0; i < Math.min(100, image.length); i++)
image[i] ^= key[i % key.length];
type EventRecord = { method: string; payload: Record<string, unknown> };
const events: EventRecord[] = [],
errors: string[] = [],
const errors: string[] = [],
missing: string[] = [],
unexpected: string[] = [],
destinations: string[] = [];
@@ -214,14 +203,8 @@ let releaseImage: (() => void) | undefined,
delayImage = false,
imageRequested = 0;
const configure = async (context: BrowserContext, ageAccepted = false) => {
await context.exposeBinding(
"__recordEvent",
(_source, event: EventRecord) => {
events.push(event);
},
);
await context.addInitScript({
content: `window.__ageClosed=0;document.addEventListener('eighteen:closed',function(){window.__ageClosed++});window.WebSDK={init:function(){return Promise.resolve()},getCommonFields:function(){return Promise.resolve({device_id:'isolated-device',device:'OTHER',sid:'isolated-sid'})},flush:function(){},track:new Proxy({},{get:function(_,method){return function(payload){return window.__recordEvent({method:String(method),payload:payload})}}})};try{${ageAccepted ? "localStorage.setItem('hg_age_ok','1');" : ""}if(!localStorage.getItem('__fixture_history_seeded')){localStorage.setItem('hgdj_search_history',JSON.stringify(['历史词']));localStorage.setItem('__fixture_history_seeded','1');}}catch{}`,
content: `window.__ageClosed=0;document.addEventListener('eighteen:closed',function(){window.__ageClosed++});try{${ageAccepted ? "localStorage.setItem('hg_age_ok','1');" : ""}if(!localStorage.getItem('__fixture_history_seeded')){localStorage.setItem('hgdj_search_history',JSON.stringify(['历史词']));localStorage.setItem('__fixture_history_seeded','1');}}catch{}`,
});
await context.route("**/*", async (route) => {
const request = route.request(),
@@ -302,7 +285,6 @@ const configure = async (context: BrowserContext, ageAccepted = false) => {
};
const ready = async (page: Page) => {
await page.waitForSelector(".hk-site-header");
await page.waitForFunction(() => !!window.__trackerCommonFields);
};
const footerQR = async (page: Page, value: string, previous = "") => {
await page.waitForFunction(({ value, previous }) => {
@@ -413,17 +395,8 @@ try {
await stack.keyboard.press("Escape");
await stack.waitForSelector("[data-dialog-ad]", { state: "detached" });
await expectOverflow(stack, "");
await stack.waitForTimeout(50);
assert.equal(
events.filter(
(event) =>
event.method === "adImpression" && event.payload.ad_id === "stack-1",
).length,
1,
);
await stacked.close();
imageRequested = 0;
events.length = 0;
// Age gate cannot be bypassed by Escape. Ads wait for actual XOR decode + image
// load, resume their queue on another page, and stay dismissed in this session.
const context = await browser.newContext({
@@ -459,7 +432,6 @@ try {
await page.waitForTimeout(150);
assert.equal(imageRequested, 1);
assert.equal(await page.locator("[data-dialog-ad]").isVisible(), false);
assert.equal(events.filter((e) => e.method === "adImpression").length, 0);
assert.equal(
await page.evaluate(() => localStorage.getItem("hg_age_ok")),
"1",
@@ -515,16 +487,6 @@ try {
await page.evaluate(() => sessionStorage.getItem("dialogAdDismissed")),
"2|1|2",
);
assert.equal(
events.filter((e) => e.method === "adImpression" && e.payload.ad_id === "1")
.length,
1,
);
assert.equal(
events.filter((e) => e.method === "adClick" && e.payload.ad_id === "1")
.length,
1,
);
await page.goto(origin + "/?ads=1&bad=1");
await ready(page);
await page.locator("[data-dialog-ad].hk-is-open").waitFor();
@@ -532,12 +494,6 @@ try {
await page.locator("[data-dialog-ad-id]").getAttribute("data-dialog-ad-id"),
"2",
);
assert.equal(
events.some(
(e) => e.method === "adImpression" && e.payload.ad_id === "bad",
),
false,
);
await page.keyboard.press("Escape");
// Actual HeaderSearch hooks, keyboard navigation and history; no account entry remains.
await page.goto(origin + "/?user=1");
@@ -1053,15 +1009,8 @@ try {
assert.ok(destinations.includes("/mock/android.apk"));
assert.ok(destinations.includes("/mock/fallback"));
assert.ok(destinations.includes("/mock/launch"));
assert.ok(
events.some(
(event) =>
event.method === "navigation" &&
event.payload.navigation_key === "header_download",
),
);
console.log(
"Native React site smoke passed: age gate/focus lock, encrypted advertisement readiness/failure/queue/cross-page session, footer QR pixels/size/URL/official fallback/empty configuration/restoration, HeaderSearch/history/keyboard/encoded URLs, configured desktop hover/focus/toggle/Escape/outside-click and mobile expandable navigation/canonical/external/download links, no login/user-menu/account entries, PC download and configured/fallback iOS/Android tabs/actions/deep links, drawer/footer/main entries, scroll/focus restoration and analytics. All APIs and outbound destinations isolated.",
"Native React site smoke passed: age gate/focus lock, encrypted advertisement readiness/failure/queue/cross-page session, footer QR pixels/size/URL/official fallback/empty configuration/restoration, HeaderSearch/history/keyboard/encoded URLs, configured desktop hover/focus/toggle/Escape/outside-click and mobile expandable navigation/canonical/external/download links, no login/user-menu/account entries, PC download and configured/fallback iOS/Android tabs/actions/deep links, drawer/footer/main entries, scroll/focus restoration. All APIs and outbound destinations isolated.",
);
} finally {
releaseImage?.();
-10
View File
@@ -39,13 +39,6 @@ async function expectPageStyles(page: Page, required: string[], absent: string[]
const canonical = "https://canonical.drama.test";
const userAgent =
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) NativeSSRSmoke/1.0";
const sdkCommon = JSON.stringify({
sid: "ssr-sdk-session",
device_id: "ssr-sdk-device",
device: "iOS",
device_fingerprint: "ssr-fingerprint",
fp_version: "2",
});
const indexNow = "native_ssr_verification_20261010";
const verifyFilename = `native-ssr-${randomBytes(6).toString("hex")}.html`;
const verifyPath = resolve("public", verifyFilename);
@@ -189,7 +182,6 @@ const mock = createServer(async (req, res) => {
seoNavInfo: {},
navPageList: [],
advitise: {},
reportSdkConfig: "",
});
return;
}
@@ -381,7 +373,6 @@ function headers(caseName: string, token = ""): Headers {
"user-agent": userAgent,
"x-forwarded-for": "203.0.113.42, 198.51.100.7",
"x-user-agent": "DevType=SSR smoke;SysType=h5_ios;sid=original",
"x-sdk-common": sdkCommon,
...(token
? {
cookie: `token=${token}; nickName=STALE_COOKIE; userId=1; isRegister=${token.startsWith("guest-") ? "0" : "1"}; channel=smoke-channel; client_cookie=must_forward`,
@@ -540,7 +531,6 @@ try {
call.headers["x-user-agent"],
"DevType=SSR smoke;SysType=h5_ios;sid=original",
);
assert.equal(call.headers["x-sdk-common"], sdkCommon);
assert.equal(
Object.keys(call.headers).some((key) => key.startsWith("x-hk-")),
false,
+4 -81
View File
@@ -86,7 +86,7 @@ const encrypted = decryptImageBytes(
const entry = join(temp, "fixture.tsx");
await writeFile(
entry,
`import {useState} from 'react';import {createRoot} from 'react-dom/client';import {SiteProvider} from '${cwd}/src/components/providers/SiteProvider';import {TrackerProvider} from '${cwd}/src/components/analytics/TrackerProvider';import {DramaWatchPage} from '${cwd}/src/features/playback/DramaWatchPage';import {SeriesWatchPage} from '${cwd}/src/features/playback/SeriesWatchPage';import {EncryptedImage} from '${cwd}/src/components/media/EncryptedImage';function Fixture(){const [current,setCurrent]=useState(window.fixtureData);window.changePlaybackFixture=setCurrent;return <TrackerProvider boot={{appId:'JHA-4001',encryptedConfig:'mock',uid:'mock_1001',pageKey:'play',pageName:'短剧播放页'}}><SiteProvider data={current} dialogs={false}>{current.series?<SeriesWatchPage key={current.fixtureKey} data={current}/>:<DramaWatchPage key={current.fixtureKey} data={current}/>}<EncryptedImage id="encrypted-image-test" src="/encrypted.png" eager style={{width:32,height:32}} onResolved={()=>window.imageResolved=(window.imageResolved||0)+1} onResolveError={()=>window.imageErrors=(window.imageErrors||0)+1}/><EncryptedImage id="failed-image-test" src="/missing-image" eager style={{width:32,height:32}} onResolved={()=>window.failedImageResolved=true} onResolveError={()=>window.failedImageError=true}/></SiteProvider></TrackerProvider>};createRoot(document.getElementById('root')).render(<Fixture/>);`,
`import {useState} from 'react';import {createRoot} from 'react-dom/client';import {SiteProvider} from '${cwd}/src/components/providers/SiteProvider';import {DramaWatchPage} from '${cwd}/src/features/playback/DramaWatchPage';import {SeriesWatchPage} from '${cwd}/src/features/playback/SeriesWatchPage';import {EncryptedImage} from '${cwd}/src/components/media/EncryptedImage';function Fixture(){const [current,setCurrent]=useState(window.fixtureData);window.changePlaybackFixture=setCurrent;return <SiteProvider data={current} dialogs={false}>{current.series?<SeriesWatchPage key={current.fixtureKey} data={current}/>:<DramaWatchPage key={current.fixtureKey} data={current}/>}<EncryptedImage id="encrypted-image-test" src="/encrypted.png" eager style={{width:32,height:32}} onResolved={()=>window.imageResolved=(window.imageResolved||0)+1} onResolveError={()=>window.imageErrors=(window.imageErrors||0)+1}/><EncryptedImage id="failed-image-test" src="/missing-image" eager style={{width:32,height:32}} onResolved={()=>window.failedImageResolved=true} onResolveError={()=>window.failedImageError=true}/></SiteProvider>};createRoot(document.getElementById('root')).render(<Fixture/>);`,
);
const navStub = join(temp, "navigation.ts");
await writeFile(
@@ -159,18 +159,6 @@ const baseData = () => ({
{ id: 102, currentEpisode: 2, payType: 1, needUnlock: true },
],
},
trackerDrama: {
DramaID: 10,
DramaTitle: "播放测试",
VideoID: 101,
Episode: 1,
TotalEpisode: 2,
DramaDuration: 12,
DramaTypeID: "2",
DramaTypeName: "都市",
DramaTagKey: "3",
DramaTagName: "剧情",
},
hotList: [],
guessLike: [
{ id: 30, slug: "guess", playName: "猜你喜欢测试", cover: "/static/brand/placeholder.svg", totalEpisode: 3, updateStatus: 2 },
@@ -194,13 +182,6 @@ const server = createServer(async (req, res) => {
res.end(playbackStyles.css);
return;
}
if (url.pathname === "/static/common/web-sdk-v1.2.0.js") {
res.setHeader("content-type", "application/javascript");
res.end(
'window.sdkEvents=[];window.WebSDK={init:options=>{window.sdkInit=options},track:new Proxy({},{get:(_,method)=>payload=>{window.sdkEvents.push({method,payload});return Promise.resolve({ok:true})}}),getCommonFields:()=>Promise.resolve({sid:"mock-session",device_id:"mock-device",device:"PC",device_fingerprint:"mock-fp",fp_version:"1"}),flush:()=>Promise.resolve(),setUid:()=>{}};',
);
return;
}
if (url.pathname.startsWith("/api/")) {
res.setHeader("content-type", "application/json");
let data: unknown = {};
@@ -378,35 +359,6 @@ try {
return video.play();
});
await page.waitForFunction(() => document.querySelector("video")!.ended);
const events = await page.evaluate(
() =>
(
window as unknown as {
sdkEvents: { method: string; payload: Record<string, unknown> }[];
}
).sdkEvents,
);
for (const behavior of [
"ep_view",
"ep_play",
"ep_pause",
"ep_forward",
"ep_complete",
])
assert(
events.some(
(event) =>
event.method === "dramaEvent" &&
event.payload.play_behavior_key === behavior,
),
behavior,
);
assert(
events.some(
(event) =>
event.method === "dramaEvent" && event.payload.play_speed === 150,
),
);
assert.equal(
await page
.locator("#failed-image-test")
@@ -440,30 +392,11 @@ try {
),
true,
);
const beforePreview = events.filter(
(event) =>
event.method === "dramaEvent" &&
event.payload.play_behavior_key === "ep_play",
).length;
await page.getByRole("button", { name: "预告片", exact: true }).click();
await page.waitForFunction(() => {
const video = document.querySelector("video");
return !!video && !video.paused && video.currentTime > 0.1;
});
await page.waitForTimeout(300);
const afterPreview = await page.evaluate(
() =>
(
window as unknown as {
sdkEvents: { method: string; payload: Record<string, unknown> }[];
}
).sdkEvents.filter(
(event) =>
event.method === "dramaEvent" &&
event.payload.play_behavior_key === "ep_play",
).length,
);
assert.equal(afterPreview, beforePreview);
assert.equal(
await page.locator(".hk-playback-player").getAttribute("data-play-mode"),
"preview",
@@ -493,18 +426,8 @@ try {
const watchReq = requests.find(
(req) => req.url === "/api/playlet/chapter/watch/report",
);
assert.equal(
watchReq?.headers["x-sdk-common"],
Buffer.from(
JSON.stringify({
sid: "mock-session",
device_id: "mock-device",
device: "PC",
device_fingerprint: "mock-fp",
fp_version: "1",
}),
).toString("base64"),
);
assert(watchReq, "watch report request");
assert.equal(watchReq.headers["x-sdk-common"], undefined);
await page.evaluate(() => window.scrollTo(0, 0));
await page.screenshot({ path: join(output, device + "-player.png") });
const paid = {
@@ -667,7 +590,7 @@ try {
fullscreen: true,
errorRetry: true,
realMp4: true,
recommendationsAndTracking: true,
recommendations: true,
encryptedImageAndFallback: true,
lockedAppDownloadOnly: true,
});
-170
View File
@@ -1,170 +0,0 @@
import assert from "node:assert/strict";
import { createCipheriv, randomBytes } from "node:crypto";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { build } from "esbuild";
import { chromium } from "@playwright/test";
// Exercise the shipped SDK and React provider. Every report terminates in a
// browser route; synthetic configuration never reaches an analytics service.
const root = process.cwd();
const temp = await mkdtemp(join(tmpdir(), "huakui-tracker-"));
const sdk = await readFile(join(root, "public/static/common/web-sdk-v1.2.0.js"), "utf8");
const configKey = sdk.match(/_secondGcmKeyBase64="([^"\n]+)"/)?.[1];
assert.ok(configKey, "The SDK configuration encryption contract is available");
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", Buffer.from(configKey, "base64"), iv);
const encryptedConfig = Buffer.concat([
iv,
cipher.update(JSON.stringify({
batchUrls: ["https://analytics.fixture.invalid"],
eventCodes: ["app_page_view", "ad_impression", "page_click", "ad_click"],
signKey: "synthetic-tracker-fixture-key",
securityKeyVersion: "fixture",
})),
cipher.final(),
cipher.getAuthTag(),
]).toString("base64");
const appFile = join(temp, "app.js");
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined;
let server: ReturnType<typeof createServer> | undefined;
try {
await build({
stdin: {
contents: `import {createRoot} from 'react-dom/client';
import {TrackerProvider} from './src/components/analytics/TrackerProvider';
createRoot(document.getElementById('root')).render(
<TrackerProvider boot={window.__fixtureBoot}>
<a href="#fixture" data-track-ad="external" data-ad-id="fixture-ad"
data-ad-clickable="1" data-ad-type="banner"
style={{display:'block',width:300,height:100}}>广告验证</a>
</TrackerProvider>);`,
loader: "tsx",
resolveDir: root,
},
outfile: appFile,
bundle: true,
platform: "browser",
format: "iife",
tsconfig: join(root, "tsconfig.json"),
define: { "process.env.NODE_ENV": '"production"' },
});
const app = await readFile(appFile);
server = createServer((request, response) => {
const path = new URL(request.url || "/", "http://fixture").pathname;
if (path === "/app.js") {
response.setHeader("Content-Type", "application/javascript").end(app);
} else if (path === "/static/common/web-sdk-v1.2.0.js") {
response.setHeader("Content-Type", "application/javascript").end(sdk);
} else if (path === "/") {
response.setHeader("Content-Type", "text/html; charset=utf-8").end(
'<!doctype html><html><head><meta charset="utf-8"><title>Tracker fixture</title></head><body><div id="root"></div><script>window.__fixtureBoot=' +
JSON.stringify({
appId: "JHA-4001",
encryptedConfig,
appVersion: "1.0.0",
pageKey: "home",
pageName: "首页",
uid: "fixture_user",
}).replace(/</g, "\\u003c") +
'</script><script src="/app.js"></script></body></html>',
);
} else response.writeHead(404).end();
});
await new Promise<void>((done) => server!.listen(0, "127.0.0.1", done));
const address = server.address();
assert.ok(address && typeof address !== "string");
const origin = `http://127.0.0.1:${address.port}`;
browser = await chromium.launch({
executablePath: process.env.CHROME_PATH || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
headless: true,
});
const commonKeys = ["sid", "device_id", "device", "device_fingerprint", "fp_version"].sort();
const results: Record<string, unknown>[] = [];
for (const fixture of [
{
device: "iOS", system: "iOS", version: "18.0",
ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Version/18.0 Mobile/15E148 Safari/604.1",
},
{
device: "Android", system: "Android", version: "14",
ua: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/130.0.0.0 Mobile Safari/537.36",
},
{
device: "PC", system: "Windows", version: "10.0",
ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/130.0.0.0 Safari/537.36",
},
{
device: "PC", system: "macOS", version: "14.6",
ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) AppleWebKit/605.1.15 Version/18.0 Safari/605.1.15",
},
]) {
const context = await browser.newContext({ userAgent: fixture.ua });
const reports: Record<string, unknown>[] = [];
const errors: string[] = [];
await context.route("**/*", async (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.origin === origin) return route.continue();
if (url.hostname === "analytics.fixture.invalid") {
if (request.method() === "POST") {
const body: unknown = JSON.parse(request.postData() || "[]");
assert.ok(Array.isArray(body), "SDK reports use a top-level batch array");
for (const record of body) {
assert.ok(record && typeof record === "object");
reports.push(record as Record<string, unknown>);
}
}
return route.fulfill({ status: 200, contentType: "application/json", body: "{}" });
}
return route.abort();
});
const page = await context.newPage();
page.on("pageerror", (error) => errors.push(error.message));
await page.goto(origin, { waitUntil: "load" });
await page.waitForFunction(() => !!window.__trackerCommonFields, null, { timeout: 15000 });
await page.waitForFunction(() => !!window.tracker, null, { timeout: 15000 });
const common = await page.evaluate(async () => {
const fields = await window.WebSDK!.getCommonFields!(["device", "system_name", "system_version"]);
const header = await window.tracker!.getSDKCommonHeader();
return { fields, headerKeys: Object.keys(JSON.parse(atob(header))).sort() };
});
assert.equal(common.fields.device, fixture.device);
assert.equal(common.fields.system_name, fixture.system);
assert.equal(common.fields.system_version, fixture.version);
assert.deepEqual(common.headerKeys, commonKeys, "X-SDK-Common retains its five-field contract");
await page.waitForFunction(async () => {
await window.tracker!.flush();
return true;
});
await assertEventually(() => reports.some((r) => r.event === "app_page_view") &&
reports.some((r) => r.event === "ad_impression"));
for (const event of ["app_page_view", "ad_impression"]) {
const report = reports.find((record) => record.event === event);
assert.ok(report, `${fixture.system}: ${event} was sent through the real SDK`);
assert.equal(report.device, fixture.device);
assert.equal(report.system_name, fixture.system);
assert.equal(report.system_version, fixture.version);
assert.equal(report.app_id, "JHA-4001");
assert.ok(typeof report.event_sig === "string" && report.event_sig.length > 0, "Real SDK signs the report");
}
assert.deepEqual(errors, []);
results.push({ system: fixture.system, version: fixture.version, events: ["app_page_view", "ad_impression"], headerKeys: common.headerKeys.length });
await context.close();
}
console.log(JSON.stringify({ passed: true, cases: results }, null, 2));
} finally {
await browser?.close();
if (server) await new Promise<void>((done) => server!.close(() => done()));
await rm(temp, { recursive: true, force: true });
}
async function assertEventually(predicate: () => boolean) {
const deadline = Date.now() + 15000;
while (!predicate() && Date.now() < deadline)
await new Promise((done) => setTimeout(done, 50));
assert.ok(predicate(), "The SDK sent both the page view and visible ad impression");
}