import assert from "node:assert/strict";
import { createServer } from "node:http";
import { readFile, mkdtemp, rm } from "node:fs/promises";
import { existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, extname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";
import { chromium, type Page, type BrowserContext } from "@playwright/test";
import { installGuideData } from "../src/server/services/install-guide";
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.
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const temporary = await mkdtemp(join(tmpdir(), "huakui-native-site-")),
bundle = join(temporary, "site.js");
await build({
stdin: {
resolveDir: root,
loader: "tsx",
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';
import {SiteNavigationPage} from '@/features/content/SiteNavigationPage';
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
}
function render(data){app.render(data.requestPath==='/navigation/'? :真实 React 共享组件隔离检查 点击下载 页面按钮 );}window.__rerender=render;render(window.__fixture);
`,
},
bundle: true,
platform: "browser",
format: "iife",
outfile: bundle,
tsconfig: join(root, "tsconfig.json"),
banner: { js: 'var process={env:{NODE_ENV:"development"}};' },
define: {
"process.env.NODE_ENV": '"development"',
"process.env.__NEXT_ROUTER_BASEPATH": '""',
},
});
const styles = [
"base",
"header",
"footer",
"eighteen",
"dialogAd",
"downloadPop",
"installGuide",
"toast",
"nav",
];
const siteStyles = await createStyleFixture(styles);
const server = createServer(async (request, response) => {
try {
const url = new URL(request.url || "/", "http://" + request.headers.host),
path = url.pathname;
if (path === siteStyles.href) {
response.setHeader("Content-Type", "text/css;charset=utf-8");
response.end(siteStyles.css);
return;
}
if (path === "/site.js") {
response.setHeader("Content-Type", "application/javascript");
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") + "/")) {
response.writeHead(403).end();
return;
}
const mime: Record = {
".css": "text/css",
".svg": "image/svg+xml",
".png": "image/png",
".webp": "image/webp",
".woff2": "font/woff2",
};
response.setHeader(
"Content-Type",
mime[extname(file)] || "application/octet-stream",
);
response.end(await readFile(file));
return;
}
if (path.startsWith("/mock/")) {
response.setHeader("Content-Type", "text/html");
response.end("Isolated destination
");
return;
}
const fallback = url.origin + "/mock/fallback",
configured = url.searchParams.get("configured") !== "0",
user = url.searchParams.has("user");
const guide = installGuideData(
fallback,
configured
? {
iosStoreURL: url.origin + "/mock/ios-store",
iosProfileURL: url.origin + "/mock/ios-profile",
androidAPKURL: url.origin + "/mock/android.apk",
deepLinkURL: url.origin + "/mock/launch",
}
: {
iosStoreURL: "",
iosProfileURL: "",
androidAPKURL: "",
deepLinkURL: "",
},
);
const ads = url.searchParams.has("ads")
? [
{
id: url.searchParams.has("bad")
? "bad"
: url.searchParams.has("stack")
? "stack-1"
: "1",
images: url.searchParams.has("bad")
? "/mock/bad-image.png"
: "/mock/encrypted-a.png",
link: url.origin + "/mock/ad-1",
title: "广告一",
location: 1,
},
{
id: url.searchParams.has("stack") ? "stack-2" : "2",
images: "/mock/encrypted-b.png",
link: url.origin + "/mock/ad-2",
title: "广告二",
location: 2,
},
]
: [];
const data = {
requestPath: path,
pageKey: "home",
pageName: "首页",
trackerAppId: "isolated",
isLoggedIn: user,
isRegistered: false,
userName: "隔离游客",
userID: 12345,
recharge: 1234,
avatar: "/static/images/profile/avatar.png",
favoriteCount: 3,
installGuide: guide,
appConfig: { appAddr: fallback, dialogList: ads },
config: { appAddr: fallback },
categoryList: [
{ name: "首页", urlValue: "/" },
{ name: "短剧", urlValue: "/dramas/" },
{
name: "友情链接", urlValue: "#", categoryType: 2,
translationList: [
{ name: "黄果视频", urlValue: "https://huangguo.com", isFriendAds: false },
{ name: "爱情专题", urlValue: "/topic/romance/" },
{ name: "下载 App", urlValue: "/download-app/" },
],
},
{ name: "下载 App", urlValue: "/app/download/" },
],
themeList: [{ id: 3, slug: "city", name: "都市" }],
homeConfigPlaylets: [{ id: 1, slug: "popular", name: "热门推荐" }],
searchTrends: [{ Rank: 1, Title: "都市", Heat: "1 万", Badge: "热" }],
};
response.setHeader("Content-Type", "text/html; charset=utf-8");
response.end(
' ' +
` ` +
'
',
);
} catch {
response.writeHead(404).end();
}
});
await new Promise((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}`;
const image = Buffer.from(
await readFile(join(root, "public/static/images/profile/avatar.png")),
),
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 };
const events: EventRecord[] = [],
errors: string[] = [],
missing: string[] = [],
unexpected: string[] = [],
destinations: string[] = [];
let browser: Awaited> | undefined;
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{}`,
});
await context.route("**/*", async (route) => {
const request = route.request(),
url = new URL(request.url());
if (url.hostname === "avatars.test") {
await route.fulfill({
body: image,
contentType: "application/octet-stream",
});
return;
}
if (url.origin !== origin) {
await route.fulfill({ body: "", contentType: "text/plain" });
return;
}
if (url.pathname.startsWith("/api/")) {
if (url.pathname === "/api/web/search/suggest") {
const keyword = url.searchParams.get("keyword") || "";
await route.fulfill({
json: {
code: 200,
keyword,
feature: {
title: keyword + "作品",
cover: "/static/images/profile/avatar.png",
url: "/drama/suggest/",
meta: "全 8 集",
},
items: [
{
title: keyword,
query: keyword,
kind: "搜索",
url: "/search/" + encodeURIComponent(keyword) + "/",
},
],
},
});
return;
}
if (url.pathname === "/api/auth/session") {
await route.fulfill({ json: { code: 200, data: null } });
return;
}
unexpected.push(url.pathname);
await route.fulfill({ status: 500, json: { code: 500, data: null } });
return;
}
if (url.pathname === "/mock/bad-image.png") {
await route.fulfill({ status: 500, body: "" });
return;
}
if (url.pathname.startsWith("/mock/encrypted-")) {
imageRequested++;
if (delayImage && url.pathname.endsWith("a.png"))
await new Promise((done) => {
releaseImage = done;
});
await route.fulfill({
body: image,
contentType: "application/octet-stream",
});
return;
}
if (url.pathname.startsWith("/mock/")) destinations.push(url.pathname);
await route.continue();
});
context.on("page", (page) => {
page.on("pageerror", (error) => errors.push(error.message));
page.on("response", (response) => {
if (
response.status() >= 400 &&
new URL(response.url()).pathname.startsWith("/static/")
)
missing.push(response.url());
});
});
};
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 }) => {
const canvas = document.querySelector(".hk-site-footer .hk-app-download-qr canvas");
if (!canvas || canvas.getAttribute("data-qr") !== value) return false;
const pixels = canvas.getContext("2d")?.getImageData(0, 0, canvas.width, canvas.height).data;
if (!pixels || !pixels.some((channel, index) => index % 4 === 3 && channel > 0)) return false;
return !previous || canvas.toDataURL() !== previous;
}, { value, previous });
const result = await page.locator(".hk-site-footer .hk-app-download-qr canvas").evaluate((node) => {
const canvas = node as HTMLCanvasElement;
const pixels = canvas.getContext("2d")!.getImageData(0, 0, canvas.width, canvas.height).data;
let dark = 0, light = 0;
for (let index = 0; index < pixels.length; index += 4) {
if (pixels[index + 3] !== 255) continue;
if (pixels[index] === 0 && pixels[index + 1] === 0 && pixels[index + 2] === 0) dark++;
if (pixels[index] === 255 && pixels[index + 1] === 255 && pixels[index + 2] === 255) light++;
}
const bounds = canvas.getBoundingClientRect();
return { width: canvas.width, height: canvas.height, displayWidth: bounds.width, displayHeight: bounds.height, dark, light, png: canvas.toDataURL() };
});
assert.equal(result.width, 104);
assert.equal(result.height, 104);
assert(result.displayWidth > 0);
assert.equal(result.displayWidth, result.displayHeight);
assert(result.dark > 100 && result.light > 100, "QR canvas contains opaque black modules and a white background");
return result.png;
};
const expectOverflow = async (page: Page, value: string) =>
assert.equal(
await page.locator("body").evaluate((node) => node.style.overflow),
value,
);
const closeGuide = async (page: Page) => {
await page.keyboard.press("Escape");
await page.waitForSelector("[data-install-guide], [data-download-pop]", {
state: "detached",
});
await expectOverflow(page, "");
};
try {
const chrome =
process.env.CHROME_PATH ||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
browser = await chromium.launch({
headless: true,
...(existsSync(chrome) ? { executablePath: chrome } : {}),
});
// Two visible dialogs share only the highest layer's keyboard/focus ownership.
const stacked = await browser.newContext({
viewport: { width: 1440, height: 900 },
});
await configure(stacked);
const stack = await stacked.newPage();
await stack.goto(origin + "/?auth=login");
await ready(stack);
await stack.locator("[data-eighteen]").waitFor();
await stack.waitForTimeout(100);
assert.equal(
await stack.evaluate(
() => !!document.activeElement?.closest("[data-eighteen]"),
),
true,
);
await stack.keyboard.press("Tab");
assert.equal(
await stack.evaluate(
() => !!document.activeElement?.closest("[data-eighteen]"),
),
true,
);
await stack.keyboard.press("Escape");
assert.equal(await stack.locator("[data-eighteen]").isVisible(), true);
await stack.locator("[data-eighteen-enter]").click();
await stack.waitForSelector("[data-eighteen]", { state: "detached" });
// The account system was removed: no login dialog is queued or opened on demand.
await stack.evaluate(() =>
document.dispatchEvent(new CustomEvent("auth:required")),
);
await stack.waitForTimeout(100);
assert.equal(await stack.locator("[data-login-pop], [data-register-pop], [data-recharge-pop]").count(), 0);
await expectOverflow(stack, "");
await stack.goto(origin + "/?ads=1&stack=1");
await ready(stack);
await stack.locator("[data-dialog-ad].hk-is-open").waitFor();
// A higher-priority site dialog suspends the advertisement queue and resumes it on close.
await stack.evaluate(() =>
document.querySelector(".hk-site-header__download")?.click(),
);
await stack.locator("[data-download-pop]").waitFor();
assert.equal(await stack.locator("[data-dialog-ad]").count(), 0);
await stack.keyboard.press("Escape");
await stack.waitForSelector("[data-download-pop]", { state: "detached" });
await stack.locator("[data-dialog-ad].hk-is-open").waitFor();
assert.equal(
await stack
.locator("[data-dialog-ad-id]")
.getAttribute("data-dialog-ad-id"),
"stack-1",
);
await stack.keyboard.press("Escape");
await stack.waitForFunction(
() =>
document
.querySelector("[data-dialog-ad-id]")
?.getAttribute("data-dialog-ad-id") === "stack-2",
);
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({
viewport: { width: 1440, height: 900 },
});
await configure(context);
const page = await context.newPage();
await page.goto(origin + "/?ads=1");
await ready(page);
await page.locator("[data-eighteen]").waitFor();
await expectOverflow(page, "hidden");
assert.equal(imageRequested, 0);
await page.keyboard.press("Escape");
assert.equal(await page.locator("[data-eighteen]").isVisible(), true);
await page.locator("[data-eighteen-enter]").focus();
await page.keyboard.press("Shift+Tab");
assert.equal(
await page
.locator("[data-eighteen-exit]")
.evaluate((el) => el === document.activeElement),
true,
);
await page.keyboard.press("Tab");
assert.equal(
await page
.locator("[data-eighteen-enter]")
.evaluate((el) => el === document.activeElement),
true,
);
delayImage = true;
await page.locator("[data-eighteen-enter]").click();
await page.waitForFunction(() => !document.querySelector("[data-eighteen]"));
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",
);
assert.equal(
await page.evaluate(
() => (window as unknown as { __ageClosed: number }).__ageClosed,
),
1,
);
delayImage = false;
releaseImage?.();
await page.locator("[data-dialog-ad].hk-is-open").waitFor({ timeout: 15000 });
assert.equal(
await page
.locator(".hk-advertisement-dialog-image")
.evaluate(
(el: HTMLImageElement) =>
el.currentSrc.startsWith("blob:") && el.naturalWidth > 0,
),
true,
);
await expectOverflow(page, "hidden");
const outboundPromise = context.waitForEvent("page");
await page.locator(".hk-advertisement-dialog-cta").click();
const outbound = await outboundPromise;
await outbound.waitForLoadState();
assert.equal(new URL(outbound.url()).pathname, "/mock/ad-1");
await outbound.close();
await page.locator("[data-dialog-ad-close]").click();
await page.waitForFunction(
() =>
document
.querySelector("[data-dialog-ad-id]")
?.getAttribute("data-dialog-ad-id") === "2",
);
await page.goto(origin + "/dramas/?ads=1");
await ready(page);
await page.locator("[data-dialog-ad].hk-is-open").waitFor();
assert.equal(
await page.locator("[data-dialog-ad-id]").getAttribute("data-dialog-ad-id"),
"2",
);
assert.equal(await page.locator("[data-eighteen]").count(), 0);
await page.keyboard.press("Escape");
await page.waitForSelector("[data-dialog-ad]", { state: "detached" });
await expectOverflow(page, "");
await page.goto(origin + "/series/?ads=1");
await ready(page);
await page.waitForTimeout(100);
assert.equal(await page.locator("[data-dialog-ad]").count(), 0);
assert.equal(
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();
assert.equal(
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");
await ready(page);
assert.equal(
await page.locator(
"[data-login-open], [data-register-open], .hk-site-header__login, [data-user-menu-toggle], .hk-user-menu, [data-header-recharge], a[href^='/account']",
).count(),
0,
"Header renders no login, user menu or account entry even for a guest session",
);
// Configured navigation uses the same links on hover, keyboard and touch.
const friendMenu = page.locator('[data-navigation-submenu="desktop"]'),
friendToggle = friendMenu.locator(".hk-site-navigation__submenu-toggle"),
friendPanel = friendMenu.locator(".hk-site-navigation__submenu");
assert.equal(await friendToggle.getAttribute("aria-expanded"), "false");
assert.equal(await friendPanel.isVisible(), false);
assert.equal(await friendToggle.getAttribute("aria-controls"), await friendPanel.getAttribute("id"));
assert.equal(await friendPanel.locator('a[data-navigation-name="爱情专题"]').getAttribute("href"), "/collection/romance/");
await friendToggle.hover();
await friendPanel.waitFor({ state: "visible" });
const toggleBounds = await friendToggle.boundingBox(),
panelBounds = await friendPanel.boundingBox();
assert.ok(toggleBounds && panelBounds);
assert.ok(panelBounds.x >= 0 && panelBounds.x + panelBounds.width <= 1440);
await page.mouse.move(toggleBounds.x + toggleBounds.width / 2, (toggleBounds.y + toggleBounds.height + panelBounds.y) / 2);
await page.waitForTimeout(40);
assert.equal(await friendPanel.isVisible(), true, "pointer can cross the gap into the submenu");
await friendPanel.locator("a").first().hover();
assert.equal(await friendToggle.getAttribute("aria-expanded"), "true");
await page.locator("#outside").click();
await friendPanel.waitFor({ state: "hidden" });
await page.keyboard.press("Tab");
await friendToggle.focus();
await friendPanel.waitFor({ state: "visible" });
await page.keyboard.press("Tab");
assert.equal(await friendPanel.locator("a").first().evaluate((node) => node === document.activeElement), true);
await page.keyboard.press("Escape");
await friendPanel.waitFor({ state: "hidden" });
assert.equal(await friendToggle.evaluate((node) => node === document.activeElement), true);
assert.equal(await friendToggle.getAttribute("aria-expanded"), "false");
await friendToggle.press("Space");
await friendPanel.waitFor({ state: "visible" });
await friendToggle.press("Space");
await friendPanel.waitFor({ state: "hidden" });
await friendToggle.press("Enter");
await friendPanel.waitFor({ state: "visible" });
await page.locator("#outside").click();
await friendPanel.waitFor({ state: "hidden" });
await friendToggle.hover();
const friendWindowPromise = context.waitForEvent("page");
await friendPanel.locator('a[data-navigation-name="黄果视频"]').click();
const friendWindow = await friendWindowPromise;
await friendWindow.waitForLoadState();
assert.equal(friendWindow.url(), "https://huangguo.com/");
await friendWindow.close();
await friendPanel.waitFor({ state: "hidden" });
await friendToggle.hover();
await friendPanel.locator("[data-download-open]").click();
await page.locator("[data-download-pop]").waitFor();
await friendPanel.waitFor({ state: "hidden" });
await closeGuide(page);
// A real canvas is generated from the same URL as the footer download link.
const primaryQR = await footerQR(page, origin + "/mock/fallback");
await page.evaluate((official) => {
const fixture = window as unknown as {
__fixture: { appConfig: Record; config: Record };
__rerender: (data: Record) => void;
};
fixture.__rerender({
...fixture.__fixture,
appConfig: { ...fixture.__fixture.appConfig, appAddr: "", officialAppAddr: official },
config: { ...fixture.__fixture.config, appAddr: "", officialAppAddr: official },
});
}, origin + "/mock/official");
await footerQR(page, origin + "/mock/official", primaryQR);
assert.equal(
await page.locator('[data-navigation-key="footer_download"]').getAttribute("href"),
origin + "/mock/official",
);
await page.evaluate(() => {
const fixture = window as unknown as {
__fixture: { appConfig: Record; config: Record };
__rerender: (data: Record) => void;
};
fixture.__rerender({
...fixture.__fixture,
appConfig: { ...fixture.__fixture.appConfig, appAddr: "", officialAppAddr: "" },
config: { ...fixture.__fixture.config, appAddr: "", officialAppAddr: "" },
});
});
await page.waitForSelector(".hk-site-footer .hk-app-download-qr img[src=\"/static/brand/placeholder.svg\"]");
assert.equal(await page.locator(".hk-site-footer .hk-app-download-qr canvas").count(), 0);
assert.equal(
await page.locator('[data-navigation-key="footer_download"]').getAttribute("href"),
"/app/download/",
);
await page.evaluate(() => {
const fixture = window as unknown as { __fixture: Record; __rerender: (data: Record) => void };
fixture.__rerender(fixture.__fixture);
});
assert.equal(await footerQR(page, origin + "/mock/fallback"), primaryQR);
const input = page.locator("[data-header-search-input]");
await input.focus();
assert.equal(
await page.locator("[data-header-history-chips]").innerText(),
"历史词",
);
await page.locator("[data-header-history-clear]").click();
assert.equal(await page.locator("[data-header-search-history]").count(), 0);
await input.press("Escape");
assert.equal(
await page.locator("[data-header-search-dropdown]").isVisible(),
false,
);
await input.focus();
await page.locator("#outside").click();
assert.equal(
await page.locator("[data-header-search-dropdown]").isVisible(),
false,
);
await input.fill("a/b");
await page.waitForSelector(".hk-search-dropdown__feature");
assert.equal(
await page.locator(".hk-search-dropdown__feature strong").textContent(),
"a/b作品",
);
await input.press("ArrowDown");
assert.equal(
await page.locator(".hk-search-dropdown__item.hk-is-active").count(),
1,
);
await input.press("Enter");
await page.waitForURL(origin + "/search/a%2Fb/");
assert.equal(
await page.evaluate(() =>
JSON.parse(localStorage.getItem("hgdj_search_history") || "[]").includes(
"a/b",
),
),
true,
);
await page.goto(origin + "/?user=1");
await ready(page);
await page.evaluate(() => {
document.cookie = "token=isolated; Path=/";
document.cookie = "avatar=v3%2Fimage%2Fguest.png; Path=/";
document.dispatchEvent(new CustomEvent("auth:login"));
});
await page.waitForFunction(
() =>
document.querySelector("#display-avatar")?.getAttribute("data-value") ===
"v3/image/guest.png",
);
assert.equal(
await page.evaluate(() =>
document.cookie.includes("avatar=v3%2Fimage%2Fguest.png"),
),
true,
);
await page.evaluate(() => {
document.cookie =
"avatar=%2Fstatic%2Fimages%2Fprofile%2Favatar.png; Path=/";
document.dispatchEvent(new CustomEvent("auth:login"));
});
await page.waitForFunction(
() =>
document.querySelector("#display-avatar")?.getAttribute("data-value") ===
"/static/images/profile/avatar.png",
);
await page.evaluate(() => {
(
window as unknown as {
__setAuthSession: (user: Record) => void;
}
).__setAuthSession({
token: "isolated",
userId: 12345,
type: 1,
nickName: "旧版注册用户",
});
});
await page.waitForFunction(
() =>
document
.querySelector("#display-avatar")
?.getAttribute("data-registered") === "true",
);
assert.equal(
await page.evaluate(() =>
document.cookie
.split("; ")
.find((value) => value.startsWith("isRegister=")),
),
"isRegister=1",
"Client auth cookie registration matches the BFF type=1 fallback",
);
// A new RSC props snapshot must replace retained client authentication state.
await page.evaluate(() => {
(
window as unknown as {
__fixture: Record;
__rerender: (data: Record) => void;
}
).__rerender({
...(window as unknown as { __fixture: Record })
.__fixture,
userName: "RSC 更新用户",
recharge: 4321,
});
});
await page.waitForFunction(
() =>
document.querySelector("#display-avatar")?.getAttribute("data-name") ===
"RSC 更新用户",
);
assert.equal(await page.locator("#display-avatar").getAttribute("data-balance"), "4321");
assert.equal(await page.locator("[data-header-recharge], [data-user-menu-toggle]").count(), 0);
// PC download uses the real fallback and restores focus/scroll after Escape.
await page.locator(".hk-site-header__download").click();
await page.locator("[data-download-pop]").waitFor();
assert.equal(
await page.locator(".hk-app-download-dialog__download").getAttribute("href"),
origin + "/mock/fallback",
);
await expectOverflow(page, "hidden");
await page.keyboard.press("Shift+Tab");
assert.equal(
await page
.locator(".hk-app-download-dialog__download")
.evaluate((el) => el === document.activeElement),
true,
);
await page.keyboard.press("Tab");
assert.equal(
await page
.locator(".hk-app-download-dialog__close")
.evaluate((el) => el === document.activeElement),
true,
);
await closeGuide(page);
assert.equal(
await page
.locator(".hk-site-header__download")
.evaluate((el) => el === document.activeElement),
true,
);
await page.locator('[data-navigation-key="footer_download"]').click();
await page.locator("button[data-download-close]").click();
await expectOverflow(page, "");
await context.close();
for (const device of ["ios", "android"] as const)
for (const configured of [true, false]) {
const mobile = await browser.newContext({
viewport: { width: 375, height: 812 },
isMobile: true,
deviceScaleFactor: 1,
userAgent:
device === "ios"
? "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"
: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/129.0.0.0 Mobile Safari/537.36",
});
await configure(mobile, true);
const p = await mobile.newPage();
await p.goto(origin + "/?configured=" + (configured ? "1" : "0"));
await ready(p);
await p.locator(".hk-site-header__download").click();
const guide = p.locator("[data-install-guide]");
await guide.waitFor();
await p.waitForFunction(
(platform) =>
document
.querySelector("[data-install-guide]")
?.getAttribute("data-platform") === platform,
device,
);
assert.equal(await p.locator("[data-download-pop]").count(), 0);
await expectOverflow(p, "hidden");
const panel = device === "ios" ? "store" : "android";
assert.equal(
await guide.locator(`[data-install-panel="${panel}"]`).isVisible(),
true,
);
assert.equal(
await guide.locator("[data-install-tabs]").isVisible(),
device === "ios",
);
assert.equal(
await guide
.locator(`[data-install-action="${panel}"]`)
.getAttribute("href"),
origin +
(configured
? device === "ios"
? "/mock/ios-store"
: "/mock/android.apk"
: "/mock/fallback"),
);
await guide.locator(".hk-installation-guide__close").focus();
await p.keyboard.press("Shift+Tab");
assert.equal(
await guide
.locator(
configured
? "[data-install-launch]"
: `[data-install-action="${panel}"]`,
)
.evaluate((el) => el === document.activeElement),
true,
);
await p.keyboard.press("Tab");
assert.equal(
await guide
.locator(".hk-installation-guide__close")
.evaluate((el) => el === document.activeElement),
true,
);
if (device === "ios") {
await guide.locator('[data-install-tab="store"]').focus();
await p.keyboard.press("ArrowRight");
assert.equal(
await guide
.locator('[data-install-tab="profile"]')
.getAttribute("aria-selected"),
"true",
);
assert.equal(
await guide.locator('[data-install-panel="profile"]').isVisible(),
true,
);
assert.equal(
await guide
.locator('[data-install-action="profile"]')
.getAttribute("href"),
origin + (configured ? "/mock/ios-profile" : "/mock/fallback"),
);
await p.keyboard.press("Home");
assert.equal(
await guide
.locator('[data-install-tab="store"]')
.getAttribute("aria-selected"),
"true",
);
await p.keyboard.press("End");
assert.equal(
await guide
.locator('[data-install-tab="profile"]')
.getAttribute("aria-selected"),
"true",
);
}
const action = guide.locator(
`[data-install-action="${device === "ios" ? "profile" : "android"}"]`,
),
downloadPromise = mobile.waitForEvent("page");
await action.click();
const download = await downloadPromise;
await download.waitForLoadState();
assert.equal(
new URL(download.url()).pathname,
configured
? device === "ios"
? "/mock/ios-profile"
: "/mock/android.apk"
: "/mock/fallback",
);
await download.close();
await closeGuide(p);
assert.equal(
await p
.locator(".hk-site-header__download")
.evaluate((el) => el === document.activeElement),
true,
);
// Drawer swaps to the download guide without leaving its scroll lock active.
await p.locator("[data-drawer-open]").click();
await p.locator("[data-drawer]").waitFor();
await expectOverflow(p, "hidden");
assert.equal(
await p.locator("[data-drawer] :is(.hk-drawer__user, [data-login-open], [data-register-open], a[href^='/account'])").count(),
0,
"Drawer has no user block or account links",
);
if (device === "ios" && configured) {
const group = p.locator('[data-navigation-submenu="mobile"]'),
toggle = group.locator(".hk-drawer__nav-toggle"),
submenu = group.locator(".hk-drawer__submenu");
assert.equal(await toggle.getAttribute("aria-expanded"), "false");
assert.equal(await submenu.isVisible(), false);
assert.equal(await toggle.getAttribute("aria-controls"), await submenu.getAttribute("id"));
await toggle.click();
await submenu.waitFor({ state: "visible" });
assert.equal(await toggle.getAttribute("aria-expanded"), "true");
const friendPromise = mobile.waitForEvent("page");
await submenu.locator('a[data-navigation-name="黄果视频"]').click();
const friend = await friendPromise;
await friend.waitForLoadState();
assert.equal(friend.url(), "https://huangguo.com/");
await friend.close();
await p.waitForSelector("[data-drawer]", { state: "detached" });
await expectOverflow(p, "");
await p.locator("[data-drawer-open]").click();
await toggle.click();
await submenu.waitFor({ state: "visible" });
await toggle.click();
await submenu.waitFor({ state: "hidden" });
await toggle.click();
await p.setViewportSize({ width: 375, height: 480 });
await submenu.locator("a").last().scrollIntoViewIfNeeded();
assert.equal(await submenu.locator("a").last().isVisible(), true);
assert.equal(await p.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
await p.setViewportSize({ width: 375, height: 812 });
await submenu.locator('a[data-navigation-name="爱情专题"]').click();
await p.waitForURL(origin + "/collection/romance/");
await ready(p);
assert.equal(await p.locator("[data-drawer]").count(), 0);
await expectOverflow(p, "");
await p.locator("[data-drawer-open]").click();
await p.locator("[data-drawer]").waitFor();
}
await p.keyboard.press("Escape");
await p.waitForSelector("[data-drawer]", { state: "detached" });
assert.equal(
await p
.locator("[data-drawer-open]")
.evaluate((el) => el === document.activeElement),
true,
);
await expectOverflow(p, "");
await p.locator("[data-drawer-open]").click();
if (device === "ios" && configured) {
const group = p.locator('[data-navigation-submenu="mobile"]');
await group.locator(".hk-drawer__nav-toggle").click();
await group.locator(".hk-drawer__submenu [data-download-open]").click();
} else await p.locator(".hk-drawer__links [data-download-open]").click();
await p.waitForSelector("[data-drawer]", { state: "detached" });
await guide.waitFor();
await expectOverflow(p, "hidden");
await closeGuide(p);
assert.equal(
await p
.locator("[data-drawer-open]")
.evaluate((el) => el === document.activeElement),
true,
);
await p.locator('[data-navigation-key="footer_download_mobile"]').click();
await guide.waitFor();
await guide.locator(".hk-installation-guide__close").click();
await expectOverflow(p, "");
await p.locator(".hk-app-cta__download").click();
await guide.waitFor();
await closeGuide(p);
// Original installation-guide behavior closes across the desktop breakpoint.
await p.locator("body").evaluate((el) => (el.style.overflow = "clip"));
await p.locator(".hk-app-cta__download").click();
await guide.waitFor();
await p.setViewportSize({ width: 1024, height: 900 });
await p.waitForSelector("[data-install-guide], [data-download-pop]", {
state: "detached",
timeout: 5000,
});
await expectOverflow(p, "clip");
await p.locator("body").evaluate((el) => (el.style.overflow = ""));
await p.setViewportSize({ width: 375, height: 812 });
if (configured) {
await p.locator(".hk-site-header__download").click();
await guide.locator("[data-install-launch]").click();
await p.waitForURL(origin + "/mock/launch");
}
assert.equal(
await p.evaluate(
() => document.documentElement.scrollWidth > innerWidth,
),
false,
);
await mobile.close();
}
// Navigation has no site header/age/ad gate, but its download entries work.
const navContext = await browser.newContext({
viewport: { width: 1440, height: 900 },
});
await configure(navContext);
const nav = await navContext.newPage();
await nav.goto(origin + "/navigation/");
await nav.waitForSelector(".hk-site-navigation-app");
assert.equal(
await nav
.locator(".hk-site-header,.hk-site-footer,[data-eighteen],[data-dialog-ad]")
.count(),
0,
);
await nav.locator(".hk-site-navigation-app").click();
await nav.locator("[data-download-pop]").waitFor();
await closeGuide(nav);
await nav.setViewportSize({ width: 375, height: 812 });
await nav.locator(".hk-site-navigation-icon[data-download-open]").click();
await nav.locator("[data-install-guide]").waitFor();
await closeGuide(nav);
await navContext.close();
const underage = await browser.newContext();
await configure(underage);
const exit = await underage.newPage();
await exit.goto(origin);
await exit.locator("[data-eighteen-exit]").click();
await exit.waitForURL("https://www.google.com/");
assert.equal(exit.url(), "https://www.google.com/");
await underage.close();
assert.deepEqual(errors, []);
assert.deepEqual(missing, []);
assert.deepEqual(unexpected, []);
assert.ok(destinations.includes("/mock/ad-1"));
assert.ok(destinations.includes("/mock/ios-profile"));
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.",
);
} finally {
releaseImage?.();
await browser?.close();
await new Promise((done) => server.close(() => done()));
await rm(temporary, { recursive: true, force: true });
}