526 lines
22 KiB
JavaScript
526 lines
22 KiB
JavaScript
'use strict';
|
||
|
||
const childProcess = require('child_process');
|
||
const fs = require('fs');
|
||
const http = require('http');
|
||
const net = require('net');
|
||
const os = require('os');
|
||
const path = require('path');
|
||
const WebSocket = require('ws');
|
||
|
||
const REPO_DIR = path.resolve(__dirname, '..', '..');
|
||
const OUTPUT_DIR = path.join(__dirname, 'screenshots');
|
||
const REPORT_PATH = path.join(__dirname, 'visual-report.json');
|
||
const CHROME_PATH = process.env.CHROME_PATH
|
||
|| '/home/hdzx/.cache/ms-playwright/chromium-1228/chrome-linux64/chrome';
|
||
const PASSWORD = 'UsageVisual!234';
|
||
const THEMES = [
|
||
'washi', 'coolvibe', 'editorial', 'sage', 'ink', 'dawn',
|
||
'carbon', 'nocturne', 'cinder', 'gilded', 'wasteland',
|
||
];
|
||
|
||
function sleep(ms) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
|
||
async 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));
|
||
});
|
||
});
|
||
}
|
||
|
||
async function getJson(url) {
|
||
return new Promise((resolve, reject) => {
|
||
const request = http.get(url, (response) => {
|
||
let body = '';
|
||
response.setEncoding('utf8');
|
||
response.on('data', (chunk) => { body += chunk; });
|
||
response.on('end', () => {
|
||
try {
|
||
resolve(JSON.parse(body));
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
});
|
||
});
|
||
request.on('error', reject);
|
||
request.setTimeout(2000, () => request.destroy(new Error(`请求超时:${url}`)));
|
||
});
|
||
}
|
||
|
||
async function waitFor(check, timeoutMs = 15000, intervalMs = 80) {
|
||
const started = Date.now();
|
||
let lastError = null;
|
||
while (Date.now() - started < timeoutMs) {
|
||
try {
|
||
const value = await check();
|
||
if (value) return value;
|
||
} catch (error) {
|
||
lastError = error;
|
||
}
|
||
await sleep(intervalMs);
|
||
}
|
||
throw lastError || new Error(`等待条件超时(${timeoutMs}ms)`);
|
||
}
|
||
|
||
function writeUsageFixtures(sessionsDir) {
|
||
const tools = [
|
||
['search-server', 'web_search'],
|
||
['data-server', 'query_data'],
|
||
['code-server', 'run_code'],
|
||
['doc-server', 'fetch_doc'],
|
||
['api-server', 'api_request'],
|
||
['file-server', 'read_file'],
|
||
['memory-server', 'remember'],
|
||
['browser-server', 'open_page'],
|
||
['terminal-server', 'run_task'],
|
||
['github-server', 'list_issues'],
|
||
];
|
||
const skills = ['数据分析', '报表生成', '文档总结', '代码解释', '翻译助手'];
|
||
for (let day = 1; day <= 31; day += 1) {
|
||
const messages = [];
|
||
const messageCount = 7 + (day % 7);
|
||
for (let index = 0; index < messageCount; index += 1) {
|
||
const hour = 1 + (index % 20);
|
||
const minute = (index * 7) % 60;
|
||
const timestamp = new Date(Date.UTC(2026, 6, day, hour, minute)).toISOString();
|
||
const skill = skills[(day + index) % skills.length];
|
||
messages.push({
|
||
role: 'user',
|
||
content: `视觉验收消息 ${day}-${index}`,
|
||
timestamp,
|
||
crossConversation: (day + index) % 4 === 0,
|
||
composerMentions: index % 2 === 0
|
||
? [{ kind: 'skill', name: skill, label: `$${skill}` }]
|
||
: [],
|
||
});
|
||
|
||
if (index % 2 === 0) {
|
||
const [server, tool] = tools[(day + index) % tools.length];
|
||
const failed = (day + index) % 9 === 0;
|
||
const other = !failed && (day + index) % 13 === 0;
|
||
messages.push({
|
||
role: 'assistant',
|
||
content: `视觉验收回复 ${day}-${index}`,
|
||
timestamp: new Date(Date.UTC(2026, 6, day, hour, minute + 1)).toISOString(),
|
||
toolCalls: [{
|
||
name: 'McpToolCall',
|
||
kind: 'mcp_tool_call',
|
||
input: { server, tool, arguments: { fixture: true } },
|
||
result: 'visual-fixture-result',
|
||
done: !other,
|
||
meta: {
|
||
kind: 'mcp_tool_call',
|
||
status: failed ? 'failed' : other ? 'pending' : 'completed',
|
||
},
|
||
}],
|
||
});
|
||
}
|
||
}
|
||
const created = new Date(Date.UTC(2026, 6, day, 0, 0)).toISOString();
|
||
const updated = new Date(Date.UTC(2026, 6, day, 23, 30)).toISOString();
|
||
const id = `visual-usage-${String(day).padStart(2, '0')}`;
|
||
fs.writeFileSync(path.join(sessionsDir, `${id}.json`), JSON.stringify({
|
||
id,
|
||
title: `使用统计视觉验收会话 ${String(day).padStart(2, '0')}`,
|
||
agent: day % 2 === 0 ? 'codexapp' : 'claude',
|
||
cwd: `/tmp/usage-visual/project-${day % 4}`,
|
||
projectName: `视觉项目 ${day % 4}`,
|
||
created,
|
||
updated,
|
||
messages,
|
||
}, null, 2));
|
||
}
|
||
}
|
||
|
||
async function connectCdp(webSocketUrl) {
|
||
const socket = new WebSocket(webSocketUrl);
|
||
await new Promise((resolve, reject) => {
|
||
socket.once('open', resolve);
|
||
socket.once('error', reject);
|
||
});
|
||
let sequence = 0;
|
||
const pending = new Map();
|
||
const rejectPending = (error) => {
|
||
for (const entry of pending.values()) entry.reject(error);
|
||
pending.clear();
|
||
};
|
||
socket.on('message', (payload) => {
|
||
const message = JSON.parse(String(payload));
|
||
if (!message.id || !pending.has(message.id)) return;
|
||
const entry = pending.get(message.id);
|
||
pending.delete(message.id);
|
||
if (message.error) entry.reject(new Error(message.error.message || JSON.stringify(message.error)));
|
||
else entry.resolve(message.result || {});
|
||
});
|
||
socket.on('close', () => rejectPending(new Error('Chrome CDP 连接已关闭')));
|
||
socket.on('error', (error) => rejectPending(error));
|
||
return {
|
||
socket,
|
||
send(method, params = {}) {
|
||
sequence += 1;
|
||
const id = sequence;
|
||
return new Promise((resolve, reject) => {
|
||
pending.set(id, { resolve, reject });
|
||
socket.send(JSON.stringify({ id, method, params }), (error) => {
|
||
if (!error) return;
|
||
pending.delete(id);
|
||
reject(error);
|
||
});
|
||
});
|
||
},
|
||
};
|
||
}
|
||
|
||
async function evaluate(cdp, expression) {
|
||
const result = await cdp.send('Runtime.evaluate', {
|
||
expression,
|
||
returnByValue: true,
|
||
awaitPromise: true,
|
||
});
|
||
if (result.exceptionDetails) {
|
||
throw new Error(result.exceptionDetails.text || '页面脚本执行失败');
|
||
}
|
||
return result.result?.value;
|
||
}
|
||
|
||
async function setViewport(cdp, width, height) {
|
||
await cdp.send('Emulation.setDeviceMetricsOverride', {
|
||
width,
|
||
height,
|
||
deviceScaleFactor: 1,
|
||
mobile: false,
|
||
screenWidth: width,
|
||
screenHeight: height,
|
||
});
|
||
await sleep(120);
|
||
}
|
||
|
||
async function capture(cdp, fileName) {
|
||
const result = await cdp.send('Page.captureScreenshot', {
|
||
format: 'png',
|
||
fromSurface: true,
|
||
captureBeyondViewport: false,
|
||
});
|
||
fs.writeFileSync(path.join(OUTPUT_DIR, fileName), Buffer.from(result.data, 'base64'));
|
||
}
|
||
|
||
async function stopChildProcess(child) {
|
||
if (!child || child.exitCode !== null) return;
|
||
const exited = new Promise((resolve) => child.once('exit', resolve));
|
||
child.kill('SIGTERM');
|
||
await Promise.race([exited, sleep(2000)]);
|
||
}
|
||
|
||
async function collectGeometry(cdp, theme, width, height) {
|
||
return evaluate(cdp, `(() => {
|
||
const rect = (selector) => {
|
||
const element = document.querySelector(selector);
|
||
if (!element) return null;
|
||
const box = element.getBoundingClientRect();
|
||
return {
|
||
x: Math.round(box.x * 10) / 10,
|
||
y: Math.round(box.y * 10) / 10,
|
||
width: Math.round(box.width * 10) / 10,
|
||
height: Math.round(box.height * 10) / 10,
|
||
};
|
||
};
|
||
const metricRects = Array.from(document.querySelectorAll('.usage-dashboard__metric')).map((element) => {
|
||
const box = element.getBoundingClientRect();
|
||
return { x: Math.round(box.x), y: Math.round(box.y), width: Math.round(box.width), height: Math.round(box.height) };
|
||
});
|
||
const overflowSelectors = [
|
||
'.usage-dashboard__metric-label',
|
||
'.usage-dashboard__metric-copy strong',
|
||
'.usage-dashboard__panel-heading h3',
|
||
'.usage-dashboard__tool-name',
|
||
'.usage-dashboard__rank-list strong',
|
||
'.usage-dashboard__session-link',
|
||
];
|
||
const textOverflow = overflowSelectors.flatMap((selector) => Array.from(document.querySelectorAll(selector))
|
||
.filter((element) => element.scrollWidth > element.clientWidth + 1)
|
||
.map((element) => ({ selector, text: element.textContent.trim().slice(0, 80) })));
|
||
const dashboard = document.querySelector('.usage-dashboard');
|
||
const body = document.querySelector('.usage-dashboard__body');
|
||
const sparklineCanvases = document.querySelectorAll('[data-usage-spark] canvas').length;
|
||
const donutCanvases = document.querySelectorAll('.usage-dashboard__donut-chart canvas').length;
|
||
return {
|
||
theme: ${JSON.stringify(theme)},
|
||
viewport: { width: ${width}, height: ${height} },
|
||
dashboard: rect('.usage-dashboard'),
|
||
header: rect('.usage-dashboard__header'),
|
||
toolbar: rect('.usage-dashboard__toolbar'),
|
||
metricGrid: rect('.usage-dashboard__metric-grid'),
|
||
metrics: metricRects,
|
||
primary: rect('.usage-dashboard__content-grid--primary'),
|
||
trend: rect('.usage-dashboard__panel--trend'),
|
||
mcp: rect('.usage-dashboard__panel--mcp'),
|
||
lower: rect('.usage-dashboard__content-grid--lower'),
|
||
status: rect('.usage-dashboard__panel--status'),
|
||
projects: rect('.usage-dashboard__panel--projects'),
|
||
sessions: rect('.usage-dashboard__panel--sessions'),
|
||
dashboardOverflowX: Math.max(0, dashboard.scrollWidth - dashboard.clientWidth),
|
||
bodyOverflowX: Math.max(0, body.scrollWidth - body.clientWidth),
|
||
bodyScrollHeight: body.scrollHeight,
|
||
bodyClientHeight: body.clientHeight,
|
||
metricColumns: getComputedStyle(document.querySelector('.usage-dashboard__metric-grid')).gridTemplateColumns,
|
||
primaryColumns: getComputedStyle(document.querySelector('.usage-dashboard__content-grid--primary')).gridTemplateColumns,
|
||
panelBackground: getComputedStyle(document.querySelector('.usage-dashboard__panel')).backgroundColor,
|
||
pageBackground: getComputedStyle(dashboard).backgroundColor,
|
||
charts: {
|
||
runtime: typeof window.echarts?.init === 'function',
|
||
sparklines: sparklineCanvases,
|
||
trend: document.querySelectorAll('#usage-dashboard-trend canvas').length,
|
||
donuts: donutCanvases,
|
||
ready: sparklineCanvases === 5
|
||
&& document.querySelectorAll('#usage-dashboard-trend canvas').length === 1
|
||
&& donutCanvases === 3,
|
||
},
|
||
skillTextVisible: /Skill/i.test(dashboard.innerText),
|
||
textOverflow,
|
||
};
|
||
})()`);
|
||
}
|
||
|
||
async function main() {
|
||
if (!fs.existsSync(CHROME_PATH)) throw new Error(`找不到 Chrome:${CHROME_PATH}`);
|
||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-usage-visual-'));
|
||
const configDir = path.join(tempRoot, 'config');
|
||
const sessionsDir = path.join(tempRoot, 'sessions');
|
||
const logsDir = path.join(tempRoot, 'logs');
|
||
const profileDir = path.join(tempRoot, 'chrome-profile');
|
||
[configDir, sessionsDir, logsDir, profileDir].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
|
||
writeUsageFixtures(sessionsDir);
|
||
|
||
const port = await getFreePort();
|
||
const debugPort = await getFreePort();
|
||
let server = null;
|
||
let chrome = null;
|
||
let cdp = null;
|
||
try {
|
||
server = childProcess.spawn(process.execPath, ['server.js'], {
|
||
cwd: REPO_DIR,
|
||
env: {
|
||
...process.env,
|
||
PORT: String(port),
|
||
CC_WEB_PASSWORD: PASSWORD,
|
||
CC_WEB_CONFIG_DIR: configDir,
|
||
CC_WEB_SESSIONS_DIR: sessionsDir,
|
||
CC_WEB_LOGS_DIR: logsDir,
|
||
CC_WEB_USAGE_STATISTICS: '1',
|
||
},
|
||
stdio: ['ignore', 'pipe', 'pipe'],
|
||
});
|
||
let serverError = '';
|
||
server.stderr.on('data', (chunk) => { serverError += String(chunk); });
|
||
await waitFor(() => new Promise((resolve) => {
|
||
const request = http.get(`http://127.0.0.1:${port}/`, (response) => {
|
||
response.resume();
|
||
resolve(response.statusCode === 200);
|
||
});
|
||
request.on('error', () => resolve(false));
|
||
}), 20000);
|
||
if (server.exitCode !== null) throw new Error(serverError || '测试服务提前退出');
|
||
|
||
chrome = childProcess.spawn(CHROME_PATH, [
|
||
'--headless=new',
|
||
'--no-sandbox',
|
||
'--disable-gpu',
|
||
'--disable-dev-shm-usage',
|
||
'--hide-scrollbars',
|
||
'--remote-allow-origins=*',
|
||
`--remote-debugging-port=${debugPort}`,
|
||
`--user-data-dir=${profileDir}`,
|
||
'--window-size=1467,943',
|
||
'about:blank',
|
||
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
||
|
||
const targets = await waitFor(async () => {
|
||
const list = await getJson(`http://127.0.0.1:${debugPort}/json/list`);
|
||
return list.find((target) => target.type === 'page') ? list : null;
|
||
}, 20000);
|
||
const pageTarget = targets.find((target) => target.type === 'page');
|
||
cdp = await connectCdp(pageTarget.webSocketDebuggerUrl);
|
||
await cdp.send('Page.enable');
|
||
await cdp.send('Runtime.enable');
|
||
await cdp.send('Network.enable');
|
||
await cdp.send('Network.setBlockedURLs', { urls: ['https://cdnjs.cloudflare.com/*'] });
|
||
await cdp.send('Page.addScriptToEvaluateOnNewDocument', {
|
||
source: `(() => {
|
||
class Renderer {}
|
||
window.marked = { Renderer, setOptions() {}, parse: (value) => String(value || '') };
|
||
window.hljs = {
|
||
getLanguage: () => false,
|
||
highlight: (_value, options) => ({ value: String(options?.language || '') }),
|
||
highlightAuto: (value) => ({ value: String(value || '') }),
|
||
};
|
||
window.mermaid = { initialize() {}, render: async () => ({ svg: '' }) };
|
||
})();`,
|
||
});
|
||
await setViewport(cdp, 1467, 943);
|
||
const navigation = await cdp.send('Page.navigate', { url: `http://127.0.0.1:${port}/` });
|
||
if (navigation.errorText) throw new Error(`页面导航失败:${navigation.errorText}`);
|
||
try {
|
||
await waitFor(() => evaluate(cdp, "document.readyState === 'complete'"), 30000);
|
||
} catch (error) {
|
||
const diagnostic = await evaluate(cdp, `(() => ({
|
||
href: location.href,
|
||
readyState: document.readyState,
|
||
title: document.title,
|
||
body: document.body?.textContent?.trim().slice(0, 240) || '',
|
||
}))()`);
|
||
throw new Error(`页面未完成加载:${JSON.stringify(diagnostic)};${error.message}`);
|
||
}
|
||
await waitFor(() => evaluate(cdp, "!!document.querySelector('#login-form')"), 10000);
|
||
await sleep(500);
|
||
await evaluate(cdp, `(() => {
|
||
document.querySelector('#login-password').value = ${JSON.stringify(PASSWORD)};
|
||
document.querySelector('#login-form').requestSubmit();
|
||
return true;
|
||
})()`);
|
||
try {
|
||
try {
|
||
await waitFor(() => evaluate(cdp, "document.querySelector('#login-overlay').hidden && !document.querySelector('#usage-dashboard-open').hidden"), 3000);
|
||
} catch {
|
||
await evaluate(cdp, `(() => {
|
||
document.querySelector('#login-password').value = ${JSON.stringify(PASSWORD)};
|
||
document.querySelector('#login-form').requestSubmit();
|
||
return true;
|
||
})()`);
|
||
await waitFor(() => evaluate(cdp, "document.querySelector('#login-overlay').hidden && !document.querySelector('#usage-dashboard-open').hidden"), 17000);
|
||
}
|
||
} catch (error) {
|
||
const diagnostic = await evaluate(cdp, `(() => ({
|
||
loginHidden: document.querySelector('#login-overlay')?.hidden,
|
||
loginError: document.querySelector('#login-error')?.hidden ? '' : document.querySelector('#login-error')?.textContent,
|
||
appHidden: document.querySelector('#app')?.hidden,
|
||
usageHidden: document.querySelector('#usage-dashboard-open')?.hidden,
|
||
marked: typeof window.marked?.parse,
|
||
echarts: typeof window.echarts?.init,
|
||
}))()`);
|
||
throw new Error(`登录后页面未就绪:${JSON.stringify(diagnostic)};${error.message}`);
|
||
}
|
||
await evaluate(cdp, "document.querySelector('#usage-dashboard-open').click(); true");
|
||
await waitFor(() => evaluate(cdp, "!document.querySelector('#usage-dashboard-panel').hidden"), 10000);
|
||
await waitFor(() => evaluate(cdp, "document.querySelector('#usage-dashboard-panel').dataset.state !== 'loading'"), 30000);
|
||
await sleep(320);
|
||
const loadingLayout = await evaluate(cdp, `(() => {
|
||
const metricGrid = document.querySelector('.usage-dashboard__metric-grid');
|
||
const beforeTop = metricGrid.getBoundingClientRect().top;
|
||
document.querySelector('#usage-dashboard-refresh').click();
|
||
const afterTop = metricGrid.getBoundingClientRect().top;
|
||
return {
|
||
state: document.querySelector('#usage-dashboard-panel').dataset.state,
|
||
label: document.querySelector('#usage-dashboard-refresh-label').textContent,
|
||
busy: document.querySelector('#usage-dashboard-refresh').getAttribute('aria-busy'),
|
||
contentShift: Math.round(Math.abs(afterTop - beforeTop) * 10) / 10,
|
||
standaloneLoading: !!document.querySelector('#usage-dashboard-loading'),
|
||
};
|
||
})()`);
|
||
if (loadingLayout.state !== 'loading'
|
||
|| loadingLayout.label !== '整理中'
|
||
|| loadingLayout.busy !== 'true'
|
||
|| loadingLayout.contentShift > 0.5
|
||
|| loadingLayout.standaloneLoading) {
|
||
throw new Error(`加载状态仍引发布局变化:${JSON.stringify(loadingLayout)}`);
|
||
}
|
||
await waitFor(() => evaluate(cdp, `document.querySelector('#usage-dashboard-panel').dataset.state !== 'loading'
|
||
&& document.querySelector('#usage-dashboard-refresh-label').textContent === '刷新'`), 30000);
|
||
await sleep(300);
|
||
await evaluate(cdp, `(() => {
|
||
const from = document.querySelector('#usage-dashboard-from');
|
||
const to = document.querySelector('#usage-dashboard-to');
|
||
from.value = '2026-07-01';
|
||
to.value = '2026-07-31';
|
||
from.dispatchEvent(new Event('change', { bubbles: true }));
|
||
to.dispatchEvent(new Event('change', { bubbles: true }));
|
||
document.querySelector('#usage-dashboard-apply').click();
|
||
return true;
|
||
})()`);
|
||
try {
|
||
await waitFor(() => evaluate(cdp, `document.querySelector('#usage-dashboard-panel').dataset.state === 'ready'
|
||
&& document.querySelector('[data-usage-metric="newSessions"]').textContent !== '0'
|
||
&& document.querySelectorAll('[data-usage-spark] canvas').length === 5
|
||
&& document.querySelectorAll('#usage-dashboard-trend canvas').length === 1
|
||
&& document.querySelectorAll('.usage-dashboard__donut-chart canvas').length === 3
|
||
&& document.querySelectorAll('#usage-dashboard-project-rows li').length === 4
|
||
&& !/Skill/i.test(document.querySelector('#usage-dashboard-panel').innerText)`), 30000);
|
||
} catch (error) {
|
||
const diagnostic = await evaluate(cdp, `(() => ({
|
||
state: document.querySelector('#usage-dashboard-panel').dataset.state,
|
||
error: document.querySelector('#usage-dashboard-error').textContent,
|
||
from: document.querySelector('#usage-dashboard-from').value,
|
||
to: document.querySelector('#usage-dashboard-to').value,
|
||
applyDisabled: document.querySelector('#usage-dashboard-apply').disabled,
|
||
newSessions: document.querySelector('[data-usage-metric="newSessions"]').textContent,
|
||
status: document.querySelector('#usage-dashboard-status').textContent,
|
||
}))()`);
|
||
throw new Error(`自定义统计查询未返回预期数据:${JSON.stringify(diagnostic)};${error.message}`);
|
||
}
|
||
|
||
await evaluate(cdp, `(() => {
|
||
const rows = document.querySelectorAll('#usage-dashboard-mcp-rows tr').length;
|
||
const more = document.querySelector('#usage-dashboard-mcp-more');
|
||
if (rows !== 6 || !more || more.hidden || !/其余/.test(more.textContent)) {
|
||
throw new Error(JSON.stringify({ rows, moreHidden: more?.hidden, moreText: more?.textContent }));
|
||
}
|
||
more.click();
|
||
return true;
|
||
})()`);
|
||
await waitFor(() => evaluate(cdp, `document.querySelectorAll('#usage-dashboard-mcp-rows tr').length === 10
|
||
&& document.querySelector('#usage-dashboard-mcp-more').getAttribute('aria-expanded') === 'true'`), 5000);
|
||
await evaluate(cdp, "document.querySelector('#usage-dashboard-mcp-more').click(); true");
|
||
await waitFor(() => evaluate(cdp, "document.querySelectorAll('#usage-dashboard-mcp-rows tr').length === 6"), 5000);
|
||
|
||
const report = { generatedAt: new Date().toISOString(), chrome: CHROME_PATH, views: [], themes: [] };
|
||
const viewports = [
|
||
{ width: 1467, height: 943, name: 'reference' },
|
||
{ width: 1024, height: 768, name: 'desktop-narrow' },
|
||
{ width: 768, height: 1024, name: 'tablet' },
|
||
{ width: 390, height: 844, name: 'mobile' },
|
||
];
|
||
await evaluate(cdp, "document.documentElement.dataset.theme = 'washi'; document.querySelector('.usage-dashboard__body').scrollTop = 0; true");
|
||
for (const viewport of viewports) {
|
||
await setViewport(cdp, viewport.width, viewport.height);
|
||
await evaluate(cdp, "document.querySelector('.usage-dashboard__body').scrollTop = 0; true");
|
||
await sleep(100);
|
||
await capture(cdp, `dashboard-washi-${viewport.width}x${viewport.height}.png`);
|
||
report.views.push(await collectGeometry(cdp, 'washi', viewport.width, viewport.height));
|
||
}
|
||
|
||
await setViewport(cdp, 1467, 943);
|
||
await evaluate(cdp, "document.querySelector('.usage-dashboard__body').scrollTop = 0; document.querySelector('.usage-dashboard__rate-action')?.click(); true");
|
||
await waitFor(() => evaluate(cdp, "!document.querySelector('#usage-dashboard-detail').hidden"), 5000);
|
||
await sleep(220);
|
||
await capture(cdp, 'dashboard-washi-detail-1467x943.png');
|
||
await evaluate(cdp, "document.querySelector('#usage-dashboard-detail-close').click(); document.querySelector('.usage-dashboard__body').scrollTop = 0; true");
|
||
|
||
await setViewport(cdp, 1440, 900);
|
||
for (const theme of THEMES) {
|
||
await evaluate(cdp, `document.documentElement.dataset.theme = ${JSON.stringify(theme)}; document.querySelector('.usage-dashboard__body').scrollTop = 0; true`);
|
||
await sleep(80);
|
||
await capture(cdp, `dashboard-theme-${theme}-1440x900.png`);
|
||
report.themes.push(await collectGeometry(cdp, theme, 1440, 900));
|
||
}
|
||
fs.writeFileSync(REPORT_PATH, JSON.stringify(report, null, 2));
|
||
process.stdout.write(`${JSON.stringify({ report: REPORT_PATH, screenshots: fs.readdirSync(OUTPUT_DIR).length }, null, 2)}\n`);
|
||
} finally {
|
||
if (cdp?.socket?.readyState === WebSocket.OPEN) cdp.socket.close();
|
||
await stopChildProcess(chrome);
|
||
await stopChildProcess(server);
|
||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||
}
|
||
}
|
||
|
||
main().catch((error) => {
|
||
console.error(error.stack || error.message || String(error));
|
||
process.exitCode = 1;
|
||
});
|