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:4178"; const output = path.resolve(process.env.PLAYER_AUDIT_DIR || ".artifacts/player-round-1"); const cases = [ { name: "mobile-390", width: 390, height: 844 }, { name: "desktop-1440", width: 1440, height: 900 } ]; fs.mkdirSync(output, { recursive: true }); const browser = await chromium.launch({ headless: true }); const report = []; for (const profile of cases) { const context = await browser.newContext({ viewport: profile, locale: "zh-CN" }); 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()}`)); await page.goto(`${baseUrl}/#/player/v1`, { waitUntil: "networkidle" }); const geometry = await page.evaluate(() => { const stage = document.querySelector(".player-stage-v2").getBoundingClientRect(); const back = document.querySelector('.player-topbar button[aria-label="返回"]').getBoundingClientRect(); const fullscreen = document.querySelector('.player-controls-row button[aria-label="全屏"]').getBoundingClientRect(); const visibleControls = [...document.querySelectorAll(".player-stage-v2 button")].filter(button => { const box = button.getBoundingClientRect(); const style = getComputedStyle(button); return style.display !== "none" && style.visibility !== "hidden" && box.width > 0 && box.height > 0; }); return { viewportWidth: innerWidth, documentWidth: document.documentElement.scrollWidth, stage: { x: stage.x, width: stage.width, height: stage.height, right: stage.right }, ratio: stage.width / stage.height, backVisible: back.left >= 0 && back.right <= innerWidth, fullscreenVisible: fullscreen.left >= 0 && fullscreen.right <= innerWidth, tapHighlight: getComputedStyle(document.querySelector(".main-play")).webkitTapHighlightColor, controlsBelow44: visibleControls.filter(button => { const box = button.getBoundingClientRect(); return box.width < 44 || box.height < 44; }).length }; }); assert.equal(geometry.documentWidth, geometry.viewportWidth, `${profile.name} 存在横向溢出`); assert.ok(Math.abs(geometry.ratio - 16 / 9) < 0.02, `${profile.name} 播放舞台不是 16:9`); assert.equal(geometry.backVisible, true, `${profile.name} 返回按钮被裁切`); assert.equal(geometry.fullscreenVisible, true, `${profile.name} 全屏按钮被裁切`); if (profile.width <= 720) assert.equal(geometry.controlsBelow44, 0, `${profile.name} 存在小于 44px 的播放器按钮`); if (profile.width <= 720) assert.ok(geometry.tapHighlight === "rgba(0, 0, 0, 0)" || geometry.tapHighlight === "transparent", `${profile.name} 仍存在系统蓝色点击高亮`); assert.equal(await page.locator(".rate-control").textContent(), "1×"); await page.screenshot({ path: path.join(output, `${profile.name}-player.png`), fullPage: true }); await page.locator(".player-tap-surface").click({ position: { x: 10, y: 100 } }); await page.locator(".player-stage-v2.controls-hidden").waitFor({ state: "visible" }); await page.waitForTimeout(150); await page.getByRole("button", { name: "显示播放控件" }).click({ position: { x: 10, y: 100 } }); await page.locator(".player-stage-v2.controls-visible").waitFor({ state: "visible" }); const beforeSkip = Number(await page.locator('.timeline input[type="range"]').inputValue()); await page.getByRole("button", { name: "快进 10 秒" }).click(); const afterSkip = Number(await page.locator('.timeline input[type="range"]').inputValue()); assert.ok(afterSkip > beforeSkip, `${profile.name} 快进没有更新进度`); await page.locator(".rate-control").click(); assert.equal(await page.locator(".rate-control").textContent(), "1.25×"); await page.getByRole("button", { name: "时间点", exact: true }).click(); await page.locator('.player-drawer[aria-label="视频时间点"]').waitFor({ state: "visible" }); await page.waitForTimeout(250); await page.screenshot({ path: path.join(output, `${profile.name}-markers.png`), fullPage: true }); await page.keyboard.press("Escape"); await page.locator(".player-drawer").waitFor({ state: "detached" }); await page.getByRole("button", { name: "合集目录" }).click(); await page.locator('.player-drawer[aria-label="合集目录"]').waitFor({ state: "visible" }); await page.locator(".player-panel-scrim").click({ position: { x: 4, y: 4 } }); await page.locator(".player-drawer").waitFor({ state: "detached" }); await page.locator(".player-title-row").getByRole("button", { name: "喜欢" }).click(); assert.equal(await page.locator(".player-title-row").getByRole("button", { name: "喜欢" }).getAttribute("aria-pressed"), "true"); if (profile.width <= 720) { await page.locator(".main-play").click(); await page.waitForTimeout(3750); await page.locator(".player-stage-v2.controls-hidden").waitFor({ state: "visible" }); await page.locator(".player-tap-surface").click({ position: { x: 10, y: 100 } }); await page.locator(".main-play").click(); await page.waitForTimeout(3750); assert.equal(await page.locator(".player-stage-v2").getAttribute("class").then(value => value.includes("controls-visible")), true, "暂停后控件不应自动隐藏"); } report.push({ ...profile, geometry, errors: [...new Set(errors)] }); await context.close(); } await browser.close(); fs.writeFileSync(path.join(output, "report.json"), JSON.stringify(report, null, 2)); console.log(JSON.stringify(report, null, 2)); assert.equal(report.flatMap(item => item.errors).length, 0, "播放器运行时出现控制台错误");