t56-latest-detail-chain.mjs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. import assert from 'node:assert/strict'
  2. import { createPinia, setActivePinia } from 'pinia'
  3. import { useDetailStore } from '../../../src/modules/cockpit/stores/detailStore.js'
  4. import { metricRows, metricsSourceText, sectionHasContent, sectionPlaceholder } from '../../../src/modules/cockpit/domain/equipmentDetailView.js'
  5. import { TIME_LABELS } from '../../../src/modules/cockpit/domain/fields.js'
  6. // Literal API-contract fixture, not a reimplementation of the backend selection.
  7. const readings = [
  8. { metricCode: 'ENV_COD', metricName: 'COD', value: 32.5, unit: 'mg/L', sampledAt: '2026-10-29T10:00:00+08:00', timeSource: 'CREATED_AT', freshness: 'STALE' },
  9. { metricCode: 'ENV_PH', metricName: 'pH', value: 0, unit: '', sampledAt: '2026-10-28T09:00:00+08:00', timeSource: 'CREATED_AT', freshness: 'CURRENT' },
  10. { metricCode: 'ENV_TEMPERATURE', metricName: '温度', value: null, unit: '℃', sampledAt: '2026-10-29T10:00:00+08:00', timeSource: 'CREATED_AT', freshness: 'STALE' },
  11. ]
  12. function aggregate(code, metrics, state = 'STALE') {
  13. return {
  14. deviceCode: code, aggregateState: 'READY',
  15. archive: { state: 'SUCCESS', data: { deviceCode: code, deviceName: '仁科环境监测器', deviceType: '15', deviceTypeName: '仁科环境监测器' } },
  16. metrics: { state, data: { deviceCode: code, state, stateReason: state === 'STALE' ? 'SAMPLE_OUTDATED' : 'READY', metrics } },
  17. warnings: { state: 'SUCCESS', data: [] }, alarms: { state: 'SUCCESS', data: [] },
  18. workOrders: { state: 'EMPTY', data: null }, timelines: { state: 'EMPTY', data: null },
  19. alarmWarningRelations: { state: 'EMPTY', data: null }, latestImage: { state: 'EMPTY', data: null },
  20. }
  21. }
  22. const paths = []
  23. setActivePinia(createPinia())
  24. const store = useDetailStore()
  25. store.configure({
  26. config: { dataSource: 'real', api: { baseUrl: 'http://fixture.invalid/api/cockpit/v1' } },
  27. fetchImpl: async (url) => {
  28. paths.push(url)
  29. assert.ok(url.endsWith('/equipments/21119740/detail-aggregate'))
  30. return Response.json({ code: 200, data: aggregate('21119740', readings) })
  31. },
  32. })
  33. await store.openDevice('21119740')
  34. const metrics = store.sections.equipmentAggregate.data.sections.metrics
  35. assert.ok(sectionHasContent(metrics), sectionPlaceholder(metrics))
  36. assert.equal(metrics.state, 'stale')
  37. assert.equal(metrics.retained, true)
  38. assert.equal(paths.length, 1, 'Populated typed aggregate must not fan out to a guessed trend endpoint')
  39. const rows = metricRows(metrics.data)
  40. assert.deepEqual(rows.map(({ name, value, unit, sampledAt }) => ({ name, value, unit, sampledAt })), [
  41. { name: 'COD', value: '32.5', unit: 'mg/L', sampledAt: '2026-10-29T10:00:00+08:00' },
  42. { name: 'pH', value: '0', unit: '', sampledAt: '2026-10-28T09:00:00+08:00' },
  43. { name: '温度', value: '—', unit: '℃', sampledAt: '2026-10-29T10:00:00+08:00' },
  44. ])
  45. assert.equal(rows[0].sampledAtLabel, TIME_LABELS.ingested, 'A source create_time must not be labeled as physical sampling time')
  46. assert.equal(metricsSourceText(metrics.data), '指标记录已超过配置周期')
  47. assert.deepEqual(rows.map((row) => row.freshnessLabel), ['已过期', '', '已过期'], 'Mixed fresh/stale factors need their own stale labels')
  48. // A slow previous device response must never overwrite the newly selected device.
  49. let releaseA
  50. const lateA = new Promise((resolve) => { releaseA = resolve })
  51. store.closeDetail()
  52. store.configure({ fetchImpl: async (url) => {
  53. if (url.endsWith('/equipments/A/detail-aggregate')) return lateA
  54. assert.ok(url.endsWith('/equipments/B/detail-aggregate'))
  55. return Response.json({ code: 200, data: aggregate('B', [
  56. { metricCode: 'PRESSURE', metricName: '压力', value: 0.6, unit: 'MPa', sampledAt: '2026-11-02T10:00:00+08:00', timeSource: 'REPORTED_AT' },
  57. ], 'SUCCESS') })
  58. } })
  59. const openingA = store.openDevice('A')
  60. await store.openDevice('B')
  61. releaseA(Response.json({ code: 200, data: aggregate('A', readings) }))
  62. await openingA
  63. assert.equal(store.selection.deviceCode, 'B')
  64. assert.equal(store.sections.equipmentAggregate.data.deviceCode, 'B')
  65. const bRows = metricRows(store.sections.equipmentAggregate.data.sections.metrics.data)
  66. assert.equal(bRows[0].value, '0.6')
  67. assert.equal(bRows[0].sampledAtLabel, TIME_LABELS.sampled)
  68. store.configure({ fetchImpl: async () => Response.json({ code: 200, data: aggregate('D-failed', [], 'FAILED') }) })
  69. await store.openDevice('D-failed')
  70. const failed = store.sections.equipmentAggregate.data.sections
  71. assert.equal(failed.metrics.state, 'failed')
  72. assert.equal(failed.archive.state, 'success')
  73. assert.equal(failed.warnings.state, 'success')
  74. assert.equal(failed.alarms.state, 'success')
  75. console.log('PASS real store -> typed metric rows: per-factor time, stale values, zero/null, time source, device race and independent sections')