- 原生 Next.js App Router + 严格 TypeScript 实现花魁短剧 PC/H5 页面,按 src/features/{catalog,content,playback} 组织,保留完整 SEO(metadata、JSON-LD、canonical、robots、llms、sitemap、IndexNow)。
- 主域名通过 SITE_URL=https://huakuidj.com 强制覆盖后端 foreverUrlAddr(config/*.env、src/server/config.ts、src/server/seo/shared.ts、src/server/services/shared.ts),支持邮箱改为 support@huakuidj.com。
- 移除登录与账户体系:删除 /account 全部页面、登录/注册/充值弹窗、收藏点赞、评论、金币解锁及对应 BFF 接口;旧账户地址 GET/HEAD 统一 301 到首页(src/lib/routes.ts REMOVED_ACCOUNT_PATHS),保留游客会话、反馈、图片上传、观看上报与搜索联想。
- 播放页:评论位改为同类推荐 + 热播榜单,锁定集改为 App 观看遮罩与底部弹层,新增简介标签、分集图例与独立视觉皮肤(src/features/playback、src/styles/pages/playback.css)。
- 卡片:短剧/视频卡状态与时长移至信息行胶囊标签,封面角标移至左上角(src/components/catalog/Cards.tsx、src/styles/foundations/base.css)。
- 同步更新单元测试、浏览器冒烟脚本、路由清单与 README。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1072 lines
42 KiB
TypeScript
1072 lines
42 KiB
TypeScript
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 <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);
|
|
`,
|
|
},
|
|
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<string, string> = {
|
|
".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("<p>Isolated destination</p>");
|
|
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(
|
|
'<!doctype html><html data-hk-page-styles="' + siteStyles.tokens + '"><head><meta name="viewport" content="width=device-width,initial-scale=1"><meta charset="utf-8">' +
|
|
`<link rel="stylesheet" href="${siteStyles.href}">` +
|
|
'</head><body class="' +
|
|
(path === "/navigation/" ? "hk-site-navigation-page-body" : "") +
|
|
'"><div id="root"></div><script>window.__fixture=' +
|
|
JSON.stringify(data).replace(/</g, "\\u003c") +
|
|
'</script><script src="/site.js"></script></body></html>',
|
|
);
|
|
} catch {
|
|
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}`;
|
|
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<string, unknown> };
|
|
const events: EventRecord[] = [],
|
|
errors: string[] = [],
|
|
missing: string[] = [],
|
|
unexpected: string[] = [],
|
|
destinations: string[] = [];
|
|
let browser: Awaited<ReturnType<typeof chromium.launch>> | 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<void>((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<HTMLCanvasElement>(".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<HTMLButtonElement>(".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<string, unknown>; config: Record<string, unknown> };
|
|
__rerender: (data: Record<string, unknown>) => 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<string, unknown>; config: Record<string, unknown> };
|
|
__rerender: (data: Record<string, unknown>) => 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<string, unknown>; __rerender: (data: Record<string, unknown>) => 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<string, unknown>) => 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<string, unknown>;
|
|
__rerender: (data: Record<string, unknown>) => void;
|
|
}
|
|
).__rerender({
|
|
...(window as unknown as { __fixture: Record<string, unknown> })
|
|
.__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<void>((done) => server.close(() => done()));
|
|
await rm(temporary, { recursive: true, force: true });
|
|
}
|