Files
work-hours-tracker/png/verify_settings_drawer.js
OpenClaw Agent 2329955e32 baseline: 薪酬逻辑收口到 shared/payroll.js 后的当前全量状态
这是第一个包含完整工作区的提交,也是后续「拆 SFC / 上 Vite」的回滚基线。

此前的提交均为 index.html 单文件历史快照,由 .bak 文件重建。

当前状态(提交前已实测):
- index.html 内联薪酬算法已删除,改调 WHTPayroll
- shared/payroll.js 为唯一薪酬实现,浏览器与 Node 共用
- 对外接口未变:aggregateMonth / isCutEdge / payDayKind 等保留同名薄包装

未入库内容见 .gitignore:依赖、Chrome 验证 profile、验证截图、
运行时 sqlite 数据与日志、时间戳备份、.trash。
2026-09-05 14:35:00 +08:00

129 lines
7.7 KiB
JavaScript
Raw Permalink 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.
// 验收:设置改底部抽屉(齿轮 + 底部Tab 触发;三种关闭;不动 tabs 数组)
const { spawn } = require('child_process');
const fs = require('fs');
const CHROME = '/usr/bin/chromium';
const OUT_DIR = __dirname;
const URL = 'http://127.0.0.1:8388/index.html';
const chrome = spawn(CHROME, [
'--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage',
'--remote-debugging-port=9234', `--user-data-dir=${OUT_DIR}/.chrome-drawer`,
'--window-size=420,1000','about:blank'
], { stdio: ['ignore','ignore','pipe'] });
const stderr = []; chrome.stderr.on('data', d => stderr.push(d.toString()));
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function getWsUrl() {
for (let i=0;i<40;i++){ try{ const t=await(await fetch('http://127.0.0.1:9234/json/list')).json(); if(t.length)return t[0].webSocketDebuggerUrl; }catch(e){} await sleep(250);} throw new Error('CDP fail: '+stderr.join('').slice(-400));
}
let pass=0, fail=0;
const ok = (n,c,extra='') => { if(c){pass++;console.log(' ✅ '+n+(extra?' → '+extra:''));} else {fail++;console.log(' ❌ '+n+(extra?' → '+extra:''));} };
(async () => {
const ws = new WebSocket(await getWsUrl());
let id=0; const pending=new Map(); const logs=[];
let openRes; const openP=new Promise(r=>openRes=r); ws.onopen=openRes;
ws.onmessage=e=>{ const m=JSON.parse(e.data);
if(m.id&&pending.has(m.id)){pending.get(m.id)(m);pending.delete(m.id);}
else if(m.method==='Runtime.consoleAPICalled'){const t=(m.params.args||[]).map(a=>a.value??a.description??'').join(' '); if(!/tailwindcss/.test(t))logs.push('C '+t);}
else if(m.method==='Runtime.exceptionThrown')logs.push('EXC '+JSON.stringify(m.params.exceptionDetails).slice(0,300)); };
await openP;
const send=(me,p={})=>new Promise(r=>{const i2=++id;pending.set(i2,r);ws.send(JSON.stringify({id:i2,method:me,params:p}));});
const ev=async(x)=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});
if(r.result&&r.result.exceptionDetails)return{__err:JSON.stringify(r.result.exceptionDetails).slice(0,300)};
return r.result&&r.result.result&&r.result.result.value;};
const shot=async(n)=>{const s=await send('Page.captureScreenshot',{format:'png',captureBeyondViewport:true});fs.writeFileSync(OUT_DIR+'/'+n,Buffer.from(s.result.data,'base64'));};
// 状态探针:抽屉可见性 / 当前 tab / 底栏指示条
const probe = () => ev(`(function(){
const ov = Array.from(document.querySelectorAll('div.fixed.inset-0.z-50'))
.find(d => /设置/.test((d.querySelector('span.text-lg')||{}).textContent||''));
const navBtns = Array.from(document.querySelectorAll('nav button'));
const activeLabel = (navBtns.find(b => b.querySelector('span.absolute.opacity-100, span.absolute.w-8')) || {});
// 通过指示条 class 判断哪个 tab 处于选中态
let active = null;
navBtns.forEach(b => { const bar=b.querySelector('span.absolute');
if (bar && /opacity-100/.test(bar.className)) active = (b.innerText||'').trim(); });
return {
drawerInDom: !!ov,
drawerVisible: !!(ov && ov.getBoundingClientRect().height > 100),
activeTabLabel: active,
calendarGridVisible: !!(document.querySelector('.grid.grid-cols-7.gap-1\\\\.5')||{}).offsetParent
};
})()`);
const click = (sel) => ev(`(function(){ const e=${sel}; if(!e) return 'NOTFOUND'; e.click(); return 'ok'; })()`);
await send('Runtime.enable'); await send('Page.enable');
await send('Page.navigate',{url:URL}); await sleep(1600);
console.log('── 用例A:tabs 数组契约未被破坏(verify_payroll 用例20 依赖)');
const tabsInfo = await ev(`(function(){
const bs = Array.from(document.querySelectorAll('nav button'));
return { count: bs.length, labels: bs.map(b=>(b.innerText||'').trim()) };
})()`);
ok('底栏 3 个 Tab', tabsInfo.count===3, tabsInfo.count);
ok('顺序 打卡/汇总/设置', JSON.stringify(tabsInfo.labels)==='["打卡","汇总","设置"]', tabsInfo.labels.join(','));
console.log('\n── 用例B:初始态');
let s = await probe();
ok('抽屉未渲染', s.drawerInDom===false);
ok('停在打卡页', s.activeTabLabel==='打卡', s.activeTabLabel);
console.log('\n── 用例C:header 齿轮打开抽屉,且不切 tab');
await click(`Array.from(document.querySelectorAll('header button')).find(b=>b.title==='设置')`);
await sleep(450);
s = await probe();
ok('抽屉已展开', s.drawerVisible===true);
ok('tab 仍是打卡(未整页切换)', s.activeTabLabel==='打卡', s.activeTabLabel);
const formOk = await ev(`(function(){ const t=document.body.innerText; return { 时薪: /基础时薪/.test(t), 数据: /导出备份/.test(t), 滚动容器: !!document.querySelector('.overflow-y-auto.no-scrollbar[class*="72vh"]') }; })()`);
ok('抽屉内含薪酬规则表单', formOk.时薪===true);
ok('抽屉内含数据管理', formOk.数据===true);
ok('内容区有 72vh 滚动容器', formOk.滚动容器===true);
await shot('d1_drawer_open.png');
console.log('\n── 用例D:关闭方式① 点 ✕');
await click(`(function(){ const ov=Array.from(document.querySelectorAll('div.fixed.inset-0.z-50')).find(d=>/设置/.test((d.querySelector('span.text-lg')||{}).textContent||'')); return ov && ov.querySelectorAll('button')[0]; })()`);
await sleep(450);
s = await probe();
ok('抽屉已关闭', s.drawerInDom===false);
ok('tab 仍是打卡', s.activeTabLabel==='打卡', s.activeTabLabel);
console.log('\n── 用例E:底部 Tab「设置」打开抽屉,且不切 tab');
await click(`Array.from(document.querySelectorAll('nav button')).find(b=>(b.innerText||'').trim()==='设置')`);
await sleep(450);
s = await probe();
ok('抽屉已展开', s.drawerVisible===true);
ok('tab 未切到 settings(底栏高亮仍在打卡)', s.activeTabLabel==='打卡', s.activeTabLabel);
console.log('\n── 用例F:关闭方式② 点遮罩(@click.self)');
await click(`Array.from(document.querySelectorAll('div.fixed.inset-0.z-50')).find(d=>/设置/.test((d.querySelector('span.text-lg')||{}).textContent||''))`);
await sleep(450);
s = await probe();
ok('抽屉已关闭', s.drawerInDom===false);
console.log('\n── 用例G:关闭方式③ 点下滑把手');
await click(`Array.from(document.querySelectorAll('nav button')).find(b=>(b.innerText||'').trim()==='设置')`);
await sleep(450);
await click(`(function(){ const ov=Array.from(document.querySelectorAll('div.fixed.inset-0.z-50')).find(d=>/设置/.test((d.querySelector('span.text-lg')||{}).textContent||'')); return ov && ov.querySelector('div.pt-2\\\\.5'); })()`);
await sleep(450);
s = await probe();
ok('抽屉已关闭', s.drawerInDom===false);
console.log('\n── 用例H:抽屉与「汇总」页共存(在汇总页也能开)');
await click(`Array.from(document.querySelectorAll('nav button')).find(b=>(b.innerText||'').trim()==='汇总')`);
await sleep(400);
await click(`Array.from(document.querySelectorAll('nav button')).find(b=>(b.innerText||'').trim()==='设置')`);
await sleep(450);
s = await probe();
ok('汇总页也能开抽屉', s.drawerVisible===true);
ok('底栏高亮仍在汇总', s.activeTabLabel==='汇总', s.activeTabLabel);
await shot('d2_drawer_on_summary.png');
console.log('\n=== 异常/控制台 ===');
console.log(logs.length?logs.join('\n'):'(无异常)');
console.log('\n==============================================');
console.log(`通过 ${pass} 项,失败 ${fail} 项`);
console.log('==============================================');
ws.close(); chrome.kill(); process.exit(fail?1:0);
})().catch(e=>{ console.error('FAILED:',e.message); console.error(stderr.join('').slice(-400)); try{chrome.kill();}catch(_){}process.exit(1); });