waterSupplyHomeContract.test.mjs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import test from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import { readFile } from 'node:fs/promises'
  4. import { parse as parseSfc } from '@vue/compiler-sfc'
  5. import { parse as parseJs } from '@babel/parser'
  6. const homePath = 'src/views/subSystem/waterSupply/components/WaterSupplyHome.vue'
  7. test('home API imports all resolve to real named exports', async () => {
  8. const { descriptor } = parseSfc(await readFile(homePath, 'utf8'))
  9. const imports = parseJs(descriptor.scriptSetup.content, { sourceType: 'module' }).program.body
  10. .filter(node => node.type === 'ImportDeclaration' && node.source.value.startsWith('@/api/'))
  11. for (const declaration of imports) {
  12. const apiPath = declaration.source.value.replace('@/', 'src/') + '.js'
  13. const nodes = parseJs(await readFile(apiPath, 'utf8'), { sourceType: 'module' }).program.body
  14. const names = nodes.filter(node => node.type === 'ExportNamedDeclaration').flatMap(node => {
  15. if (node.declaration?.type === 'VariableDeclaration') return node.declaration.declarations.map(item => item.id.name)
  16. return node.declaration?.id ? [node.declaration.id.name] : node.specifiers.map(item => item.exported.name)
  17. })
  18. for (const item of declaration.specifiers.filter(item => item.type === 'ImportSpecifier')) {
  19. assert.ok(names.includes(item.imported.name), `${apiPath} does not export ${item.imported.name}`)
  20. }
  21. }
  22. })
  23. test('home dashboard uses the unified scoped endpoint instead of the removed ledger dashboard', async () => {
  24. const source = await readFile(homePath, 'utf8')
  25. assert.doesNotMatch(source, /getAlarmLedgerDashboard/u)
  26. assert.match(source, /getAlarmDashboard\(\{ rootTypeId: WATER_DEVICE_ROOT_TYPE_ID, days: trendDays \}\)/u)
  27. const api = (await readFile('src/api/alarm.js', 'utf8'))
  28. .replace("import request from '@/utils/request'", 'const request = config => Promise.resolve(config)')
  29. const { getAlarmDashboard } = await import('data:text/javascript;base64,' + Buffer.from(api).toString('base64'))
  30. assert.deepEqual(await getAlarmDashboard({ rootTypeId: 1, days: 7 }), {
  31. url: '/api/alarm/dashboard', method: 'get', params: { rootTypeId: 1, days: 7 }
  32. })
  33. })
  34. test('home removes the A/B/C placeholder headings', async () => {
  35. const source = await readFile(homePath, 'utf8')
  36. assert.doesNotMatch(source, /A · 指挥台/u)
  37. assert.doesNotMatch(source, /B · 异常待办/u)
  38. assert.doesNotMatch(source, /C · 数据总览/u)
  39. })
  40. test('home realtime alarms split between audit and work order by display status', async () => {
  41. const source = await readFile(homePath, 'utf8')
  42. assert.match(source, /openRealtimeAlarm\(item\)/u)
  43. assert.match(source, /realtimeAlarmJumpTarget\(item\)/u)
  44. assert.match(source, /goPage\(target\.path, target\.query\)/u)
  45. assert.match(source, /item\.displayStatusText/u)
  46. assert.match(source, /item\.displayStatusType/u)
  47. assert.match(source, /goPage\('\/device\/maintain'/u)
  48. })
  49. test('home overview uses the shared GIS map canvas and aggregates all one-map layers', async () => {
  50. const source = await readFile(homePath, 'utf8')
  51. assert.match(source, /<GisMapCanvas/u)
  52. assert.match(source, /ref="overviewMapRef"/u)
  53. assert.match(source, /:points="overviewMapPoints"/u)
  54. assert.match(source, /:lines="overviewMapLines"/u)
  55. assert.match(source, /@point-click="openOverviewPoint"/u)
  56. assert.match(source, /@line-click="openOverviewPoint"/u)
  57. assert.match(source, /buildHomeGisOverview/u)
  58. assert.match(source, /getWaterFacilityFeatures\(\)/u)
  59. assert.match(source, /getAlarmMap\(\{ rootTypeId: WATER_DEVICE_ROOT_TYPE_ID \}\)/u)
  60. assert.match(source, /alarmPoints: unwrapResponse\(alarmRes\) \|\| \[\]/u)
  61. assert.doesNotMatch(source, /listAlarmMapPoints/u)
  62. assert.match(source, /getDeviceGisAllPoints\(\)/u)
  63. })
  64. test('home overview map takes at least 60% of the same row in desktop layout', async () => {
  65. const source = await readFile(homePath, 'utf8')
  66. assert.match(source, /grid-template-columns:\s*minmax\(240px,\s*1fr\)\s+minmax\(0,\s*3fr\)\s+minmax\(240px,\s*1fr\)/u)
  67. })
  68. test('home work order jumps point to device maintenance dispatch page', async () => {
  69. const source = await readFile(homePath, 'utf8')
  70. const jumps = source.match(/goPage\('\/device\/maintain'/gu) || []
  71. assert.ok(jumps.length >= 3, 'header, work order rows and related order should all route to device maintain')
  72. assert.doesNotMatch(source, /monitorAlarm\/workOrder/u)
  73. assert.match(source, />设备维修派单<\/el-button>/u)
  74. })
  75. test('home B-road detail panel is 1:1 with the todo and shortcut panels', async () => {
  76. const source = await readFile(homePath, 'utf8')
  77. assert.match(source, /workbench-grid--equal/u)
  78. assert.match(source, /repeat\(3,\s*minmax\(0,\s*1fr\)\)/u)
  79. const sectionB = source.match(/<section class="home-section">([\s\S]*?)<el-card shadow="never" class="water-panel home-panel detail-panel">/u)
  80. assert.ok(sectionB, 'detail panel should live in the equal-width B section')
  81. })
  82. test('home list panels scroll to reveal the full queue instead of pinning a few rows', async () => {
  83. const source = await readFile(homePath, 'utf8')
  84. const listSelector = source.match(/\.compact-list\s*\{[^}]*\}/u)
  85. assert.ok(listSelector, 'compact list style block exists')
  86. assert.match(listSelector[0], /max-height:\s*[\w.]+/u)
  87. assert.match(listSelector[0], /overflow-y:\s*auto/u)
  88. assert.match(source, /listRealtimeAlarms\(\),/u)
  89. })
  90. test('home overview map fills the full map card body', async () => {
  91. const source = await readFile(homePath, 'utf8')
  92. assert.match(source, /\.map-panel[\s\S]{0,160}?display:\s*flex/u)
  93. assert.match(source, /\.map-stage[\s\S]{0,140}?flex:\s*1/u)
  94. })
  95. test('home overview legend and detail cover unified device and facility status colors', async () => {
  96. const source = await readFile(homePath, 'utf8')
  97. assert.match(source, /gisPointStatusColors\.alarm[\s\S]{0,20}报警\/异常/u)
  98. assert.match(source, /gisPointStatusColors\.workOrder[\s\S]{0,20}工单\/检修中/u)
  99. assert.match(source, /gisPointStatusColors\.warning[\s\S]{0,20}预警\/欠费/u)
  100. assert.match(source, /gisPointStatusColors\.offline[\s\S]{0,20}离线/u)
  101. assert.match(source, /gisPointStatusColors\.normal[\s\S]{0,20}正常/u)
  102. assert.match(source, /overviewDetail\.statusText \|\| gisStatusLabel\(overviewDetail\.statusKey\)/u)
  103. })
  104. test('home device data sources track the unified equipment endpoints', async () => {
  105. const source = await readFile(homePath, 'utf8')
  106. // 设备台账已统一到 /equipment-base:统计与列表都走统一设备接口,供水范围用 rootTypeId 限定
  107. assert.match(source, /getEquipmentStatusStatistics\(/u)
  108. assert.match(source, /listEquipment\(/u)
  109. assert.match(source, /rootTypeId/u)
  110. assert.match(source, /normalizeDeviceStatistics/u)
  111. // 供水专属的旧设备接口已下线,不得再被首页引用
  112. assert.doesNotMatch(source, /getDeviceLedgerStatistics/u)
  113. assert.doesNotMatch(source, /listAbnormalDevices/u)
  114. })
  115. test('home surfaces partial load failures instead of swallowing them', async () => {
  116. const source = await readFile(homePath, 'utf8')
  117. // settle 自身吞掉异常并返回兜底值,因此失败上报必须由 settle 内部完成;
  118. // 链式 .catch(recordProblem(...)) 永不触发,会让“部分数据加载失败”提示变成死代码。
  119. assert.match(source, /async function settle\(fn, fallback = null, onError = null\)/u)
  120. assert.match(source, /onError\?\.\(\)/u)
  121. assert.doesNotMatch(source, /\.catch\(recordProblem\(/u)
  122. assert.match(source, /settle\(\(\) => getWaterFacilityOverview\(\), \[\], recordProblem\('设施概览'\)\)/u)
  123. assert.match(source, /settle\(\(\) => listRealtimeAlarms\(\), \[\], recordProblem\('实时报警'\)\)/u)
  124. })