139 lines
6.8 KiB
JavaScript
139 lines
6.8 KiB
JavaScript
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;
|