import { chromium } from "@playwright/test"; import { mkdir, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; const origin = process.env.SMOKE_URL || "http://localhost:25131"; const output = process.env.SMOKE_OUTPUT_DIR || "/tmp/hkdj-browser-smoke"; await mkdir(output, { recursive: true }); const browser = await chromium.launch({ executablePath: process.env.CHROME_PATH || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", headless: true, }); const results: Record[] = []; try { for (const [name, viewport] of [ ["desktop", { width: 1440, height: 1000 }], ["mobile", { width: 390, height: 844 }], ] as const) { const context = await browser.newContext({ viewport, isMobile: name === "mobile", deviceScaleFactor: 1, }); await context.addInitScript(() => localStorage.setItem("hg_age_ok", "1")); await context.route("**/*", async (route) => { const request = route.request(); const local = new URL(request.url()).origin === origin; // Exercise UI without recording synthetic analytics, purchases or user actions. if ( !local && (request.method() !== "GET" || request.resourceType() === "script") ) return route.abort(); return route.continue(); }); const page = await context.newPage(); const errors: string[] = []; const missingAssets: string[] = []; page.on("pageerror", (error) => errors.push(error.message)); page.on("response", (response) => { if ( response.status() >= 400 && response.url().startsWith(origin + "/static/") ) missingAssets.push(new URL(response.url()).pathname); }); for (const path of [ "/", "/dramas/", "/discover/search/", ]) { const response = await page.goto(origin + path, { waitUntil: "domcontentloaded", timeout: 40000, }); await page.waitForTimeout(500); if (path !== "/discover/search/") await page.waitForFunction( () => typeof (window as unknown as Record).tracker === "object", null, { timeout: 15000 }, ); for (let index = 0; index < 12; index++) { const close = page.locator( "[data-dialog-ad].hk-is-open [data-dialog-ad-item]:not([hidden]) [data-dialog-ad-close]", ); if (!(await close.count()) || !(await close.first().isVisible())) break; await close.first().click({ force: true }); } const layout = await page.evaluate(() => ({ viewport: innerWidth, document: document.documentElement.scrollWidth, heading: document.querySelector("h1")?.textContent?.trim() || "", legacyScripts: Array.from(document.scripts).some((script) => /\/static\/js\//.test(script.src), ), tracker: typeof (window as unknown as Record).tracker === "object", })); results.push({ device: name, path, status: response?.status(), ...layout, }); if (response?.status() !== 200) throw new Error(`${name} ${path} returned ${response?.status()}`); if (layout.document > layout.viewport + 1) throw new Error( `${name} ${path} overflows horizontally: ${layout.document}`, ); if ( layout.legacyScripts || (path !== "/discover/search/" && !layout.tracker) ) throw new Error(`${name} ${path} missing shared client runtime`); if (path === "/") { const accountEntries = await page.locator( "[data-login-open], [data-register-open], .hk-user-menu, a[href^='/account']", ).count(); if (accountEntries) throw new Error(`${name} home still renders ${accountEntries} account entries`); if (name === "desktop" && await page.locator("[data-hero]").count()) { await page.hover("[data-hero]"); await page.waitForFunction(() => { const slide = document.querySelector(".hk-home-hero__slide.hk-is-active"); const image = slide?.querySelector(".hk-home-hero__poster"); return !!image && image.complete && image.naturalWidth > 0 && !image.hasAttribute("data-decrypt-failed") && !image.currentSrc.includes("/static/brand/placeholder.svg") && getComputedStyle(image).opacity === "1" && getComputedStyle(slide!).opacity === "1"; }, null, { timeout: 45000 }); } await page.screenshot({ path: resolve(output, name + "-home.png"), fullPage: false, animations: "disabled", }); if (name === "mobile") { await page.locator("[data-drawer-open]").click(); if ( !(await page .locator("[data-drawer]") .evaluate((node) => node.classList.contains("hk-is-open"))) ) throw new Error("Mobile drawer did not open"); await page.locator(".hk-drawer__close").click(); } else { const input = page.locator("[data-header-search-input]"); const suggestionResponse = page.waitForResponse( (response) => response.url().includes("/api/web/search/suggest"), { timeout: 15000 }, ); await input.fill("测试"); await suggestionResponse; if ( !(await page.locator("[data-header-search-dropdown]").isVisible()) ) throw new Error("Search suggestions did not open"); const download = page.locator("[data-download-open]").first(); if (await download.isVisible()) { await download.click(); if ( !(await page .locator("[data-download-pop]") .evaluate((node) => node.classList.contains("hk-is-open"))) ) throw new Error("Download dialog did not open"); await page.keyboard.press("Escape"); } } } } for (const removed of ["/account/", "/account/settings/", "/account/recharge/", "/login/"]) { const response = await context.request.get(origin + removed, { maxRedirects: 0 }); if (response.status() !== 301 || response.headers().location !== "/") throw new Error(`${name} ${removed} should 301 to /, got ${response.status()} ${response.headers().location}`); } const playURL = await page.evaluate(() => document .querySelector('a[href^="/drama/"]') ?.getAttribute("href"), ); if (playURL) { await page.goto(origin + playURL, { waitUntil: "domcontentloaded" }); const player = page.locator("[data-play-player]"); results.push({ device: name, path: "play", player: await player.count(), xgplayer: await page.locator(".xgplayer").count(), video: await page.locator("video").count(), }); } if (errors.length || missingAssets.length) throw new Error(JSON.stringify({ device: name, errors, missingAssets })); await context.close(); } await writeFile( resolve(output, "results.json"), JSON.stringify(results, null, 2), ); console.log( JSON.stringify( { pages: results.length, devices: 2, errors: 0, output }, null, 2, ), ); } finally { await browser.close(); }