样式修改
This commit is contained in:
@@ -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']",
|
||||
|
||||
@@ -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(['历史词']));
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user