|
|
@@ -0,0 +1,78 @@
|
|
|
+import assert from 'node:assert/strict'
|
|
|
+import { createPinia, setActivePinia } from 'pinia'
|
|
|
+import { useDetailStore } from '../../../src/modules/cockpit/stores/detailStore.js'
|
|
|
+import { metricRows, metricsSourceText, sectionHasContent, sectionPlaceholder } from '../../../src/modules/cockpit/domain/equipmentDetailView.js'
|
|
|
+import { TIME_LABELS } from '../../../src/modules/cockpit/domain/fields.js'
|
|
|
+
|
|
|
+// Literal API-contract fixture, not a reimplementation of the backend selection.
|
|
|
+const readings = [
|
|
|
+ { metricCode: 'ENV_COD', metricName: 'COD', value: 32.5, unit: 'mg/L', sampledAt: '2026-10-29T10:00:00+08:00', timeSource: 'CREATED_AT', freshness: 'STALE' },
|
|
|
+ { metricCode: 'ENV_PH', metricName: 'pH', value: 0, unit: '', sampledAt: '2026-10-28T09:00:00+08:00', timeSource: 'CREATED_AT', freshness: 'CURRENT' },
|
|
|
+ { metricCode: 'ENV_TEMPERATURE', metricName: '温度', value: null, unit: '℃', sampledAt: '2026-10-29T10:00:00+08:00', timeSource: 'CREATED_AT', freshness: 'STALE' },
|
|
|
+]
|
|
|
+function aggregate(code, metrics, state = 'STALE') {
|
|
|
+ return {
|
|
|
+ deviceCode: code, aggregateState: 'READY',
|
|
|
+ archive: { state: 'SUCCESS', data: { deviceCode: code, deviceName: '仁科环境监测器', deviceType: '15', deviceTypeName: '仁科环境监测器' } },
|
|
|
+ metrics: { state, data: { deviceCode: code, state, stateReason: state === 'STALE' ? 'SAMPLE_OUTDATED' : 'READY', metrics } },
|
|
|
+ warnings: { state: 'SUCCESS', data: [] }, alarms: { state: 'SUCCESS', data: [] },
|
|
|
+ workOrders: { state: 'EMPTY', data: null }, timelines: { state: 'EMPTY', data: null },
|
|
|
+ alarmWarningRelations: { state: 'EMPTY', data: null }, latestImage: { state: 'EMPTY', data: null },
|
|
|
+ }
|
|
|
+}
|
|
|
+const paths = []
|
|
|
+setActivePinia(createPinia())
|
|
|
+const store = useDetailStore()
|
|
|
+store.configure({
|
|
|
+ config: { dataSource: 'real', api: { baseUrl: 'http://fixture.invalid/api/cockpit/v1' } },
|
|
|
+ fetchImpl: async (url) => {
|
|
|
+ paths.push(url)
|
|
|
+ assert.ok(url.endsWith('/equipments/21119740/detail-aggregate'))
|
|
|
+ return Response.json({ code: 200, data: aggregate('21119740', readings) })
|
|
|
+ },
|
|
|
+})
|
|
|
+await store.openDevice('21119740')
|
|
|
+const metrics = store.sections.equipmentAggregate.data.sections.metrics
|
|
|
+assert.ok(sectionHasContent(metrics), sectionPlaceholder(metrics))
|
|
|
+assert.equal(metrics.state, 'stale')
|
|
|
+assert.equal(metrics.retained, true)
|
|
|
+assert.equal(paths.length, 1, 'Populated typed aggregate must not fan out to a guessed trend endpoint')
|
|
|
+const rows = metricRows(metrics.data)
|
|
|
+assert.deepEqual(rows.map(({ name, value, unit, sampledAt }) => ({ name, value, unit, sampledAt })), [
|
|
|
+ { name: 'COD', value: '32.5', unit: 'mg/L', sampledAt: '2026-10-29T10:00:00+08:00' },
|
|
|
+ { name: 'pH', value: '0', unit: '', sampledAt: '2026-10-28T09:00:00+08:00' },
|
|
|
+ { name: '温度', value: '—', unit: '℃', sampledAt: '2026-10-29T10:00:00+08:00' },
|
|
|
+])
|
|
|
+assert.equal(rows[0].sampledAtLabel, TIME_LABELS.ingested, 'A source create_time must not be labeled as physical sampling time')
|
|
|
+assert.equal(metricsSourceText(metrics.data), '指标记录已超过配置周期')
|
|
|
+assert.deepEqual(rows.map((row) => row.freshnessLabel), ['已过期', '', '已过期'], 'Mixed fresh/stale factors need their own stale labels')
|
|
|
+
|
|
|
+// A slow previous device response must never overwrite the newly selected device.
|
|
|
+let releaseA
|
|
|
+const lateA = new Promise((resolve) => { releaseA = resolve })
|
|
|
+store.closeDetail()
|
|
|
+store.configure({ fetchImpl: async (url) => {
|
|
|
+ if (url.endsWith('/equipments/A/detail-aggregate')) return lateA
|
|
|
+ assert.ok(url.endsWith('/equipments/B/detail-aggregate'))
|
|
|
+ return Response.json({ code: 200, data: aggregate('B', [
|
|
|
+ { metricCode: 'PRESSURE', metricName: '压力', value: 0.6, unit: 'MPa', sampledAt: '2026-11-02T10:00:00+08:00', timeSource: 'REPORTED_AT' },
|
|
|
+ ], 'SUCCESS') })
|
|
|
+} })
|
|
|
+const openingA = store.openDevice('A')
|
|
|
+await store.openDevice('B')
|
|
|
+releaseA(Response.json({ code: 200, data: aggregate('A', readings) }))
|
|
|
+await openingA
|
|
|
+assert.equal(store.selection.deviceCode, 'B')
|
|
|
+assert.equal(store.sections.equipmentAggregate.data.deviceCode, 'B')
|
|
|
+const bRows = metricRows(store.sections.equipmentAggregate.data.sections.metrics.data)
|
|
|
+assert.equal(bRows[0].value, '0.6')
|
|
|
+assert.equal(bRows[0].sampledAtLabel, TIME_LABELS.sampled)
|
|
|
+
|
|
|
+store.configure({ fetchImpl: async () => Response.json({ code: 200, data: aggregate('D-failed', [], 'FAILED') }) })
|
|
|
+await store.openDevice('D-failed')
|
|
|
+const failed = store.sections.equipmentAggregate.data.sections
|
|
|
+assert.equal(failed.metrics.state, 'failed')
|
|
|
+assert.equal(failed.archive.state, 'success')
|
|
|
+assert.equal(failed.warnings.state, 'success')
|
|
|
+assert.equal(failed.alarms.state, 'success')
|
|
|
+console.log('PASS real store -> typed metric rows: per-factor time, stale values, zero/null, time source, device race and independent sections')
|