Files
code-utils/tools/verify-md.js

104 lines
7.8 KiB
JavaScript
Raw Normal View History

2026-08-14 07:52:01 +08:00
// verify-md.js - end-to-end check of markdown rendering in todos/tickets via CDP.
// Usage: node verify-md.js <stage> stages: todo-editor | preview | save | ticket | settings | cleanup
const PORT = process.env.CC_DEVTOOLS_PORT || '9222';
let seq = 0; const pending = new Map(); let ws;
function send(method, params = {}) {
return new Promise((resolve, reject) => {
const id = ++seq; pending.set(id, { resolve, reject });
ws.send(JSON.stringify({ id, method, params }));
});
}
async function connect() {
const res = await fetch(`http://127.0.0.1:${PORT}/json/list`);
const page = (await res.json()).find(t => t.type === 'page');
ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((ok, bad) => { ws.onopen = ok; ws.onerror = bad; });
ws.onmessage = ev => {
const msg = JSON.parse(ev.data);
if (msg.id && pending.has(msg.id)) {
const { resolve, reject } = pending.get(msg.id); pending.delete(msg.id);
msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result);
} else if (msg.method === 'Page.javascriptDialogOpening') {
send('Page.handleJavaScriptDialog', { accept: true }).catch(() => {});
}
};
await send('Page.enable'); await send('Runtime.enable');
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function evalIn(expression) {
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
return r.result?.value;
}
const setInput = (sel, val) => `(() => { const el = document.querySelector(${JSON.stringify(sel)}); el.value = ${JSON.stringify(val)}; el.dispatchEvent(new Event('input', { bubbles: true })); return el.value.length })()`;
const setSelect = (sel, expr) => `(() => { const el = document.querySelector(${JSON.stringify(sel)}); el.value = ${expr}; el.dispatchEvent(new Event('change', { bubbles: true })); return el.value })()`;
const TODO_TITLE = 'MD渲染验证-待办';
const TICKET_TITLE = 'MD渲染验证-工单';
// canvas 生成一张可见的渐变图,模拟 base64 模式产物;再引用一张真实存在的本地路径图。
const CANVAS_IMG = `(() => { const c = document.createElement('canvas'); c.width = 200; c.height = 100; const g = c.getContext('2d'); const gr = g.createLinearGradient(0, 0, 200, 100); gr.addColorStop(0, '#7367f5'); gr.addColorStop(1, '#43c996'); g.fillStyle = gr; g.fillRect(0, 0, 200, 100); g.fillStyle = '#fff'; g.font = 'bold 22px sans-serif'; g.fillText('IMG', 74, 58); return c.toDataURL('image/png') })()`;
const LOCAL_IMG = 'D:/myCode/code-count/view/tools/shot-todos.png';
function mdBody(dataURL) {
return ['### 修复登录页样式问题', '', '- [x] 复现并定位 `z-index` 冲突', '- [ ] **回归测试**全部表单', '', '> 参考 [MDN 文档](https://developer.mozilla.org/) 的层叠上下文说明', '',
'```css', '.topbar{backdrop-filter:blur(28px)}', '```', '', `![渐变图](${dataURL})`, '', `![本地路径图](${LOCAL_IMG})`].join('\n');
}
const stages = {
async 'todo-editor'() {
await evalIn(`location.hash = '#/todos'`); await sleep(600);
await evalIn(`document.querySelector('.page-head .actions .btn.primary').click()`); await sleep(400);
console.log('title-len', await evalIn(setInput('.modal input', TODO_TITLE)));
const dataURL = await evalIn(CANVAS_IMG);
console.log('content-len', await evalIn(setInput('.md-editor textarea', mdBody(dataURL))));
await sleep(300);
console.log('toolbar', await evalIn(`document.querySelectorAll('.md-toolbar .tabs button').length`));
},
async preview() {
await evalIn(`document.querySelectorAll('.md-toolbar .tabs button')[1].click()`); await sleep(900);
console.log('preview-imgs', await evalIn(`[...document.querySelectorAll('.md-preview-box img')].map(i => i.complete && i.naturalWidth > 0)`));
console.log('preview-has', await evalIn(`(() => { const b = document.querySelector('.md-preview-box'); return { h3: !!b.querySelector('h3'), code: !!b.querySelector('pre code'), quote: !!b.querySelector('blockquote'), check: b.querySelectorAll('input[type=checkbox]').length } })()`));
},
async save() {
await evalIn(`document.querySelector('.modal footer .btn.primary').click()`); await sleep(1500);
console.log('card', await evalIn(`(() => { const card = [...document.querySelectorAll('.todo-card')].find(c => c.querySelector('b')?.textContent.includes(${JSON.stringify(TODO_TITLE)})); if (!card) return 'NOT_FOUND'; const md = card.querySelector('.md-content'); return { imgs: [...md.querySelectorAll('img')].map(i => i.complete && i.naturalWidth > 0), h3: !!md.querySelector('h3'), clamp: md.classList.contains('md-clamp') } })()`));
},
async ticket() {
await evalIn(`location.hash = '#/tickets'`); await sleep(600);
await evalIn(`document.querySelector('.page-head .actions .btn.primary').click()`); await sleep(400);
console.log('title-len', await evalIn(setInput('.modal input', TICKET_TITLE)));
const dataURL = await evalIn(CANVAS_IMG);
console.log('desc-len', await evalIn(setInput('.md-editor textarea', '**必须**本周内完成,涉及 `sync.go`\n\n![截图](' + dataURL + ')')));
console.log('project', await evalIn(setSelect('.modal-grid label:nth-child(2) select', `[...document.querySelectorAll('.modal-grid label:nth-child(2) select option')].find(o => o.value !== '0').value`)));
console.log('due', await evalIn(setInput('.modal-grid input[type=date]:nth-of-type(1)', '2026-08-20')));
await evalIn(`(() => { const due = [...document.querySelectorAll('.modal-grid input[type=date]')][1]; due.value = '2026-08-20'; due.dispatchEvent(new Event('input', { bubbles: true })) })()`);
await sleep(200);
await evalIn(`document.querySelector('.modal footer .btn.primary').click()`); await sleep(1500);
console.log('row', await evalIn(`(() => { const row = [...document.querySelectorAll('.ticket-row')].find(r => r.querySelector('b')?.textContent.includes(${JSON.stringify(TICKET_TITLE)})); if (!row) return 'NOT_FOUND'; const md = row.querySelector('.md-content'); return { img: !!md.querySelector('img'), bold: !!md.querySelector('strong'), code: !!md.querySelector('code') } })()`));
},
async settings() {
// 图片存储设置已随账号资料迁至个人主页
await evalIn(`location.hash = '#/profile'`); await sleep(800);
console.log('img-mode', await evalIn(`(() => { const labels = [...document.querySelectorAll('.form-panel label')]; const l = labels.find(x => x.textContent.includes('内容图片存储')); if (!l) return 'NOT_FOUND'; l.scrollIntoView({ block: 'center' }); const s = l.querySelector('select'); return { value: s.value, options: [...s.options].map(o => o.value) } })()`));
await sleep(400);
},
async cleanup() {
await evalIn(`location.hash = '#/todos'`); await sleep(600);
await evalIn(`(() => { const card = [...document.querySelectorAll('.todo-card')].find(c => c.querySelector('b')?.textContent.includes(${JSON.stringify(TODO_TITLE)})); card?.querySelector('.todo-remove')?.click() })()`);
await sleep(900);
await evalIn(`location.hash = '#/tickets'`); await sleep(600);
await evalIn(`(() => { const row = [...document.querySelectorAll('.ticket-row')].find(r => r.querySelector('b')?.textContent.includes(${JSON.stringify(TICKET_TITLE)})); row?.querySelector('.icon-actions button:last-child')?.click() })()`);
await sleep(900);
console.log('left', await evalIn(`[...document.querySelectorAll('.ticket-row b')].filter(b => b.textContent.includes('MD渲染验证')).length`));
},
};
(async () => {
await connect();
const stage = process.argv[2];
if (!stages[stage]) throw new Error('unknown stage: ' + stage);
await stages[stage]();
await sleep(200); ws.close();
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });