301 lines
32 KiB
JavaScript
301 lines
32 KiB
JavaScript
|
|
// 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); });
|