import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import test from 'node:test' import { ABNORMAL_TYPE_OPTIONS, buildAbnormalCsv, buildAbnormalExportQuery, buildAbnormalListQuery, computeAbnormalSummary, filterAbnormalDevices, normalizeAbnormalRow, resolveAbnormalTypes } from '../src/utils/waterDeviceAbnormalModel.js' // 独立构造的样例设备:三台异常来源各不相同,一台完全正常用于排除断言。 const alarmDevice = { equipmentId: 'eq-alarm', equipmentCode: 'CN-GS-202609020001', equipmentName: '城南压力计', equipmentTypeName: '压力计', district: 0, alarmStatus: 1, onlineStatus: 1, currentStatus: 0, equipmentImage: '/profile/upload/alarm.png' } const offlineDevice = { equipmentId: 'eq-offline', equipmentCode: 'CB-GS-202609020002', equipmentName: '城北漏失监测器', equipmentTypeName: '漏失监测仪', district: 1, alarmStatus: 0, onlineStatus: 0, currentStatus: 0 } const maintenanceDevice = { equipmentId: 'eq-maintenance', equipmentCode: 'TC-GS-202609020003', equipmentName: '太常流量计', equipmentTypeName: '超声波外夹流量计', district: 2, alarmStatus: 0, onlineStatus: 1, currentStatus: 1 } const healthyDevice = { equipmentId: 'eq-healthy', equipmentCode: 'CN-GS-202609020004', equipmentName: '城南消火栓压力终端', equipmentTypeName: '消火栓压力监测终端', district: 0, alarmStatus: 0, onlineStatus: 1, currentStatus: 0 } const allRows = [alarmDevice, offlineDevice, maintenanceDevice, healthyDevice] test('abnormal source covers alarm, offline and maintenance from the device status table', () => { assert.deepEqual(resolveAbnormalTypes(alarmDevice), ['alarm']) assert.deepEqual(resolveAbnormalTypes(offlineDevice), ['offline']) assert.deepEqual(resolveAbnormalTypes(maintenanceDevice), ['maintenance']) assert.deepEqual(resolveAbnormalTypes(healthyDevice), []) const bothSources = { ...offlineDevice, alarmStatus: 1, currentStatus: 1 } assert.deepEqual(resolveAbnormalTypes(bothSources), ['alarm', 'offline', 'maintenance']) }) test('abnormal type filters keep only the matching abnormal sources', () => { assert.deepEqual( filterAbnormalDevices(allRows, { type: 'all' }).map(row => row.equipmentId), ['eq-alarm', 'eq-offline', 'eq-maintenance'] ) assert.deepEqual( filterAbnormalDevices(allRows, { type: 'alarm' }).map(row => row.equipmentId), ['eq-alarm'] ) assert.deepEqual( filterAbnormalDevices(allRows, { type: 'offline' }).map(row => row.equipmentId), ['eq-offline'] ) assert.deepEqual( filterAbnormalDevices(allRows, { type: 'maintenance' }).map(row => row.equipmentId), ['eq-maintenance'] ) // 离线且报警要求两个条件同时成立,单独离线不算命中 assert.deepEqual(filterAbnormalDevices(allRows, { type: 'offline_alarm' }), []) assert.deepEqual( filterAbnormalDevices([{ ...alarmDevice, onlineStatus: 0 }, ...allRows], { type: 'offline_alarm' }) .map(row => row.equipmentId), ['eq-alarm'] ) assert.ok(ABNORMAL_TYPE_OPTIONS.some(option => option.value === 'offline_alarm')) }) test('keyword filter matches either the device code or the device name', () => { assert.deepEqual( filterAbnormalDevices(allRows, { type: 'all', keyword: 'TC-GS' }).map(row => row.equipmentId), ['eq-maintenance'] ) assert.deepEqual( filterAbnormalDevices(allRows, { type: 'all', keyword: '漏失' }).map(row => row.equipmentId), ['eq-offline'] ) assert.deepEqual(filterAbnormalDevices(allRows, { type: 'all', keyword: '不存在' }), []) }) test('abnormal rows expose Chinese status text, reason and resolvable image url', () => { const row = normalizeAbnormalRow(alarmDevice) assert.equal(row.abnormalReasonText, '设备报警') assert.equal(row.alarmStatusText, '报警') assert.equal(row.onlineStatusText, '在线') assert.equal(row.currentStatusText, '在用') assert.equal(row.equipmentTypeName, '压力计') assert.ok(row.abnormalTypeLabels.includes('报警中')) assert.match(row.equipmentImageUrl, /\/profile\/upload\/alarm\.png$/u) const offlineRow = normalizeAbnormalRow(offlineDevice) assert.equal(offlineRow.abnormalReasonText, '设备离线') const maintenanceRow = normalizeAbnormalRow(maintenanceDevice) assert.equal(maintenanceRow.abnormalReasonText, '设备维修中') }) test('abnormal summary counts each abnormal source independently', () => { const summary = computeAbnormalSummary([ { ...alarmDevice, onlineStatus: 0 }, offlineDevice, maintenanceDevice, healthyDevice ]) assert.deepEqual(summary, { total: 3, alarm: 1, offline: 2, maintenance: 1, offlineAlarm: 1 }) }) test('list query is scoped to the water root type and fetches the whole filtered set', () => { assert.deepEqual(buildAbnormalListQuery({ total: 4 }), { rootTypeId: 1, pageNum: 1, pageSize: 4 }) assert.deepEqual(buildAbnormalListQuery({ total: 0, equipmentCode: 'TC-GS', equipmentName: '流量' }), { rootTypeId: 1, pageNum: 1, pageSize: 1, equipmentCode: 'TC-GS', equipmentName: '流量' }) // 导出沿用同一筛选口径,但只按条件取数,不再带分页 assert.deepEqual(buildAbnormalExportQuery({ equipmentCode: 'TC-GS' }), { rootTypeId: 1, pageNum: 1, pageSize: 1000, equipmentCode: 'TC-GS' }) }) test('csv export carries a BOM, Chinese headers and the filtered rows', () => { const csv = buildAbnormalCsv([normalizeAbnormalRow(alarmDevice)]) assert.ok(csv.startsWith('\ufeff'), '导出内容需带 BOM 以便 Excel 正确识别中文') assert.match(csv, /设备编码/u) assert.match(csv, /异常原因/u) assert.match(csv, /CN-GS-202609020001/u) assert.match(csv, /设备报警/u) assert.equal(csv.split(/\r?\n/u).filter(Boolean).length, 2) }) const routePath = 'src/views/subSystem/waterSupply/device/abnormal.vue' const pagePath = 'src/views/subSystem/waterSupply/components/WaterDeviceAbnormalPage.vue' test('abnormal management menu entry renders the dedicated abnormal board', async () => { const route = await readFile(routePath, 'utf8') assert.match(route, /WaterDeviceAbnormalPage/u) }) test('abnormal board reads the unified equipment module within the water root type', async () => { const source = await readFile(pagePath, 'utf8') assert.match(source, /listEquipment\(/u) assert.match(source, /getEquipmentStatusStatistics\(/u) assert.match(source, /buildAbnormalListQuery\(/u) assert.match(source, /WATER_DEVICE_ROOT_TYPE_ID/u) // 取数范围由模型统一带上 rootTypeId,页面只传供水根类型常量 assert.match(source, /getEquipmentStatusStatistics\(WATER_DEVICE_ROOT_TYPE_ID\)/u) // 供水专属的旧设备接口已下线,异常看板不得再引用 assert.doesNotMatch(source, /waterSupply\/device\/(ledger|abnormal)/u) assert.doesNotMatch(source, /getDeviceLedgerStatistics/u) assert.doesNotMatch(source, /listAbnormalDevices/u) }) test('abnormal board is a read-only board without dispatch or work-order creation', async () => { const source = await readFile(pagePath, 'utf8') assert.match(source, /异常类型/u) assert.match(source, /设备编码/u) assert.match(source, /设备名称/u) assert.match(source, /批量导出/u) assert.match(source, / { const source = await readFile(pagePath, 'utf8') assert.match(source, /设备当前状态/u) assert.match(source, /设备报警状态/u) assert.match(source, /在线状态/u) assert.match(source, /关联未办结工单/u) assert.match(source, /orderStatusText/u) assert.match( source, /from '@\/views\/subSystem\/pipeNetwork\/ops\/workOrder\/js\/workOrderDict\.js'/u, '异常看板应复用统一工单字典' ) assert.doesNotMatch(source, /function\s+workOrderStatusType/u) }) test('abnormal board keeps the backend aggregate for the device indicator band', async () => { const source = await readFile(pagePath, 'utf8') const statsBlock = source.slice( source.indexOf('const abnormalMetrics = computed'), source.indexOf('const filteredRows = computed') ) assert.ok(statsBlock, '缺少异常指标带计算块') assert.doesNotMatch(statsBlock, /page\.value\.records/u, '指标带不应基于当前分页行计数') assert.match(statsBlock, /statistics\.value\.total/u) assert.match(statsBlock, /statistics\.value\.offline/u) assert.match(statsBlock, /summary\.value\.maintenance/u) })