562 lines
24 KiB
JavaScript
562 lines
24 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import net from "node:net";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const screenshotDir = path.join(__dirname, "screenshots");
|
|
const baseUrl = process.env.BRAINWAVE_PROTOTYPE_URL ?? "http://127.0.0.1:4173/prototype/";
|
|
|
|
const cases = [
|
|
{ view: "welcome", file: "01-welcome.png", fullPage: true },
|
|
{ view: "question", file: "02-question.png", fullPage: true },
|
|
{ view: "casting", file: "03-casting.png", fullPage: true },
|
|
{ view: "result", file: "04-result-moving.png", fullPage: true },
|
|
{ view: "static", file: "05-result-static.png", fullPage: true },
|
|
{ view: "consent", file: "06-ai-consent.png", fullPage: false },
|
|
{ view: "explanation", file: "07-ai-explanation.png", fullPage: true },
|
|
{ view: "error", file: "08-ai-error.png", fullPage: true },
|
|
{ view: "home", file: "09-home-returning.png", fullPage: true },
|
|
{ view: "home-empty", file: "10-home-empty.png", fullPage: true },
|
|
{ view: "history", file: "11-history-list.png", fullPage: true },
|
|
{ view: "history-empty", file: "12-history-empty.png", fullPage: true },
|
|
{ view: "history-detail", file: "13-history-detail.png", fullPage: true },
|
|
{ view: "settings", file: "14-settings-default.png", fullPage: true },
|
|
{ view: "delete", file: "15-delete-confirm.png", fullPage: false },
|
|
{ view: "settings-off", file: "16-settings-off.png", fullPage: true },
|
|
{ view: "result-not-saved", file: "17-result-not-saved.png", fullPage: true },
|
|
{ view: "clear-all", file: "18-clear-all-confirm.png", fullPage: false }
|
|
];
|
|
|
|
function findChrome() {
|
|
const candidates = process.platform === "win32"
|
|
? [
|
|
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe"
|
|
]
|
|
: process.platform === "darwin"
|
|
? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"]
|
|
: ["/usr/bin/google-chrome", "/usr/bin/chromium", "/usr/bin/chromium-browser"];
|
|
|
|
const executable = candidates.find(existsSync);
|
|
if (!executable) {
|
|
throw new Error("未找到 Chrome/Edge。请安装浏览器,或扩展 capture.mjs 中的候选路径。");
|
|
}
|
|
return executable;
|
|
}
|
|
|
|
function getFreePort() {
|
|
return new Promise((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.unref();
|
|
server.on("error", reject);
|
|
server.listen(0, "127.0.0.1", () => {
|
|
const address = server.address();
|
|
server.close(() => resolve(address.port));
|
|
});
|
|
});
|
|
}
|
|
|
|
function delay(milliseconds) {
|
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
}
|
|
|
|
async function waitForDebugTarget(port) {
|
|
const endpoint = `http://127.0.0.1:${port}/json/list`;
|
|
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
try {
|
|
const response = await fetch(endpoint);
|
|
if (response.ok) {
|
|
const targets = await response.json();
|
|
const page = targets.find((target) => target.type === "page");
|
|
if (page?.webSocketDebuggerUrl) return page.webSocketDebuggerUrl;
|
|
}
|
|
} catch {
|
|
// Chrome may still be starting; retry within the bounded loop.
|
|
}
|
|
await delay(100);
|
|
}
|
|
throw new Error("Chrome DevTools endpoint did not become ready.");
|
|
}
|
|
|
|
class CdpClient {
|
|
constructor(webSocketUrl) {
|
|
this.socket = new WebSocket(webSocketUrl);
|
|
this.nextId = 1;
|
|
this.pending = new Map();
|
|
this.listeners = new Map();
|
|
}
|
|
|
|
async connect() {
|
|
await new Promise((resolve, reject) => {
|
|
this.socket.addEventListener("open", resolve, { once: true });
|
|
this.socket.addEventListener("error", reject, { once: true });
|
|
});
|
|
this.socket.addEventListener("message", (event) => this.#handleMessage(event));
|
|
}
|
|
|
|
#handleMessage(event) {
|
|
const message = JSON.parse(String(event.data));
|
|
if (message.id) {
|
|
const pending = this.pending.get(message.id);
|
|
if (!pending) return;
|
|
this.pending.delete(message.id);
|
|
if (message.error) pending.reject(new Error(message.error.message));
|
|
else pending.resolve(message.result);
|
|
return;
|
|
}
|
|
|
|
const listeners = this.listeners.get(message.method) ?? [];
|
|
for (const listener of listeners) listener(message.params);
|
|
}
|
|
|
|
send(method, params = {}) {
|
|
const id = this.nextId;
|
|
this.nextId += 1;
|
|
return new Promise((resolve, reject) => {
|
|
this.pending.set(id, { resolve, reject });
|
|
this.socket.send(JSON.stringify({ id, method, params }));
|
|
});
|
|
}
|
|
|
|
on(method, listener) {
|
|
const listeners = this.listeners.get(method) ?? [];
|
|
listeners.push(listener);
|
|
this.listeners.set(method, listeners);
|
|
}
|
|
|
|
waitFor(method, timeoutMs = 5000) {
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${method}`)), timeoutMs);
|
|
const listener = (params) => {
|
|
clearTimeout(timeout);
|
|
const listeners = this.listeners.get(method) ?? [];
|
|
this.listeners.set(method, listeners.filter((candidate) => candidate !== listener));
|
|
resolve(params);
|
|
};
|
|
this.on(method, listener);
|
|
});
|
|
}
|
|
|
|
close() {
|
|
this.socket.close();
|
|
}
|
|
}
|
|
|
|
async function evaluate(client, expression) {
|
|
const result = await client.send("Runtime.evaluate", {
|
|
expression,
|
|
returnByValue: true,
|
|
awaitPromise: true
|
|
});
|
|
if (result.exceptionDetails) {
|
|
throw new Error(result.exceptionDetails.text ?? "Runtime evaluation failed.");
|
|
}
|
|
return result.result.value;
|
|
}
|
|
|
|
async function navigate(client, view) {
|
|
const loaded = client.waitFor("Page.loadEventFired");
|
|
await client.send("Page.navigate", { url: `${baseUrl}?view=${view}` });
|
|
await loaded;
|
|
await delay(320);
|
|
}
|
|
|
|
async function capture(client, item) {
|
|
await navigate(client, item.view);
|
|
const dimensions = await evaluate(client, `(() => ({
|
|
width: document.documentElement.scrollWidth,
|
|
height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)
|
|
}))()`);
|
|
const height = item.fullPage ? dimensions.height : 792;
|
|
const screenshot = await client.send("Page.captureScreenshot", {
|
|
format: "png",
|
|
fromSurface: true,
|
|
captureBeyondViewport: true,
|
|
clip: { x: 0, y: 0, width: 360, height, scale: 1 }
|
|
});
|
|
await writeFile(path.join(screenshotDir, item.file), Buffer.from(screenshot.data, "base64"));
|
|
return { view: item.view, file: item.file, width: 360, height };
|
|
}
|
|
|
|
async function auditView(client, view) {
|
|
await navigate(client, view);
|
|
return evaluate(client, `(() => {
|
|
const visible = (element) => {
|
|
const style = getComputedStyle(element);
|
|
const rect = element.getBoundingClientRect();
|
|
return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
|
|
};
|
|
const buttons = [...document.querySelectorAll("button")].filter(visible);
|
|
const touchIssues = buttons.map((button) => {
|
|
const rect = button.getBoundingClientRect();
|
|
return {
|
|
name: button.getAttribute("aria-label") || button.innerText.trim(),
|
|
width: Math.round(rect.width),
|
|
height: Math.round(rect.height)
|
|
};
|
|
}).filter((item) => item.width < 48 || item.height < 48);
|
|
const unnamedButtons = buttons
|
|
.filter((button) => !(button.getAttribute("aria-label") || button.innerText.trim()))
|
|
.length;
|
|
const checkboxes = [...document.querySelectorAll('input[type="checkbox"]')].filter(visible);
|
|
const smallCheckboxLabels = checkboxes.filter((checkbox) => {
|
|
const label = checkbox.closest("label");
|
|
if (!label) return true;
|
|
const rect = label.getBoundingClientRect();
|
|
return rect.width < 48 || rect.height < 48;
|
|
}).length;
|
|
return { view: ${JSON.stringify(view)}, unnamedButtons, touchIssues, smallCheckboxLabels };
|
|
})()`);
|
|
}
|
|
|
|
async function auditLayout(client, { name, width, height, view }) {
|
|
await client.send("Emulation.setDeviceMetricsOverride", {
|
|
width,
|
|
height,
|
|
deviceScaleFactor: 1,
|
|
mobile: false,
|
|
screenWidth: width,
|
|
screenHeight: height
|
|
});
|
|
await navigate(client, view);
|
|
return evaluate(client, `(() => ({
|
|
name: ${JSON.stringify(name)},
|
|
view: ${JSON.stringify(view)},
|
|
viewport: { width: innerWidth, height: innerHeight },
|
|
documentWidth: document.documentElement.scrollWidth,
|
|
horizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
|
mainReachable: Boolean(document.querySelector('#screen'))
|
|
}))()`);
|
|
}
|
|
|
|
async function run() {
|
|
const health = await fetch(baseUrl);
|
|
if (!health.ok) {
|
|
throw new Error(`原型服务不可用:${baseUrl} (${health.status})`);
|
|
}
|
|
|
|
const chrome = findChrome();
|
|
const port = await getFreePort();
|
|
const profileDir = await mkdtemp(path.join(os.tmpdir(), "brainwave-prototype-capture-"));
|
|
const chromeProcess = spawn(chrome, [
|
|
"--headless=new",
|
|
"--disable-gpu",
|
|
"--no-first-run",
|
|
"--no-default-browser-check",
|
|
`--remote-debugging-port=${port}`,
|
|
`--user-data-dir=${profileDir}`,
|
|
"about:blank"
|
|
], { stdio: "ignore", windowsHide: true });
|
|
|
|
let client;
|
|
try {
|
|
const webSocketUrl = await waitForDebugTarget(port);
|
|
client = new CdpClient(webSocketUrl);
|
|
await client.connect();
|
|
await Promise.all([
|
|
client.send("Page.enable"),
|
|
client.send("Runtime.enable"),
|
|
client.send("Network.enable"),
|
|
client.send("Log.enable"),
|
|
client.send("Emulation.setDeviceMetricsOverride", {
|
|
width: 360,
|
|
height: 792,
|
|
deviceScaleFactor: 1,
|
|
mobile: false,
|
|
screenWidth: 360,
|
|
screenHeight: 792
|
|
})
|
|
]);
|
|
|
|
const consoleErrors = [];
|
|
const runtimeErrors = [];
|
|
const externalRequests = [];
|
|
client.on("Log.entryAdded", ({ entry }) => {
|
|
if (entry.level === "error") consoleErrors.push(entry.text);
|
|
});
|
|
client.on("Runtime.exceptionThrown", ({ exceptionDetails }) => {
|
|
runtimeErrors.push(exceptionDetails.exception?.description ?? exceptionDetails.text);
|
|
});
|
|
client.on("Network.requestWillBeSent", ({ request }) => {
|
|
const url = new URL(request.url);
|
|
if (!["127.0.0.1", "localhost"].includes(url.hostname) && url.protocol !== "data:") {
|
|
externalRequests.push(request.url);
|
|
}
|
|
});
|
|
|
|
await mkdir(screenshotDir, { recursive: true });
|
|
const screenshots = [];
|
|
for (const item of cases) screenshots.push(await capture(client, item));
|
|
|
|
await navigate(client, "casting");
|
|
for (let remainingRound = 0; remainingRound < 4; remainingRound += 1) {
|
|
for (let coinIndex = 0; coinIndex < 3; coinIndex += 1) {
|
|
await evaluate(client, `document.querySelector('[data-action="toggle-coin"][data-index="${coinIndex}"]')?.click()`);
|
|
}
|
|
await evaluate(client, "document.querySelector('[data-action=\"confirm-line\"]')?.click()");
|
|
}
|
|
const completedCastingSave = await evaluate(client, `(() => ({
|
|
resultVisible: Boolean(document.querySelector('#result-title')),
|
|
status: document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, " ").trim()
|
|
}))()`);
|
|
|
|
await navigate(client, "result");
|
|
const movingResult = await evaluate(client, `(() => ({
|
|
names: [...document.querySelectorAll(".hexagram-name")].map((node) => node.textContent.trim()),
|
|
numbers: [...document.querySelectorAll(".hexagram-number")].map((node) => node.textContent.trim()),
|
|
moving: document.querySelector(".result-kicker")?.textContent.replace(/\\s+/g, " ").trim()
|
|
}))()`);
|
|
|
|
await navigate(client, "static");
|
|
const staticResult = await evaluate(client, `(() => ({
|
|
names: [...document.querySelectorAll(".hexagram-name")].map((node) => node.textContent.trim()),
|
|
transformCount: document.querySelectorAll(".transform-arrow").length,
|
|
label: document.querySelector(".static-indicator")?.textContent.trim()
|
|
}))()`);
|
|
|
|
await navigate(client, "consent");
|
|
const consentBefore = await evaluate(client, "document.querySelector('[data-action=\"confirm-ai\"]')?.disabled");
|
|
await evaluate(client, `(() => {
|
|
document.querySelector('[data-action="consent-toggle"]').click();
|
|
return document.querySelector('[data-action="confirm-ai"]').disabled;
|
|
})()`);
|
|
const consentAfter = await evaluate(client, "document.querySelector('[data-action=\"confirm-ai\"]')?.disabled");
|
|
await evaluate(client, "document.querySelector('[data-action=\"confirm-ai\"]')?.click()");
|
|
await delay(1050);
|
|
const explanationSource = await evaluate(client, "document.querySelector('.source-badge')?.textContent.trim()");
|
|
const explanationSaveStatus = await evaluate(client, "document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, ' ').trim()");
|
|
|
|
await navigate(client, "home");
|
|
const homeState = await evaluate(client, `(() => ({
|
|
primaryActions: document.querySelectorAll('[data-action="begin"]').length,
|
|
recentRoute: document.querySelector('.recent-card__route')?.textContent.replace(/\\s+/g, " ").trim(),
|
|
historyEntry: document.querySelectorAll('[data-action="open-history"]').length,
|
|
settingsEntry: document.querySelectorAll('[data-action="open-settings"]').length,
|
|
localNotice: document.querySelector('[data-local-data-notice]')?.textContent.replace(/\\s+/g, " ").trim(),
|
|
bottomNavigation: document.querySelectorAll('nav, [role="navigation"]').length
|
|
}))()`);
|
|
await evaluate(client, "document.querySelector('[data-action=\"begin\"]')?.click()");
|
|
const homeForwardTitle = await evaluate(client, "document.querySelector('#question-title')?.textContent.trim()");
|
|
await evaluate(client, "document.querySelector('[data-action=\"back\"]')?.click()");
|
|
const homeBackTitle = await evaluate(client, "document.querySelector('#home-title')?.textContent.trim()");
|
|
|
|
await navigate(client, "history");
|
|
const historyState = await evaluate(client, `(() => ({
|
|
count: document.querySelectorAll('.history-card').length,
|
|
firstRoute: document.querySelector('.history-card__route')?.textContent.replace(/\\s+/g, " ").trim()
|
|
}))()`);
|
|
|
|
await navigate(client, "history-empty");
|
|
const historyEmptyState = await evaluate(client, `(() => ({
|
|
hasEmptyGuidance: Boolean(document.querySelector('[data-history-empty]')),
|
|
hasPrimaryAction: Boolean(document.querySelector('[data-action="begin"]')),
|
|
hasSettingsEntry: Boolean(document.querySelector('[data-action="open-settings"]'))
|
|
}))()`);
|
|
|
|
await navigate(client, "settings");
|
|
const settingsDefaults = await evaluate(client, `(() => ({
|
|
autoSave: document.querySelector('[data-action="auto-save-toggle"]')?.checked,
|
|
question: document.querySelector('[data-action="save-question-setting"]')?.checked,
|
|
explanation: document.querySelector('[data-action="save-explanation-setting"]')?.checked,
|
|
action: document.querySelector('[data-action="save-action-setting"]')?.checked,
|
|
detailDisabled: [...document.querySelectorAll('.settings-card__details input')].some((input) => input.disabled),
|
|
boundary: document.querySelector('.settings-boundary')?.textContent.replace(/\\s+/g, " ").trim()
|
|
}))()`);
|
|
await evaluate(client, "document.querySelector('[data-action=\"auto-save-toggle\"]')?.click()");
|
|
await delay(100);
|
|
const settingsDisabled = await evaluate(client, `(() => ({
|
|
autoSave: document.querySelector('[data-action="auto-save-toggle"]')?.checked,
|
|
detailsDisabled: [...document.querySelectorAll('.settings-card__details input')].every((input) => input.disabled),
|
|
retainedValues: [...document.querySelectorAll('.settings-card__details input')].every((input) => input.checked)
|
|
}))()`);
|
|
|
|
await navigate(client, "result");
|
|
const autoSaveBefore = await evaluate(client, `(() => ({
|
|
status: document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, " ").trim(),
|
|
canOptOut: Boolean(document.querySelector('[data-action="remove-current-record"]'))
|
|
}))()`);
|
|
await evaluate(client, "document.querySelector('[data-action=\"remove-current-record\"]')?.click()");
|
|
const optOutDialogVisible = await evaluate(client, "Boolean(document.querySelector('[role=\"alertdialog\"]'))");
|
|
await evaluate(client, "document.querySelector('[data-action=\"confirm-delete\"]')?.click()");
|
|
await delay(100);
|
|
const afterOptOut = await evaluate(client, `(() => ({
|
|
status: document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, " ").trim(),
|
|
canSave: Boolean(document.querySelector('[data-action="save-current"]'))
|
|
}))()`);
|
|
await evaluate(client, "document.querySelector('[data-action=\"save-current\"]')?.click()");
|
|
await delay(100);
|
|
const afterManualSave = await evaluate(client, "document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, ' ').trim()");
|
|
|
|
await navigate(client, "result-not-saved");
|
|
const autoSaveOffResult = await evaluate(client, `(() => ({
|
|
status: document.querySelector('[data-current-save-status]')?.textContent.replace(/\\s+/g, " ").trim(),
|
|
canSave: Boolean(document.querySelector('[data-action="save-current"]'))
|
|
}))()`);
|
|
|
|
await navigate(client, "clear-all");
|
|
const clearBefore = await evaluate(client, `(() => ({
|
|
dialogVisible: Boolean(document.querySelector('[role="alertdialog"]')),
|
|
countText: document.querySelector('#clear-modal-description')?.textContent.trim()
|
|
}))()`);
|
|
await evaluate(client, "document.querySelector('[data-action=\"confirm-clear-all\"]')?.click()");
|
|
await delay(100);
|
|
const clearAfter = await evaluate(client, `(() => ({
|
|
settingsVisible: document.querySelector('#settings-title')?.textContent.trim(),
|
|
recordCount: document.querySelector('.settings-danger p')?.textContent.trim(),
|
|
clearDisabled: document.querySelector('[data-action="open-clear-all"]')?.disabled,
|
|
autoSaveRetained: document.querySelector('[data-action="auto-save-toggle"]')?.checked,
|
|
modalClosed: !document.querySelector('[role="alertdialog"]')
|
|
}))()`);
|
|
|
|
await navigate(client, "delete");
|
|
const deleteBefore = await evaluate(client, `(() => ({
|
|
dialogVisible: Boolean(document.querySelector('[role="alertdialog"]')),
|
|
detailVisible: Boolean(document.querySelector('#history-detail-title'))
|
|
}))()`);
|
|
await evaluate(client, "document.querySelector('[data-action=\"confirm-delete\"]')?.click()");
|
|
await delay(100);
|
|
const deleteAfter = await evaluate(client, `(() => ({
|
|
onHistory: document.querySelector('#history-title')?.textContent.trim(),
|
|
historyCount: document.querySelectorAll('.history-card').length,
|
|
modalClosed: !document.querySelector('[role="alertdialog"]')
|
|
}))()`);
|
|
|
|
const accessibility = [];
|
|
for (const item of cases) accessibility.push(await auditView(client, item.view));
|
|
|
|
const layoutAudits = [];
|
|
for (const layout of [
|
|
{ name: "small-phone", width: 375, height: 812, view: "home" },
|
|
{ name: "large-phone", width: 430, height: 932, view: "history" },
|
|
{ name: "large-phone-settings", width: 430, height: 932, view: "settings" },
|
|
{ name: "phone-landscape", width: 792, height: 360, view: "history-detail" }
|
|
]) {
|
|
layoutAudits.push(await auditLayout(client, layout));
|
|
}
|
|
|
|
await client.send("Emulation.setEmulatedMedia", {
|
|
features: [{ name: "prefers-reduced-motion", value: "reduce" }]
|
|
});
|
|
await navigate(client, "home");
|
|
const reducedMotion = await evaluate(client, `(() => {
|
|
const page = document.querySelector('.page');
|
|
const duration = Number.parseFloat(getComputedStyle(page).animationDuration) || 0;
|
|
return { animationDurationSeconds: duration, reduced: duration <= 0.001 };
|
|
})()`);
|
|
await client.send("Emulation.setEmulatedMedia", { features: [] });
|
|
|
|
const report = {
|
|
chrome,
|
|
baseUrl,
|
|
screenshots,
|
|
completedCastingSave,
|
|
movingResult,
|
|
staticResult,
|
|
consentGate: {
|
|
disabledBeforeConsent: consentBefore,
|
|
disabledAfterConsent: consentAfter,
|
|
explanationSource,
|
|
explanationSaveStatus
|
|
},
|
|
homeState: { ...homeState, forwardTitle: homeForwardTitle, backTitle: homeBackTitle },
|
|
historyState,
|
|
historyEmptyState,
|
|
settingsFlow: { defaults: settingsDefaults, disabled: settingsDisabled },
|
|
autoSaveFlow: { before: autoSaveBefore, optOutDialogVisible, afterOptOut, afterManualSave, autoSaveOffResult },
|
|
clearAllFlow: { before: clearBefore, after: clearAfter },
|
|
deleteFlow: { before: deleteBefore, after: deleteAfter },
|
|
consoleErrors,
|
|
runtimeErrors,
|
|
externalRequests: [...new Set(externalRequests)],
|
|
accessibility,
|
|
layoutAudits,
|
|
reducedMotion
|
|
};
|
|
|
|
const failed =
|
|
completedCastingSave.resultVisible !== true ||
|
|
!completedCastingSave.status?.includes("已自动保存到本机") ||
|
|
movingResult.names.join(",") !== "复,坤" ||
|
|
movingResult.numbers.join(",") !== "第 24 卦,第 2 卦" ||
|
|
!movingResult.moving.includes("1 个动爻") ||
|
|
staticResult.names.join(",") !== "乾" ||
|
|
staticResult.transformCount !== 0 ||
|
|
consentBefore !== true ||
|
|
consentAfter !== false ||
|
|
explanationSource !== "AI 生成" ||
|
|
!explanationSaveStatus?.includes("解读已加入本次记录") ||
|
|
homeState.primaryActions !== 1 ||
|
|
homeState.recentRoute !== "复 之 坤" ||
|
|
homeState.historyEntry < 1 ||
|
|
homeState.settingsEntry !== 1 ||
|
|
!homeState.localNotice?.includes("起卦与历史默认保存在本机,不主动上传") ||
|
|
!homeState.localNotice?.includes("只有你选择 AI 解读时,本次所需内容才会发送") ||
|
|
homeState.bottomNavigation !== 0 ||
|
|
homeForwardTitle !== "你想看清什么?" ||
|
|
homeBackTitle !== "此刻,有什么想安静看清?" ||
|
|
historyState.count !== 3 ||
|
|
historyState.firstRoute !== "复 之 坤" ||
|
|
historyEmptyState.hasEmptyGuidance !== true ||
|
|
historyEmptyState.hasPrimaryAction !== true ||
|
|
historyEmptyState.hasSettingsEntry !== true ||
|
|
settingsDefaults.autoSave !== true ||
|
|
settingsDefaults.question !== true ||
|
|
settingsDefaults.explanation !== true ||
|
|
settingsDefaults.action !== true ||
|
|
settingsDefaults.detailDisabled !== false ||
|
|
!settingsDefaults.boundary?.includes("默认排除系统备份、设备迁移与云同步") ||
|
|
settingsDisabled.autoSave !== false ||
|
|
settingsDisabled.detailsDisabled !== true ||
|
|
settingsDisabled.retainedValues !== true ||
|
|
!autoSaveBefore.status?.includes("已自动保存到本机") ||
|
|
autoSaveBefore.canOptOut !== true ||
|
|
optOutDialogVisible !== true ||
|
|
!afterOptOut.status?.includes("本次未保存") ||
|
|
!afterOptOut.status?.includes("你已选择不保留这次记录") ||
|
|
afterOptOut.canSave !== true ||
|
|
!afterManualSave?.includes("已保存到本机问卦簿") ||
|
|
!autoSaveOffResult.status?.includes("自动保存已关闭") ||
|
|
autoSaveOffResult.canSave !== true ||
|
|
clearBefore.dialogVisible !== true ||
|
|
!clearBefore.countText?.includes("当前 3 条记录") ||
|
|
clearAfter.settingsVisible !== "保存设置" ||
|
|
!clearAfter.recordCount?.includes("0 条记录") ||
|
|
clearAfter.clearDisabled !== true ||
|
|
clearAfter.autoSaveRetained !== true ||
|
|
clearAfter.modalClosed !== true ||
|
|
deleteBefore.dialogVisible !== true ||
|
|
deleteBefore.detailVisible !== true ||
|
|
deleteAfter.onHistory !== "问卦簿" ||
|
|
deleteAfter.historyCount !== 2 ||
|
|
deleteAfter.modalClosed !== true ||
|
|
consoleErrors.length > 0 ||
|
|
runtimeErrors.length > 0 ||
|
|
externalRequests.length > 0 ||
|
|
accessibility.some((item) =>
|
|
item.unnamedButtons > 0 || item.touchIssues.length > 0 || item.smallCheckboxLabels > 0
|
|
) ||
|
|
layoutAudits.some((item) => item.horizontalOverflow || !item.mainReachable) ||
|
|
reducedMotion.reduced !== true;
|
|
|
|
console.log(JSON.stringify(report, null, 2));
|
|
if (failed) process.exitCode = 1;
|
|
} finally {
|
|
client?.close();
|
|
chromeProcess.kill();
|
|
await delay(150);
|
|
await rm(profileDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
run().catch((error) => {
|
|
console.error(error.stack ?? error.message);
|
|
process.exitCode = 1;
|
|
});
|