feat: add project usage analytics with ECharts
This commit is contained in:
502
.planning/usage-statistics-reference-refit/visual-check.js
Normal file
502
.planning/usage-statistics-reference-refit/visual-check.js
Normal file
@@ -0,0 +1,502 @@
|
||||
'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);
|
||||
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;
|
||||
});
|
||||
Reference in New Issue
Block a user