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,64 @@
|
||||
// 无头实测缓存策略:全新浏览器首访 → 二次刷新,分别记录实际发出的网络请求。
|
||||
// 关注三件事:
|
||||
// 1. 全新浏览器(无 localStorage)能否正常起 Vue,页面里有几条记录
|
||||
// 2. 第二次刷新时 vendor/ 是否真的不再走网络
|
||||
// 3. index.html 是否命中 304
|
||||
const puppeteer = require('/root/.openclaw/workspace/web_tasks/node_modules/puppeteer');
|
||||
const fs = require('fs');
|
||||
|
||||
const URL = 'http://127.0.0.1:9522/';
|
||||
const PROFILE = '/tmp/wht-cache-test-profile';
|
||||
|
||||
(async () => {
|
||||
fs.rmSync(PROFILE, { recursive: true, force: true });
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/usr/bin/chromium',
|
||||
headless: 'new',
|
||||
userDataDir: PROFILE,
|
||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--window-size=390,844']
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 390, height: 844, isMobile: true, deviceScaleFactor: 2 });
|
||||
|
||||
const log = [];
|
||||
page.on('response', r => {
|
||||
const u = r.url().replace(URL, '/');
|
||||
log.push({ u, status: r.status(), fromCache: r.fromCache(), cc: r.headers()['cache-control'] || '-' });
|
||||
});
|
||||
|
||||
const dump = tag => {
|
||||
console.log(`\n===== ${tag} =====`);
|
||||
for (const r of log) {
|
||||
console.log(
|
||||
`${String(r.status).padEnd(4)} ${r.fromCache ? 'CACHE ' : 'NET '} ${r.u.slice(0, 60).padEnd(62)} ${r.cc}`
|
||||
);
|
||||
}
|
||||
log.length = 0;
|
||||
};
|
||||
|
||||
// ---- 第一次:全新浏览器,零 localStorage ----
|
||||
await page.goto(URL, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
dump('首访(全新浏览器)');
|
||||
|
||||
const s1 = await page.evaluate(() => ({
|
||||
vue: typeof Vue !== 'undefined',
|
||||
bootFailVisible: !document.getElementById('boot-fail')?.hidden,
|
||||
appExists: !!document.getElementById('app'),
|
||||
// 页面真的渲染出内容了才算挂载成功
|
||||
mounted: !!document.querySelector('#app')?.innerText?.trim().length,
|
||||
localRecords: Object.keys(JSON.parse(localStorage.getItem('work_records') || '{}')).length,
|
||||
hasToken: !!localStorage.getItem('work_sync_token'),
|
||||
headline: document.querySelector('#app')?.innerText?.split('\n').filter(Boolean).slice(0, 6)
|
||||
}));
|
||||
console.log('\n首访页面状态:', JSON.stringify(s1, null, 2));
|
||||
await page.screenshot({ path: '/root/.openclaw/workspace/work-hours-tracker/png/newbrowser_first.png' });
|
||||
|
||||
// ---- 第二次:普通刷新(不清缓存),看还剩多少字节要走网络 ----
|
||||
await page.reload({ waitUntil: 'networkidle2', timeout: 30000 });
|
||||
dump('二次刷新(同一浏览器)');
|
||||
|
||||
const s2 = await page.evaluate(() => typeof Vue !== 'undefined');
|
||||
console.log('\n二次刷新 Vue 可用:', s2);
|
||||
|
||||
await browser.close();
|
||||
})().catch(e => { console.error('FAIL', e); process.exit(1); });
|
||||
@@ -0,0 +1,98 @@
|
||||
// 无头实测:点齿轮 -> 设置抽屉是否弹出(returnByValue 版)
|
||||
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:9522/index.html';
|
||||
|
||||
const chrome = spawn(CHROME, [
|
||||
'--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage',
|
||||
'--remote-debugging-port=9225', `--user-data-dir=${OUT_DIR}/.chrome-test`,
|
||||
'--window-size=420,900','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 tabs = await (await fetch('http://127.0.0.1:9225/json/list')).json();
|
||||
if (tabs.length) return tabs[0].webSocketDebuggerUrl;
|
||||
} catch (e) {}
|
||||
await sleep(250);
|
||||
}
|
||||
throw new Error('CDP 连接失败: ' + stderr.join('').slice(-400));
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const wsUrl = await getWsUrl();
|
||||
const ws = new WebSocket(wsUrl);
|
||||
let id = 0; const pending = new Map(); const logs = [];
|
||||
let openRes; const openP = new Promise(r => openRes = r);
|
||||
ws.onopen = openRes;
|
||||
ws.onmessage = ev => {
|
||||
const m = JSON.parse(ev.data);
|
||||
if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); }
|
||||
else if (m.method === 'Runtime.consoleAPICalled') logs.push('C ' + (m.params.args||[]).map(a=>a.value??a.description??'').join(' '));
|
||||
else if (m.method === 'Runtime.exceptionThrown') logs.push('EXC ' + JSON.stringify(m.params.exceptionDetails));
|
||||
};
|
||||
await openP;
|
||||
const send = (method, params={}) => new Promise(res => { const mid=++id; pending.set(mid,res); ws.send(JSON.stringify({id:mid,method,params})); });
|
||||
const ev = async (expr) => {
|
||||
const r = await send('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true });
|
||||
if (r.result && r.result.exceptionDetails) return { __err: JSON.stringify(r.result.exceptionDetails) };
|
||||
return r.result && r.result.result && r.result.result.value;
|
||||
};
|
||||
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
await send('Page.navigate', { url: URL }); await sleep(2200);
|
||||
|
||||
// 1) 初始状态有多少 .z-50
|
||||
const before = await ev(`({ z50: document.querySelectorAll('.z-50').length, gear: !!document.querySelector('button[title="设置"]') })`);
|
||||
console.log('初始:', JSON.stringify(before));
|
||||
|
||||
// 2) 点齿轮
|
||||
const after = await ev(`
|
||||
new Promise(r => {
|
||||
const btn = document.querySelector('button[title="设置"]');
|
||||
if (!btn) return r({ err: 'no gear btn' });
|
||||
btn.click();
|
||||
setTimeout(() => {
|
||||
const all = document.querySelectorAll('.fixed.inset-0');
|
||||
const viz = [];
|
||||
all.forEach(d => {
|
||||
const s = getComputedStyle(d);
|
||||
viz.push({ cls: d.className.replace(/\\s+/g,' ').slice(0,70), disp: s.display, vis: s.visibility, op: s.opacity });
|
||||
});
|
||||
r({
|
||||
z50: document.querySelectorAll('.z-50').length,
|
||||
overlays: viz,
|
||||
toolbarCooked: document.body.innerText.includes('薪酬规则'),
|
||||
gearIcon: btn.outerHTML.slice(0,120)
|
||||
});
|
||||
}, 450);
|
||||
})
|
||||
`);
|
||||
console.log('点齿轮后:', JSON.stringify(after, null, 2));
|
||||
|
||||
// 3) 强制直接验证 settingsOpen 是否被置 true(绕过 DOM)
|
||||
const stateProbe = await ev(`(function(){
|
||||
// 尝试从 app 实例读状态不可行(生产vue未暴露),改为再点一次+立即读 body
|
||||
return 'probe';
|
||||
})()`);
|
||||
|
||||
const shot = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(OUT_DIR + '/gear_final.png', Buffer.from(shot.result.data, 'base64'));
|
||||
console.log('截图 gear_final.png');
|
||||
|
||||
console.log('\n=== 控制台 ===');
|
||||
console.log(logs.length ? logs.join('\n') : '(无)');
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
} catch (e) {
|
||||
console.error('FAILED:', e.message); console.error(stderr.join('').slice(-400));
|
||||
try { chrome.kill(); } catch(_){} process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,64 @@
|
||||
// 专门验刷新循环的防线。
|
||||
//
|
||||
// 模拟最刁的一种故障:fetch 能拿到完整文件(所以重试判定为"成功"),
|
||||
// 但 <script src> 始终起不来(插件拦截、CSP、解析失败都可能这样)。
|
||||
// 这时每次 reload 都会重新触发一次"成功→reload",若没有计数器就是无限刷新,
|
||||
// 用户连导出备份都点不到。
|
||||
//
|
||||
// 用 resourceType 区分:script 全部 abort,fetch 全部放行。
|
||||
const puppeteer = require('/root/.openclaw/workspace/web_tasks/node_modules/puppeteer');
|
||||
const fs = require('fs');
|
||||
|
||||
const URL = 'http://127.0.0.1:9522/';
|
||||
const VUE = 'vue.global.prod.js';
|
||||
const PROFILE = '/tmp/wht-loop-guard';
|
||||
|
||||
(async () => {
|
||||
fs.rmSync(PROFILE, { recursive: true, force: true });
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/usr/bin/chromium',
|
||||
headless: 'new',
|
||||
userDataDir: PROFILE,
|
||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 390, height: 844, isMobile: true, deviceScaleFactor: 2 });
|
||||
|
||||
let scriptBlocked = 0, fetchAllowed = 0, docLoads = 0;
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', r => {
|
||||
const isVue = r.url().includes(VUE);
|
||||
if (isVue && r.resourceType() === 'script') { scriptBlocked++; return r.abort('failed'); }
|
||||
if (isVue) fetchAllowed++;
|
||||
if (r.resourceType() === 'document') docLoads++;
|
||||
r.continue();
|
||||
});
|
||||
|
||||
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||||
await new Promise(r => setTimeout(r, 12000)); // 留足时间,若会循环这里必然刷很多次
|
||||
|
||||
const s = await page.evaluate(() => ({
|
||||
cardShown: !document.getElementById('boot-fail')?.hidden,
|
||||
title: document.getElementById('boot-title')?.textContent,
|
||||
detail: document.getElementById('boot-detail')?.textContent,
|
||||
btnUsable: document.getElementById('boot-retry')?.hidden === false,
|
||||
retryCount: sessionStorage.getItem('wht_boot_retry')
|
||||
}));
|
||||
|
||||
// 文档加载次数 = 1 次首访 + 最多 MAX_AUTO(2) 次 reload
|
||||
const capped = docLoads <= 3;
|
||||
const settled = s.cardShown && s.btnUsable;
|
||||
const pass = capped && settled;
|
||||
|
||||
console.log(`[${pass ? 'PASS' : 'FAIL'}] 刷新循环防线`);
|
||||
console.log(` 文档加载 ${docLoads} 次(上限应为 3 = 首访 + 2 次自动 reload)`);
|
||||
console.log(` script 被拦 ${scriptBlocked} 次 / fetch 放行 ${fetchAllowed} 次`);
|
||||
console.log(` 重试计数=${s.retryCount} 卡片显示=${s.cardShown} 按钮可点=${s.btnUsable}`);
|
||||
console.log(` 标题="${s.title}"`);
|
||||
console.log(` 详情="${s.detail}"`);
|
||||
|
||||
await page.screenshot({ path: '/root/.openclaw/workspace/work-hours-tracker/png/retry_loopguard.png' });
|
||||
await browser.close();
|
||||
fs.rmSync(PROFILE, { recursive: true, force: true });
|
||||
process.exit(pass ? 0 : 1);
|
||||
})().catch(e => { console.error('FAIL', e); process.exit(1); });
|
||||
@@ -0,0 +1,60 @@
|
||||
// 深挖:为什么 nav button = 0 —— 检查底部 nav 真实 DOM
|
||||
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=9227', `--user-data-dir=${OUT_DIR}/.chrome-test3`,
|
||||
'--window-size=420,900','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:9227/json/list')).json(); if(t.length)return t[0].webSocketDebuggerUrl; }catch(e){} await sleep(250);} throw new Error('CDP fail: '+stderr.join('').slice(-400));
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
const wsUrl = await getWsUrl();
|
||||
const ws = new WebSocket(wsUrl);
|
||||
let id=0; const pending=new Map(); const logs=[];
|
||||
let openRes; const openP=new Promise(r=>openRes=r);
|
||||
ws.onopen=openRes;
|
||||
ws.onmessage=ev=>{ const m=JSON.parse(ev.data);
|
||||
if(m.id&&pending.has(m.id)){pending.get(m.id)(m);pending.delete(m.id);}
|
||||
else if(m.method==='Runtime.consoleAPICalled')logs.push('C '+(m.params.args||[]).map(a=>a.value??a.description??'').join(' '));
|
||||
else if(m.method==='Runtime.exceptionThrown')logs.push('EXC '+JSON.stringify(m.params.exceptionDetails)); };
|
||||
await openP;
|
||||
const send=(method,params={})=>new Promise(res=>{const mid=++id;pending.set(mid,res);ws.send(JSON.stringify({id:mid,method,params}));});
|
||||
const ev=async(expr)=>{const r=await send('Runtime.evaluate',{expression:expr,awaitPromise:true,returnByValue:true}); if(r.result&&r.result.exceptionDetails)return{__err:JSON.stringify(r.result.exceptionDetails)}; return r.result&&r.result.result&&r.result.result.value;};
|
||||
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
await send('Page.navigate',{url:URL}); await sleep(2200);
|
||||
|
||||
const probe = await ev(`
|
||||
(function(){
|
||||
const navs = document.querySelectorAll('nav');
|
||||
const allButtons = document.querySelectorAll('button');
|
||||
const withIco = document.querySelectorAll('svg.ico').length;
|
||||
// 找包含 settings 图标的按钮
|
||||
const gear = document.querySelector('button[title="设置"]');
|
||||
const gearInfo = gear ? { found:true, parent: gear.parentElement.tagName+'.'+(gear.parentElement.className||'').slice(0,40), parentPar: gear.parentElement.parentElement.tagName+'.'+(gear.parentElement.parentElement.className||'').slice(0,40) } : { found:false };
|
||||
return {
|
||||
navCount: navs.length,
|
||||
navOuterHTMLlen: navs.length ? navs[0].outerHTML.length : 0,
|
||||
navFirst80: navs.length ? navs[0].outerHTML.slice(0,80) : null,
|
||||
allButtonCount: allButtons.length,
|
||||
buttonsText: Array.from(allButtons).map(b=>'['+((b.innerText||'').trim()||'~')+']').join(','),
|
||||
svgIconCount: withIco,
|
||||
gearInfo
|
||||
};
|
||||
})()
|
||||
`);
|
||||
console.log('DOM 深挖:', JSON.stringify(probe, null, 2));
|
||||
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
} catch(e){ console.error('FAILED:',e.message); console.error(stderr.join('').slice(-400)); try{chrome.kill();}catch(_){} process.exit(1); }
|
||||
})();
|
||||
@@ -0,0 +1,66 @@
|
||||
// 一次性核查:重构后页面上的关键交互元素还在不在。
|
||||
// 起因:png/gear_test.js 与 png/tab_settings_test.js 报「no gear btn」「navBtnCount:0」,
|
||||
// 需要分清是重构改坏了 DOM,还是那两个脚本本身找错了选择器。
|
||||
const puppeteer = require('/root/.openclaw/workspace/web_tasks/node_modules/puppeteer');
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/usr/bin/chromium',
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 390, height: 844, isMobile: true, deviceScaleFactor: 2 });
|
||||
const errs = [];
|
||||
page.on('pageerror', e => errs.push('pageerror: ' + e.message));
|
||||
page.on('console', m => { if (m.type() === 'error') errs.push('console: ' + m.text()); });
|
||||
|
||||
await page.goto('http://127.0.0.1:9522/', { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
await new Promise(r => setTimeout(r, 1200));
|
||||
|
||||
const probe = await page.evaluate(() => {
|
||||
const q = (s) => document.querySelector(s);
|
||||
const all = (s) => Array.from(document.querySelectorAll(s));
|
||||
const navBtns = all('nav button, footer button, .fixed.bottom-0 button');
|
||||
return {
|
||||
mounted: !!q('#app') && q('#app').innerText.trim().length > 0,
|
||||
hasWHTPayroll: typeof window.WHTPayroll !== 'undefined',
|
||||
payrollKeys: typeof window.WHTPayroll !== 'undefined' ? Object.keys(window.WHTPayroll).length : 0,
|
||||
gearByTitle: !!q('button[title="设置"]'),
|
||||
navBtnCount: navBtns.length,
|
||||
navLabels: navBtns.map(b => (b.innerText || '').trim()).filter(Boolean),
|
||||
// 日历格子数与首格偏移:验证 daysInMonth / firstDayOfWeek 接线正确
|
||||
dayCells: all('#app [class*="aspect"]').length,
|
||||
// 大字看板:验证 boardMain / moneyParts 接线
|
||||
board: (q('#app') ? q('#app').innerText : '').split('\n').slice(0, 8),
|
||||
cutEdge: all('.cut-edge').length,
|
||||
payRingA: all('.pay-ring-a').length,
|
||||
payRingB: all('.pay-ring-b').length
|
||||
};
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(probe, null, 2));
|
||||
console.log('\n控制台错误:', errs.length ? errs : '(无)');
|
||||
|
||||
// 点一下汇总 Tab,再点设置 Tab,看是否真的切得动
|
||||
const clicked = await page.evaluate(() => {
|
||||
const btns = Array.from(document.querySelectorAll('button'));
|
||||
const find = (t) => btns.find(b => (b.innerText || '').trim() === t);
|
||||
const out = {};
|
||||
for (const t of ['汇总', '设置', '打卡']) out[t] = !!find(t);
|
||||
const s = find('设置');
|
||||
if (s) s.click();
|
||||
return out;
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 600));
|
||||
const afterSettings = await page.evaluate(() => {
|
||||
const app = document.querySelector('#app');
|
||||
const txt = app ? app.innerText : '';
|
||||
return { showsRateField: txt.includes('基础时薪'), showsFullDays: txt.includes('满勤') };
|
||||
});
|
||||
console.log('\nTab 按钮存在性:', clicked);
|
||||
console.log('点设置后:', afterSettings);
|
||||
|
||||
await page.screenshot({ path: '/root/.openclaw/workspace/work-hours-tracker/png/postrefactor_settings.png' });
|
||||
await browser.close();
|
||||
})().catch(e => { console.error('FAIL', e.message); process.exit(1); });
|
||||
@@ -0,0 +1,41 @@
|
||||
// 量胶囊实际宽度:模拟 360px(小屏,DPR3 → 1080px 截图)与 390/430
|
||||
const { spawn } = require('child_process');
|
||||
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=9236', `--user-data-dir=${OUT_DIR}/.chrome-pill`,'--window-size=360,900','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 getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9236/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('CDP fail '+stderr.join('').slice(-300));}
|
||||
(async()=>{
|
||||
const ws=new WebSocket(await getWs()); let id=0; const pending=new Map();
|
||||
let or; const op=new Promise(r=>or=r); ws.onopen=or;
|
||||
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);}};
|
||||
await op;
|
||||
const send=(me,p={})=>new Promise(r=>{const i=++id;pending.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:p}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
|
||||
for (const W of [360, 390, 414, 430]) {
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:W,height:900,deviceScaleFactor:1,mobile:true});
|
||||
await send('Page.navigate',{url:URL}); await sleep(1300);
|
||||
// 灌数据造出 ¥282.8 这种较长的数字
|
||||
await ev(`(function(){const rec={};for(let d=1;d<=2;d++)rec['2026-09-0'+d]={status:'work',shift_type:'day',hours:d===1?8:6.5,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};localStorage.setItem('work_records',JSON.stringify(rec));return 1;})()`);
|
||||
await send('Page.navigate',{url:URL}); await sleep(1300);
|
||||
const r = await ev(`(function(){
|
||||
const wrap = document.querySelector('header .flex.flex-wrap');
|
||||
const ps = Array.from(document.querySelectorAll('header .pill'));
|
||||
const cs = getComputedStyle(wrap);
|
||||
const gap = parseFloat(cs.gap)||6;
|
||||
const items = ps.map(p=>({t:p.innerText.replace(/\\s+/g,' ').trim(), w:+p.getBoundingClientRect().width.toFixed(1), top:+p.getBoundingClientRect().top.toFixed(1)}));
|
||||
const rows = new Set(items.map(i=>i.top)).size;
|
||||
const sum = items.reduce((a,b)=>a+b.w,0) + gap*(items.length-1);
|
||||
return { vw: innerWidth, wrapW:+wrap.getBoundingClientRect().width.toFixed(1), gap, rows, sum:+sum.toFixed(1), items };
|
||||
})()`);
|
||||
console.log(`\n【视口 ${W}px】容器 ${r.wrapW}px | gap ${r.gap}px | 实际行数 ${r.rows} | 三胶囊总需 ${r.sum}px | 溢出 ${(r.sum - r.wrapW).toFixed(1)}px`);
|
||||
r.items.forEach(i=>console.log(` ${String(i.w).padStart(6)}px top=${i.top} "${i.t}"`));
|
||||
}
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);console.error(stderr.join('').slice(-300));try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,41 @@
|
||||
// 最坏情况宽度预算:27天×12h=324.0h,¥6318.0(5位数),360px 小屏
|
||||
const { spawn } = require('child_process');
|
||||
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=9237', `--user-data-dir=${OUT_DIR}/.chrome-pill2`,'--window-size=360,900','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 getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9237/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('CDP fail '+stderr.join('').slice(-300));}
|
||||
(async()=>{
|
||||
const ws=new WebSocket(await getWs()); let id=0; const pending=new Map();
|
||||
let or; const op=new Promise(r=>or=r); ws.onopen=or;
|
||||
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);}};
|
||||
await op;
|
||||
const send=(me,p={})=>new Promise(r=>{const i=++id;pending.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:p}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:360,height:900,deviceScaleFactor:1,mobile:true});
|
||||
await send('Page.navigate',{url:URL}); await sleep(1300);
|
||||
// 满月 27 天 × 12h = 324.0h → 工时收入 ¥6318.0
|
||||
await ev(`(function(){const rec={};for(let d=1;d<=27;d++)rec['2026-09-'+String(d).padStart(2,'0')]={status:'work',shift_type:'night',hours:12,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};localStorage.setItem('work_records',JSON.stringify(rec));return 1;})()`);
|
||||
await send('Page.navigate',{url:URL}); await sleep(1400);
|
||||
const r = await ev(`(function(){
|
||||
const wrap=document.querySelector('header .flex.flex-wrap');
|
||||
const ps=Array.from(document.querySelectorAll('header .pill'));
|
||||
const gap=parseFloat(getComputedStyle(wrap).gap)||6;
|
||||
const items=ps.map(p=>({t:p.innerText.replace(/\\s+/g,' ').trim(),w:+p.getBoundingClientRect().width.toFixed(1),top:+p.getBoundingClientRect().top.toFixed(1)}));
|
||||
const cs=getComputedStyle(ps[0]);
|
||||
return { wrapW:+wrap.getBoundingClientRect().width.toFixed(1), gap, rows:new Set(items.map(i=>i.top)).size,
|
||||
sum:+(items.reduce((a,b)=>a+b.w,0)+gap*(items.length-1)).toFixed(1),
|
||||
pillPad:cs.padding, pillFont:cs.fontSize, pillGap:cs.gap, items };
|
||||
})()`);
|
||||
console.log('【最坏情况 @360px】');
|
||||
console.log(` 容器 ${r.wrapW}px | wrapper gap ${r.gap}px | pill padding ${r.pillPad} | font ${r.pillFont} | icon gap ${r.pillGap}`);
|
||||
console.log(` 三胶囊总需 ${r.sum}px → 溢出 ${(r.sum-r.wrapW).toFixed(1)}px | 实际行数 ${r.rows}`);
|
||||
r.items.forEach(i=>console.log(` ${String(i.w).padStart(6)}px "${i.t}"`));
|
||||
console.log(`\n 需要压缩至少 ${(r.sum-r.wrapW).toFixed(1)}px(外加安全余量)`);
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);console.error(stderr.join('').slice(-300));try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,78 @@
|
||||
// 实测自动重试:用请求拦截模拟「Vue 前 N 次取不到」,看页面能否自己恢复。
|
||||
//
|
||||
// 场景对应真实故障:服务端日志里失败的请求压根没有记录,说明请求没到服务器(链路丢包)。
|
||||
// 这里用 request.abort() 复现同一效果 —— 浏览器侧看到的就是「拿不到这个文件」。
|
||||
const puppeteer = require('/root/.openclaw/workspace/web_tasks/node_modules/puppeteer');
|
||||
const fs = require('fs');
|
||||
|
||||
const URL = 'http://127.0.0.1:9522/';
|
||||
const VUE = 'vue.global.prod.js';
|
||||
|
||||
async function run(label, failTimes, expect) {
|
||||
const profile = `/tmp/wht-retry-${failTimes}`;
|
||||
fs.rmSync(profile, { recursive: true, force: true });
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/usr/bin/chromium',
|
||||
headless: 'new',
|
||||
userDataDir: profile,
|
||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 390, height: 844, isMobile: true, deviceScaleFactor: 2 });
|
||||
|
||||
let blocked = 0, served = 0;
|
||||
await page.setRequestInterception(true);
|
||||
page.on('request', r => {
|
||||
if (r.url().includes(VUE)) {
|
||||
if (blocked < failTimes) { blocked++; return r.abort('failed'); }
|
||||
served++;
|
||||
}
|
||||
r.continue();
|
||||
});
|
||||
|
||||
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||||
// 自动重试有 800ms 递增退避 + reload,留足时间
|
||||
await new Promise(r => setTimeout(r, 6000));
|
||||
|
||||
const s = await page.evaluate(() => {
|
||||
const card = document.getElementById('boot-fail');
|
||||
return {
|
||||
vue: typeof Vue !== 'undefined',
|
||||
cardShown: card ? !card.hidden : false,
|
||||
title: document.getElementById('boot-title')?.textContent || null,
|
||||
detail: document.getElementById('boot-detail')?.textContent || null,
|
||||
btnHidden: document.getElementById('boot-retry')?.hidden ?? null,
|
||||
mounted: !!document.querySelector('#app')?.innerText?.trim().length,
|
||||
retryCount: sessionStorage.getItem('wht_boot_retry')
|
||||
};
|
||||
});
|
||||
|
||||
const pass = expect(s);
|
||||
console.log(`\n[${pass ? 'PASS' : 'FAIL'}] ${label}`);
|
||||
console.log(` 拦截 ${blocked} 次 / 放行 ${served} 次`);
|
||||
console.log(` Vue=${s.vue} 已挂载=${s.mounted} 卡片显示=${s.cardShown} 重试计数=${s.retryCount}`);
|
||||
if (s.cardShown) console.log(` 标题="${s.title}"\n 详情="${s.detail}"`);
|
||||
|
||||
await page.screenshot({
|
||||
path: `/root/.openclaw/workspace/work-hours-tracker/png/retry_fail${failTimes}.png`
|
||||
});
|
||||
await browser.close();
|
||||
fs.rmSync(profile, { recursive: true, force: true });
|
||||
return pass;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const results = [];
|
||||
// 0 次失败:正常路径,卡片不该出现
|
||||
results.push(await run('正常加载(不拦截)', 0, s => s.vue && s.mounted && !s.cardShown));
|
||||
// 1 次失败:应自动重试成功,用户看不到失败卡
|
||||
results.push(await run('丢包 1 次 → 自动恢复', 1, s => s.vue && s.mounted && !s.cardShown));
|
||||
// 999 次失败:自动重试全败,应显示卡片且按钮可点
|
||||
results.push(await run('持续丢包 → 显示卡片且不刷新循环', 999,
|
||||
s => !s.vue && s.cardShown && s.title === '页面没能加载完成' && s.btnHidden === false));
|
||||
|
||||
console.log('\n' + '='.repeat(46));
|
||||
console.log(results.every(Boolean) ? `全部通过(${results.length}/${results.length})` : '有失败项');
|
||||
console.log('='.repeat(46));
|
||||
process.exit(results.every(Boolean) ? 0 : 1);
|
||||
})().catch(e => { console.error('FAIL', e); process.exit(1); });
|
||||
@@ -0,0 +1,103 @@
|
||||
// 实测:点击右下角「设置」Tab -> 设置抽屉是否弹出
|
||||
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:9522/index.html';
|
||||
|
||||
const chrome = spawn(CHROME, [
|
||||
'--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage',
|
||||
'--remote-debugging-port=9226', `--user-data-dir=${OUT_DIR}/.chrome-test2`,
|
||||
'--window-size=420,900','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 tabs = await (await fetch('http://127.0.0.1:9226/json/list')).json();
|
||||
if (tabs.length) return tabs[0].webSocketDebuggerUrl;
|
||||
} catch (e) {}
|
||||
await sleep(250);
|
||||
}
|
||||
throw new Error('CDP 连接失败: ' + stderr.join('').slice(-400));
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const wsUrl = await getWsUrl();
|
||||
const ws = new WebSocket(wsUrl);
|
||||
let id = 0; const pending = new Map(); const logs = [];
|
||||
let openRes; const openP = new Promise(r => openRes = r);
|
||||
ws.onopen = openRes;
|
||||
ws.onmessage = ev => {
|
||||
const m = JSON.parse(ev.data);
|
||||
if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); }
|
||||
else if (m.method === 'Runtime.consoleAPICalled') logs.push('C ' + (m.params.args||[]).map(a=>a.value??a.description??'').join(' '));
|
||||
else if (m.method === 'Runtime.exceptionThrown') logs.push('EXC ' + JSON.stringify(m.params.exceptionDetails));
|
||||
};
|
||||
await openP;
|
||||
const send = (method, params={}) => new Promise(res => { const mid=++id; pending.set(mid,res); ws.send(JSON.stringify({id:mid,method,params})); });
|
||||
const ev = async (expr) => {
|
||||
const r = await send('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true });
|
||||
if (r.result && r.result.exceptionDetails) return { __err: JSON.stringify(r.result.exceptionDetails) };
|
||||
return r.result && r.result.result && r.result.result.value;
|
||||
};
|
||||
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
await send('Page.navigate', { url: URL }); await sleep(2200);
|
||||
|
||||
// 定位底部 tab 按钮(文字为「设置」的 <button>)
|
||||
const probe = await ev(`
|
||||
(function(){
|
||||
const btns = Array.from(document.querySelectorAll('nav button'));
|
||||
const info = btns.map(b => ({ t: (b.innerText||'').trim(), rect: JSON.stringify({x:b.getBoundingClientRect().left,y:b.getBoundingClientRect().top,w:b.getBoundingClientRect().width,h:b.getBoundingClientRect().height}), htmlOk: b.outerHTML.slice(0,100) }));
|
||||
// 命中最上层元素(判断按钮是否被其他元素遮挡)
|
||||
const sBtn = btns.find(b => (b.innerText||'').trim() === '设置');
|
||||
let hitTop = null;
|
||||
if (sBtn) {
|
||||
const r = sBtn.getBoundingClientRect();
|
||||
const cx = r.left + r.width/2, cy = r.top + r.height/2;
|
||||
const topEl = document.elementFromPoint(cx, cy);
|
||||
hitTop = topEl ? { tag: topEl.tagName, cls: topEl.className && String(topEl.className).slice(0,80) } : null;
|
||||
}
|
||||
return { navBtnCount: btns.length, btns: info, settingsBtnHitTest: hitTop };
|
||||
})()
|
||||
`);
|
||||
console.log('底部导航按钮:', JSON.stringify(probe, null, 2));
|
||||
|
||||
// 点击「设置」tab
|
||||
const clickSetup = await ev(`
|
||||
new Promise(r => {
|
||||
const btn = Array.from(document.querySelectorAll('nav button')).find(b => (b.innerText||'').trim() === '设置');
|
||||
if (!btn) return r({ err: 'no settings btn' });
|
||||
btn.click();
|
||||
setTimeout(() => {
|
||||
const z50 = document.querySelectorAll('.z-50').length;
|
||||
const overlay = document.querySelector('.fixed.inset-0.z-50');
|
||||
r({
|
||||
z50,
|
||||
overlayPresent: !!overlay,
|
||||
overlayText: overlay ? (overlay.innerText||'').replace(/\\s+/g,' ').slice(0,60) : null,
|
||||
bodyHasJiangchou: document.body.innerText.includes('薪酬规则')
|
||||
});
|
||||
}, 450);
|
||||
})
|
||||
`);
|
||||
console.log('点击设置 Tab 后:', JSON.stringify(clickSetup, null, 2));
|
||||
|
||||
const shot = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(OUT_DIR + '/tab_settings_click.png', Buffer.from(shot.result.data, 'base64'));
|
||||
console.log('截图 tab_settings_click.png');
|
||||
|
||||
console.log('\n=== 控制台 ===');
|
||||
console.log(logs.length ? logs.join('\n') : '(无)');
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
} catch (e) {
|
||||
console.error('FAILED:', e.message); console.error(stderr.join('').slice(-400));
|
||||
try { chrome.kill(); } catch(_){} process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,60 @@
|
||||
// 复现排查:用真实浏览器分别从 127.0.0.1 与公网 IP 打开,看 boot-fail 会不会出现
|
||||
const { spawn } = require('child_process');
|
||||
const OUT = '/root/.openclaw/workspace/work-hours-tracker/png';
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
const TARGETS = [
|
||||
['本机同源', 'http://127.0.0.1:9522/index.html'],
|
||||
['公网 IP ', 'http://66.235.106.12:9522/index.html']
|
||||
];
|
||||
|
||||
(async () => {
|
||||
const chrome = spawn('/usr/bin/chromium', [
|
||||
'--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage',
|
||||
'--remote-debugging-port=9264', `--user-data-dir=${OUT}/.chrome-boot`,
|
||||
'--window-size=360,900', 'about:blank'
|
||||
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
||||
|
||||
const wsUrl = await (async () => {
|
||||
for (let i = 0; i < 40; i++) {
|
||||
try { const t = await (await fetch('http://127.0.0.1:9264/json/list')).json(); if (t.length) return t[0].webSocketDebuggerUrl; } catch {}
|
||||
await sleep(250);
|
||||
}
|
||||
throw new Error('CDP 起不来');
|
||||
})();
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
let id = 0; const pnd = new Map();
|
||||
await new Promise(r => { ws.onopen = r; });
|
||||
|
||||
const reqs = [];
|
||||
ws.onmessage = e => {
|
||||
const m = JSON.parse(e.data);
|
||||
if (m.id && pnd.has(m.id)) { pnd.get(m.id)(m); pnd.delete(m.id); }
|
||||
if (m.method === 'Network.responseReceived') {
|
||||
reqs.push(`${m.params.response.status} ${m.params.response.url.replace(/^https?:\/\/[^/]+/, '')}`);
|
||||
}
|
||||
if (m.method === 'Network.loadingFailed') reqs.push(`FAILED ${m.params.errorText}`);
|
||||
};
|
||||
const send = (method, params = {}) => new Promise(r => { const i = ++id; pnd.set(i, r); ws.send(JSON.stringify({ id: i, method, params })); });
|
||||
const ev = async x => {
|
||||
const r = await send('Runtime.evaluate', { expression: x, awaitPromise: true, returnByValue: true });
|
||||
return r.result && r.result.result && r.result.result.value;
|
||||
};
|
||||
await send('Runtime.enable'); await send('Page.enable'); await send('Network.enable');
|
||||
|
||||
for (const [label, url] of TARGETS) {
|
||||
reqs.length = 0;
|
||||
await send('Page.navigate', { url }); await sleep(2600);
|
||||
console.log(`\n=== ${label} ${url} ===`);
|
||||
console.log(' Vue 是否加载 =', await ev(`typeof Vue`));
|
||||
console.log(' boot-fail 是否显示 =', await ev(`(function(){const b=document.getElementById('boot-fail');return b?!b.hidden:'元素不存在(旧版页面)';})()`));
|
||||
console.log(' #app 是否存在 =', await ev(`!!document.getElementById('app')`));
|
||||
console.log(' 首屏文本 =', String(await ev(`document.body.innerText.replace(/\\n+/g,' | ').slice(0,110)`)));
|
||||
console.log(' 资源请求:');
|
||||
reqs.forEach(r => console.log(' ' + r));
|
||||
}
|
||||
|
||||
ws.close(); chrome.kill();
|
||||
process.exit(0);
|
||||
})().catch(e => { console.error('FAILED:', e.message); process.exit(1); });
|
||||
@@ -0,0 +1,114 @@
|
||||
// 验收:两套口径分区(挣得 / 进账)+ 归属期徽章 + 结转标注
|
||||
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=9232', `--user-data-dir=${OUT_DIR}/.chrome-caliber`,
|
||||
'--window-size=420,1200','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:9232/json/list')).json(); if(t.length)return t[0].webSocketDebuggerUrl; }catch(e){} await sleep(250);} throw new Error('CDP fail: '+stderr.join('').slice(-400));
|
||||
}
|
||||
(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'));};
|
||||
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
await send('Page.navigate',{url:URL}); await sleep(1200);
|
||||
// 8月满月打卡(含奖惩)+ 9月上半月打卡,制造出「挣得 ≠ 进账」的典型场景
|
||||
await ev(`(function(){
|
||||
const rec = {};
|
||||
for (let d=1; d<=28; d++) rec['2026-08-'+String(d).padStart(2,'0')] = { status:'work', shift_type: d%3?'day':'night', hours: d%3?8:10, contraband_found: d===5?3:0, contraband_missed: d===9?2:0, other_penalty: d===12?50:null, penalty_reason: d===12?'迟到':'' };
|
||||
for (let d=1; d<=20; d++) rec['2026-09-'+String(d).padStart(2,'0')] = { status:'work', shift_type:'day', hours:8, contraband_found: d===3?1:0, contraband_missed:0, other_penalty:null, penalty_reason:'' };
|
||||
localStorage.setItem('work_records', JSON.stringify(rec));
|
||||
localStorage.setItem('work_month_adjust', JSON.stringify({'2026-08':{status:'final',finalAmount:80,note:'与班长核对',finalizedAt:'2026-09-01T02:00:00.000Z'}}));
|
||||
return 1;
|
||||
})()`);
|
||||
await send('Page.navigate',{url:URL}); await sleep(1800);
|
||||
|
||||
// ① 顶部看板:主数字换成「挣得」,副标题带结转说明
|
||||
const board = await ev(`(function(){
|
||||
const h = document.querySelector('header').innerText.replace(/\\s+/g,' ');
|
||||
return { text: h.slice(0, 210) };
|
||||
})()`);
|
||||
console.log('① 看板:', JSON.stringify(board));
|
||||
await shot('c1_board.png');
|
||||
|
||||
// ② 汇总页两分区
|
||||
const sum = await ev(`new Promise(r=>{
|
||||
Array.from(document.querySelectorAll('nav button')).find(b=>(b.innerText||'').trim()==='汇总').click();
|
||||
setTimeout(()=>{
|
||||
const sec = document.querySelector("section[class*='px-4'] , section");
|
||||
const secs = Array.from(document.querySelectorAll('section'));
|
||||
const s = secs.find(x=>x.innerText.includes('挣得'));
|
||||
const t = s ? s.innerText.replace(/\\s+/g,' ') : null;
|
||||
const badges = Array.from(document.querySelectorAll('.own-badge')).map(b=>({cls:b.className.replace('own-badge ',''),txt:b.textContent.trim()}));
|
||||
return r({
|
||||
has挣得: !!(t&&t.includes('9月挣得')), has进账: !!(t&&t.includes('9月进账')),
|
||||
has何时到手: !!(t&&t.includes('何时到手')),
|
||||
has合计应得: !!(t&&t.includes('合计应得')), has合计进账: !!(t&&t.includes('合计进账')),
|
||||
has结转标注: !!(t&&t.includes('结转10月10号发')),
|
||||
has本月22: !!(t&&t.includes('本月22号发')),
|
||||
badges,
|
||||
snippet: t ? t.slice(0, 420) : null
|
||||
});
|
||||
}, 500);
|
||||
})`);
|
||||
console.log('② 汇总分区:', JSON.stringify(sum, null, 1));
|
||||
await shot('c2_summary.png');
|
||||
|
||||
// ③ 数字自洽校验:挣得 = 工时收入+奖惩净额;进账 = 两笔之和;结转 = payoutNextB
|
||||
const nums = await ev(`(function(){
|
||||
const b = document.body.innerText.replace(/\\s+/g,' ');
|
||||
const g = re => { const m = b.match(re); return m ? m[1] : null; };
|
||||
return {
|
||||
工时收入: g(/工时收入\\S*?\\s*确定\\s*¥([\\d,.]+)/),
|
||||
奖惩净额: g(/奖惩净额\\s*\\S+\\s*([+-]¥[\\d,.]+)/),
|
||||
合计应得: g(/合计应得\\s*¥([\\d,.]+)/),
|
||||
上半月已发: g(/9\\/22 上半月工时([^)]*)\\s*¥([\\d,.]+)/),
|
||||
结转: g(/10\\/10 下半月工时\\+奖惩(结转)\\s*¥([\\d,.]+)/),
|
||||
合计进账: g(/合计进账\\s*¥([\\d,.]+)/),
|
||||
看板结转: g(/9月进账 ¥([\\d,.]+)/)
|
||||
};
|
||||
})()`);
|
||||
console.log('③ 数字:', JSON.stringify(nums));
|
||||
|
||||
// ④ 文本报告分段
|
||||
const rpt = await ev(`(function(){
|
||||
const app = document.querySelector('#app');
|
||||
return null;
|
||||
})()`);
|
||||
const rptText = await ev(`(function(){
|
||||
// 通过点「复制文本报告」不便读剪贴板,改为直接取按钮上方渲染不到的内容:用 clipboard 拦截
|
||||
let captured = null;
|
||||
const orig = navigator.clipboard && navigator.clipboard.writeText;
|
||||
return new Promise(res=>{
|
||||
if (navigator.clipboard) navigator.clipboard.writeText = (t)=>{ captured = t; return Promise.resolve(); };
|
||||
const btn = Array.from(document.querySelectorAll('button')).find(b=>/复制文本报告/.test(b.innerText||''));
|
||||
btn.click();
|
||||
setTimeout(()=>{ res(captured); }, 400);
|
||||
});
|
||||
})()`);
|
||||
console.log('④ 报告:\n' + (rptText || '(未捕获)'));
|
||||
|
||||
console.log('\n=== 异常/控制台 ===');
|
||||
console.log(logs.length?logs.join('\n'):'(无异常)');
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
})().catch(e=>{ console.error('FAILED:',e.message); console.error(stderr.join('').slice(-400)); try{chrome.kill();}catch(_){}process.exit(1); });
|
||||
@@ -0,0 +1,64 @@
|
||||
// 分别模拟:CDN 完全不可达 / CDN 很慢,看刷新瞬间屏幕上到底是什么
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const OUT = __dirname;
|
||||
const URL_ = 'http://127.0.0.1:9523/index.html';
|
||||
const chrome = spawn('/usr/bin/chromium', ['--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage','--remote-debugging-port=9262',`--user-data-dir=${OUT}/.chrome-cdn`,'--window-size=360,900','about:blank'], { stdio:['ignore','ignore','pipe'] });
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
async function getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9262/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('cdp');}
|
||||
(async () => {
|
||||
const ws = new WebSocket(await getWs()); let id=0; const p=new Map(); let or; const o=new Promise(r=>or=r); ws.onopen=or;
|
||||
ws.onmessage = e => { const m=JSON.parse(e.data); if(m.id&&p.has(m.id)){p.get(m.id)(m);p.delete(m.id);} };
|
||||
await o;
|
||||
const send=(me,pa={})=>new Promise(r=>{const i=++id;p.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:pa}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable'); await send('Page.enable'); await send('Network.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:360,height:900,deviceScaleFactor:2,mobile:true});
|
||||
await send('Network.setCacheDisabled',{cacheDisabled:true});
|
||||
|
||||
const probe = async (tag) => {
|
||||
const hasMustache = await ev(`/\\{\\{[^}]*\\}\\}/.test(document.body.innerHTML)`);
|
||||
const twStyle = await ev(`Array.from(document.querySelectorAll('style')).some(s=>/tailwind|--tw-/.test(s.textContent))`);
|
||||
const bodyH = await ev(`document.body.scrollHeight`);
|
||||
const txt = await ev(`document.body.innerText.replace(/\\s+/g,' ').slice(0,150)`);
|
||||
console.log(` ${tag} 裸模板=${hasMustache?'是':'否'} tailwind样式=${twStyle?'有':'无'} 页高=${bodyH}px`);
|
||||
console.log(` 屏上文字="${txt}"`);
|
||||
};
|
||||
|
||||
console.log('=== 场景 A:两个 CDN 全部不可达(模拟被墙/断外网)===');
|
||||
await send('Network.setBlockedURLs',{urls:['*cdn.tailwindcss.com*','*unpkg.com*']});
|
||||
await send('Page.navigate',{url:URL_}); await sleep(2500);
|
||||
await probe('2500ms');
|
||||
let s = await send('Page.captureScreenshot',{format:'png'});
|
||||
fs.writeFileSync(OUT+'/fouc_cdn_blocked.png', Buffer.from(s.result.data,'base64'));
|
||||
console.log(' -> png/fouc_cdn_blocked.png');
|
||||
|
||||
console.log('\n=== 场景 B:只有 tailwind 不可达,Vue 正常 ===');
|
||||
await send('Network.setBlockedURLs',{urls:['*cdn.tailwindcss.com*']});
|
||||
await send('Page.navigate',{url:URL_}); await sleep(2500);
|
||||
await probe('2500ms');
|
||||
s = await send('Page.captureScreenshot',{format:'png'});
|
||||
fs.writeFileSync(OUT+'/fouc_tailwind_blocked.png', Buffer.from(s.result.data,'base64'));
|
||||
console.log(' -> png/fouc_tailwind_blocked.png');
|
||||
|
||||
console.log('\n=== 场景 C:只有 Vue 不可达,tailwind 正常 ===');
|
||||
await send('Network.setBlockedURLs',{urls:['*unpkg.com*']});
|
||||
await send('Page.navigate',{url:URL_}); await sleep(2500);
|
||||
await probe('2500ms');
|
||||
s = await send('Page.captureScreenshot',{format:'png'});
|
||||
fs.writeFileSync(OUT+'/fouc_vue_blocked.png', Buffer.from(s.result.data,'base64'));
|
||||
console.log(' -> png/fouc_vue_blocked.png');
|
||||
|
||||
console.log('\n=== 场景 D:CDN 很慢(3G 限速),逐帧看 ===');
|
||||
await send('Network.setBlockedURLs',{urls:[]});
|
||||
await send('Network.emulateNetworkConditions',{offline:false,latency:400,downloadThroughput:400*1024/8,uploadThroughput:400*1024/8});
|
||||
const t0 = Date.now();
|
||||
send('Page.navigate',{url:URL_});
|
||||
for (const at of [600, 1200, 2000, 3500, 6000]) {
|
||||
while (Date.now()-t0 < at) await sleep(20);
|
||||
await probe(String(at)+'ms');
|
||||
if (at===1200){ s=await send('Page.captureScreenshot',{format:'png'}); fs.writeFileSync(OUT+'/fouc_slow_1200ms.png', Buffer.from(s.result.data,'base64')); }
|
||||
}
|
||||
console.log(' -> png/fouc_slow_1200ms.png');
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,46 @@
|
||||
// 日期格子是 div 不是 button,上一版选择器点不中。这版按 .calendar-cell 的父 div 点。
|
||||
const { spawn } = require('child_process');
|
||||
const OUT='/root/.openclaw/workspace/work-hours-tracker/png';
|
||||
const URL='http://127.0.0.1:9522/index.html';
|
||||
const chrome=spawn('/usr/bin/chromium',['--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage','--remote-debugging-port=9254',`--user-data-dir=${OUT}/.chrome-drawer2`,'--window-size=360,900','about:blank'],{stdio:['ignore','ignore','pipe']});
|
||||
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
||||
async function getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9254/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('cdp');}
|
||||
(async()=>{
|
||||
const ws=new WebSocket(await getWs());let id=0;const p=new Map();let or;const o=new Promise(r=>or=r);ws.onopen=or;
|
||||
ws.onmessage=e=>{const m=JSON.parse(e.data);if(m.id&&p.has(m.id)){p.get(m.id)(m);p.delete(m.id);}};await o;
|
||||
const send=(me,pa={})=>new Promise(r=>{const i=++id;p.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:pa}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable');await send('Page.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:360,height:900,deviceScaleFactor:2,mobile:true});
|
||||
await send('Page.navigate',{url:URL});await sleep(1300);
|
||||
await ev(`(function(){localStorage.clear();localStorage.setItem('work_records',JSON.stringify({
|
||||
'2026-09-08':{status:'work',shift_type:'day',hours:8,contraband_found:1,contraband_missed:0,other_penalty:null,penalty_reason:''}
|
||||
}));return 1;})()`);
|
||||
|
||||
const openDay = async (d) => {
|
||||
await send('Page.navigate',{url:URL});await sleep(1500);
|
||||
const hit = await ev(`(function(){
|
||||
const cells=Array.from(document.querySelectorAll('.grid.grid-cols-7 > div'));
|
||||
const t=cells.find(c=>{const n=c.querySelector('.day-num, .rounded-full');return n && n.textContent.trim()==='${d}';});
|
||||
if(!t) return 'NOT_FOUND';
|
||||
t.click(); return 'CLICKED';
|
||||
})()`);
|
||||
await sleep(700);
|
||||
const head = await ev(`(function(){
|
||||
const ds=Array.from(document.querySelectorAll('.fixed.inset-0.z-50'));
|
||||
const d=ds.find(x=>/\\u6253\\u5361/.test(x.innerText));
|
||||
return d ? d.innerText.split('\\n').filter(s=>s.trim()).slice(0,5).join(' | ') : 'NO_DRAWER';
|
||||
})()`);
|
||||
return { hit, head };
|
||||
};
|
||||
|
||||
const r8 = await openDay(8);
|
||||
console.log('8日(上半月) click=%s\n 抽屉前5行 = %s', r8.hit, JSON.stringify(r8.head));
|
||||
const s=await send('Page.captureScreenshot',{format:'png'});
|
||||
require('fs').writeFileSync(OUT+'/drawer_no_subtitle.png',Buffer.from(s.result.data,'base64'));
|
||||
const r20 = await openDay(20);
|
||||
console.log('20日(下半月) click=%s\n 抽屉前5行 = %s', r20.hit, JSON.stringify(r20.head));
|
||||
console.log('全页残留旧副标题 =', await ev(`/工时计入|奖惩定稿后随次月/.test(document.body.innerText)`));
|
||||
console.log('screenshot -> png/drawer_no_subtitle.png');
|
||||
ws.close();chrome.kill();process.exit(0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,41 @@
|
||||
// 复现「刷新瞬间闪出未渲染内容」:在导航后极早的时间点连续抓 DOM 文本与截图
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const OUT = __dirname;
|
||||
const URL_ = 'http://127.0.0.1:9523/index.html';
|
||||
const chrome = spawn('/usr/bin/chromium', ['--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage','--remote-debugging-port=9261',`--user-data-dir=${OUT}/.chrome-fouc`,'--window-size=360,900','about:blank'], { stdio:['ignore','ignore','pipe'] });
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
async function getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9261/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('cdp');}
|
||||
(async () => {
|
||||
const ws = new WebSocket(await getWs()); let id=0; const p=new Map(); let or; const o=new Promise(r=>or=r); ws.onopen=or;
|
||||
ws.onmessage = e => { const m=JSON.parse(e.data); if(m.id&&p.has(m.id)){p.get(m.id)(m);p.delete(m.id);} };
|
||||
await o;
|
||||
const send=(me,pa={})=>new Promise(r=>{const i=++id;p.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:pa}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable'); await send('Page.enable'); await send('Network.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:360,height:900,deviceScaleFactor:2,mobile:true});
|
||||
|
||||
// 记录每个资源的耗时
|
||||
const timings = [];
|
||||
ws.addEventListener('message', e => {
|
||||
const m = JSON.parse(e.data);
|
||||
if (m.method === 'Network.responseReceived') timings.push({ url: m.params.response.url, status: m.params.response.status, t: Date.now() });
|
||||
});
|
||||
|
||||
console.log('=== 冷启动(无缓存)逐帧抓取 ===');
|
||||
await send('Network.setCacheDisabled',{cacheDisabled:true});
|
||||
const t0 = Date.now();
|
||||
send('Page.navigate',{url:URL_});
|
||||
for (const at of [80, 200, 400, 800, 1500, 2500]) {
|
||||
while (Date.now() - t0 < at) await sleep(20);
|
||||
const txt = await ev(`(document.getElementById('app')||document.body).innerText.replace(/\\s+/g,' ').slice(0,110)`);
|
||||
const hasMustache = await ev(`/\\{\\{[^}]*\\}\\}/.test(document.body.innerHTML)`);
|
||||
const tw = await ev(`!!window.tailwind`);
|
||||
const vue = await ev(`!!window.Vue`);
|
||||
console.log(` ${String(at).padStart(4)}ms 裸模板=${hasMustache?'是':'否'} tailwind=${tw?'已载':'未载'} vue=${vue?'已载':'未载'} 屏上文字="${txt}"`);
|
||||
if (at === 400) { const s = await send('Page.captureScreenshot',{format:'png'}); fs.writeFileSync(OUT+'/fouc_400ms.png', Buffer.from(s.result.data,'base64')); }
|
||||
}
|
||||
console.log('\n=== 各资源加载耗时(相对导航开始)===');
|
||||
for (const r of timings) console.log(` ${String(r.t-t0).padStart(5)}ms ${r.status} ${r.url.slice(0,72)}`);
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,76 @@
|
||||
// 修复后复测:冷启动逐帧 + 完全断外网 + 3G 限速,确认任何时刻都不闪裸模板
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const OUT = __dirname;
|
||||
const URL_ = 'http://127.0.0.1:9522/index.html';
|
||||
const chrome = spawn('/usr/bin/chromium', ['--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage','--remote-debugging-port=9263',`--user-data-dir=${OUT}/.chrome-fixed`,'--window-size=360,900','about:blank'], { stdio:['ignore','ignore','pipe'] });
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
async function getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9263/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('cdp');}
|
||||
(async () => {
|
||||
const ws = new WebSocket(await getWs()); let id=0; const p=new Map(); let or; const o=new Promise(r=>or=r); ws.onopen=or;
|
||||
ws.onmessage = e => { const m=JSON.parse(e.data); if(m.id&&p.has(m.id)){p.get(m.id)(m);p.delete(m.id);} };
|
||||
await o;
|
||||
const send=(me,pa={})=>new Promise(r=>{const i=++id;p.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:pa}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable'); await send('Page.enable'); await send('Network.enable'); await send('Log.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:360,height:900,deviceScaleFactor:2,mobile:true});
|
||||
await send('Network.setCacheDisabled',{cacheDisabled:true});
|
||||
|
||||
const reqs=[]; const errs=[];
|
||||
ws.addEventListener('message', e => { const m=JSON.parse(e.data);
|
||||
if(m.method==='Network.responseReceived') reqs.push(`${m.params.response.status} ${m.params.response.url}`);
|
||||
if(m.method==='Log.entryAdded'&&m.params.entry.level==='error') errs.push(m.params.entry.text); });
|
||||
|
||||
// 可见的裸模板:只算真正显示在屏上的(v-cloak 隐藏的不算)
|
||||
const VISIBLE_MUSTACHE = `(function(){
|
||||
const app=document.getElementById('app');
|
||||
if(!app) return 'NO_APP';
|
||||
const cs=getComputedStyle(app);
|
||||
if(cs.visibility==='hidden'||cs.display==='none') return 'HIDDEN';
|
||||
return /\\{\\{[^}]*\\}\\}/.test(app.innerHTML) ? 'LEAK' : 'CLEAN';
|
||||
})()`;
|
||||
|
||||
const probe = async tag => {
|
||||
const st = await ev(VISIBLE_MUSTACHE);
|
||||
const txt = await ev(`document.body.innerText.replace(/\\s+/g,' ').slice(0,80)`);
|
||||
const styled = await ev(`(function(){const a=document.getElementById('app');return a?getComputedStyle(a).maxWidth:'-'})()`);
|
||||
const flag = st==='LEAK' ? '❌ 屏上出现裸模板' : st==='HIDDEN' ? '✅ 已遮住(v-cloak)' : '✅ 已渲染';
|
||||
console.log(` ${tag.padStart(6)} ${flag} app.maxWidth=${styled} "${txt}"`);
|
||||
return st;
|
||||
};
|
||||
|
||||
console.log('=== 场景1:冷启动逐帧(本地资源,无缓存)===');
|
||||
let leak=false; const t0=Date.now();
|
||||
send('Page.navigate',{url:URL_});
|
||||
for (const at of [50,100,200,300,500,800,1500]) {
|
||||
while(Date.now()-t0<at) await sleep(10);
|
||||
if (await probe(at+'ms')==='LEAK') leak=true;
|
||||
}
|
||||
let s=await send('Page.captureScreenshot',{format:'png'});
|
||||
fs.writeFileSync(OUT+'/fixed_cold.png', Buffer.from(s.result.data,'base64'));
|
||||
|
||||
console.log('\n=== 场景2:完全断外网(原先这里只剩裸模板)===');
|
||||
await send('Network.setBlockedURLs',{urls:['*cdn.tailwindcss.com*','*unpkg.com*','*://*.googleapis.com/*']});
|
||||
await send('Page.navigate',{url:URL_}); await sleep(2000);
|
||||
if (await probe('2000ms')==='LEAK') leak=true;
|
||||
s=await send('Page.captureScreenshot',{format:'png'});
|
||||
fs.writeFileSync(OUT+'/fixed_offline.png', Buffer.from(s.result.data,'base64'));
|
||||
|
||||
console.log('\n=== 场景3:3G 限速逐帧 ===');
|
||||
await send('Network.setBlockedURLs',{urls:[]});
|
||||
await send('Network.emulateNetworkConditions',{offline:false,latency:400,downloadThroughput:400*1024/8,uploadThroughput:400*1024/8});
|
||||
const t1=Date.now(); send('Page.navigate',{url:URL_});
|
||||
for (const at of [300,800,1500,3000,5000,8000]) {
|
||||
while(Date.now()-t1<at) await sleep(20);
|
||||
if (await probe(at+'ms')==='LEAK') leak=true;
|
||||
}
|
||||
s=await send('Page.captureScreenshot',{format:'png'});
|
||||
fs.writeFileSync(OUT+'/fixed_slow.png', Buffer.from(s.result.data,'base64'));
|
||||
|
||||
console.log('\n=== 全部网络请求 ===');
|
||||
[...new Set(reqs)].forEach(r=>console.log(' '+r));
|
||||
console.log('\n=== 控制台错误 ===');
|
||||
console.log(errs.length? errs.slice(0,5).map(x=>' '+x).join('\n') : ' 无');
|
||||
console.log('\n结论:' + (leak ? '❌ 仍有裸模板闪现' : '✅ 三个场景下均未出现裸模板'));
|
||||
ws.close(); chrome.kill(); process.exit(leak?1:0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,36 @@
|
||||
// 渲染实测:满勤达标 + 奖惩定稿 → 奖惩卡尾行「计入应得合计」= 满勤 + 奖惩
|
||||
const { spawn } = require('child_process');
|
||||
const OUT='/root/.openclaw/workspace/work-hours-tracker/png';
|
||||
const URL='http://127.0.0.1:9522/index.html';
|
||||
const chrome=spawn('/usr/bin/chromium',['--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage','--remote-debugging-port=9252',`--user-data-dir=${OUT}/.chrome-fullsum`,'--window-size=360,900','about:blank'],{stdio:['ignore','ignore','pipe']});
|
||||
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
||||
async function getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9252/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('cdp');}
|
||||
(async()=>{
|
||||
const ws=new WebSocket(await getWs());let id=0;const p=new Map();let or;const o=new Promise(r=>or=r);ws.onopen=or;
|
||||
ws.onmessage=e=>{const m=JSON.parse(e.data);if(m.id&&p.has(m.id)){p.get(m.id)(m);p.delete(m.id);}};await o;
|
||||
const send=(me,pa={})=>new Promise(r=>{const i=++id;p.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:pa}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable');await send('Page.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:360,height:900,deviceScaleFactor:2,mobile:true});
|
||||
await send('Page.navigate',{url:URL});await sleep(1300);
|
||||
// 27天×8h=216h 满勤,其中5日查获5件(+50),定稿 +50
|
||||
await ev(`(function(){localStorage.clear();const rec={};
|
||||
for(let d=1;d<=27;d++)rec['2026-09-'+String(d).padStart(2,'0')]={status:'work',shift_type:'day',hours:8,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
rec['2026-09-05'].contraband_found=5;
|
||||
localStorage.setItem('work_records',JSON.stringify(rec));
|
||||
localStorage.setItem('work_month_adjust',JSON.stringify({'2026-09':{status:'final',finalAmount:50,note:'组长核定',finalizedAt:'2026-10-01T09:00:00.000Z'}}));
|
||||
return 1;})()`);
|
||||
await send('Page.navigate',{url:URL});await sleep(1500);
|
||||
console.log('打卡页顶部 =',JSON.stringify(await ev(`document.querySelector('header').innerText.replace(/\\n/g,' | ')`)));
|
||||
await ev(`(function(){const b=Array.from(document.querySelectorAll('button')).find(x=>/汇总/.test(x.innerText));if(b)b.click();return 1;})()`);
|
||||
await sleep(600);
|
||||
const s=await ev(`(function(){const c=Array.from(document.querySelectorAll('.card'));
|
||||
const a=c.find(x=>/总劳动应得/.test(x.innerText)),r=c.find(x=>/奖惩记录/.test(x.innerText));
|
||||
return {earned:a?a.innerText.replace(/\\n/g,' | '):null, review:r?r.innerText.replace(/\\n/g,' | '):null};})()`);
|
||||
console.log('应得卡 =',JSON.stringify(s.earned));
|
||||
console.log('奖惩卡 =',JSON.stringify(s.review));
|
||||
const shot=await send('Page.captureScreenshot',{format:'png'});
|
||||
require('fs').writeFileSync(OUT+'/review_full_final.png',Buffer.from(shot.result.data,'base64'));
|
||||
console.log('screenshot -> png/review_full_final.png');
|
||||
ws.close();chrome.kill();process.exit(0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,310 @@
|
||||
// 独立验证「首次登录」的真实 UI 路径(doLogin),不走底层 pullAll。
|
||||
//
|
||||
// 为什么要单独测:verify_sync_e2e.js 的 C 步直接调 window.WHTSync.pullAll(),
|
||||
// 而这个底层方法的语义本身就是「用服务器覆盖本机」—— 它把本地 2 天冲掉是预期行为,
|
||||
// 证明不了 UI 登录路径安全。UI 走 doLogin,必须用真实点击路径独立验证。
|
||||
//
|
||||
// 六个场景(全部走「填表单 + 点登录按钮」):
|
||||
// A 远端空 + 本地有 → 本地不能丢,且应上传
|
||||
// B 远端有 + 本地空 → 应拉全量
|
||||
// C 远端有 + 本地有(不同天) → 双向并集,两边都不能丢
|
||||
// D 同一天内容不同 → 弹合并窗;选「保留本机」
|
||||
// E 同一天内容不同 → 弹合并窗;选「用服务器版本」
|
||||
// F 同一天内容不同 → 选「先不处理」→ 必须冻结自动同步,数据一动不动
|
||||
//
|
||||
// 前置:server 跑在 9524(ALLOW_REGISTER=1,独立 DB /tmp/wht-login.db),静态页同源
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const OUT = '/root/.openclaw/workspace/work-hours-tracker/png';
|
||||
const BASE = 'http://127.0.0.1:9524';
|
||||
const PAGE = BASE + '/index.html';
|
||||
const PW = 'login-test-2026';
|
||||
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
let pass = 0, fail = 0;
|
||||
const ck = (name, got, want) => {
|
||||
const ok = String(got) === String(want);
|
||||
ok ? pass++ : fail++;
|
||||
console.log(` ${ok ? '✅' : '❌'} ${name}`);
|
||||
if (!ok) console.log(` 实际=${got}\n 期望=${want}`);
|
||||
};
|
||||
|
||||
const REC = (h, sh = 'day', extra = {}) => Object.assign({
|
||||
status: 'work', shift_type: sh, hours: h,
|
||||
contraband_found: 0, contraband_missed: 0, other_penalty: null, penalty_reason: ''
|
||||
}, extra);
|
||||
|
||||
// ---------- node 侧直连服务端(独立 token,与浏览器会话互不干扰)----------
|
||||
// 注意:core/api.js 的登录限流是 10 次/分钟/IP,而 clientKey 在没有代理头时统一落到 'local'。
|
||||
// node 侧预置 6 个账号各登一次 + 浏览器 6 次登录 = 12 次,会把同一个桶打穿(实测 429)。
|
||||
// 所以 node 侧请求带上各自的 x-forwarded-for,分到独立桶,把限额留给真正要测的 UI 登录。
|
||||
const ipOf = u => '10.9.' + (users.indexOf(u) + 1) + '.1';
|
||||
const jf = async (path, opt = {}) => {
|
||||
const r = await fetch(BASE + path, opt);
|
||||
return r.json();
|
||||
};
|
||||
const H = (u, extra = {}) => Object.assign({ 'content-type': 'application/json', 'x-forwarded-for': ipOf(u) }, extra);
|
||||
|
||||
async function reg(u) {
|
||||
const d = await jf('/api/register', { method: 'POST', headers: H(u), body: JSON.stringify({ username: u, password: PW }) });
|
||||
if (!d.ok) throw new Error('注册失败 ' + u + ': ' + d.error);
|
||||
}
|
||||
async function tok(u) {
|
||||
const d = await jf('/api/login', { method: 'POST', headers: H(u), body: JSON.stringify({ username: u, password: PW }) });
|
||||
if (!d.ok) throw new Error('登录失败 ' + u + ': ' + d.error);
|
||||
return d.token;
|
||||
}
|
||||
async function srvPush(u, body) {
|
||||
const d = await jf('/api/push', {
|
||||
method: 'POST', headers: H(u, { authorization: 'Bearer ' + T[u] }), body: JSON.stringify(body)
|
||||
});
|
||||
if (!d.ok) throw new Error('push 失败: ' + d.error);
|
||||
return d;
|
||||
}
|
||||
async function srvDump(u) {
|
||||
return jf('/api/data', { headers: { authorization: 'Bearer ' + T[u], 'x-forwarded-for': ipOf(u) } });
|
||||
}
|
||||
async function srvDays(u) {
|
||||
const d = await srvDump(u);
|
||||
return Object.keys(d.records || {}).sort().join(',') || '(空)';
|
||||
}
|
||||
async function srvDay(u, day) {
|
||||
const d = await srvDump(u);
|
||||
const r = (d.records || {})[day];
|
||||
return r ? `${r.shift_type} ${r.hours}h` : '(无)';
|
||||
}
|
||||
|
||||
const users = ['ut.a', 'ut.b', 'ut.c', 'ut.d', 'ut.e', 'ut.f'];
|
||||
const T = {};
|
||||
|
||||
(async () => {
|
||||
console.log('=== 0. 预置服务端账号与数据 ===');
|
||||
for (const u of users) await reg(u);
|
||||
for (const u of users) T[u] = await tok(u);
|
||||
|
||||
// B/C:服务端 2 天,与本地不重叠
|
||||
const seedBC = { records: { '2026-09-01': REC(8), '2026-09-03': REC(10, 'night') } };
|
||||
await srvPush('ut.b', seedBC);
|
||||
await srvPush('ut.c', seedBC);
|
||||
|
||||
// D/E/F:服务端 9-01 白班 8h(本地同一天将是夜班 12h → 冲突)+ 服务端独有 9-03
|
||||
const seedConf = { records: { '2026-09-01': REC(8, 'day'), '2026-09-03': REC(10, 'night') } };
|
||||
for (const u of ['ut.d', 'ut.e', 'ut.f']) await srvPush(u, seedConf);
|
||||
|
||||
for (const u of users) console.log(` ${u} 服务端 =`, await srvDays(u));
|
||||
|
||||
// ---------- 起浏览器 ----------
|
||||
const chrome = spawn('/usr/bin/chromium', [
|
||||
'--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage',
|
||||
'--remote-debugging-port=9262', `--user-data-dir=${OUT}/.chrome-login`,
|
||||
'--window-size=360,900', 'about:blank'
|
||||
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
||||
|
||||
const wsUrl = await (async () => {
|
||||
for (let i = 0; i < 40; i++) {
|
||||
try { const t = await (await fetch('http://127.0.0.1:9262/json/list')).json(); if (t.length) return t[0].webSocketDebuggerUrl; } catch {}
|
||||
await sleep(250);
|
||||
}
|
||||
throw new Error('CDP 起不来');
|
||||
})();
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
let id = 0; const pnd = new Map();
|
||||
await new Promise(r => { ws.onopen = r; });
|
||||
ws.onmessage = e => { const m = JSON.parse(e.data); if (m.id && pnd.has(m.id)) { pnd.get(m.id)(m); pnd.delete(m.id); } };
|
||||
const send = (method, params = {}) => new Promise(r => { const i = ++id; pnd.set(i, r); ws.send(JSON.stringify({ id: i, method, params })); });
|
||||
const ev = async expr => {
|
||||
const r = await send('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true });
|
||||
if (r.result && r.result.exceptionDetails) {
|
||||
const ed = r.result.exceptionDetails;
|
||||
return 'JS_ERR: ' + ((ed.exception && ed.exception.description) || ed.text);
|
||||
}
|
||||
return r.result && r.result.result && r.result.result.value;
|
||||
};
|
||||
await send('Runtime.enable'); await send('Page.enable'); await send('Log.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 360, height: 900, deviceScaleFactor: 2, mobile: true });
|
||||
const errs = [];
|
||||
ws.addEventListener('message', e => {
|
||||
const m = JSON.parse(e.data);
|
||||
if (m.method === 'Log.entryAdded' && m.params.entry.level === 'error') errs.push(m.params.entry.text);
|
||||
});
|
||||
|
||||
// ---------- 页面操作原语 ----------
|
||||
const openSettings = () => ev(`(function(){const b=Array.from(document.querySelectorAll('button')).find(x=>x.getAttribute&&x.getAttribute('title')==='设置');if(b){b.click();return 'ok';}return 'NO_GEAR';})()`);
|
||||
|
||||
/** 走真实 UI:开设置抽屉 → 填账号密码 → 点「登录」。waitMs 为登录后等待时长。 */
|
||||
const uiLogin = async (u, waitMs = 2600) => {
|
||||
const g = await openSettings();
|
||||
if (g !== 'ok') throw new Error('齿轮点不到: ' + g);
|
||||
await sleep(600);
|
||||
const filled = await ev(`(function(){
|
||||
const card=Array.from(document.querySelectorAll('.card')).find(c=>/云端同步/.test(c.innerText));
|
||||
if(!card) return 'NO_CARD';
|
||||
const ins=card.querySelectorAll('input');
|
||||
const u=Array.from(ins).find(i=>i.type==='text'&&/用户名/.test(i.placeholder));
|
||||
const p=Array.from(ins).find(i=>i.type==='password');
|
||||
if(!u||!p) return 'NO_INPUT';
|
||||
const set=(el,v)=>{el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));};
|
||||
set(u,${JSON.stringify(u)}); set(p,${JSON.stringify(PW)});
|
||||
return 'ok';
|
||||
})()`);
|
||||
if (filled !== 'ok') throw new Error('表单填写失败: ' + filled);
|
||||
await sleep(250);
|
||||
const clicked = await ev(`(function(){
|
||||
const card=Array.from(document.querySelectorAll('.card')).find(c=>/云端同步/.test(c.innerText));
|
||||
const b=Array.from(card.querySelectorAll('button')).find(x=>x.innerText.trim()==='登录');
|
||||
if(!b) return 'NO_BTN'; b.click(); return 'ok';
|
||||
})()`);
|
||||
if (clicked !== 'ok') throw new Error('登录按钮点不到: ' + clicked);
|
||||
await sleep(waitMs);
|
||||
};
|
||||
|
||||
const localDays = () => ev(`Object.keys(JSON.parse(localStorage.getItem('work_records')||'{}')).sort().join(',')||'(空)'`);
|
||||
const localDay = d => ev(`(function(){const r=(JSON.parse(localStorage.getItem('work_records')||'{}'))[${JSON.stringify(d)}];return r?r.shift_type+' '+r.hours+'h':'(无)';})()`);
|
||||
|
||||
// toast 只停留两秒就消,事后去读 DOM 必然是空。用 MutationObserver 全程录下来。
|
||||
const installToastRecorder = () => ev(`(function(){
|
||||
window.__toasts=[];
|
||||
const seen=new Set();
|
||||
const scan=()=>{
|
||||
document.querySelectorAll('div').forEach(d=>{
|
||||
const c=d.className;
|
||||
if(typeof c==='string'&&c.indexOf('bg-slate-800/95')>=0&&c.indexOf('rounded-full')>=0){
|
||||
const t=(d.innerText||'').trim();
|
||||
if(t&&!seen.has(t)){seen.add(t);window.__toasts.push(t);}
|
||||
}
|
||||
});
|
||||
};
|
||||
if(window.__toastRec) window.__toastRec.disconnect();
|
||||
window.__toastRec=new MutationObserver(scan);
|
||||
window.__toastRec.observe(document.body,{childList:true,subtree:true,characterData:true});
|
||||
scan();
|
||||
return 'ok';
|
||||
})()`);
|
||||
const toasts = () => ev(`((window.__toasts||[]).join(' ‖ '))||'(无 toast)'`);
|
||||
const clearToasts = () => ev(`(function(){window.__toasts=[];return 1;})()`);
|
||||
|
||||
const mergeVisible = () => ev(`(function(){const els=Array.from(document.querySelectorAll('h3')).filter(h=>/本机与服务器有冲突/.test(h.innerText));return els.length>0;})()`);
|
||||
const mergeBody = () => ev(`(function(){const h=Array.from(document.querySelectorAll('h3')).find(x=>/本机与服务器有冲突/.test(x.innerText));if(!h)return'(无弹窗)';return h.closest('div.w-full').innerText.replace(/\\n+/g,' | ');})()`);
|
||||
const clickMerge = re => ev(`(function(){
|
||||
const h=Array.from(document.querySelectorAll('h3')).find(x=>/本机与服务器有冲突/.test(x.innerText));
|
||||
if(!h) return 'NO_DIALOG';
|
||||
const b=Array.from(h.closest('div.w-full').querySelectorAll('button')).find(x=>${re}.test(x.innerText));
|
||||
if(!b) return 'NO_BTN'; b.click(); return 'ok';
|
||||
})()`);
|
||||
|
||||
const resetLocal = async (recs) => {
|
||||
await send('Page.navigate', { url: PAGE }); await sleep(1400);
|
||||
await ev(`(function(){localStorage.clear();${recs ? `localStorage.setItem('work_records',JSON.stringify(${JSON.stringify(recs)}));` : ''}return 1;})()`);
|
||||
await send('Page.navigate', { url: PAGE }); await sleep(1700);
|
||||
await installToastRecorder();
|
||||
};
|
||||
|
||||
// ==================== A ====================
|
||||
console.log('\n=== A. 远端空 + 本地 2 天 → 本地不能丢,且应上传 ===');
|
||||
await resetLocal({ '2026-09-20': REC(10, 'night'), '2026-09-21': REC(8) });
|
||||
console.log(' 登录前本地 =', await localDays());
|
||||
await uiLogin('ut.a');
|
||||
console.log(' toast =', await toasts());
|
||||
ck('A1 登录后本地记录(不能丢)', await localDays(), '2026-09-20,2026-09-21');
|
||||
ck('A2 服务端已收到本地数据', await srvDays('ut.a'), '2026-09-20,2026-09-21');
|
||||
ck('A3 pending 归零', await ev(`window.WHTSync.state.pending`), '0');
|
||||
ck('A4 未误弹合并窗', await mergeVisible(), 'false');
|
||||
|
||||
// ==================== B ====================
|
||||
console.log('\n=== B. 远端 2 天 + 本地空 → 应拉全量 ===');
|
||||
await resetLocal(null);
|
||||
console.log(' 登录前本地 =', await localDays());
|
||||
await uiLogin('ut.b');
|
||||
console.log(' toast =', await toasts());
|
||||
ck('B1 拉到远端 2 天', await localDays(), '2026-09-01,2026-09-03');
|
||||
ck('B2 服务端未被空本地覆盖', await srvDays('ut.b'), '2026-09-01,2026-09-03');
|
||||
|
||||
// ==================== C ====================
|
||||
console.log('\n=== C. 远端 2 天 + 本地 1 天(不同) → 双向并集 ===');
|
||||
await resetLocal({ '2026-09-25': REC(12, 'night') });
|
||||
console.log(' 登录前本地 =', await localDays());
|
||||
await uiLogin('ut.c');
|
||||
console.log(' toast =', await toasts());
|
||||
ck('C1 本地应含三天', await localDays(), '2026-09-01,2026-09-03,2026-09-25');
|
||||
ck('C2 服务端应含三天', await srvDays('ut.c'), '2026-09-01,2026-09-03,2026-09-25');
|
||||
ck('C3 无冲突不该弹窗', await mergeVisible(), 'false');
|
||||
|
||||
// ==================== D ====================
|
||||
console.log('\n=== D. 同一天内容不同 → 弹窗 + 选「保留本机」===');
|
||||
await resetLocal({ '2026-09-01': REC(12, 'night'), '2026-09-25': REC(8) });
|
||||
console.log(' 登录前本地 9-01 =', await localDay('2026-09-01'), ' 服务端 9-01 =', await srvDay('ut.d', '2026-09-01'));
|
||||
await uiLogin('ut.d', 2000);
|
||||
ck('D1 弹出合并窗', await mergeVisible(), 'true');
|
||||
console.log(' 弹窗内容 =', (await mergeBody()).slice(0, 260));
|
||||
ck('D2 弹窗未擅自改本地', await localDay('2026-09-01'), 'night 12h');
|
||||
const shotD = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(OUT + '/merge_dialog.png', Buffer.from(shotD.result.data, 'base64'));
|
||||
ck('D3 点「保留本机」', await clickMerge('/保留本机/'), 'ok');
|
||||
await sleep(2600);
|
||||
console.log(' toast =', await toasts());
|
||||
ck('D4 冲突日保留本机值', await localDay('2026-09-01'), 'night 12h');
|
||||
ck('D5 服务端冲突日被本机覆盖', await srvDay('ut.d', '2026-09-01'), 'night 12h');
|
||||
ck('D6 本地含三天(并集)', await localDays(), '2026-09-01,2026-09-03,2026-09-25');
|
||||
ck('D7 服务端含三天(并集)', await srvDays('ut.d'), '2026-09-01,2026-09-03,2026-09-25');
|
||||
ck('D8 合并前留了本机快照', await ev(`!!(window.WHTSync.getLocalSnapshot()||{}).records`), 'true');
|
||||
|
||||
// ==================== E ====================
|
||||
console.log('\n=== E. 同一天内容不同 → 选「用服务器版本」===');
|
||||
await resetLocal({ '2026-09-01': REC(12, 'night'), '2026-09-25': REC(8) });
|
||||
await uiLogin('ut.e', 2000);
|
||||
ck('E1 弹出合并窗', await mergeVisible(), 'true');
|
||||
ck('E2 点「用服务器版本」', await clickMerge('/服务器版本/'), 'ok');
|
||||
await sleep(2600);
|
||||
console.log(' toast =', await toasts());
|
||||
ck('E3 冲突日改用服务器值', await localDay('2026-09-01'), 'day 8h');
|
||||
ck('E4 本机独有的天仍在(没被覆盖掉)', await localDay('2026-09-25'), 'day 8h');
|
||||
ck('E5 本地含三天', await localDays(), '2026-09-01,2026-09-03,2026-09-25');
|
||||
ck('E6 服务端含三天', await srvDays('ut.e'), '2026-09-01,2026-09-03,2026-09-25');
|
||||
ck('E7 服务端冲突日保持原值', await srvDay('ut.e', '2026-09-01'), 'day 8h');
|
||||
|
||||
// ==================== F ====================
|
||||
console.log('\n=== F. 选「先不处理」→ 必须冻结自动同步 ===');
|
||||
await resetLocal({ '2026-09-01': REC(12, 'night'), '2026-09-25': REC(8) });
|
||||
await uiLogin('ut.f', 2000);
|
||||
ck('F1 弹出合并窗', await mergeVisible(), 'true');
|
||||
ck('F2 点「先不处理」', await clickMerge('/先不处理/'), 'ok');
|
||||
await sleep(800);
|
||||
console.log(' toast =', await toasts());
|
||||
ck('F3 已置同步冻结标志', await ev(`!!window.WHTSync.state.hold`), 'true');
|
||||
ck('F4 本地一动不动', await localDays(), '2026-09-01,2026-09-25');
|
||||
ck('F5 状态胶囊提示冲突', await ev(`(function(){const c=Array.from(document.querySelectorAll('.card')).find(x=>/云端同步/.test(x.innerText));return c?/已暂停同步/.test(c.innerText):'NO_CARD';})()`), 'true');
|
||||
|
||||
console.log(' —— 冻结期间做一次本地改动,验证不会偷偷同步 ——');
|
||||
await ev(`(function(){
|
||||
const r=JSON.parse(localStorage.getItem('work_records')||'{}');
|
||||
r['2026-09-28']={status:'work',shift_type:'day',hours:8,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
localStorage.setItem('work_records',JSON.stringify(r));
|
||||
window.WHTSync.markDirty('record','2026-09-28');
|
||||
return 1;})()`);
|
||||
await sleep(4000); // 远超 1.5s 防抖窗口
|
||||
ck('F6 改动只攒着不上传', await ev(`window.WHTSync.state.pending`), '1');
|
||||
ck('F7 服务端未被偷偷写入', await srvDays('ut.f'), '2026-09-01,2026-09-03');
|
||||
ck('F8 本地 9-01 仍是本机值(远端没覆盖过来)', await localDay('2026-09-01'), 'night 12h');
|
||||
|
||||
console.log(' —— 冻结后从同步面板重新进合并 ——');
|
||||
await openSettings(); await sleep(500);
|
||||
ck('F9 点「比对本机与服务器」', await ev(`(function(){const c=Array.from(document.querySelectorAll('.card')).find(x=>/云端同步/.test(x.innerText));const b=Array.from(c.querySelectorAll('button')).find(x=>/比对本机与服务器/.test(x.innerText));if(!b)return'NO_BTN';b.click();return 'ok';})()`), 'ok');
|
||||
await sleep(2200);
|
||||
ck('F10 合并窗重新弹出', await mergeVisible(), 'true');
|
||||
ck('F11 选「保留本机」收尾', await clickMerge('/保留本机/'), 'ok');
|
||||
await sleep(2800);
|
||||
ck('F12 冻结已解除', await ev(`!!window.WHTSync.state.hold`), 'false');
|
||||
ck('F13 服务端补齐四天', await srvDays('ut.f'), '2026-09-01,2026-09-03,2026-09-25,2026-09-28');
|
||||
ck('F14 pending 归零', await ev(`window.WHTSync.state.pending`), '0');
|
||||
|
||||
const shot = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(OUT + '/login_merge_done.png', Buffer.from(shot.result.data, 'base64'));
|
||||
|
||||
console.log('\n=== 控制台错误 ===');
|
||||
console.log(errs.length ? errs.slice(0, 6).join('\n') : ' 无');
|
||||
console.log(`\n结果:通过 ${pass} / 失败 ${fail}`);
|
||||
ws.close(); chrome.kill();
|
||||
process.exit(fail > 0 ? 1 : 0);
|
||||
})().catch(e => { console.error('FAILED:', e.message); process.exit(1); });
|
||||
@@ -0,0 +1,150 @@
|
||||
// 验收:发薪日标记 + 奖惩终稿修正入口
|
||||
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=9231', `--user-data-dir=${OUT_DIR}/.chrome-verify`,
|
||||
'--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:9231/json/list')).json(); if(t.length)return t[0].webSocketDebuggerUrl; }catch(e){} await sleep(250);} throw new Error('CDP fail: '+stderr.join('').slice(-400));
|
||||
}
|
||||
(async () => {
|
||||
const wsUrl = await getWsUrl();
|
||||
const ws = new WebSocket(wsUrl);
|
||||
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;};
|
||||
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
|
||||
// 预置:2026-08 有打卡+奖惩数据,便于验证上月核对入口
|
||||
await send('Page.navigate',{url:URL}); await sleep(1200);
|
||||
await ev(`(function(){
|
||||
const rec = {};
|
||||
for (let d=1; d<=28; d++) {
|
||||
rec['2026-08-'+String(d).padStart(2,'0')] = { status:'work', shift_type: d%3?'day':'night', hours: d%3?8:10, contraband_found: d===5?3:0, contraband_missed: d===9?2:0, other_penalty: d===12?50:null, penalty_reason: d===12?'迟到':'' };
|
||||
}
|
||||
for (let d=1; d<=20; d++) {
|
||||
rec['2026-09-'+String(d).padStart(2,'0')] = { status:'work', shift_type:'day', hours:8, contraband_found: d===3?1:0, contraband_missed:0, other_penalty:null, penalty_reason:'' };
|
||||
}
|
||||
localStorage.setItem('work_records', JSON.stringify(rec));
|
||||
localStorage.removeItem('work_month_adjust');
|
||||
return 'seeded';
|
||||
})()`);
|
||||
await send('Page.navigate',{url:URL}); await sleep(2000);
|
||||
|
||||
// ① 日历发薪日标记
|
||||
const cal = await ev(`(function(){
|
||||
const marks = Array.from(document.querySelectorAll('.pay-mark')).map(m=>({cls:m.className, txt:m.textContent}));
|
||||
const ringA = document.querySelectorAll('.pay-ring-a').length;
|
||||
const ringB = document.querySelectorAll('.pay-ring-b').length;
|
||||
return { payMarkCount: marks.length, marks: marks.slice(0,6), ringA, ringB };
|
||||
})()`);
|
||||
console.log('① 发薪日标记:', JSON.stringify(cal));
|
||||
|
||||
const s1 = await send('Page.captureScreenshot',{format:'png'});
|
||||
fs.writeFileSync(OUT_DIR+'/v1_calendar.png', Buffer.from(s1.result.data,'base64'));
|
||||
|
||||
// ② 点发薪日格子 -> 到账横幅
|
||||
const banner = await ev(`new Promise(r=>{
|
||||
const cells = Array.from(document.querySelectorAll('.grid.grid-cols-7.gap-1\\\\.5 > div'));
|
||||
const target = cells.find(c => c.querySelector('.pay-mark'));
|
||||
if(!target) return r({err:'no payday cell'});
|
||||
target.click();
|
||||
setTimeout(()=>{
|
||||
const ov = document.querySelector('.fixed.inset-0.z-50');
|
||||
const txt = ov ? ov.innerText.replace(/\\s+/g,' ') : null;
|
||||
r({ drawerOpen: !!ov, hasBanner: !!(txt && txt.includes('今日到账')), snippet: txt ? txt.slice(0,110) : null });
|
||||
}, 420);
|
||||
})`);
|
||||
console.log('② 发薪日到账横幅:', JSON.stringify(banner));
|
||||
|
||||
const s2 = await send('Page.captureScreenshot',{format:'png'});
|
||||
fs.writeFileSync(OUT_DIR+'/v2_payday_drawer.png', Buffer.from(s2.result.data,'base64'));
|
||||
|
||||
// 关抽屉
|
||||
await ev(`(function(){ const b=document.querySelector('.fixed.inset-0.z-50 button'); if(b) b.click(); return 1; })()`);
|
||||
await sleep(400);
|
||||
|
||||
// ③ 汇总页 -> 上月核对入口
|
||||
const summary = await ev(`new Promise(r=>{
|
||||
const t = Array.from(document.querySelectorAll('nav button')).find(b=>(b.innerText||'').trim()==='汇总');
|
||||
t.click();
|
||||
setTimeout(()=>{
|
||||
const btns = Array.from(document.querySelectorAll('button')).filter(b=>/核对修正|查看\\/改稿/.test(b.innerText||''));
|
||||
const tags = Array.from(document.querySelectorAll('.stat-tag')).map(x=>x.textContent.trim());
|
||||
const body = document.body.innerText.replace(/\\s+/g,' ');
|
||||
r({ correctBtnCount: btns.length, tags, hasRawReview: body.includes('日常累计'), hasKoujing: body.includes('奖惩口径') });
|
||||
}, 400);
|
||||
})`);
|
||||
console.log('③ 汇总页修正入口:', JSON.stringify(summary));
|
||||
|
||||
const s3 = await send('Page.captureScreenshot',{format:'png'});
|
||||
fs.writeFileSync(OUT_DIR+'/v3_summary.png', Buffer.from(s3.result.data,'base64'));
|
||||
|
||||
// ④ 打开上月核对抽屉,填终稿 80,定稿
|
||||
const adj = await ev(`new Promise(r=>{
|
||||
const btn = Array.from(document.querySelectorAll('button')).filter(b=>/核对修正|查看\\/改稿/.test(b.innerText||''))[0];
|
||||
if(!btn) return r({err:'no correct btn'});
|
||||
btn.click();
|
||||
setTimeout(()=>{
|
||||
const ov = document.querySelector('.fixed.inset-0.z-50');
|
||||
const txt = ov ? ov.innerText.replace(/\\s+/g,' ') : null;
|
||||
const input = ov && ov.querySelector('input[type=number]');
|
||||
r({ open: !!ov, hasRawSum: !!(txt&&txt.includes('累计小计')), prefill: input ? input.value : null, snippet: txt?txt.slice(0,150):null });
|
||||
}, 420);
|
||||
})`);
|
||||
console.log('④ 核对抽屉:', JSON.stringify(adj));
|
||||
|
||||
const s4 = await send('Page.captureScreenshot',{format:'png'});
|
||||
fs.writeFileSync(OUT_DIR+'/v4_adjust_drawer.png', Buffer.from(s4.result.data,'base64'));
|
||||
|
||||
// 填 80 并定稿
|
||||
const fin = await ev(`new Promise(r=>{
|
||||
const ov = document.querySelector('.fixed.inset-0.z-50');
|
||||
const input = ov.querySelector('input[type=number]');
|
||||
input.value = '80';
|
||||
input.dispatchEvent(new Event('input',{bubbles:true}));
|
||||
setTimeout(()=>{
|
||||
const delta = ov.innerText.replace(/\\s+/g,' ');
|
||||
const okBtn = Array.from(ov.querySelectorAll('button')).find(b=>/确认定稿/.test(b.innerText||''));
|
||||
okBtn.click();
|
||||
setTimeout(()=>{
|
||||
const store = JSON.parse(localStorage.getItem('work_month_adjust')||'{}');
|
||||
const body = document.body.innerText.replace(/\\s+/g,' ');
|
||||
r({ deltaShown: /上调|下调/.test(delta), stored: store, bodyHasFinal: body.includes('已定稿') });
|
||||
}, 500);
|
||||
}, 300);
|
||||
})`);
|
||||
console.log('⑤ 定稿写入:', JSON.stringify(fin));
|
||||
|
||||
const s5 = await send('Page.captureScreenshot',{format:'png'});
|
||||
fs.writeFileSync(OUT_DIR+'/v5_after_final.png', Buffer.from(s5.result.data,'base64'));
|
||||
|
||||
// ⑥ 定稿后发放口径是否跟着变(切到 9 月看 9月10日到账)
|
||||
const payout = await ev(`(function(){
|
||||
const body = document.body.innerText.replace(/\\s+/g,' ');
|
||||
const m = body.match(/9月10日[^¥]*¥([\\d,.]+)/);
|
||||
return { snippet: body.slice(body.indexOf('本月到账'), body.indexOf('本月到账')+160), matched: m?m[1]:null };
|
||||
})()`);
|
||||
console.log('⑥ 发放口径:', JSON.stringify(payout));
|
||||
|
||||
console.log('\n=== 异常/控制台 ===');
|
||||
console.log(logs.length?logs.join('\n'):'(无异常)');
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
})().catch(e=>{ console.error('FAILED:',e.message); console.error(stderr.join('').slice(-400)); try{chrome.kill();}catch(_){}process.exit(1); });
|
||||
@@ -0,0 +1,48 @@
|
||||
const { spawn } = require('child_process');
|
||||
const CHROME = '/usr/bin/chromium';
|
||||
const OUT_DIR = __dirname;
|
||||
const URL = 'http://127.0.0.1:9522/index.html';
|
||||
const chrome = spawn(CHROME, ['--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage',
|
||||
'--remote-debugging-port=9251', `--user-data-dir=${OUT_DIR}/.chrome-newtop`,'--window-size=390,900','about:blank'
|
||||
], { stdio:['ignore','ignore','pipe'] });
|
||||
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
||||
async function getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9251/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('CDP fail');}
|
||||
const SCENES = {
|
||||
partial: `(function(){localStorage.clear();const rec={};
|
||||
rec['2026-09-01']={status:'work',shift_type:'day',hours:8,contraband_found:1,contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
rec['2026-09-02']={status:'work',shift_type:'day',hours:8,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
rec['2026-09-03']={status:'work',shift_type:'day',hours:6,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
localStorage.setItem('work_records',JSON.stringify(rec));return 1;})()`,
|
||||
full: `(function(){localStorage.clear();const rec={};
|
||||
for(let d=1;d<=27;d++)rec['2026-09-'+String(d).padStart(2,'0')]={status:'work',shift_type:'day',hours:8,contraband_found:(d===5?2:0),contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
localStorage.setItem('work_records',JSON.stringify(rec));return 1;})()`
|
||||
};
|
||||
(async()=>{
|
||||
const ws=new WebSocket(await getWs()); let id=0; const pending=new Map();
|
||||
let or; const op=new Promise(r=>or=r); ws.onopen=or;
|
||||
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);}};
|
||||
await op;
|
||||
const send=(me,p={})=>new Promise(r=>{const i=++id;pending.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:p}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:390,height:900,deviceScaleFactor:2,mobile:true});
|
||||
await send('Page.navigate',{url:URL}); await sleep(1300);
|
||||
const fs=require('fs');
|
||||
for (const [name, script] of Object.entries(SCENES)) {
|
||||
await ev(script);
|
||||
await send('Page.navigate',{url:URL}); await sleep(1500);
|
||||
const t = await ev(`document.querySelector('header').innerText`);
|
||||
console.log(`\n===== 场景 ${name} =====`);
|
||||
console.log(t);
|
||||
const r = await send('Page.captureScreenshot',{format:'png'});
|
||||
fs.writeFileSync(`${OUT_DIR}/new_top_${name}.png`, Buffer.from(r.result.data,'base64'));
|
||||
// 汇总页
|
||||
await ev(`tab && (tab.value='summary')`);
|
||||
await ev(`(function(){const b=[...document.querySelectorAll('nav button')].find(x=>x.innerText.includes('汇总'));if(b)b.click();return 1;})()`);
|
||||
await sleep(900);
|
||||
const r2 = await send('Page.captureScreenshot',{format:'png',captureBeyondViewport:true});
|
||||
fs.writeFileSync(`${OUT_DIR}/new_sum_${name}.png`, Buffer.from(r2.result.data,'base64'));
|
||||
}
|
||||
console.log('\nscreenshots: png/new_top_{partial,full}.png, png/new_sum_{partial,full}.png');
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,73 @@
|
||||
// 验收:顶部三胶囊必须单行(不折行)。覆盖 320/360/390/414/430 五档 × 普通/最坏两种数据
|
||||
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=9238', `--user-data-dir=${OUT_DIR}/.chrome-pillv`,'--window-size=390,900','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 getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9238/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('CDP fail '+stderr.join('').slice(-300));}
|
||||
|
||||
let pass=0, fail=0;
|
||||
const ok=(n,c,x='')=>{ if(c){pass++;console.log(' ✅ '+n+(x?' → '+x:''));} else {fail++;console.log(' ❌ '+n+(x?' → '+x:''));} };
|
||||
|
||||
const DATA = {
|
||||
normal: `(function(){const rec={};for(let d=1;d<=2;d++)rec['2026-09-0'+d]={status:'work',shift_type:'day',hours:d===1?8:6.5,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};localStorage.setItem('work_records',JSON.stringify(rec));return 1;})()`,
|
||||
worst: `(function(){const rec={};for(let d=1;d<=27;d++)rec['2026-09-'+String(d).padStart(2,'0')]={status:'work',shift_type:'night',hours:12,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};localStorage.setItem('work_records',JSON.stringify(rec));return 1;})()`
|
||||
};
|
||||
|
||||
(async()=>{
|
||||
const ws=new WebSocket(await getWs()); let id=0; const pending=new Map(); const logs=[];
|
||||
let or; const op=new Promise(r=>or=r); ws.onopen=or;
|
||||
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.exceptionThrown')logs.push('EXC '+JSON.stringify(m.params.exceptionDetails).slice(0,250));};
|
||||
await op;
|
||||
const send=(me,p={})=>new Promise(r=>{const i=++id;pending.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:p}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});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'));};
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
|
||||
const measure = () => ev(`(function(){
|
||||
const wrap=document.querySelector('header .flex.flex-nowrap');
|
||||
if(!wrap) return {err:'wrap not found'};
|
||||
const ps=Array.from(document.querySelectorAll('header .pill'));
|
||||
const items=ps.map(p=>{const r=p.getBoundingClientRect();return {t:p.innerText.replace(/\\s+/g,' ').trim(),w:+r.width.toFixed(1),top:+r.top.toFixed(1),h:+r.height.toFixed(1)};});
|
||||
const gap=parseFloat(getComputedStyle(wrap).gap)||0;
|
||||
const need=items.reduce((a,b)=>a+b.w,0)+gap*(items.length-1);
|
||||
const wr=wrap.getBoundingClientRect();
|
||||
return { vw:innerWidth, wrapW:+wr.width.toFixed(1), wrapH:+wr.height.toFixed(1), gap,
|
||||
rows:new Set(items.map(i=>i.top)).size, need:+need.toFixed(1),
|
||||
scrollW:wrap.scrollWidth, clientW:wrap.clientWidth,
|
||||
pillH:items[0]?items[0].h:0, items };
|
||||
})()`);
|
||||
|
||||
for (const [kind, seed] of Object.entries(DATA)) {
|
||||
for (const W of [320, 360, 390, 414, 430]) {
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:W,height:900,deviceScaleFactor:1,mobile:true});
|
||||
await send('Page.navigate',{url:URL}); await sleep(1100);
|
||||
await ev(seed);
|
||||
await send('Page.navigate',{url:URL}); await sleep(1250);
|
||||
const r = await measure();
|
||||
console.log(`\n── ${kind==='normal'?'常规数据':'最坏数据(27天×12h → ¥6318.0)'} @ ${W}px`);
|
||||
if (r.err) { ok('容器存在', false, r.err); continue; }
|
||||
console.log(` 容器 ${r.wrapW}px | gap ${r.gap}px | 需 ${r.need}px | scrollW ${r.scrollW} / clientW ${r.clientW}`);
|
||||
r.items.forEach(i=>console.log(` ${String(i.w).padStart(6)}px top=${i.top} "${i.t}"`));
|
||||
ok(`单行(所有胶囊 top 相同)`, r.rows===1, `rows=${r.rows}`);
|
||||
ok(`容器高度为单行高(≈${r.pillH}px)`, Math.abs(r.wrapH - r.pillH) < 2, `wrapH=${r.wrapH} pillH=${r.pillH}`);
|
||||
if (W >= 360) ok(`360+ 无需横滑即可容纳`, r.scrollW <= r.clientW + 1, `scrollW=${r.scrollW} clientW=${r.clientW}`);
|
||||
if (kind==='worst' && W===360) await shot('p_worst_360.png');
|
||||
if (kind==='worst' && W===320) await shot('p_worst_320.png');
|
||||
if (kind==='normal' && W===360) await shot('p_normal_360.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(-300));try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,72 @@
|
||||
// 验证:奖惩未定稿 → 一律按 ¥0 计(与满勤未达标同口径);定稿后才计入
|
||||
const { spawn } = require('child_process');
|
||||
const CHROME = '/usr/bin/chromium';
|
||||
const OUT_DIR = __dirname;
|
||||
const URL = 'http://127.0.0.1:9522/index.html';
|
||||
const chrome = spawn(CHROME, ['--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage',
|
||||
'--remote-debugging-port=9251', `--user-data-dir=${OUT_DIR}/.chrome-review`,'--window-size=360,900','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 getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9251/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('CDP fail '+stderr.join('').slice(-300));}
|
||||
|
||||
// 场景:9月 3天22h(8+8+6),其中1天查获1件 → 工时 429,查获记录 +10
|
||||
const RECORDS = `{
|
||||
'2026-09-01':{status:'work',shift_type:'day',hours:8,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''},
|
||||
'2026-09-02':{status:'work',shift_type:'day',hours:8,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''},
|
||||
'2026-09-03':{status:'work',shift_type:'day',hours:6,contraband_found:1,contraband_missed:0,other_penalty:null,penalty_reason:''}
|
||||
}`;
|
||||
|
||||
(async()=>{
|
||||
const ws=new WebSocket(await getWs()); let id=0; const pending=new Map();
|
||||
let or; const op=new Promise(r=>or=r); ws.onopen=or;
|
||||
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);}};
|
||||
await op;
|
||||
const send=(me,p={})=>new Promise(r=>{const i=++id;pending.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:p}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:360,height:900,deviceScaleFactor:2,mobile:true});
|
||||
await send('Page.navigate',{url:URL}); await sleep(1300);
|
||||
|
||||
const readAll = async (label, shot) => {
|
||||
const snap = await ev(`(function(){
|
||||
const hb=document.querySelector('header');
|
||||
// 切到汇总页读「合计应得」卡
|
||||
const tabs=Array.from(document.querySelectorAll('nav button, footer button, button'));
|
||||
return { header: hb.innerText.replace(/\\n/g,' | ') };
|
||||
})()`);
|
||||
console.log('【'+label+'】');
|
||||
console.log(' 打卡页顶部 =', JSON.stringify(snap.header));
|
||||
// 切汇总页
|
||||
await ev(`(function(){const el=Array.from(document.querySelectorAll('button')).find(b=>/汇总/.test(b.innerText));if(el)el.click();return 1;})()`);
|
||||
await sleep(500);
|
||||
const sum = await ev(`(function(){
|
||||
const cards=Array.from(document.querySelectorAll('.card'));
|
||||
const c=cards.find(x=>/总劳动应得/.test(x.innerText));
|
||||
const r=cards.find(x=>/奖惩记录/.test(x.innerText));
|
||||
return { earned: c?c.innerText.replace(/\\n/g,' | '):null, review: r?r.innerText.replace(/\\n/g,' | '):null };
|
||||
})()`);
|
||||
console.log(' 汇总·应得卡 =', JSON.stringify(sum.earned));
|
||||
console.log(' 汇总·奖惩卡 =', JSON.stringify(sum.review));
|
||||
if (shot) {
|
||||
const r=await send('Page.captureScreenshot',{format:'png',captureBeyondViewport:true});
|
||||
require('fs').writeFileSync(OUT_DIR+'/'+shot, Buffer.from(r.result.data,'base64'));
|
||||
console.log(' screenshot -> png/'+shot);
|
||||
}
|
||||
// 切回打卡页
|
||||
await ev(`(function(){const el=Array.from(document.querySelectorAll('button')).find(b=>/打卡/.test(b.innerText));if(el)el.click();return 1;})()`);
|
||||
await sleep(300);
|
||||
};
|
||||
|
||||
// ① 未定稿
|
||||
await ev(`(function(){localStorage.clear();localStorage.setItem('work_records',JSON.stringify(${RECORDS}));return 1;})()`);
|
||||
await send('Page.navigate',{url:URL}); await sleep(1500);
|
||||
await readAll('未定稿:查获1件只作记录,应得应为 ¥429.0', 'review_draft.png');
|
||||
|
||||
// ② 组长审核定稿 +10
|
||||
await ev(`(function(){localStorage.setItem('work_month_adjust',JSON.stringify({'2026-09':{status:'final',finalAmount:10,note:'组长核定',finalizedAt:'2026-10-01T09:00:00.000Z'}}));return 1;})()`);
|
||||
await send('Page.navigate',{url:URL}); await sleep(1500);
|
||||
await readAll('已定稿 +10:应得应为 ¥439.0', 'review_final.png');
|
||||
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -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); });
|
||||
@@ -0,0 +1,90 @@
|
||||
// 端到端:浏览器里跑真实同步流程(登录 → 本地记账自动上传 → 换 origin 拉回)
|
||||
const { spawn } = require('child_process');
|
||||
const OUT='/root/.openclaw/workspace/work-hours-tracker/png';
|
||||
const A='http://127.0.0.1:9524/index.html'; // 新服务:静态 + API 同源
|
||||
const chrome=spawn('/usr/bin/chromium',['--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage','--remote-debugging-port=9260',`--user-data-dir=${OUT}/.chrome-sync`,'--window-size=360,900','about:blank'],{stdio:['ignore','ignore','pipe']});
|
||||
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
||||
async function getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9260/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('cdp');}
|
||||
(async()=>{
|
||||
const ws=new WebSocket(await getWs());let id=0;const p=new Map();let or;const o=new Promise(r=>or=r);ws.onopen=or;
|
||||
ws.onmessage=e=>{const m=JSON.parse(e.data);if(m.id&&p.has(m.id)){p.get(m.id)(m);p.delete(m.id);}};await o;
|
||||
const send=(me,pa={})=>new Promise(r=>{const i=++id;p.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:pa}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});
|
||||
if(r.result&&r.result.exceptionDetails) return 'JS_ERR: '+JSON.stringify(r.result.exceptionDetails.exception&&r.result.exceptionDetails.exception.description||r.result.exceptionDetails.text);
|
||||
return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable');await send('Page.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:360,height:900,deviceScaleFactor:2,mobile:true});
|
||||
|
||||
const errs=[];
|
||||
await send('Log.enable');
|
||||
ws.addEventListener('message',e=>{const m=JSON.parse(e.data);
|
||||
if(m.method==='Log.entryAdded'&&m.params.entry.level==='error')errs.push(m.params.entry.text);});
|
||||
|
||||
console.log('=== A. 首次打开:干净 localStorage,未登录 ===');
|
||||
await send('Page.navigate',{url:A});await sleep(1600);
|
||||
await ev(`(function(){localStorage.clear();return 1;})()`);
|
||||
await send('Page.navigate',{url:A});await sleep(1800);
|
||||
console.log(' WHTSync 已加载 =', await ev(`typeof window.WHTSync`));
|
||||
console.log(' health 探测到的用户数 =', await ev(`window.WHTSync.state.serverUsers`));
|
||||
console.log(' 同步状态 =', await ev(`window.WHTSync.state.status`));
|
||||
|
||||
console.log('\n=== B. 本地先记 2 天(未登录也能记)===');
|
||||
await ev(`(function(){
|
||||
localStorage.setItem('work_records',JSON.stringify({
|
||||
'2026-09-20':{status:'work',shift_type:'night',hours:10,contraband_found:2,contraband_missed:0,other_penalty:null,penalty_reason:''},
|
||||
'2026-09-21':{status:'work',shift_type:'day',hours:8,contraband_found:0,contraband_missed:1,other_penalty:null,penalty_reason:''}
|
||||
}));return 1;})()`);
|
||||
await send('Page.navigate',{url:A});await sleep(1800);
|
||||
console.log(' 本地记录天数 =', await ev(`Object.keys(JSON.parse(localStorage.getItem('work_records'))).length`));
|
||||
console.log(' 打卡页大字 =', await ev(`document.querySelector('header').innerText.replace(/\\n/g,' | ').slice(0,150)`));
|
||||
|
||||
console.log('\n=== C. 登录(服务端已有 9/1、9/3 与定稿奖惩)===');
|
||||
console.log(' login →', await ev(`(async()=>{try{const r=await window.WHTSync.login('kory','kory-work-2026');return 'ok user='+r.user.username+' rev='+r.rev;}catch(e){return 'ERR '+e.message;}})()`));
|
||||
console.log(' pullAll →', await ev(`(async()=>{const d=await window.WHTSync.pullAll();return 'rev='+d.rev+' 远端天数='+Object.keys(d.records).length;})()`));
|
||||
await sleep(400);
|
||||
console.log(' 拉取后本地天数 =', await ev(`Object.keys(JSON.parse(localStorage.getItem('work_records'))).length`));
|
||||
console.log(' 拉取后本地键 =', await ev(`Object.keys(JSON.parse(localStorage.getItem('work_records'))).sort().join(',')`));
|
||||
console.log(' 奖惩定稿是否同步下来 =', await ev(`JSON.stringify(JSON.parse(localStorage.getItem('work_month_adjust')||'{}'))`));
|
||||
|
||||
console.log('\n=== D. 本地全推上去(合并两边)===');
|
||||
console.log(' pushAll →', await ev(`(async()=>{const r=await window.WHTSync.pushAll();return 'rev='+r.rev;})()`));
|
||||
await sleep(300);
|
||||
|
||||
console.log('\n=== E. 新记一天 → 自动上传(不手动点同步)===');
|
||||
await ev(`(function(){
|
||||
const app=document.querySelector('#app');
|
||||
// 走真实 UI 路径:点日历 25 号 → 保存
|
||||
const cells=Array.from(document.querySelectorAll('.grid.grid-cols-7 > div'));
|
||||
const t=cells.find(c=>{const n=c.querySelector('.day-num, .rounded-full');return n&&n.textContent.trim()==='25';});
|
||||
t.click(); return 1;})()`);
|
||||
await sleep(600);
|
||||
await ev(`(function(){const b=Array.from(document.querySelectorAll('button')).find(x=>/^保存/.test(x.innerText.trim()));if(b)b.click();return b?1:'NO_SAVE_BTN';})()`);
|
||||
await sleep(500);
|
||||
console.log(' 保存后 pending =', await ev(`window.WHTSync.state.pending`));
|
||||
console.log(' 等待自动同步(防抖 1.5s)…');
|
||||
await sleep(3200);
|
||||
console.log(' 自动同步后 pending =', await ev(`window.WHTSync.state.pending`));
|
||||
console.log(' 状态 =', await ev(`window.WHTSync.state.status`), ' rev =', await ev(`window.WHTSync.state.rev`));
|
||||
|
||||
console.log('\n=== F. 模拟「换设备」:清空本地后重新登录拉全量 ===');
|
||||
const tok = await ev(`localStorage.getItem('work_sync_token')`);
|
||||
await ev(`(function(){localStorage.clear();return 1;})()`);
|
||||
await send('Page.navigate',{url:A});await sleep(1700);
|
||||
console.log(' 清空后本地天数 =', await ev(`Object.keys(JSON.parse(localStorage.getItem('work_records')||'{}')).length`));
|
||||
console.log(' 重新登录 →', await ev(`(async()=>{try{await window.WHTSync.login('kory','kory-work-2026');const d=await window.WHTSync.pullAll();return '拉到 '+Object.keys(d.records).length+' 天, rev='+d.rev;}catch(e){return 'ERR '+e.message}})()`));
|
||||
await sleep(600);
|
||||
console.log(' 恢复后本地键 =', await ev(`Object.keys(JSON.parse(localStorage.getItem('work_records'))).sort().join(',')`));
|
||||
console.log(' 恢复后打卡页 =', await ev(`document.querySelector('header').innerText.replace(/\\n/g,' | ').slice(0,160)`));
|
||||
|
||||
console.log('\n=== G. 设置页同步面板 ===');
|
||||
await ev(`(function(){const b=Array.from(document.querySelectorAll('button[title=设置],button')).find(x=>x.getAttribute&&x.getAttribute('title')==='设置');if(b)b.click();return 1;})()`);
|
||||
await sleep(700);
|
||||
console.log(' 面板文本 =', await ev(`(function(){const cs=Array.from(document.querySelectorAll('.card'));const c=cs.find(x=>/云端同步/.test(x.innerText));return c?c.innerText.replace(/\\n+/g,' | '):'NOT_FOUND';})()`));
|
||||
const shot=await send('Page.captureScreenshot',{format:'png'});
|
||||
require('fs').writeFileSync(OUT+'/sync_panel.png',Buffer.from(shot.result.data,'base64'));
|
||||
console.log(' screenshot -> png/sync_panel.png');
|
||||
|
||||
console.log('\n=== H. 控制台错误 ===');
|
||||
console.log(errs.length? errs.slice(0,5).join('\n') : ' 无');
|
||||
ws.close();chrome.kill();process.exit(0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,42 @@
|
||||
// 验证:打卡页顶部主数字 = 纯工时(429),不含查获奖惩(+10)与未达标满勤奖
|
||||
const { spawn } = require('child_process');
|
||||
const CHROME = '/usr/bin/chromium';
|
||||
const OUT_DIR = __dirname;
|
||||
const URL = 'http://127.0.0.1:9522/index.html';
|
||||
const chrome = spawn(CHROME, ['--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage',
|
||||
'--remote-debugging-port=9243', `--user-data-dir=${OUT_DIR}/.chrome-topearned`,'--window-size=360,900','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 getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9243/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('CDP fail '+stderr.join('').slice(-300));}
|
||||
(async()=>{
|
||||
const ws=new WebSocket(await getWs()); let id=0; const pending=new Map();
|
||||
let or; const op=new Promise(r=>or=r); ws.onopen=or;
|
||||
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);}};
|
||||
await op;
|
||||
const send=(me,p={})=>new Promise(r=>{const i=++id;pending.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:p}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:360,height:900,deviceScaleFactor:2,mobile:true});
|
||||
await send('Page.navigate',{url:URL}); await sleep(1300);
|
||||
// 先装载同源页面后注入(storage 属于 9522 origin),再重新导航加载
|
||||
await ev(`(function(){localStorage.clear();const rec={};
|
||||
rec['2026-09-01']={status:'work',shift_type:'day',hours:8,contraband_found:1,contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
rec['2026-09-02']={status:'work',shift_type:'day',hours:8,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
rec['2026-09-03']={status:'work',shift_type:'day',hours:6,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
localStorage.setItem('work_records',JSON.stringify(rec));return 1;})()`);
|
||||
await send('Page.navigate',{url:URL}); await sleep(1500);
|
||||
const snap = await ev(`(function(){
|
||||
const m = document.querySelector('main');
|
||||
const headerBox = document.querySelector('header');
|
||||
return { headerText: headerBox ? headerBox.innerText.slice(0,400) : null, pills: Array.from(headerBox.querySelectorAll('.pill')).map(p=>p.innerText.replace(/\\s+/g,' ').trim()) };
|
||||
})()`);
|
||||
console.log('【9月 3天22h + 1查获】@360px');
|
||||
console.log(' 顶部文本 =', JSON.stringify(snap.headerText));
|
||||
console.log(' 胶囊 =', JSON.stringify(snap.pills));
|
||||
await send('Page.captureScreenshot',{format:'png'}).then(r=>{
|
||||
const fs=require('fs'); fs.writeFileSync(OUT_DIR+'/verify_top_earned.png', Buffer.from(r.result.data,'base64'));
|
||||
console.log(' screenshot -> png/verify_top_earned.png');
|
||||
});
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);console.error(stderr.join('').slice(-300));try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,39 @@
|
||||
// 验证:满勤达标后打卡页主数字叠加满勤奖并显示金标
|
||||
const { spawn } = require('child_process');
|
||||
const CHROME = '/usr/bin/chromium';
|
||||
const OUT_DIR = __dirname;
|
||||
const URL = 'http://127.0.0.1:9522/index.html';
|
||||
const chrome = spawn(CHROME, ['--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage',
|
||||
'--remote-debugging-port=9244', `--user-data-dir=${OUT_DIR}/.chrome-fullbonus`,'--window-size=360,900','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 getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9244/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('CDP fail '+stderr.join('').slice(-300));}
|
||||
(async()=>{
|
||||
const ws=new WebSocket(await getWs()); let id=0; const pending=new Map();
|
||||
let or; const op=new Promise(r=>or=r); ws.onopen=or;
|
||||
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);}};
|
||||
await op;
|
||||
const send=(me,p={})=>new Promise(r=>{const i=++id;pending.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:p}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:360,height:900,deviceScaleFactor:2,mobile:true});
|
||||
await send('Page.navigate',{url:URL}); await sleep(1300);
|
||||
// 满勤 27 天 × 8h = 216h → 工时收入 4212 + 满勤奖 216 = 4428
|
||||
await ev(`(function(){localStorage.clear();const rec={};
|
||||
for(let d=1;d<=27;d++)rec['2026-09-'+String(d).padStart(2,'0')]={status:'work',shift_type:'day',hours:8,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
localStorage.setItem('work_records',JSON.stringify(rec));return 1;})()`);
|
||||
await send('Page.navigate',{url:URL}); await sleep(1500);
|
||||
const snap = await ev(`(function(){
|
||||
const hb=document.querySelector('header');
|
||||
return { headerText: hb.innerText.slice(0,320), pills: Array.from(hb.querySelectorAll('.pill')).map(p=>p.innerText.replace(/\\s+/g,' ').trim()) };
|
||||
})()`);
|
||||
console.log('【9月 满勤27天×8h】@360px');
|
||||
console.log(' 顶部文本 =', JSON.stringify(snap.headerText));
|
||||
console.log(' 胶囊 =', JSON.stringify(snap.pills));
|
||||
await send('Page.captureScreenshot',{format:'png'}).then(r=>{
|
||||
const fs=require('fs'); fs.writeFileSync(OUT_DIR+'/verify_top_earned_full.png', Buffer.from(r.result.data,'base64'));
|
||||
console.log(' screenshot -> png/verify_top_earned_full.png');
|
||||
});
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,39 @@
|
||||
const { spawn } = require('child_process');
|
||||
const CHROME = '/usr/bin/chromium';
|
||||
const OUT_DIR = __dirname;
|
||||
const URL = 'http://127.0.0.1:9522/index.html';
|
||||
const chrome = spawn(CHROME, ['--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage',
|
||||
'--remote-debugging-port=9250', `--user-data-dir=${OUT_DIR}/.chrome-topnew`,'--window-size=390,840','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 getWs(){for(let i=0;i<40;i++){try{const t=await(await fetch('http://127.0.0.1:9250/json/list')).json();if(t.length)return t[0].webSocketDebuggerUrl;}catch(e){}await sleep(250);}throw new Error('CDP fail '+stderr.join('').slice(-300));}
|
||||
(async()=>{
|
||||
const ws=new WebSocket(await getWs()); let id=0; const pending=new Map();
|
||||
let or; const op=new Promise(r=>or=r); ws.onopen=or;
|
||||
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);}};
|
||||
await op;
|
||||
const send=(me,p={})=>new Promise(r=>{const i=++id;pending.set(i,r);ws.send(JSON.stringify({id:i,method:me,params:p}));});
|
||||
const ev=async x=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});return r.result&&r.result.result&&r.result.result.value;};
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:390,height:840,deviceScaleFactor:2,mobile:true});
|
||||
await send('Page.navigate',{url:URL}); await sleep(1300);
|
||||
// 用户场景:9月3天 22h(8+8+6),其中1件查获
|
||||
await ev(`(function(){localStorage.clear();const rec={};
|
||||
rec['2026-09-01']={status:'work',shift_type:'day',hours:8,contraband_found:1,contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
rec['2026-09-02']={status:'work',shift_type:'day',hours:8,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
rec['2026-09-03']={status:'work',shift_type:'day',hours:6,contraband_found:0,contraband_missed:0,other_penalty:null,penalty_reason:''};
|
||||
localStorage.setItem('work_records',JSON.stringify(rec));return 1;})()`);
|
||||
await send('Page.navigate',{url:URL}); await sleep(1500);
|
||||
const snap = await ev(`(function(){
|
||||
const hb=document.querySelector('header');
|
||||
return hb.innerText.slice(0,350);
|
||||
})()`);
|
||||
console.log('【9月 3天22h + 1查获】@390px 顶部内容:');
|
||||
console.log(JSON.stringify(snap, null, 1));
|
||||
await send('Page.captureScreenshot',{format:'png'}).then(r=>{
|
||||
const fs=require('fs'); fs.writeFileSync(OUT_DIR+'/verify_top_new.png', Buffer.from(r.result.data,'base64'));
|
||||
console.log('screenshot -> png/verify_top_new.png');
|
||||
});
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
})().catch(e=>{console.error('FAILED:',e.message);try{chrome.kill();}catch(_){}process.exit(1);});
|
||||
@@ -0,0 +1,65 @@
|
||||
// 决定性探针:点设置后,直接读 Vue 应用实例里的 settingsOpen 状态
|
||||
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=9228', `--user-data-dir=${OUT_DIR}/.chrome-test4`,
|
||||
'--window-size=420,900','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:9228/json/list')).json(); if(t.length)return t[0].webSocketDebuggerUrl; }catch(e){} await sleep(250);} throw new Error('CDP fail: '+stderr.join('').slice(-400));
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
const wsUrl = await getWsUrl();
|
||||
const ws = new WebSocket(wsUrl);
|
||||
let id=0; const pending=new Map(); const logs=[];
|
||||
let openRes; const openP=new Promise(r=>openRes=r);
|
||||
ws.onopen=openRes;
|
||||
ws.onmessage=ev=>{ const m=JSON.parse(ev.data);
|
||||
if(m.id&&pending.has(m.id)){pending.get(m.id)(m);pending.delete(m.id);}
|
||||
else if(m.method==='Runtime.consoleAPICalled')logs.push('C '+(m.params.args||[]).map(a=>a.value??a.description??'').join(' '));
|
||||
else if(m.method==='Runtime.exceptionThrown')logs.push('EXC '+JSON.stringify(m.params.exceptionDetails)); };
|
||||
await openP;
|
||||
const send=(method,params={})=>new Promise(res=>{const mid=++id;pending.set(mid,res);ws.send(JSON.stringify({id:mid,method,params}));});
|
||||
const ev=async(expr)=>{const r=await send('Runtime.evaluate',{expression:expr,awaitPromise:true,returnByValue:true}); if(r.result&&r.result.exceptionDetails)return{__err:JSON.stringify(r.result.exceptionDetails)}; return r.result&&r.result.result&&r.result.result.value;};
|
||||
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
await send('Page.navigate',{url:URL}); await sleep(2200);
|
||||
|
||||
// 通过 __vue_app__ 拿实例(Vue3 挂在根元素上)
|
||||
const probe1 = await ev(`
|
||||
(function(){
|
||||
const appEl = document.querySelector('#app');
|
||||
const vue = appEl && (appEl.__vue_app__ || appEl._vnode);
|
||||
let keys = null;
|
||||
if (appEl && appEl.__vue_app__) {
|
||||
// 生产构建通常隐藏,尽力而为
|
||||
keys = Object.keys(appEl).filter(k=>k.startsWith('__'));
|
||||
}
|
||||
// 读所有 ref 状态:尝试从 props/暴露的 setupState
|
||||
let setupKeys = null;
|
||||
try {
|
||||
const inst = appEl.__vue_app__;
|
||||
// 遍历组件实例
|
||||
setupKeys = Object.keys(inst).filter(k=>/devtools|app/i.test(k));
|
||||
} catch(e) { setupKeys = 'ERR '+e.message; }
|
||||
return {
|
||||
appElExists: !!appEl,
|
||||
appKeys: keys,
|
||||
scan: setupKeys
|
||||
};
|
||||
})()
|
||||
`);
|
||||
console.log('Vue 实例扫描:', JSON.stringify(probe1, null, 2));
|
||||
|
||||
ws.close(); chrome.kill(); process.exit(0);
|
||||
} catch(e){ console.error('FAILED:',e.message); console.error(stderr.join('').slice(-400)); try{chrome.kill();}catch(_){} process.exit(1); }
|
||||
})();
|
||||
@@ -0,0 +1,101 @@
|
||||
// 决定性探针 v2:深入 __vue_app__ 组件实例树读 settingsOpen/tab
|
||||
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=9229', `--user-data-dir=${OUT_DIR}/.chrome-test5`,
|
||||
'--window-size=420,900','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:9229/json/list')).json(); if(t.length)return t[0].webSocketDebuggerUrl; }catch(e){} await sleep(250);} throw new Error('CDP fail: '+stderr.join('').slice(-400));
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
const wsUrl = await getWsUrl();
|
||||
const ws = new WebSocket(wsUrl);
|
||||
let id=0; const pending=new Map(); const logs=[];
|
||||
let openRes; const openP=new Promise(r=>openRes=r);
|
||||
ws.onopen=openRes;
|
||||
ws.onmessage=ev=>{ const m=JSON.parse(ev.data);
|
||||
if(m.id&&pending.has(m.id)){pending.get(m.id)(m);pending.delete(m.id);}
|
||||
else if(m.method==='Runtime.consoleAPICalled')logs.push('C '+(m.params.args||[]).map(a=>a.value??a.description??'').join(' '));
|
||||
else if(m.method==='Runtime.exceptionThrown')logs.push('EXC '+JSON.stringify(m.params.exceptionDetails)); };
|
||||
await openP;
|
||||
const send=(method,params={})=>new Promise(res=>{const mid=++id;pending.set(mid,res);ws.send(JSON.stringify({id:mid,method,params}));});
|
||||
const ev=async(expr)=>{const r=await send('Runtime.evaluate',{expression:expr,awaitPromise:true,returnByValue:true}); if(r.result&&r.result.exceptionDetails)return{__err:JSON.stringify(r.result.exceptionDetails)}; return r.result&&r.result.result&&r.result.result.value;};
|
||||
|
||||
await send('Runtime.enable'); await send('Page.enable');
|
||||
await send('Page.navigate',{url:URL}); await sleep(2200);
|
||||
|
||||
// 深入 __vue_app__ 拿 setupState(Vue3:app._instance.setupState / 通过组件的 exposed)
|
||||
const probe = await ev(`
|
||||
(function(){
|
||||
const appEl = document.querySelector('#app');
|
||||
const app = appEl.__vue_app__;
|
||||
const out = {};
|
||||
// vue 3.3+: app._instance (root instance)
|
||||
const root = app._instance;
|
||||
out.hasRootInstance = !!root;
|
||||
if (root) {
|
||||
const setup = root.setupState;
|
||||
const ctx = root.ctx;
|
||||
out.setupKeys = setup ? Object.keys(setup) : null;
|
||||
const get = (name) => {
|
||||
// ref 或 getter
|
||||
const v = setup ? setup[name] : undefined;
|
||||
if (v === undefined && ctx) return ctx[name];
|
||||
return v;
|
||||
};
|
||||
function deref(v) {
|
||||
if (v === undefined || v === null) return v;
|
||||
if (typeof v === 'object' && '_value' in v) return v._value; // ref
|
||||
return v;
|
||||
}
|
||||
const sOpen = deref(get('settingsOpen'));
|
||||
const tabVal = deref(get('tab'));
|
||||
const iso = deref(get('isDrawerOpen'));
|
||||
out.settingsOpen = sOpen;
|
||||
out.isDrawerOpen = iso;
|
||||
out.tab = tabVal && tabVal._value !== undefined ? tabVal._value : tabVal;
|
||||
out.tab_raw = tabVal;
|
||||
} else {
|
||||
// 尝试通过子组件
|
||||
const sub = app._container && app._container.__vue_app__;
|
||||
out.subSearched = !!sub;
|
||||
}
|
||||
return out;
|
||||
})()
|
||||
`);
|
||||
console.log('深度读取:', JSON.stringify(probe, null, 2));
|
||||
|
||||
// 现在点击设置,再看 settingsOpen
|
||||
const clickThen = await ev(`
|
||||
new Promise(r => {
|
||||
const btn = Array.from(document.querySelectorAll('nav button')).find(b=>(b.innerText||'').trim()==='设置');
|
||||
if (!btn) return r({err:'no btn'});
|
||||
btn.click();
|
||||
setTimeout(() => {
|
||||
const appEl = document.querySelector('#app');
|
||||
const root = appEl.__vue_app__._instance;
|
||||
const setup = root.setupState;
|
||||
function deref(v){ if(v&&typeof v==='object'&&('_value' in v))return v._value; return v; }
|
||||
r({
|
||||
settingsOpen_afterClick: deref(setup.settingsOpen),
|
||||
tab_afterClick: deref(setup.tab) && deref(setup.tab)._value!==undefined ? deref(setup.tab)._value : deref(setup.tab),
|
||||
z50: document.querySelectorAll('.z-50').length
|
||||
});
|
||||
}, 400);
|
||||
})
|
||||
`);
|
||||
console.log('点击设置后:', JSON.stringify(clickThen, null, 2));
|
||||
|
||||
ws.close(); chrome.kill(); process.exit(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