Files
code-utils/tools/verify-quick.js
2026-08-14 07:52:01 +08:00

301 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// verify-quick.js - check markdown editor inside calendar quick-create modal via CDP.
// Usage: node verify-quick.js <stage> stages: todo | preview | create | ticket | 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;
}
async function shot(file) {
const s = await send('Page.captureScreenshot', { format: 'png' });
require('fs').writeFileSync(file, Buffer.from(s.data, 'base64'));
console.log('saved', file);
}
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 TODO_TITLE = '快速创建MD-待办';
const MD = ['**重点**:联调 `SaveTodo` 接口', '', '- [x] 标题输入', '- [ ] 图片粘贴', '', '> 日历右键快速创建也支持 Markdown 了'].join('\n');
async function openQuickFromCtx(buttonIdx) {
await evalIn(`location.hash = '#/calendar'`); await sleep(700);
await evalIn(`(() => { const el = document.querySelector('.calendar-cell.today') || document.querySelectorAll('.calendar-cell')[17]; const r = el.getBoundingClientRect(); el.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: r.x + 40, clientY: r.y + 30 })); return 'ctx' })()`);
await sleep(400);
await evalIn(`document.querySelectorAll('.calendar-ctx button')[${buttonIdx}].click()`);
await sleep(500);
}
const stages = {
async todo() {
await openQuickFromCtx(0);
// 快速创建待办/工单已与待办页对齐:同为 modal-split 分栏布局
console.log('editor', await evalIn(`(() => { const m = document.querySelector('.modal-split'); if (!m) return 'NO_MODAL'; return { editor: !!m.querySelector('.split-editor .md-editor'), tabs: m.querySelectorAll('.md-toolbar .tabs button').length, imgBtn: !!m.querySelector('.md-img-btn'), fields: [...m.querySelectorAll('.split-fields > label, .split-fields .field-pair label')].map(l => l.firstChild.textContent.trim()), duePick: !!m.querySelector('.due-quick'), date: m.querySelector('.quick-date')?.textContent } })()`));
console.log('title-len', await evalIn(setInput('.modal-split .split-fields input', TODO_TITLE)));
console.log('content-len', await evalIn(setInput('.modal-split .md-editor textarea', MD)));
await sleep(300);
await shot('tools/shot-quick-edit.png');
},
async preview() {
await evalIn(`document.querySelectorAll('.modal-split .md-toolbar .tabs button')[1].click()`); await sleep(700);
console.log('preview', await evalIn(`(() => { const b = document.querySelector('.modal-split .md-preview-box'); return { bold: !!b.querySelector('strong'), code: !!b.querySelector('code'), quote: !!b.querySelector('blockquote'), checks: b.querySelectorAll('input[type=checkbox]').length } })()`));
await shot('tools/shot-quick-preview.png');
},
async create() {
await evalIn(`(() => { const btns = [...document.querySelectorAll('.modal-split footer .btn.primary')]; btns[btns.length - 1].click(); return 'clicked' })()`);
await sleep(1200);
await evalIn(`location.hash = '#/todos'`); await sleep(800);
// 视图模式被 localStorage 记忆,先切回看板再断言卡片与 MD 渲染
await evalIn(`(() => { [...document.querySelectorAll('.view-toggle button')][0]?.click(); return 'board' })()`); await sleep(400);
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 { md: !!md, bold: !!md?.querySelector('strong'), quote: !!md?.querySelector('blockquote') } })()`));
await shot('tools/shot-quick-card.png');
},
async ticket() {
await openQuickFromCtx(1);
console.log('ticket-editor', await evalIn(`(() => { const m = document.querySelector('.modal-split'); if (!m) return 'NO_MODAL'; return { editor: !!m.querySelector('.split-editor .md-editor'), fields: [...m.querySelectorAll('.split-fields > label')].map(l => l.firstChild.textContent.trim()), duePick: !!m.querySelector('.due-quick'), projectRequired: m.querySelector('.split-fields select')?.hasAttribute('required') } })()`));
await shot('tools/shot-quick-ticket.png');
await evalIn(`document.querySelector('.modal-split header button').click()`);
},
async reminder() {
await openQuickFromCtx(2);
console.log('reminder-modal', await evalIn(`(() => { const m = document.querySelector('.quick-modal'); if (!m) return 'NO_MODAL'; return { compact: true, time: !!m.querySelector('input[type=time]'), noEditor: !m.querySelector('.md-editor') } })()`));
await evalIn(`document.querySelector('.quick-modal .login-close').click()`);
},
async 'ticket-modal'() {
await evalIn(`document.activeElement?.blur?.(); window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))`);
await evalIn(`location.hash = '#/tickets'`); await sleep(700);
await evalIn(`document.querySelector('.page-head .actions .btn.primary').click()`); await sleep(500);
console.log('title-len', await evalIn(setInput('.split-fields input', '登录页样式回归')));
console.log('desc-len', await evalIn(setInput('.md-editor textarea', ['**必须**本周完成,涉及 `sync.go`', '', '- [ ] 联调', '- [x] 自测', '', '> 注意灰度发布窗口'].join('\n'))));
await sleep(300);
await shot('tools/shot-split-ticket-edit.png');
await evalIn(`document.querySelectorAll('.md-toolbar .tabs button')[1].click()`); await sleep(600);
await shot('tools/shot-split-ticket-preview.png');
await evalIn(`document.querySelector('.modal header button').click()`);
},
async 'calendar-egg'() {
await evalIn(`location.hash = '#/calendar'`); await sleep(800);
console.log('eggs', await evalIn(`(() => { const cells = [...document.querySelectorAll('.calendar-cell.has-egg')]; return cells.map(c => { const bg = getComputedStyle(c, '::before').backgroundImage; return { day: c.querySelector('.calendar-day').textContent, fest: c.querySelector('.calendar-fest')?.textContent, svg: bg.startsWith('url("data:image/svg+xml'), emoji: /%F0%9F|\\uD83D/.test(bg) } }) })()`));
// 选中一个节日格,检查详情面板的矢量图标
await evalIn(`document.querySelector('.calendar-cell.has-egg')?.click()`); await sleep(400);
console.log('detail-vec', await evalIn(`(() => { const v = document.querySelector('.calendar-egg-vec'); if (!v) return 'NO_VEC'; return { svg: getComputedStyle(v).backgroundImage.startsWith('url("data:image/svg+xml'), size: getComputedStyle(v).width } })()`));
await shot('tools/shot-egg-bg.png');
},
async 'daily-entry'() {
await evalIn(`location.hash = '#/calendar'`); await sleep(800);
console.log('entries', await evalIn(`(() => ({ headBtn: !!document.querySelector('.daily-open-btn'), panelBtn: !!document.querySelector('.daily-entry'), panelDisabled: document.querySelector('.daily-entry')?.disabled }))()`));
// 未来日期入口应禁用
await evalIn(`(() => { const today = new Date().getDate(); const c = [...document.querySelectorAll('.calendar-cell:not(.out)')].find(x => parseInt(x.querySelector('.calendar-day').textContent) === Math.min(today + 5, 28)); c?.click(); return 'future' })()`); await sleep(350);
console.log('future-disabled', await evalIn(`document.querySelector('.daily-entry')?.disabled`));
// 回到今天并打开卡片
await evalIn(`document.querySelector('.calendar-cell.today')?.click()`); await sleep(300);
const before = await evalIn(`localStorage.getItem('cc-daily-card')`);
await evalIn(`document.querySelector('.daily-entry').click()`); await sleep(1600);
console.log('opened', await evalIn(`(() => { const c = document.querySelector('.daily-card'); if (!c) return 'NO_CARD'; return { date: c.querySelector('.daily-date')?.textContent, history: !!c.querySelector('.daily-history-tag'), nav: document.querySelectorAll('.daily-nav').length, nextDisabled: document.querySelector('.daily-nav.next')?.disabled } })()`));
await shot('tools/shot-daily-entry-today.png');
// 翻到前一天:历史标签出现,后一天按钮可用
await evalIn(`document.querySelector('.daily-nav.prev').click()`); await sleep(1600);
console.log('prev-day', await evalIn(`(() => { const c = document.querySelector('.daily-card'); return { date: c.querySelector('.daily-date')?.textContent, history: !!c.querySelector('.daily-history-tag'), nextDisabled: document.querySelector('.daily-nav.next')?.disabled, quote: c.querySelector('.daily-quote')?.textContent.slice(0, 18) } })()`));
await shot('tools/shot-daily-history.png');
// 翻回今天:历史标签消失、后一天禁用;手动打开关闭后不写入已读标记
await evalIn(`document.querySelector('.daily-nav.next').click()`); await sleep(900);
console.log('back-today', await evalIn(`(() => ({ history: !!document.querySelector('.daily-history-tag'), nextDisabled: document.querySelector('.daily-nav.next')?.disabled }))()`));
await evalIn(`document.querySelector('.daily-close').click()`); await sleep(400);
console.log('closed', { stampUnchanged: (await evalIn(`localStorage.getItem('cc-daily-card')`)) === before, gone: await evalIn(`!document.querySelector('.daily-card')`) });
},
async 'calendar-cross'() {
await evalIn(`location.hash = '#/calendar'`); await sleep(600);
console.log('before', await evalIn(`document.querySelector('.calendar-month').textContent`));
console.log('clicked-out-date', await evalIn(`(() => { const cells = [...document.querySelectorAll('.calendar-cell.out')]; const c = cells[cells.length - 1]; const d = c.querySelector('.calendar-day').textContent; c.click(); return d })()`));
await sleep(600);
console.log('after', await evalIn(`document.querySelector('.calendar-month').textContent`));
console.log('selected-in-month', await evalIn(`(() => { const s = document.querySelector('.calendar-cell.selected'); return s ? { day: s.querySelector('.calendar-day').textContent, out: s.classList.contains('out') } : 'NONE' })()`));
await shot('tools/shot-cross-month.png');
},
async 'due-quick'() {
await evalIn(`location.hash = '#/todos'`); await sleep(700);
await evalIn(`document.querySelector('.page-head .actions .btn.primary').click()`); await sleep(500);
console.log('chips', await evalIn(`[...document.querySelectorAll('.due-quick .dq-chip:not(.dq-add)')].map(b => b.textContent.trim())`));
// 点击「3天后」断言 dueAt 被填充
await evalIn(`[...document.querySelectorAll('.dq-chip')].find(b => b.textContent.includes('3'))?.click()`); await sleep(300);
console.log('picked', await evalIn(`(() => { const v = document.querySelector('.split-fields input[type=datetime-local]').value; const d = new Date(); d.setDate(d.getDate() + 3); const exp = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + 'T18:00'; return { v, exp, ok: v === exp } })()`));
// 添加自定义 5 天标签
await evalIn(`document.querySelector('.dq-add').click()`); await sleep(250);
await evalIn(`(() => { const i = document.querySelector('.dq-input input'); i.value = '5'; i.dispatchEvent(new Event('input', { bubbles: true })); i.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true })); return 'added' })()`); await sleep(300);
console.log('after-add', await evalIn(`[...document.querySelectorAll('.due-quick .dq-chip:not(.dq-add)')].map(b => b.textContent.trim())`));
await shot('tools/shot-due-quick.png');
// 删除「7天后」标签
await evalIn(`(() => { const chip = [...document.querySelectorAll('.dq-chip')].find(b => b.textContent.includes('7')); chip.querySelector('.dq-x').dispatchEvent(new MouseEvent('click', { bubbles: true })); return 'removed' })()`); await sleep(300);
console.log('after-del', await evalIn(`[...document.querySelectorAll('.due-quick .dq-chip:not(.dq-add)')].map(b => b.textContent.trim())`));
console.log('persisted', await evalIn(`localStorage.getItem('cc-due-quick')`));
await evalIn(`document.querySelector('.modal header button').click()`);
},
async 'login-shot'() {
await evalIn(`(() => { const s = (document.querySelector('#app')?.__vue_app__ || window.app).config.globalProperties.$pinia.state.value.app; s.loginOpen = true; return 'opened' })()`);
await sleep(900);
console.log('layout', await evalIn(`(() => { const c = document.querySelector('.login-card'); if (!c) return 'NO_CARD'; const r = c.getBoundingClientRect(); return { w: Math.round(r.width), h: Math.round(r.height), art: !!c.querySelector('.lg-art'), tabs: c.querySelectorAll('.lg-tabs button').length, fields: c.querySelectorAll('.lg-field').length } })()`));
await shot('tools/shot-login-v4.png');
await evalIn(`document.querySelectorAll('.lg-tabs button')[1].click()`);
await sleep(600);
await shot('tools/shot-login-v4-reg.png');
await evalIn(`document.querySelector('.login-close').click()`);
},
async daily() {
await evalIn(`localStorage.removeItem('cc-daily-card')`);
console.log('flip-login', await evalIn(`(() => { const s = (document.querySelector('#app')?.__vue_app__ || window.app).config.globalProperties.$pinia.state.value.app; s.syncStatus.loggedIn = true; return s.syncStatus.loggedIn })()`));
await sleep(2600);
console.log('card', await evalIn(`(() => { const c = document.querySelector('.daily-card'); if (!c) return 'NO_CARD'; return { date: c.querySelector('.daily-date')?.textContent, sub: c.querySelector('.daily-sub')?.textContent, fest: c.querySelector('.daily-fest')?.textContent || '(none)', quote: c.querySelector('.daily-quote')?.textContent.slice(0, 24), yi: [...c.querySelectorAll('.daily-yi span')].map(x => x.textContent), ji: [...c.querySelectorAll('.daily-ji span')].map(x => x.textContent), img: (() => { const i = c.querySelector('.daily-hero img'); return i ? (i.complete && i.naturalWidth > 0 ? 'loaded' : 'pending') : 'fallback' })() } })()`));
await sleep(1800);
await shot('tools/shot-daily-card.png');
console.log('close', await evalIn(`(() => { document.querySelector('.daily-ok').click(); return { stored: localStorage.getItem('cc-daily-card'), gone: !document.querySelector('.daily-card') } })()`));
},
async 'fest-img'() {
await evalIn(`location.hash = '#/calendar'`); await sleep(1400);
// 0) 登录 liqi云端 id=1并清掉上次运行可能残留的立秋图保证幂等
const st0 = await evalIn(`__cc.call('GetSyncStatus')`);
console.log('login-before', { loggedIn: st0.loggedIn, userId: st0.userId });
if (!st0.loggedIn || st0.userId !== 1) {
console.log('login-result', await evalIn(`__cc.call('SyncLogin', 'liqi', 'qiqi991012').then(s => ({ loggedIn: s.loggedIn, userId: s.userId })).catch(e => 'ERR:' + e)`));
await evalIn(`(() => { const s = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app'); return s.refreshSyncStatus() })()`);
await sleep(800);
}
await evalIn(`__cc.call('RemoveFestivalImage', '立秋').catch(() => 'skip')`);
await evalIn(`document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app').loadFestivalImages()`);
await sleep(600);
// 1) 默认插画背景:节日格应有 has-art + svg 背景
console.log('default-art', await evalIn(`(() => { const cells = [...document.querySelectorAll('.calendar-cell.has-art')]; return cells.map(c => ({ day: c.querySelector('.calendar-day').textContent, fest: c.querySelector('.calendar-fest')?.textContent, svg: (getComputedStyle(c, '::before').backgroundImage || '').includes('data:image/svg+xml') })) })()`));
await shot('tools/shot-fest-art.png');
// 3) 选中立秋8/7点击头部管理按钮打开样式模态框双卡选择器无图时动态卡激活、图片卡显示上传占位
await evalIn(`(() => { const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); c?.click(); return 'sel' })()`); await sleep(400);
await evalIn(`(() => { const b = document.querySelector('.fest-admin-btn'); if (!b) return 'NO_BTN'; b.click(); return 'opened' })()`); await sleep(400);
console.log('admin-panel', await evalIn(`(() => ({ panel: !!document.querySelector('.fest-admin-modal'), rows: [...document.querySelectorAll('.fest-style-name')].map(x => x.textContent), cards: document.querySelectorAll('.fest-card').length, activeCard: document.querySelector('.fest-card.active .fest-card-label')?.textContent.trim(), upload: !!document.querySelector('.fest-card-upload') }))()`));
await shot('tools/shot-fest-picker-empty.png');
// 4) 用 canvas 生成一张"秋天"照片风图片,通过 dataURL 绑定设置
console.log('set-img', await evalIn(`(() => {
const cv = document.createElement('canvas'); cv.width = 640; cv.height = 420;
const g = cv.getContext('2d');
const lg = g.createLinearGradient(0, 0, 0, 420); lg.addColorStop(0, '#f2b26b'); lg.addColorStop(.55, '#c96a3b'); lg.addColorStop(1, '#5c2f1e');
g.fillStyle = lg; g.fillRect(0, 0, 640, 420);
g.fillStyle = 'rgba(255,236,180,.85)'; g.beginPath(); g.arc(500, 90, 46, 0, 7); g.fill();
for (let i = 0; i < 26; i++) { g.fillStyle = 'rgba(120,50,20,.' + (3 + i % 5) + ')'; g.beginPath(); g.ellipse(30 + i * 24, 300 + (i % 7) * 14, 9, 4, i, 0, 7); g.fill() }
return __cc.call('SetFestivalImageData', '立秋', cv.toDataURL('image/png')).then(f => ({ mode: f.mode, img: (f.image || '').slice(0, 26) })).catch(e => 'ERR:' + e)
})()`));
await evalIn(`document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app').loadFestivalImages()`);
await sleep(600);
console.log('photo-cell', await evalIn(`(() => { const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); const bg = getComputedStyle(c, '::before').backgroundImage || ''; const pv = document.querySelector('.fest-card-preview.photo'); const r = pv ? pv.getBoundingClientRect() : null; return { photo: c.classList.contains('has-photo'), jpeg: bg.includes('data:image/jpeg'), previewJpeg: (getComputedStyle(pv).backgroundImage || '').includes('data:image/jpeg'), previewSize: r ? Math.round(r.width) + 'x' + Math.round(r.height) : 'none', activeCard: document.querySelector('.fest-card.active .fest-card-label')?.textContent.trim() } })()`));
await shot('tools/shot-fest-photo.png');
// 5) 点击"动态插画"卡:格子回退插画但图片仍保留
await evalIn(`(() => { [...document.querySelectorAll('.fest-card')][0]?.click(); return 'ok' })()`);
await sleep(700);
console.log('mode-art', await evalIn(`(() => { const s = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app'); const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); return { art: c.classList.contains('has-art'), photo: c.classList.contains('has-photo'), kept: !!(s.festivalImages['立秋'] && s.festivalImages['立秋'].image), active: document.querySelector('.fest-card.active .fest-card-label')?.textContent.trim() } })()`));
await shot('tools/shot-fest-mode-art.png');
// 6) 点击"自定义图片"卡切回
await evalIn(`(() => { [...document.querySelectorAll('.fest-card')][1]?.click(); return 'ok' })()`);
await sleep(700);
console.log('mode-photo', await evalIn(`(() => { const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); return { photo: c.classList.contains('has-photo'), active: document.querySelector('.fest-card.active .fest-card-label')?.textContent.trim() } })()`));
// 7) 模拟图片加载失败festBroken 标记后应立即回退插画
await evalIn(`(() => { const s = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app'); s.festBroken = { '立秋': true }; return 'set' })()`);
await sleep(500);
console.log('broken-fallback', await evalIn(`(() => { const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); return { art: c.classList.contains('has-art'), photo: c.classList.contains('has-photo') } })()`));
await evalIn(`(() => { document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app').festBroken = {}; return 'clear' })()`);
// 8) 移除自定义图,恢复默认插画
await evalIn(`__cc.call('RemoveFestivalImage', '立秋')`);
await evalIn(`document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app').loadFestivalImages()`);
await sleep(500);
console.log('restored', await evalIn(`(() => { const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); return { art: c.classList.contains('has-art'), photo: c.classList.contains('has-photo'), activeCard: document.querySelector('.fest-card.active .fest-card-label')?.textContent.trim(), upload: !!document.querySelector('.fest-card-upload') } })()`));
},
async 'square'() {
await evalIn(`location.hash = '#/calendar'`); await sleep(1200);
await evalIn(`(() => { const c = [...document.querySelectorAll('.calendar-cell')].find(x => x.querySelector('.calendar-fest')?.textContent === '立秋'); c?.click(); return 'sel' })()`); await sleep(400);
await evalIn(`(() => { document.querySelector('.fest-admin-btn')?.click(); return 'opened' })()`); await sleep(400);
console.log('square', await evalIn(`(() => { const pv = document.querySelector('.fest-card-preview'); if (!pv) return 'NO_CARD'; const r = pv.getBoundingClientRect(); return { w: Math.round(r.width), h: Math.round(r.height), square: Math.abs(r.width - r.height) < 1.5 } })()`));
await shot('tools/shot-fest-square.png');
await evalIn(`(() => { document.querySelector('.fest-admin-modal header button')?.click(); return 'closed' })()`);
},
async 'todo-style'() {
// 一条内容与标题相同(不应重复展示)、一条内容不同(应显示 MD 摘要)
await evalIn(`__cc.call('SaveTodo', { id: 0, title: '样式验证-重复内容', content: '样式验证-重复内容', projectId: 0, dueAt: '', priority: 'medium', status: 'open' })`);
await evalIn(`__cc.call('SaveTodo', { id: 0, title: '样式验证-带摘要', content: '**重点**:这是不同于标题的说明', projectId: 0, dueAt: '', priority: 'high', status: 'open' })`);
await evalIn(`location.hash = '#/todos'`); await sleep(900);
console.log('cards', await evalIn(`(() => { const find = t => [...document.querySelectorAll('.todo-card')].find(c => c.querySelector('b')?.textContent.includes(t)); const dup = find('样式验证-重复内容'); const md = find('样式验证-带摘要'); return { dupHasClamp: !!dup?.querySelector('.md-clamp'), mdHasClamp: !!md?.querySelector('.md-clamp'), highGlow: md ? getComputedStyle(md).boxShadow.includes('240, 94, 104') : false, medGlow: dup ? getComputedStyle(dup).boxShadow.includes('231, 189, 53') : false, noBar: dup ? getComputedStyle(dup, '::before').content === 'none' : false } })()`));
await shot('tools/shot-todo-cards.png');
await evalIn(`(async () => { const l = await __cc.call('ListTodos', 'all', 0); for (const x of l.filter(t => t.title.startsWith('样式验证-'))) await __cc.call('DeleteTodo', x.id); return 'cleaned' })()`);
},
async 'cal-detail'() {
const today = new Date(); const ds = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
await evalIn(`__cc.call('SaveTodo', { id: 0, title: '详情验证-待办', content: '**加粗**说明\\n\\n- 第一步\\n- 第二步', projectId: 0, dueAt: '${ds}T15:00', priority: 'high', status: 'open' })`);
await evalIn(`(() => { const s = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app'); const pid = s.projects[0]?.id || 0; return __cc.call('SaveTicket', { id: 0, title: '详情验证-工单', description: '> 需求描述引用块', type: 'bug', projectId: pid, startAt: '${ds}', dueAt: '${ds}', priority: 'medium', status: 'open' }) })()`);
// 若已在日历页则 hash 不变不会重挂载,先跳工作台再进日历,确保拉到新数据
await evalIn(`location.hash = '#/'`); await sleep(400);
await evalIn(`location.hash = '#/calendar'`); await sleep(1000);
await evalIn(`(() => { [...document.querySelectorAll('.calendar-cell')].find(x => x.classList.contains('today'))?.click(); return 'sel' })()`); await sleep(500);
// 点待办条目 → 详情模态框
await evalIn(`(() => { const it = [...document.querySelectorAll('.calendar-item')].find(x => x.querySelector('b')?.textContent === '详情验证-待办'); it?.click(); return !!it })()`); await sleep(500);
console.log('todo-detail', await evalIn(`(() => { const m = document.querySelector('.cal-detail-modal'); if (!m) return 'NO_MODAL'; return { title: m.querySelector('h2')?.textContent.trim(), meta: [...m.querySelectorAll('.cal-detail-meta > span')].map(x => x.textContent.trim()), md: !!m.querySelector('.cal-detail-body strong'), tabs: [...m.querySelectorAll('.cal-detail-foot .tabs button')].map(b => b.textContent) } })()`));
await shot('tools/shot-cal-detail-todo.png');
// 状态流转:待处理 → 进行中
await evalIn(`(() => { [...document.querySelectorAll('.cal-detail-foot .tabs button')].find(b => b.textContent === '进行中')?.click(); return 'ok' })()`); await sleep(700);
console.log('todo-status', await evalIn(`(() => ({ chip: document.querySelector('.cal-detail-meta .ticket-status')?.textContent, active: document.querySelector('.cal-detail-foot .tabs button.active')?.textContent }))()`));
await evalIn(`document.querySelector('.cal-detail-modal header button').click()`); await sleep(400);
// 点工单条目 → 详情模态框 → 开始处理
await evalIn(`(() => { const it = [...document.querySelectorAll('.calendar-item')].find(x => x.querySelector('b')?.textContent === '详情验证-工单'); it?.click(); return !!it })()`); await sleep(500);
console.log('ticket-detail', await evalIn(`(() => { const m = document.querySelector('.cal-detail-modal'); if (!m) return 'NO_MODAL'; return { title: m.querySelector('h2')?.textContent.trim(), quote: !!m.querySelector('.cal-detail-body blockquote'), flows: [...m.querySelectorAll('.cal-detail-flow .flow-btn')].map(b => b.textContent.trim()) } })()`));
await shot('tools/shot-cal-detail-ticket.png');
await evalIn(`(() => { [...document.querySelectorAll('.cal-detail-flow .flow-btn')][0]?.click(); return 'ok' })()`); await sleep(700);
console.log('ticket-status', await evalIn(`(() => ({ chip: document.querySelector('.cal-detail-meta .ticket-status')?.textContent, flows: [...document.querySelectorAll('.cal-detail-flow .flow-btn')].map(b => b.textContent.trim()) }))()`));
await evalIn(`document.querySelector('.cal-detail-modal header button').click()`); await sleep(300);
// 清理测试数据
await evalIn(`(async () => { const ts = await __cc.call('ListTodos', 'all', 0); for (const x of ts.filter(t => t.title.startsWith('详情验证-'))) await __cc.call('DeleteTodo', x.id); const ks = await __cc.call('ListTickets', 'all', 0); for (const x of ks.filter(t => t.title.startsWith('详情验证-'))) await __cc.call('DeleteTicket', x.id); return 'cleaned' })()`);
console.log('cleaned', await evalIn(`Promise.all([__cc.call('ListTodos', 'all', 0), __cc.call('ListTickets', 'all', 0)]).then(([a, b]) => a.filter(x => x.title.startsWith('详情验证-')).length + b.filter(x => x.title.startsWith('详情验证-')).length)`));
},
async 'wb-style'() {
const today = new Date(); const ds = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
await evalIn(`__cc.call('SaveTodo', { id: 0, title: '样式验证-待办', content: '', projectId: 0, dueAt: '${ds}T18:00', priority: 'high', status: 'open' })`);
await evalIn(`(() => { const s = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('app'); const pid = s.projects[0]?.id || 0; return __cc.call('SaveTicket', { id: 0, title: '样式验证-工单', description: '', type: 'bug', projectId: pid, startAt: '${ds}', dueAt: '${ds}T18:00', priority: 'medium', status: 'open' }) })()`);
await evalIn(`location.hash = '#/todos'`); await sleep(400);
await evalIn(`location.hash = '#/'`); await sleep(900);
console.log('wb-items', await evalIn(`(() => { const items = [...document.querySelectorAll('.wb-item')]; const hi = items.find(x => x.classList.contains('high')); if (!hi) return 'NO_HIGH'; const cs = getComputedStyle(hi); return { count: items.length, radius: cs.borderRadius, redBorder: cs.borderColor.includes('240, 94, 104'), glow: cs.boxShadow.includes('240, 94, 104'), noBar: getComputedStyle(hi, '::before').content === 'none' } })()`));
await shot('tools/shot-wb-items.png');
await evalIn(`location.hash = '#/calendar'`); await sleep(1000);
await evalIn(`(() => { const cells = [...document.querySelectorAll('.calendar-cell:not(.other-month)')]; const t = cells.find(c => c.classList.contains('today')); t?.click(); return 'clicked' })()`); await sleep(500);
console.log('cal-items', await evalIn(`(() => { const items = [...document.querySelectorAll('.calendar-item')]; if (!items.length) return 'NO_ITEMS'; const hi = items.find(x => x.classList.contains('high')) || items[0]; const cs = getComputedStyle(hi); return { count: items.length, radius: cs.borderRadius, glow: cs.boxShadow.includes('240, 94, 104'), hasHigh: items.some(x => x.classList.contains('high')) } })()`));
await shot('tools/shot-cal-items.png');
await evalIn(`(async () => { const ts = await __cc.call('ListTodos', 'all', 0); for (const x of ts.filter(t => t.title.startsWith('样式验证-'))) await __cc.call('DeleteTodo', x.id); const ks = await __cc.call('ListTickets', 'all', 0); for (const x of ks.filter(t => t.title.startsWith('样式验证-'))) await __cc.call('DeleteTicket', x.id); return 'cleaned' })()`);
},
async cleanup() {
// 不依赖视图模式,直接走绑定清理测试待办
console.log('left', await evalIn(`(async () => { const l = await __cc.call('ListTodos', 'all', 0); for (const x of l.filter(t => t.title.includes(${JSON.stringify(TODO_TITLE)}))) await __cc.call('DeleteTodo', x.id); const after = await __cc.call('ListTodos', 'all', 0); return after.filter(t => t.title.includes(${JSON.stringify(TODO_TITLE)})).length })()`));
},
};
(async () => {
await connect();
const list = process.argv.slice(2);
if (!list.length || list.some(s => !stages[s])) throw new Error('unknown stage in: ' + list.join(' '));
for (const stage of list) { console.log('== stage:', stage); await stages[stage](); }
await sleep(200); ws.close();
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });