baseline: 薪酬逻辑收口到 shared/payroll.js 后的当前全量状态
这是第一个包含完整工作区的提交,也是后续「拆 SFC / 上 Vite」的回滚基线。 此前的提交均为 index.html 单文件历史快照,由 .bak 文件重建。 当前状态(提交前已实测): - index.html 内联薪酬算法已删除,改调 WHTPayroll - shared/payroll.js 为唯一薪酬实现,浏览器与 Node 共用 - 对外接口未变:aggregateMonth / isCutEdge / payDayKind 等保留同名薄包装 未入库内容见 .gitignore:依赖、Chrome 验证 profile、验证截图、 运行时 sqlite 数据与日志、时间戳备份、.trash。
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// 验收:设置改底部抽屉(齿轮 + 底部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); });
|
||||
Reference in New Issue
Block a user