Files
2026-10-16 14:10:10 +07:00

184 lines
6.9 KiB
TypeScript

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<string, unknown>[] = [];
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 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);
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),
),
}));
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)
throw new Error(`${name} ${path} loaded legacy business scripts`);
if (path === "/") {
const accountEntries = await page.locator(
"[data-login-open], [data-register-open], .hk-user-menu, a[href^='/account']",
).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<HTMLImageElement>(".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<HTMLAnchorElement>('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();
}