waterHomeModel.test.mjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. import test from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import {
  4. HOME_QUICK_ENTRIES,
  5. buildHomeWorkOrderQuery,
  6. normalizeFacilityTotal,
  7. normalizeHomeKpis,
  8. normalizeDeviceStatistics,
  9. normalizeWorkOrderStatusStat,
  10. normalizeAlarmOverviewCards,
  11. normalizeAlarmTrend,
  12. normalizePieRatio,
  13. normalizeRealtimeAlarmRows,
  14. realtimeAlarmJumpTarget,
  15. normalizeAbnormalTodoRows,
  16. normalizeDeviceSummary,
  17. normalizeWorkOrderRows,
  18. buildHomeGisOverview
  19. } from '../src/utils/waterHomeModel.js'
  20. test('home work order scope is constrained to the water-supply type tree', () => {
  21. assert.deepEqual(buildHomeWorkOrderQuery(), {
  22. deviceTypeParentId: '1',
  23. pageNum: 1,
  24. pageSize: 5
  25. })
  26. assert.deepEqual(buildHomeWorkOrderQuery({ pageSize: 8 }), {
  27. deviceTypeParentId: '1',
  28. pageNum: 1,
  29. pageSize: 8
  30. })
  31. })
  32. test('facility total excludes the aggregated total row', () => {
  33. const rows = [
  34. { facilityType: 'source', totalCount: 3 },
  35. { facilityType: 'plant', totalCount: 2 },
  36. { facilityType: 'pipe', totalCount: 996 },
  37. { facilityType: 'total', totalCount: 1000 }
  38. ]
  39. assert.equal(normalizeFacilityTotal(rows), 1001)
  40. assert.equal(normalizeFacilityTotal(null), 0)
  41. assert.equal(normalizeFacilityTotal([]), 0)
  42. })
  43. test('home KPIs map api payloads to workbench cards', () => {
  44. const kpis = normalizeHomeKpis({
  45. facilityRows: [{ facilityType: 'pipe', totalCount: 996 }, { facilityType: 'total', totalCount: 1000 }],
  46. ledgerStats: { total: 15, online: 13 },
  47. alarmStats: { realtimeAlarmCount: 3 }
  48. })
  49. assert.deepEqual(kpis, [
  50. { key: 'facility', label: '设施总数', value: 996, unit: '处', tone: 'primary' },
  51. { key: 'device', label: '监测设备', value: 15, unit: '台', tone: 'primary' },
  52. { key: 'online', label: '在线设备', value: 13, unit: '台', tone: 'primary' },
  53. { key: 'alarm', label: '实时报警', value: 3, unit: '条', tone: 'danger' }
  54. ])
  55. })
  56. test('home KPIs keep realtime alarm card neutral when no alarm is active', () => {
  57. const alarm = normalizeHomeKpis({ alarmStats: { realtimeAlarmCount: 0 } }).find(item => item.key === 'alarm')
  58. assert.equal(alarm.tone, 'primary')
  59. })
  60. test('work order status statistic groups by authoritative status contract', () => {
  61. const stat = normalizeWorkOrderStatusStat({
  62. total: 10,
  63. counts: [
  64. { status: 0, count: 2 },
  65. { status: 1, count: 1 },
  66. { status: 3, count: 2 },
  67. { status: 4, count: 2 },
  68. { status: 5, count: 2 },
  69. { status: 7, count: 1 }
  70. ]
  71. })
  72. assert.equal(stat.total, 10)
  73. assert.equal(stat.pendingCount, 3)
  74. assert.equal(stat.processingCount, 2)
  75. assert.equal(stat.verifyingCount, 2)
  76. assert.equal(stat.finishedCount, 3)
  77. assert.equal(stat.byStatus.find(item => item.status === 6).count, 0)
  78. })
  79. test('alarm overview cards derive online rate and verifying orders', () => {
  80. const cards = normalizeAlarmOverviewCards({
  81. dashboard: { totalCount: 16, alarmingDeviceCount: 4 },
  82. deviceStats: { onlineRate: 86.666666 },
  83. workOrderStat: {
  84. counts: [{ status: 4, count: 3 }]
  85. }
  86. })
  87. assert.deepEqual(cards, [
  88. { key: 'alarmTotal', label: '报警总数', value: 16, unit: '条', tone: 'primary' },
  89. { key: 'alarmingDevice', label: '报警中设备', value: 4, unit: '台', tone: 'danger' },
  90. { key: 'verifyingOrder', label: '待验收工单', value: 3, unit: '单', tone: 'warning' },
  91. { key: 'onlineRate', label: '在线率', value: 86.7, unit: '%', tone: 'primary' }
  92. ])
  93. })
  94. test('alarm trend returns four level series with zero-filled missing days', () => {
  95. const trend = normalizeAlarmTrend([
  96. { date: '2026-09-15', level1Count: 1, level2Count: 2 },
  97. { date: '2026-09-16' }
  98. ])
  99. assert.deepEqual(trend.dates, ['2026-09-15', '2026-09-16'])
  100. assert.equal(trend.levels.length, 4)
  101. const level2 = trend.levels.find(item => item.name === '级2')
  102. assert.deepEqual(level2.data, [2, 0])
  103. assert.equal(normalizeAlarmTrend(null).levels.length, 4)
  104. })
  105. test('ratio pie drops empty values and caps the number of segments', () => {
  106. const items = [
  107. { name: ' ' },
  108. { name: '压力', count: '8' },
  109. { name: '流量', count: '5' },
  110. { name: '离线', count: '3' },
  111. { name: '低', count: 0 }
  112. ]
  113. assert.deepEqual(normalizePieRatio(items, '未分类', 3), [
  114. { name: '压力', count: 8 },
  115. { name: '流量', count: 5 },
  116. { name: '离线', count: 3 }
  117. ])
  118. assert.deepEqual(normalizePieRatio(undefined, '未分类'), [])
  119. })
  120. test('realtime alarm rows expose chinese friendly display fields', () => {
  121. const rows = normalizeRealtimeAlarmRows([
  122. {
  123. id: 'a1',
  124. deviceCode: 'CN-GS-202609020001',
  125. deviceName: '一号压力点',
  126. warningType: '压力过低',
  127. alarmLevel: 1,
  128. alarmTime: '2026-09-15 10:00:00',
  129. equipmentLocation: '城南',
  130. displayStatus: 1
  131. }
  132. ])
  133. assert.equal(rows.length, 1)
  134. assert.equal(rows[0].deviceName, '一号压力点')
  135. assert.equal(rows[0].alarmLevelText, '一级报警')
  136. assert.equal(rows[0].location, '城南')
  137. assert.equal(rows[0].displayStatusText, '待审核')
  138. assert.equal(rows[0].displayStatusType, 'warning')
  139. assert.equal(normalizeRealtimeAlarmRows([], 5).length, 0)
  140. })
  141. test('realtime alarm rows drop released alarms and keep status for routing', () => {
  142. const rows = normalizeRealtimeAlarmRows([
  143. { id: 'a-pending', deviceCode: 'CN-GS-202609020001', displayStatus: 1, alarmTime: '2026-09-15 10:00:00' },
  144. { id: 'a-order', deviceCode: 'CN-GS-202609020002', displayStatus: 2, orderNo: 'WO202609151100001', alarmTime: '2026-09-15 09:00:00' },
  145. { id: 'a-released', deviceCode: 'CN-GS-202609020003', displayStatus: 3, alarmTime: '2026-09-15 08:00:00' }
  146. ], 5)
  147. assert.deepEqual(rows.map(item => item.id), ['a-pending', 'a-order'])
  148. assert.equal(rows.find(item => item.id === 'a-order').displayStatusText, '已转工单')
  149. assert.equal(rows.find(item => item.id === 'a-order').orderNo, 'WO202609151100001')
  150. assert.equal(rows.find(item => item.id === 'a-pending').displayStatusType, 'warning')
  151. })
  152. test('realtime alarm rows derive transferred status from a live order number when displayStatus is missing', () => {
  153. const rows = normalizeRealtimeAlarmRows([
  154. { id: 'legacy-order', deviceCode: 'CN-GS-202609020004', orderNo: 'WO202609151100009', orderStatus: 3, alarmTime: '2026-09-15 11:00:00' },
  155. { id: 'legacy-pending', deviceCode: 'CN-GS-202609020005', alarmTime: '2026-09-15 10:30:00' }
  156. ], 5)
  157. const transferred = rows.find(item => item.id === 'legacy-order')
  158. assert.equal(transferred.displayStatus, 2)
  159. assert.equal(transferred.displayStatusText, '已转工单')
  160. assert.equal(transferred.displayStatusType, 'primary')
  161. const pending = rows.find(item => item.id === 'legacy-pending')
  162. assert.equal(pending.displayStatus, 1)
  163. assert.equal(pending.displayStatusText, '待审核')
  164. })
  165. test('realtime alarm rows never treat a cancelled order as transferred', () => {
  166. const rows = normalizeRealtimeAlarmRows([
  167. { id: 'legacy-cancelled', deviceCode: 'CN-GS-202609020006', orderNo: 'WO202609151100010', orderStatus: 7, alarmTime: '2026-09-15 11:30:00' }
  168. ], 5)
  169. assert.equal(rows[0].displayStatus, 1)
  170. assert.equal(rows[0].displayStatusText, '待审核')
  171. })
  172. test('realtime alarm fallback keeps a live order as transferred even when the alarm row is already handled', () => {
  173. // 与后端 resolveDisplayStatus 同序:在办工单优先于报警是否已处理。
  174. const rows = normalizeRealtimeAlarmRows([
  175. {
  176. id: 'legacy-handled-with-order',
  177. deviceCode: 'CN-GS-202609020007',
  178. alarmStatus: 1,
  179. orderNo: 'WO202609151100011',
  180. orderStatus: 3,
  181. alarmTime: '2026-09-15 12:00:00'
  182. }
  183. ], 5)
  184. assert.equal(rows.length, 1)
  185. assert.equal(rows[0].displayStatus, 2)
  186. assert.equal(rows[0].displayStatusText, '已转工单')
  187. })
  188. test('realtime alarm jumps split between audit and work order by display status', () => {
  189. assert.deepEqual(realtimeAlarmJumpTarget({
  190. id: 'alarm-1',
  191. deviceCode: 'CN-GS-202609020001',
  192. displayStatus: 1
  193. }), {
  194. path: '/monitorAlarm/audit',
  195. query: { rootTypeId: 1, alarmId: 'alarm-1', deviceCode: 'CN-GS-202609020001' }
  196. })
  197. assert.deepEqual(realtimeAlarmJumpTarget({
  198. id: 'alarm-2',
  199. deviceCode: 'CN-GS-202609020002',
  200. displayStatus: 2,
  201. orderNo: 'WO202609151100001'
  202. }), {
  203. path: '/device/maintain',
  204. query: { keyword: 'WO202609151100001' }
  205. })
  206. // 已转工单但缺少工单号时回落到报警审核,避免无效跳转
  207. const fallback = realtimeAlarmJumpTarget({ id: 'alarm-3', displayStatus: 2 })
  208. assert.equal(fallback.path, '/monitorAlarm/audit')
  209. assert.equal(fallback.query.rootTypeId, 1)
  210. })
  211. test('abnormal todo rows reuse the shared device normalization contract', () => {
  212. const rows = normalizeAbnormalTodoRows([{
  213. equipmentId: 'eq-1',
  214. equipmentCode: 'CN-GS-202609020002',
  215. equipmentName: '二号流量点',
  216. alarmStatus: 1,
  217. onlineStatus: 1,
  218. currentStatus: 1,
  219. alertLevel: 2,
  220. relatedWorkOrderNo: 'WO202609150001',
  221. relatedWorkOrderStatus: 3
  222. }], 1)
  223. assert.equal(rows.length, 1)
  224. assert.equal(rows[0].alarmStatusText, '报警')
  225. assert.equal(rows[0].onlineStatusText, '在线')
  226. assert.equal(rows[0].alarmLevelText, '二级报警')
  227. assert.equal(rows[0].relatedOrderNo, 'WO202609150001')
  228. assert.equal(rows[0].workOrderStatusText, '处理中')
  229. })
  230. test('device summary keeps key text fields for the workbench detail panel', () => {
  231. const summary = normalizeDeviceSummary({
  232. equipmentId: 'eq-9',
  233. equipmentCode: 'CB-GS-202609020003',
  234. equipmentName: '三号漏失点',
  235. equipmentLocation: '城北',
  236. district: '城北',
  237. alarmStatus: 0,
  238. onlineStatus: 0,
  239. currentStatus: 3,
  240. equipmentTypeName: '漏失在线监测仪'
  241. })
  242. assert.equal(summary.name, '三号漏失点')
  243. assert.equal(summary.alarmStatusText, '正常')
  244. assert.equal(summary.onlineStatusText, '离线')
  245. assert.equal(summary.currentStatusText, '维修')
  246. assert.equal(summary.abnormalReasonText, '设备维修中')
  247. assert.equal(summary.typeName, '漏失在线监测仪')
  248. })
  249. test('work order rows translate status and level codes to chinese', () => {
  250. const rows = normalizeWorkOrderRows([
  251. { orderId: 5, orderNo: 'WO202609150001', orderStatus: 4, orderLevel: 0, deviceCode: 'CN-GS-202609020001', createTime: '2026-09-15 10:00:00' }
  252. ], 5)
  253. assert.equal(rows[0].orderStatusText, '待验收')
  254. assert.equal(rows[0].orderStatusType, 'primary')
  255. assert.equal(rows[0].orderLevelText, '紧急')
  256. assert.equal(rows[0].orderLevelType, 'danger')
  257. })
  258. test('home GIS overview aggregates facilities, devices and alarms with alarm priority', () => {
  259. const overview = buildHomeGisOverview({
  260. facilityPoints: [
  261. { id: 'f-1', type: 'source', name: '一水源地', code: 'S-001', lng: 110.08, lat: 28.05 }
  262. ],
  263. facilityLines: [
  264. { id: 'pipe-1', name: '城南主管', type: 'pipe', points: [{ lng: 110.1, lat: 28.1 }, { lng: 110.2, lat: 28.2 }] }
  265. ],
  266. devicePoints: [
  267. { equipmentId: 'eq-1', equipmentCode: 'CN-GS-202609020001', equipmentName: '一号压力点', longitude: 110.1, latitude: 28.1, alarmStatus: 0, onlineStatus: 1, currentStatus: 0, equipmentTypeName: '压力计' }
  268. ],
  269. alarmPoints: [
  270. { alarmId: 'alarm-1', deviceCode: 'CN-GS-202609020001', deviceName: '一号压力点', longitude: 110.1, latitude: 28.1, warningType: '压力过低', alarmLevel: 1 }
  271. ]
  272. })
  273. assert.equal(overview.lines.length, 1)
  274. assert.equal(overview.lines[0].color, '#f1c40f')
  275. assert.equal(overview.lines[0].points.length, 2)
  276. assert.ok(overview.points.some(point => point.id === 'f-1' && point.statusKey === 'normal'))
  277. const device = overview.points.find(point => point.code === 'CN-GS-202609020001')
  278. assert.ok(device)
  279. assert.equal(device.statusKey, 'alarm')
  280. assert.equal(device.meta, '压力过低')
  281. assert.ok(overview.points.every(point =>
  282. Number.isFinite(point.lng) && Number.isFinite(point.lat) && point.name && point.iconUrl))
  283. const empty = buildHomeGisOverview({})
  284. assert.deepEqual(empty, { points: [], lines: [] })
  285. })
  286. test('home GIS facility points share device status color semantics by facility status', () => {
  287. const overview = buildHomeGisOverview({
  288. facilityPoints: [
  289. { id: 'src-abnormal', type: 'source', name: '一水源地', lng: 110.08, lat: 28.05, status: 1 },
  290. { id: 'src-normal', type: 'source', name: '二水源地', lng: 110.09, lat: 28.06, status: 0 },
  291. { id: 'src-unknown', type: 'source', name: '三水源地', lng: 110.15, lat: 28.12 },
  292. { id: 'plant-stopped', type: 'plant', name: '一水厂', lng: 110.1, lat: 28.07, status: 1 },
  293. { id: 'pump-maintaining', type: 'pumpStation', name: '城南泵站', lng: 110.11, lat: 28.08, status: 2 },
  294. { id: 'pump-fault', type: 'pumpStation', name: '城北泵站', lng: 110.12, lat: 28.09, status: 1 },
  295. { id: 'user-arrears', type: 'user', name: '用水户A', lng: 110.13, lat: 28.1, status: 1 },
  296. { id: 'user-stopped', type: 'user', name: '用水户B', lng: 110.14, lat: 28.11, status: 2 },
  297. { id: 'pipe-point', type: 'pipe', name: '管网占位点', lng: 110.16, lat: 28.13 }
  298. ]
  299. })
  300. const byId = id => overview.points.find(point => point.id === id)
  301. assert.equal(byId('src-abnormal').statusKey, 'alarm')
  302. assert.equal(byId('src-abnormal').statusText, '异常')
  303. assert.match(byId('src-abnormal').iconUrl, /T_MarkPoint_Red\.png/u)
  304. assert.equal(byId('src-normal').statusKey, 'normal')
  305. assert.equal(byId('src-normal').statusText, '正常')
  306. assert.match(byId('src-normal').iconUrl, /T_MarkPoint_Green\.png/u)
  307. assert.equal(byId('src-unknown').statusKey, 'normal')
  308. assert.equal(byId('src-unknown').statusText, '正常')
  309. assert.equal(byId('plant-stopped').statusKey, 'alarm')
  310. assert.equal(byId('plant-stopped').statusText, '停产')
  311. assert.equal(byId('pump-maintaining').statusKey, 'workOrder')
  312. assert.equal(byId('pump-maintaining').statusText, '检修中')
  313. assert.match(byId('pump-maintaining').iconUrl, /T_MarkPoint_Blue\.png/u)
  314. assert.equal(byId('pump-fault').statusKey, 'alarm')
  315. assert.equal(byId('pump-fault').statusText, '故障停运')
  316. assert.equal(byId('user-arrears').statusKey, 'warning')
  317. assert.equal(byId('user-arrears').statusText, '欠费')
  318. assert.match(byId('user-arrears').iconUrl, /T_MarkPoint_Yellow\.png/u)
  319. assert.equal(byId('user-stopped').statusKey, 'alarm')
  320. assert.equal(byId('user-stopped').statusText, '停用')
  321. assert.equal(byId('pipe-point'), undefined)
  322. })
  323. test('home quick entries cover the four workbench jumps', () => {
  324. assert.deepEqual(HOME_QUICK_ENTRIES.map(item => item.label), ['设备异常管理', '报警管理', '报警审核', '监测报警一张图'])
  325. assert.ok(HOME_QUICK_ENTRIES.every(item => item.suffix.startsWith('/')))
  326. assert.equal(HOME_QUICK_ENTRIES.find(item => item.key === 'audit').query.rootTypeId, 1)
  327. })
  328. test('home device statistics derive online rate from the unified equipment endpoint', () => {
  329. assert.deepEqual(normalizeDeviceStatistics({ total: 15, online: 12, offline: 3, alarm: 5 }), {
  330. total: 15,
  331. online: 12,
  332. offline: 3,
  333. alarm: 5,
  334. onlineRate: 80
  335. })
  336. // 后端未回填 offline 时按总数推导;无设备时不产生除零
  337. assert.equal(normalizeDeviceStatistics({ total: 15, online: 12 }).offline, 3)
  338. assert.equal(normalizeDeviceStatistics({ total: 0, online: 0 }).onlineRate, 0)
  339. assert.equal(normalizeDeviceStatistics({}).onlineRate, 0)
  340. })