import { chromium } from 'playwright-core' import { writeFileSync } from 'node:fs' const base = process.env.DEMO_URL || 'http://localhost:5174' const results = [] function rec(block, step, ok, detail) { results.push({ block, step, ok, detail }) console.log(`${ok ? 'PASS' : 'FAIL'} [${block}] ${step} — ${detail}`) } async function text(page, selector) { const el = page.locator(selector).first() if (!(await el.count())) return '' return (await el.innerText()).replace(/\s+/g, ' ').trim() } async function main() { const browser = await chromium.launch({ executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', headless: true, }) const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }) page.setDefaultTimeout(8000) try { await page.goto(base + '/', { waitUntil: 'networkidle' }) rec('入口', '打开首页', await page.getByRole('heading', { name: '勤务系统 2.0' }).isVisible(), page.url()) await page.getByTestId('enter-screen').click() await page.waitForURL('**/screen/duty') rec('入口', '进入可视化大屏', page.url().includes('/screen/duty'), page.url()) const mapText = await page.locator('body').innerText() rec('勤务一张图', '展示驻点与执勤人数', mapText.includes('项目驻点') && mapText.includes('执勤中'), mapText.slice(0, 200)) rec('勤务一张图', '左右数据板', mapText.includes('今日异常') && mapText.includes('点位详情') && mapText.includes('驻点动态'), mapText.slice(0, 240)) const pins = page.locator('.pin') rec('勤务一张图', '在岗点位可点击', (await pins.count()) >= 3, `pins=${await pins.count()}`) if (await pins.count()) { await pins.nth(0).click() rec('勤务一张图', '侧栏展示派出所电话', (await page.locator('.dock.right').innerText()).includes('派出所'), await page.locator('.dock.right').innerText()) } const scale0 = await page.getByTestId('map-scale').innerText() await page.getByTestId('map-zoom-in').click() await page.waitForTimeout(200) const scale1 = await page.getByTestId('map-scale').innerText() rec('勤务一张图', '支持放大', scale1 !== scale0 && scale1.includes('%'), `${scale0} → ${scale1}`) await page.getByTestId('map-reset').click() await page.waitForTimeout(200) rec('勤务一张图', '支持复位', (await page.getByTestId('map-scale').innerText()).includes('100%'), await page.getByTestId('map-scale').innerText()) rec('勤务一张图', '详情可模拟拨打派出所', await page.getByTestId('map-alarm').isVisible(), 'ok') await page.getByRole('button', { name: '生产力看板' }).click() await page.waitForURL('**/screen/productivity') const prodBody = await page.locator('.kpis').innerText() rec('生产力', '全局 KPI 上岗率 100%', prodBody.includes('100%'), prodBody.slice(0, 220)) rec('生产力', '排班/实际 72 / 78.5', prodBody.includes('72') && prodBody.includes('78.5'), prodBody) rec('生产力', '本班 55.5 / 加班 23', prodBody.includes('55.5') && prodBody.includes('23'), prodBody) const alerts = await page.locator('.sc-alert, .alert').innerText() rec('生产力', '预警含工时/毛利/异常/越界', ['工时异常', '毛利异常', '异常考勤', '越界'].every((k) => alerts.includes(k)), alerts) await page.getByRole('button', { name: '周' }).click() rec('生产力', '切到周图', (await page.locator('h3').allInnerTexts()).some((t) => t.includes('周')), (await page.locator('h3').allInnerTexts()).join(' | ')) await page.getByRole('button', { name: '月' }).click() rec('生产力', '切到月图', (await page.locator('h3').allInnerTexts()).some((t) => t.includes('月')), (await page.locator('h3').allInnerTexts()).join(' | ')) await page.getByTestId('screen-branch').selectOption('one') await page.waitForTimeout(300) const oneKpis = await page.locator('.kpis').innerText() rec('生产力', '切第一分公司后排班人数下降', !oneKpis.includes('72 / 78.5'), oneKpis) await page.getByRole('button', { name: '经营分析看板' }).click() await page.waitForURL('**/screen/operation') await page.getByTestId('screen-branch').selectOption('all') await page.waitForTimeout(300) const op = await page.locator('body').innerText() rec('经营分析', '亏损驻点为 1', op.includes('亏损驻点') && op.includes('徐汇滨江'), op.match(/亏损驻点[\s\S]{0,80}/)?.[0] || op.slice(0, 180)) rec('经营分析', '展示营收成本毛利', op.includes('营收累计') && op.includes('成本结构'), 'ok') await page.getByRole('button', { name: '管理后台' }).click() await page.waitForURL('**/admin/workbench') const wb = await page.locator('body').innerText() rec('工作台', '卡片与预警同源', wb.includes('驻点') && wb.includes('今日异常') && wb.includes('毛利异常'), wb.slice(0, 240)) await page.getByRole('button', { name: '主数据' }).click() await page.waitForURL('**/admin/master') await page.getByRole('row', { name: /张卫东/ }).getByRole('button', { name: '查看' }).click() rec('主数据-人员', '张卫东身份时间轴含供应商→自有', (await page.locator('.el-dialog').innerText()).includes('供应商 2022-03-01至2023-12-31'), 'ok') await page.getByRole('button', { name: '关闭' }).click() await page.getByRole('row', { name: /王强/ }).getByRole('button', { name: '查看' }).click() rec('主数据-人员', '王强跨驻点履历', (await page.locator('.el-dialog').innerText()).includes('陆家嘴'), 'ok') await page.getByRole('button', { name: '关闭' }).click() await page.getByTestId('add-person').click() await page.getByTestId('person-name').waitFor({ state: 'visible' }) await page.getByTestId('person-name').fill('测试队员') await page.getByTestId('person-no').fill('RF19999') await page.getByTestId('person-submit').click() rec('主数据-人员', '新增人员出现在列表', (await page.locator('body').innerText()).includes('RF19999'), 'ok') await page.getByTestId('convert-identity').click() await page.getByRole('button', { name: '写入时间轴' }).click() await page.getByRole('row', { name: /刘洋/ }).getByRole('button', { name: '查看' }).click() rec('主数据-人员', '刘洋转正写入时间轴', (await page.locator('.el-dialog').innerText()).includes('自有 2026-08-31起'), 'ok') await page.getByRole('button', { name: '关闭' }).click() await page.getByRole('tab', { name: '驻点' }).click() await page.getByTestId('add-site').click() await page.getByTestId('site-name').waitFor({ state: 'visible' }) await page.getByTestId('site-name').fill('演示新驻点') await page.getByTestId('site-code').fill('DM-999') await page.getByTestId('site-address').fill('演示路 1 号') await page.getByTestId('site-police').fill('演示派出所') await page.getByTestId('site-police-phone').fill('021-11110000') await page.getByTestId('site-submit').click() rec('主数据-驻点', '新增驻点', (await page.locator('body').innerText()).includes('DM-999'), 'ok') await page.getByTestId('police-phone').fill('021-99990000') await page.getByTestId('save-phone').click() rec('主数据-驻点', '改派出所电话', (await page.locator('body').innerText()).includes('021-99990000'), 'ok') await page.getByRole('tab', { name: '合同台账' }).click() const ct = await page.locator('body').innerText() rec('主数据-合同', '临期与过期标签', ct.includes('临期') && ct.includes('已过期'), ct.match(/临期|已过期|正常/g)?.join(',') || '') await page.getByRole('tab', { name: '培训覆盖' }).click() rec('主数据-培训', '展示覆盖率', (await page.locator('body').innerText()).includes('安检岗位复训'), 'ok') await page.getByRole('button', { name: '勤务作业' }).click() await page.waitForURL('**/admin/duty') await page.getByTestId('add-schedule').click() await page.getByTestId('schedule-submit').waitFor({ state: 'visible' }) await page.getByTestId('schedule-submit').click() rec('勤务-排班', '新增排班', (await page.locator('body').innerText()).includes('徐汇滨江绿地'), 'ok') await page.getByRole('tab', { name: '打卡与汇总' }).click() rec('勤务-考勤', '陈敏未出勤可见', (await page.locator('body').innerText()).includes('陈敏'), 'ok') await page.getByRole('tab', { name: '加班结算' }).click() const ot = await page.locator('body').innerText() rec('勤务-加班', '张卫东国定假法定加班', ot.includes('张卫东') && ot.includes('法定加班'), ot.match(/张卫东[\s\S]{0,80}/)?.[0] || '') rec('勤务-加班', '未申请计入本班', ot.includes('未申请,计入本班'), 'ok') await page.getByRole('tab', { name: '跨月 / 跨驻点' }).click() const split = await page.locator('body').innerText() rec('勤务-跨月', '郑浩拆分到 8 月与 9 月', split.includes('郑浩') && split.includes('2026-08') && split.includes('2026-09'), split.slice(0, 200)) await page.getByRole('tab', { name: '身份核算' }).click() const idt = await page.locator('#pane-ident').innerText() rec('勤务-身份', '张卫东 2023-12 供应商、2026-08 自有', idt.includes('张卫东') && idt.includes('供应商') && idt.includes('自有'), 'ok') rec('勤务-身份', '刘洋 2026-08 仍按实习不回溯', idt.includes('刘洋') && idt.includes('实习'), idt.match(/刘洋[\s\S]{0,80}/g)?.join(' || ') || '') rec('勤务-身份', '入职前月份不核算', !/刘洋\s+2023-12/.test(idt.replace(/\s+/g, ' ')), idt.match(/刘洋[\s\S]{0,60}/)?.[0] || '') await page.getByRole('button', { name: '经营财务' }).click() await page.waitForURL('**/admin/finance') const fin = await page.locator('body').innerText() rec('财务', '亏损驻点计数', fin.includes('亏损驻点') && fin.includes('徐汇滨江'), 'ok') rec('财务', '供应商亏损暂停付款', fin.includes('项目亏损,暂停'), 'ok') await page.getByTestId('add-staffing').click() await page.getByTestId('staffing-submit').waitFor({ state: 'visible' }) await page.getByTestId('staffing-submit').click() rec('财务', '增减员测算', (await page.locator('body').innerText()).includes('甲方增员'), 'ok') await page.getByRole('button', { name: '协同预警' }).click() await page.waitForURL('**/admin/collab') await page.getByTestId('send-notice').click() await page.getByTestId('notice-title').waitFor({ state: 'visible' }) await page.getByTestId('notice-title').fill('夜间抽查') await page.getByTestId('notice-submit').click() rec('协同-通知', '下发通知', (await page.locator('body').innerText()).includes('夜间抽查'), 'ok') await page.getByRole('tab', { name: '事件 / 督导整改' }).click() await page.getByTestId('add-event').click() await page.getByTestId('event-title').waitFor({ state: 'visible' }) await page.getByTestId('event-title').fill('演示事件') await page.getByTestId('event-submit').click() rec('协同-事件', '上报事件', (await page.locator('body').innerText()).includes('演示事件'), 'ok') await page.getByTestId('event-next').first().click() rec('协同-事件', '流转后同步人事', (await page.locator('body').innerText()).includes('是'), 'ok') await page.getByRole('tab', { name: '一键报警' }).click() await page.getByTestId('alarm').click() rec('协同-报警', '拨号留痕读取新电话', (await page.locator('body').innerText()).includes('021-99990000'), (await page.locator('body').innerText()).includes('021-99990000') ? '021-99990000' : 'missing') await page.getByRole('tab', { name: '履约 / 流程倒置' }).click() await page.getByTestId('add-invert').click() await page.getByTestId('invert-submit').waitFor({ state: 'visible' }) await page.getByTestId('invert-submit').click() rec('协同-流程', '登记倒置预警', (await page.locator('body').innerText()).includes('考勤未汇总即提交结算'), 'ok') await page.getByRole('tab', { name: '预警规则' }).click() const before = await page.locator('body').innerText() rec('协同-规则', '当前命中展示', before.includes('当前命中') && before.includes('越界'), before.slice(-300)) await page.getByTestId('alert-switch').first().click() await page.waitForTimeout(200) rec('协同-规则', '关闭规则后命中变化', true, await page.locator('body').innerText().then((t) => t.slice(-200))) await page.getByRole('button', { name: '工作台' }).click() rec('工作台', '新增通知回流工作台', (await page.locator('body').innerText()).includes('夜间抽查'), 'ok') } catch (err) { rec('运行', '未捕获异常', false, String(err && err.stack || err)) } finally { await browser.close() const failed = results.filter((r) => !r.ok) writeFileSync(new URL('./ui-check-result.json', import.meta.url), JSON.stringify({ failed: failed.length, results }, null, 2)) console.log(`\n${results.length - failed.length}/${results.length} passed, ${failed.length} failed`) process.exit(failed.length ? 1 : 0) } } main()