// verify-md.js - end-to-end check of markdown rendering in todos/tickets via CDP. // Usage: node verify-md.js 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); });