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, // 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"); await build({ stdin: { resolveDir: root, loader: "tsx", contents: ` import {createRoot} from 'react-dom/client'; import {SiteProvider,useSite} from '@/components/providers/SiteProvider'; 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.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: "首页", 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]; const 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.addInitScript({ 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(), 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"); }; 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 stacked.close(); imageRequested = 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( 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", ); 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", ); 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")); 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. 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 }); }