184 lines
6.9 KiB
TypeScript
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();
|
|
}
|