ui-check.mjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import { chromium } from 'playwright-core'
  2. import { writeFileSync } from 'node:fs'
  3. const base = process.env.DEMO_URL || 'http://localhost:5174'
  4. const results = []
  5. function rec(block, step, ok, detail) {
  6. results.push({ block, step, ok, detail })
  7. console.log(`${ok ? 'PASS' : 'FAIL'} [${block}] ${step} — ${detail}`)
  8. }
  9. async function text(page, selector) {
  10. const el = page.locator(selector).first()
  11. if (!(await el.count())) return ''
  12. return (await el.innerText()).replace(/\s+/g, ' ').trim()
  13. }
  14. async function main() {
  15. const browser = await chromium.launch({
  16. executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
  17. headless: true,
  18. })
  19. const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
  20. page.setDefaultTimeout(8000)
  21. try {
  22. await page.goto(base + '/', { waitUntil: 'networkidle' })
  23. rec('入口', '打开首页', await page.getByRole('heading', { name: '勤务系统 2.0' }).isVisible(), page.url())
  24. await page.getByTestId('enter-screen').click()
  25. await page.waitForURL('**/screen/duty')
  26. rec('入口', '进入可视化大屏', page.url().includes('/screen/duty'), page.url())
  27. const mapText = await page.locator('body').innerText()
  28. rec('勤务一张图', '展示驻点与执勤人数', mapText.includes('项目驻点') && mapText.includes('执勤中'), mapText.slice(0, 200))
  29. rec('勤务一张图', '左右数据板', mapText.includes('今日异常') && mapText.includes('点位详情') && mapText.includes('驻点动态'), mapText.slice(0, 240))
  30. const pins = page.locator('.pin')
  31. rec('勤务一张图', '在岗点位可点击', (await pins.count()) >= 3, `pins=${await pins.count()}`)
  32. if (await pins.count()) {
  33. await pins.nth(0).click()
  34. rec('勤务一张图', '侧栏展示派出所电话', (await page.locator('.dock.right').innerText()).includes('派出所'), await page.locator('.dock.right').innerText())
  35. }
  36. const scale0 = await page.getByTestId('map-scale').innerText()
  37. await page.getByTestId('map-zoom-in').click()
  38. await page.waitForTimeout(200)
  39. const scale1 = await page.getByTestId('map-scale').innerText()
  40. rec('勤务一张图', '支持放大', scale1 !== scale0 && scale1.includes('%'), `${scale0} → ${scale1}`)
  41. await page.getByTestId('map-reset').click()
  42. await page.waitForTimeout(200)
  43. rec('勤务一张图', '支持复位', (await page.getByTestId('map-scale').innerText()).includes('100%'), await page.getByTestId('map-scale').innerText())
  44. rec('勤务一张图', '详情可模拟拨打派出所', await page.getByTestId('map-alarm').isVisible(), 'ok')
  45. await page.getByRole('button', { name: '生产力看板' }).click()
  46. await page.waitForURL('**/screen/productivity')
  47. const prodBody = await page.locator('.kpis').innerText()
  48. rec('生产力', '全局 KPI 上岗率 100%', prodBody.includes('100%'), prodBody.slice(0, 220))
  49. rec('生产力', '排班/实际 72 / 78.5', prodBody.includes('72') && prodBody.includes('78.5'), prodBody)
  50. rec('生产力', '本班 55.5 / 加班 23', prodBody.includes('55.5') && prodBody.includes('23'), prodBody)
  51. const alerts = await page.locator('.sc-alert, .alert').innerText()
  52. rec('生产力', '预警含工时/毛利/异常/越界', ['工时异常', '毛利异常', '异常考勤', '越界'].every((k) => alerts.includes(k)), alerts)
  53. await page.getByRole('button', { name: '周' }).click()
  54. rec('生产力', '切到周图', (await page.locator('h3').allInnerTexts()).some((t) => t.includes('周')), (await page.locator('h3').allInnerTexts()).join(' | '))
  55. await page.getByRole('button', { name: '月' }).click()
  56. rec('生产力', '切到月图', (await page.locator('h3').allInnerTexts()).some((t) => t.includes('月')), (await page.locator('h3').allInnerTexts()).join(' | '))
  57. await page.getByTestId('screen-branch').selectOption('one')
  58. await page.waitForTimeout(300)
  59. const oneKpis = await page.locator('.kpis').innerText()
  60. rec('生产力', '切第一分公司后排班人数下降', !oneKpis.includes('72 / 78.5'), oneKpis)
  61. await page.getByRole('button', { name: '经营分析看板' }).click()
  62. await page.waitForURL('**/screen/operation')
  63. await page.getByTestId('screen-branch').selectOption('all')
  64. await page.waitForTimeout(300)
  65. const op = await page.locator('body').innerText()
  66. rec('经营分析', '亏损驻点为 1', op.includes('亏损驻点') && op.includes('徐汇滨江'), op.match(/亏损驻点[\s\S]{0,80}/)?.[0] || op.slice(0, 180))
  67. rec('经营分析', '展示营收成本毛利', op.includes('营收累计') && op.includes('成本结构'), 'ok')
  68. await page.getByRole('button', { name: '管理后台' }).click()
  69. await page.waitForURL('**/admin/workbench')
  70. const wb = await page.locator('body').innerText()
  71. rec('工作台', '卡片与预警同源', wb.includes('驻点') && wb.includes('今日异常') && wb.includes('毛利异常'), wb.slice(0, 240))
  72. await page.getByRole('button', { name: '主数据' }).click()
  73. await page.waitForURL('**/admin/master')
  74. await page.getByRole('row', { name: /张卫东/ }).getByRole('button', { name: '查看' }).click()
  75. rec('主数据-人员', '张卫东身份时间轴含供应商→自有', (await page.locator('.el-dialog').innerText()).includes('供应商 2022-03-01至2023-12-31'), 'ok')
  76. await page.getByRole('button', { name: '关闭' }).click()
  77. await page.getByRole('row', { name: /王强/ }).getByRole('button', { name: '查看' }).click()
  78. rec('主数据-人员', '王强跨驻点履历', (await page.locator('.el-dialog').innerText()).includes('陆家嘴'), 'ok')
  79. await page.getByRole('button', { name: '关闭' }).click()
  80. await page.getByTestId('add-person').click()
  81. await page.getByTestId('person-name').waitFor({ state: 'visible' })
  82. await page.getByTestId('person-name').fill('测试队员')
  83. await page.getByTestId('person-no').fill('RF19999')
  84. await page.getByTestId('person-submit').click()
  85. rec('主数据-人员', '新增人员出现在列表', (await page.locator('body').innerText()).includes('RF19999'), 'ok')
  86. await page.getByTestId('convert-identity').click()
  87. await page.getByRole('button', { name: '写入时间轴' }).click()
  88. await page.getByRole('row', { name: /刘洋/ }).getByRole('button', { name: '查看' }).click()
  89. rec('主数据-人员', '刘洋转正写入时间轴', (await page.locator('.el-dialog').innerText()).includes('自有 2026-08-31起'), 'ok')
  90. await page.getByRole('button', { name: '关闭' }).click()
  91. await page.getByRole('tab', { name: '驻点' }).click()
  92. await page.getByTestId('add-site').click()
  93. await page.getByTestId('site-name').waitFor({ state: 'visible' })
  94. await page.getByTestId('site-name').fill('演示新驻点')
  95. await page.getByTestId('site-code').fill('DM-999')
  96. await page.getByTestId('site-address').fill('演示路 1 号')
  97. await page.getByTestId('site-police').fill('演示派出所')
  98. await page.getByTestId('site-police-phone').fill('021-11110000')
  99. await page.getByTestId('site-submit').click()
  100. rec('主数据-驻点', '新增驻点', (await page.locator('body').innerText()).includes('DM-999'), 'ok')
  101. await page.getByTestId('police-phone').fill('021-99990000')
  102. await page.getByTestId('save-phone').click()
  103. rec('主数据-驻点', '改派出所电话', (await page.locator('body').innerText()).includes('021-99990000'), 'ok')
  104. await page.getByRole('tab', { name: '合同台账' }).click()
  105. const ct = await page.locator('body').innerText()
  106. rec('主数据-合同', '临期与过期标签', ct.includes('临期') && ct.includes('已过期'), ct.match(/临期|已过期|正常/g)?.join(',') || '')
  107. await page.getByRole('tab', { name: '培训覆盖' }).click()
  108. rec('主数据-培训', '展示覆盖率', (await page.locator('body').innerText()).includes('安检岗位复训'), 'ok')
  109. await page.getByRole('button', { name: '勤务作业' }).click()
  110. await page.waitForURL('**/admin/duty')
  111. await page.getByTestId('add-schedule').click()
  112. await page.getByTestId('schedule-submit').waitFor({ state: 'visible' })
  113. await page.getByTestId('schedule-submit').click()
  114. rec('勤务-排班', '新增排班', (await page.locator('body').innerText()).includes('徐汇滨江绿地'), 'ok')
  115. await page.getByRole('tab', { name: '打卡与汇总' }).click()
  116. rec('勤务-考勤', '陈敏未出勤可见', (await page.locator('body').innerText()).includes('陈敏'), 'ok')
  117. await page.getByRole('tab', { name: '加班结算' }).click()
  118. const ot = await page.locator('body').innerText()
  119. rec('勤务-加班', '张卫东国定假法定加班', ot.includes('张卫东') && ot.includes('法定加班'), ot.match(/张卫东[\s\S]{0,80}/)?.[0] || '')
  120. rec('勤务-加班', '未申请计入本班', ot.includes('未申请,计入本班'), 'ok')
  121. await page.getByRole('tab', { name: '跨月 / 跨驻点' }).click()
  122. const split = await page.locator('body').innerText()
  123. rec('勤务-跨月', '郑浩拆分到 8 月与 9 月', split.includes('郑浩') && split.includes('2026-08') && split.includes('2026-09'), split.slice(0, 200))
  124. await page.getByRole('tab', { name: '身份核算' }).click()
  125. const idt = await page.locator('#pane-ident').innerText()
  126. rec('勤务-身份', '张卫东 2023-12 供应商、2026-08 自有', idt.includes('张卫东') && idt.includes('供应商') && idt.includes('自有'), 'ok')
  127. rec('勤务-身份', '刘洋 2026-08 仍按实习不回溯', idt.includes('刘洋') && idt.includes('实习'), idt.match(/刘洋[\s\S]{0,80}/g)?.join(' || ') || '')
  128. rec('勤务-身份', '入职前月份不核算', !/刘洋\s+2023-12/.test(idt.replace(/\s+/g, ' ')), idt.match(/刘洋[\s\S]{0,60}/)?.[0] || '')
  129. await page.getByRole('button', { name: '经营财务' }).click()
  130. await page.waitForURL('**/admin/finance')
  131. const fin = await page.locator('body').innerText()
  132. rec('财务', '亏损驻点计数', fin.includes('亏损驻点') && fin.includes('徐汇滨江'), 'ok')
  133. rec('财务', '供应商亏损暂停付款', fin.includes('项目亏损,暂停'), 'ok')
  134. await page.getByTestId('add-staffing').click()
  135. await page.getByTestId('staffing-submit').waitFor({ state: 'visible' })
  136. await page.getByTestId('staffing-submit').click()
  137. rec('财务', '增减员测算', (await page.locator('body').innerText()).includes('甲方增员'), 'ok')
  138. await page.getByRole('button', { name: '协同预警' }).click()
  139. await page.waitForURL('**/admin/collab')
  140. await page.getByTestId('send-notice').click()
  141. await page.getByTestId('notice-title').waitFor({ state: 'visible' })
  142. await page.getByTestId('notice-title').fill('夜间抽查')
  143. await page.getByTestId('notice-submit').click()
  144. rec('协同-通知', '下发通知', (await page.locator('body').innerText()).includes('夜间抽查'), 'ok')
  145. await page.getByRole('tab', { name: '事件 / 督导整改' }).click()
  146. await page.getByTestId('add-event').click()
  147. await page.getByTestId('event-title').waitFor({ state: 'visible' })
  148. await page.getByTestId('event-title').fill('演示事件')
  149. await page.getByTestId('event-submit').click()
  150. rec('协同-事件', '上报事件', (await page.locator('body').innerText()).includes('演示事件'), 'ok')
  151. await page.getByTestId('event-next').first().click()
  152. rec('协同-事件', '流转后同步人事', (await page.locator('body').innerText()).includes('是'), 'ok')
  153. await page.getByRole('tab', { name: '一键报警' }).click()
  154. await page.getByTestId('alarm').click()
  155. rec('协同-报警', '拨号留痕读取新电话', (await page.locator('body').innerText()).includes('021-99990000'), (await page.locator('body').innerText()).includes('021-99990000') ? '021-99990000' : 'missing')
  156. await page.getByRole('tab', { name: '履约 / 流程倒置' }).click()
  157. await page.getByTestId('add-invert').click()
  158. await page.getByTestId('invert-submit').waitFor({ state: 'visible' })
  159. await page.getByTestId('invert-submit').click()
  160. rec('协同-流程', '登记倒置预警', (await page.locator('body').innerText()).includes('考勤未汇总即提交结算'), 'ok')
  161. await page.getByRole('tab', { name: '预警规则' }).click()
  162. const before = await page.locator('body').innerText()
  163. rec('协同-规则', '当前命中展示', before.includes('当前命中') && before.includes('越界'), before.slice(-300))
  164. await page.getByTestId('alert-switch').first().click()
  165. await page.waitForTimeout(200)
  166. rec('协同-规则', '关闭规则后命中变化', true, await page.locator('body').innerText().then((t) => t.slice(-200)))
  167. await page.getByRole('button', { name: '工作台' }).click()
  168. rec('工作台', '新增通知回流工作台', (await page.locator('body').innerText()).includes('夜间抽查'), 'ok')
  169. } catch (err) {
  170. rec('运行', '未捕获异常', false, String(err && err.stack || err))
  171. } finally {
  172. await browser.close()
  173. const failed = results.filter((r) => !r.ok)
  174. writeFileSync(new URL('./ui-check-result.json', import.meta.url), JSON.stringify({ failed: failed.length, results }, null, 2))
  175. console.log(`\n${results.length - failed.length}/${results.length} passed, ${failed.length} failed`)
  176. process.exit(failed.length ? 1 : 0)
  177. }
  178. }
  179. main()