Files

139 lines
6.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from "node:fs";
import path from "node:path";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
const require = createRequire(new URL("../frontend/package.json", import.meta.url));
const { chromium } = require("playwright");
const baseUrl = process.env.PROTOTYPE_URL || "http://127.0.0.1:4177";
const output = path.resolve(process.env.PROTOTYPE_AUDIT_DIR || ".artifacts/round-2");
const routes = ["home", "search", "library", "collections", "uploads", "tags", "people", "profile", "settings", "player/v1"];
const cases = [
[390, 844, "mobile-390"],
[1440, 900, "desktop-1440"]
];
fs.mkdirSync(output, { recursive: true });
const browser = await chromium.launch({ headless: true });
const report = [];
const interactions = [];
async function check(name, action) {
try {
await action();
interactions.push({ name, status: "passed" });
} catch (error) {
interactions.push({ name, status: "failed", error: error.message });
}
}
for (const [width, height, name] of cases) {
const context = await browser.newContext({ viewport: { width, height }, locale: "zh-CN", colorScheme: "light" });
const page = await context.newPage();
const errors = [];
page.on("pageerror", error => errors.push(`pageerror: ${error.message}`));
page.on("console", message => message.type() === "error" && errors.push(`console: ${message.text()}`));
const audits = [];
for (const route of routes) {
await page.goto(`${baseUrl}/#/${route}`, { waitUntil: "networkidle" });
await page.waitForTimeout(250);
const geometry = await page.evaluate(() => {
const buttons = [...document.querySelectorAll("button")].filter(button => {
const style = getComputedStyle(button);
const box = button.getBoundingClientRect();
return style.display !== "none" && box.width > 0 && box.height > 0;
});
return {
documentWidth: document.documentElement.scrollWidth,
viewportWidth: document.documentElement.clientWidth,
bodyWidth: document.body.scrollWidth,
horizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth,
visibleButtons: buttons.length,
buttonsBelow44: buttons.filter(button => {
const box = button.getBoundingClientRect();
return box.width < 44 || box.height < 44;
}).length
};
});
audits.push({ route, ...geometry });
assert.equal(geometry.horizontalOverflow, false, `${name} ${route} 存在横向溢出`);
await page.screenshot({ path: path.join(output, `${name}-${route.replace("/", "-")}.png`), fullPage: true });
}
await page.goto(`${baseUrl}/#/home`, { waitUntil: "networkidle" });
if (width <= 720) {
await check(`${name} 更多面板支持打开、Escape 关闭与页面导航`, async () => {
await page.locator(".bottom-nav button").last().click();
await page.locator(".more-sheet").waitFor({ state: "visible" });
await page.screenshot({ path: path.join(output, `${name}-more.png`), fullPage: true });
await page.keyboard.press("Escape");
await page.locator(".more-sheet").waitFor({ state: "detached" });
await page.locator(".bottom-nav button").last().click();
await page.locator(".more-sheet").getByRole("button", { name: "资料库" }).click();
await page.waitForURL(/#\/library$/);
await page.goto(`${baseUrl}/#/home`, { waitUntil: "networkidle" });
});
await page.locator(".upload-fab").click();
} else {
await page.getByRole("button", { name: "上传中心" }).first().click();
await page.getByRole("button", { name: "继续上传" }).click();
}
await page.locator(".upload-sheet").waitFor({ state: "visible" });
await page.screenshot({ path: path.join(output, `${name}-upload-sheet.png`), fullPage: true });
await check(`${name} 上传弹窗可进入上传状态并转入后台`, async () => {
await page.getByRole("button", { name: "选择演示文件" }).click();
await page.locator(".selected-upload").waitFor({ state: "visible" });
await page.getByRole("button", { name: "关闭并在后台继续" }).click();
await page.locator(".upload-sheet").waitFor({ state: "detached" });
await page.locator(".toast").waitFor({ state: "visible" });
});
await check(`${name} 搜索结果跳转到播放器`, async () => {
await page.goto(`${baseUrl}/#/search`, { waitUntil: "networkidle" });
await page.locator(".result-row").first().click();
await page.waitForURL(/#\/player\//);
await page.locator(".player-stage").waitFor({ state: "visible" });
});
await page.goto(`${baseUrl}/#/player/v1`, { waitUntil: "networkidle" });
await check(`${name} 播放、倍速和时间点控件`, async () => {
await page.locator(".main-play").click();
await page.locator(".main-play .lucide-pause").waitFor({ state: "visible" });
await page.getByRole("button", { name: "1×", exact: true }).click();
await page.getByRole("button", { name: "1.5×", exact: true }).waitFor({ state: "visible" });
await page.getByRole("button", { name: "时间点", exact: true }).click();
await page.locator(".player-drawer").waitFor({ state: "visible" });
await page.locator(".timeline>button").first().click({ force: true });
assert.equal(await page.locator('.timeline input[type="range"]').inputValue(), "18");
});
await page.screenshot({ path: path.join(output, `${name}-player-markers.png`), fullPage: true });
await page.goto(`${baseUrl}/#/settings`, { waitUntil: "networkidle" });
await page.evaluate(() => { document.documentElement.dataset.theme = "dark"; });
await page.screenshot({ path: path.join(output, `${name}-settings-dark.png`), fullPage: true });
await check(`${name} 原型控制器切换主题与加载/空/错误状态`, async () => {
await page.goto(`${baseUrl}/?prototype=1#/settings`, { waitUntil: "networkidle" });
await page.getByRole("button", { name: "原型" }).click();
await page.getByLabel("主题").selectOption("dark");
assert.equal(await page.locator("html").getAttribute("data-theme"), "dark");
await page.getByLabel("数据状态").selectOption("loading");
await page.locator(".skeleton-grid").waitFor({ state: "visible" });
await page.getByLabel("数据状态").selectOption("empty");
await page.locator(".empty-state").waitFor({ state: "visible" });
await page.getByLabel("数据状态").selectOption("error");
await page.locator(".error-state").waitFor({ state: "visible" });
});
report.push({ name, viewport: { width, height }, audits, errors: [...new Set(errors)] });
await context.close();
}
await browser.close();
const result = { report, interactions };
fs.writeFileSync(path.join(output, "report.json"), JSON.stringify(result, null, 2));
console.log(JSON.stringify(result, null, 2));
const failures = interactions.filter(item => item.status === "failed");
if (failures.length) process.exitCode = 1;