waterDeviceAbnormalBoard.test.mjs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import assert from 'node:assert/strict'
  2. import { readFile } from 'node:fs/promises'
  3. import test from 'node:test'
  4. import {
  5. ABNORMAL_TYPE_OPTIONS,
  6. buildAbnormalCsv,
  7. buildAbnormalExportQuery,
  8. buildAbnormalListQuery,
  9. computeAbnormalSummary,
  10. filterAbnormalDevices,
  11. normalizeAbnormalRow,
  12. resolveAbnormalTypes
  13. } from '../src/utils/waterDeviceAbnormalModel.js'
  14. // 独立构造的样例设备:三台异常来源各不相同,一台完全正常用于排除断言。
  15. const alarmDevice = {
  16. equipmentId: 'eq-alarm',
  17. equipmentCode: 'CN-GS-202609020001',
  18. equipmentName: '城南压力计',
  19. equipmentTypeName: '压力计',
  20. district: 0,
  21. alarmStatus: 1,
  22. onlineStatus: 1,
  23. currentStatus: 0,
  24. equipmentImage: '/profile/upload/alarm.png'
  25. }
  26. const offlineDevice = {
  27. equipmentId: 'eq-offline',
  28. equipmentCode: 'CB-GS-202609020002',
  29. equipmentName: '城北漏失监测器',
  30. equipmentTypeName: '漏失监测仪',
  31. district: 1,
  32. alarmStatus: 0,
  33. onlineStatus: 0,
  34. currentStatus: 0
  35. }
  36. const maintenanceDevice = {
  37. equipmentId: 'eq-maintenance',
  38. equipmentCode: 'TC-GS-202609020003',
  39. equipmentName: '太常流量计',
  40. equipmentTypeName: '超声波外夹流量计',
  41. district: 2,
  42. alarmStatus: 0,
  43. onlineStatus: 1,
  44. currentStatus: 1
  45. }
  46. const healthyDevice = {
  47. equipmentId: 'eq-healthy',
  48. equipmentCode: 'CN-GS-202609020004',
  49. equipmentName: '城南消火栓压力终端',
  50. equipmentTypeName: '消火栓压力监测终端',
  51. district: 0,
  52. alarmStatus: 0,
  53. onlineStatus: 1,
  54. currentStatus: 0
  55. }
  56. const allRows = [alarmDevice, offlineDevice, maintenanceDevice, healthyDevice]
  57. test('abnormal source covers alarm, offline and maintenance from the device status table', () => {
  58. assert.deepEqual(resolveAbnormalTypes(alarmDevice), ['alarm'])
  59. assert.deepEqual(resolveAbnormalTypes(offlineDevice), ['offline'])
  60. assert.deepEqual(resolveAbnormalTypes(maintenanceDevice), ['maintenance'])
  61. assert.deepEqual(resolveAbnormalTypes(healthyDevice), [])
  62. const bothSources = { ...offlineDevice, alarmStatus: 1, currentStatus: 1 }
  63. assert.deepEqual(resolveAbnormalTypes(bothSources), ['alarm', 'offline', 'maintenance'])
  64. })
  65. test('abnormal type filters keep only the matching abnormal sources', () => {
  66. assert.deepEqual(
  67. filterAbnormalDevices(allRows, { type: 'all' }).map(row => row.equipmentId),
  68. ['eq-alarm', 'eq-offline', 'eq-maintenance']
  69. )
  70. assert.deepEqual(
  71. filterAbnormalDevices(allRows, { type: 'alarm' }).map(row => row.equipmentId),
  72. ['eq-alarm']
  73. )
  74. assert.deepEqual(
  75. filterAbnormalDevices(allRows, { type: 'offline' }).map(row => row.equipmentId),
  76. ['eq-offline']
  77. )
  78. assert.deepEqual(
  79. filterAbnormalDevices(allRows, { type: 'maintenance' }).map(row => row.equipmentId),
  80. ['eq-maintenance']
  81. )
  82. // 离线且报警要求两个条件同时成立,单独离线不算命中
  83. assert.deepEqual(filterAbnormalDevices(allRows, { type: 'offline_alarm' }), [])
  84. assert.deepEqual(
  85. filterAbnormalDevices([{ ...alarmDevice, onlineStatus: 0 }, ...allRows], { type: 'offline_alarm' })
  86. .map(row => row.equipmentId),
  87. ['eq-alarm']
  88. )
  89. assert.ok(ABNORMAL_TYPE_OPTIONS.some(option => option.value === 'offline_alarm'))
  90. })
  91. test('keyword filter matches either the device code or the device name', () => {
  92. assert.deepEqual(
  93. filterAbnormalDevices(allRows, { type: 'all', keyword: 'TC-GS' }).map(row => row.equipmentId),
  94. ['eq-maintenance']
  95. )
  96. assert.deepEqual(
  97. filterAbnormalDevices(allRows, { type: 'all', keyword: '漏失' }).map(row => row.equipmentId),
  98. ['eq-offline']
  99. )
  100. assert.deepEqual(filterAbnormalDevices(allRows, { type: 'all', keyword: '不存在' }), [])
  101. })
  102. test('abnormal rows expose Chinese status text, reason and resolvable image url', () => {
  103. const row = normalizeAbnormalRow(alarmDevice)
  104. assert.equal(row.abnormalReasonText, '设备报警')
  105. assert.equal(row.alarmStatusText, '报警')
  106. assert.equal(row.onlineStatusText, '在线')
  107. assert.equal(row.currentStatusText, '在用')
  108. assert.equal(row.equipmentTypeName, '压力计')
  109. assert.ok(row.abnormalTypeLabels.includes('报警中'))
  110. assert.match(row.equipmentImageUrl, /\/profile\/upload\/alarm\.png$/u)
  111. const offlineRow = normalizeAbnormalRow(offlineDevice)
  112. assert.equal(offlineRow.abnormalReasonText, '设备离线')
  113. const maintenanceRow = normalizeAbnormalRow(maintenanceDevice)
  114. assert.equal(maintenanceRow.abnormalReasonText, '设备维修中')
  115. })
  116. test('abnormal summary counts each abnormal source independently', () => {
  117. const summary = computeAbnormalSummary([
  118. { ...alarmDevice, onlineStatus: 0 },
  119. offlineDevice,
  120. maintenanceDevice,
  121. healthyDevice
  122. ])
  123. assert.deepEqual(summary, { total: 3, alarm: 1, offline: 2, maintenance: 1, offlineAlarm: 1 })
  124. })
  125. test('list query is scoped to the water root type and fetches the whole filtered set', () => {
  126. assert.deepEqual(buildAbnormalListQuery({ total: 4 }), {
  127. rootTypeId: 1,
  128. pageNum: 1,
  129. pageSize: 4
  130. })
  131. assert.deepEqual(buildAbnormalListQuery({ total: 0, equipmentCode: 'TC-GS', equipmentName: '流量' }), {
  132. rootTypeId: 1,
  133. pageNum: 1,
  134. pageSize: 1,
  135. equipmentCode: 'TC-GS',
  136. equipmentName: '流量'
  137. })
  138. // 导出沿用同一筛选口径,但只按条件取数,不再带分页
  139. assert.deepEqual(buildAbnormalExportQuery({ equipmentCode: 'TC-GS' }), {
  140. rootTypeId: 1,
  141. pageNum: 1,
  142. pageSize: 1000,
  143. equipmentCode: 'TC-GS'
  144. })
  145. })
  146. test('csv export carries a BOM, Chinese headers and the filtered rows', () => {
  147. const csv = buildAbnormalCsv([normalizeAbnormalRow(alarmDevice)])
  148. assert.ok(csv.startsWith('\ufeff'), '导出内容需带 BOM 以便 Excel 正确识别中文')
  149. assert.match(csv, /设备编码/u)
  150. assert.match(csv, /异常原因/u)
  151. assert.match(csv, /CN-GS-202609020001/u)
  152. assert.match(csv, /设备报警/u)
  153. assert.equal(csv.split(/\r?\n/u).filter(Boolean).length, 2)
  154. })
  155. const routePath = 'src/views/subSystem/waterSupply/device/abnormal.vue'
  156. const pagePath = 'src/views/subSystem/waterSupply/components/WaterDeviceAbnormalPage.vue'
  157. test('abnormal management menu entry renders the dedicated abnormal board', async () => {
  158. const route = await readFile(routePath, 'utf8')
  159. assert.match(route, /WaterDeviceAbnormalPage/u)
  160. })
  161. test('abnormal board reads the unified equipment module within the water root type', async () => {
  162. const source = await readFile(pagePath, 'utf8')
  163. assert.match(source, /listEquipment\(/u)
  164. assert.match(source, /getEquipmentStatusStatistics\(/u)
  165. assert.match(source, /buildAbnormalListQuery\(/u)
  166. assert.match(source, /WATER_DEVICE_ROOT_TYPE_ID/u)
  167. // 取数范围由模型统一带上 rootTypeId,页面只传供水根类型常量
  168. assert.match(source, /getEquipmentStatusStatistics\(WATER_DEVICE_ROOT_TYPE_ID\)/u)
  169. // 供水专属的旧设备接口已下线,异常看板不得再引用
  170. assert.doesNotMatch(source, /waterSupply\/device\/(ledger|abnormal)/u)
  171. assert.doesNotMatch(source, /getDeviceLedgerStatistics/u)
  172. assert.doesNotMatch(source, /listAbnormalDevices/u)
  173. })
  174. test('abnormal board is a read-only board without dispatch or work-order creation', async () => {
  175. const source = await readFile(pagePath, 'utf8')
  176. assert.match(source, /异常类型/u)
  177. assert.match(source, /设备编码/u)
  178. assert.match(source, /设备名称/u)
  179. assert.match(source, /批量导出/u)
  180. assert.match(source, /<el-drawer/u)
  181. assert.match(source, /direction="rtl"/u)
  182. assert.match(source, /设备图片/u)
  183. assert.doesNotMatch(source, /派单/u)
  184. assert.doesNotMatch(source, /addWorkOrder|dispatchWorkOrder|createWorkOrder/u)
  185. assert.doesNotMatch(source, /updateEquipmentStatus/u)
  186. })
  187. test('abnormal detail drawer shows the three independent state sources and related order', async () => {
  188. const source = await readFile(pagePath, 'utf8')
  189. assert.match(source, /设备当前状态/u)
  190. assert.match(source, /设备报警状态/u)
  191. assert.match(source, /在线状态/u)
  192. assert.match(source, /关联未办结工单/u)
  193. assert.match(source, /orderStatusText/u)
  194. assert.match(
  195. source,
  196. /from '@\/views\/subSystem\/pipeNetwork\/ops\/workOrder\/js\/workOrderDict\.js'/u,
  197. '异常看板应复用统一工单字典'
  198. )
  199. assert.doesNotMatch(source, /function\s+workOrderStatusType/u)
  200. })
  201. test('abnormal board keeps the backend aggregate for the device indicator band', async () => {
  202. const source = await readFile(pagePath, 'utf8')
  203. const statsBlock = source.slice(
  204. source.indexOf('const abnormalMetrics = computed'),
  205. source.indexOf('const filteredRows = computed')
  206. )
  207. assert.ok(statsBlock, '缺少异常指标带计算块')
  208. assert.doesNotMatch(statsBlock, /page\.value\.records/u, '指标带不应基于当前分页行计数')
  209. assert.match(statsBlock, /statistics\.value\.total/u)
  210. assert.match(statsBlock, /statistics\.value\.offline/u)
  211. assert.match(statsBlock, /summary\.value\.maintenance/u)
  212. })