waterGisConsistency.test.mjs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import test from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import { readFile } from 'node:fs/promises'
  4. const pages = {
  5. facility: 'src/views/subSystem/waterSupply/facility/gis.vue',
  6. device: 'src/views/subSystem/waterSupply/components/WaterDeviceGisMap.vue',
  7. alarm: 'src/views/subSystem/waterSupply/components/WaterAlarmPage.vue'
  8. }
  9. const shellPath = 'src/components/WaterGisPageShell.vue'
  10. test('the shared shell owns one metric grid, map error and map empty styling', async () => {
  11. const shell = await readFile(shellPath, 'utf8')
  12. assert.match(shell, /class="metric-grid"/u)
  13. assert.match(shell, /<slot name="metrics" \/>/u)
  14. assert.match(shell, /\.metric-grid :deep\(\.metric\)/u)
  15. assert.match(shell, /\.metric-grid :deep\(\.metric-icon\)/u)
  16. assert.match(shell, /\.metric-grid :deep\(\.metric-body\)/u)
  17. assert.match(shell, /\.metric-grid :deep\(\.metric-value\)/u)
  18. assert.match(shell, /:deep\(\.map-error\)/u)
  19. assert.match(shell, /:deep\(\.map-empty\)/u)
  20. })
  21. test('all three GIS maps fill the metrics slot without nesting another metric grid', async () => {
  22. for (const [name, path] of Object.entries(pages)) {
  23. const source = await readFile(path, 'utf8')
  24. const metricsSlot = source.match(/#metrics>[\s\S]*?<\/template>/u)?.[0] || ''
  25. assert.ok(metricsSlot, `${name} should declare a #metrics slot`)
  26. assert.doesNotMatch(metricsSlot, /class="metric-grid"/u, `${name} should not nest a metric grid`)
  27. assert.match(metricsSlot, /class="metric"/u, `${name} should render metric cards`)
  28. assert.match(metricsSlot, /class="metric-icon"/u, `${name} should render a metric icon`)
  29. assert.match(metricsSlot, /class="metric-body"/u, `${name} should render a metric body`)
  30. assert.match(metricsSlot, /class="metric-value"/u, `${name} should render a metric value row`)
  31. assert.match(metricsSlot, /<small>/u, `${name} should render a metric unit`)
  32. }
  33. })
  34. test('all three GIS maps keep the same refresh, empty, error and drawer contract', async () => {
  35. for (const [name, path] of Object.entries(pages)) {
  36. const source = await readFile(path, 'utf8')
  37. assert.match(source, />刷新<\/el-button>/u, `${name} should expose a refresh action`)
  38. assert.match(source, />全图<\/el-button>/u, `${name} should expose a fit-bounds action`)
  39. assert.match(source, /class="map-error"/u, `${name} should expose a map error state`)
  40. assert.match(source, /class="map-error"[\s\S]*?重试<\/el-button>/u, `${name} should offer a retry action on map errors`)
  41. assert.match(source, /class="map-empty"/u, `${name} should expose a map empty state`)
  42. assert.match(source, /<el-drawer/u, `${name} should render the shared detail drawer component`)
  43. assert.match(source, /direction="rtl"/u, `${name} should open the detail drawer from the right`)
  44. assert.match(source, /class="detail-drawer/u, `${name} should use the shared detail drawer class`)
  45. assert.match(source, /420px/u, `${name} should size the map detail drawer at 420px`)
  46. }
  47. })
  48. test('all three GIS maps share the searchable result list structure', async () => {
  49. for (const [name, path] of Object.entries(pages)) {
  50. const source = await readFile(path, 'utf8')
  51. assert.match(source, /class="device-result-list"/u, `${name} should render the shared result list`)
  52. assert.match(source, /class="device-name"/u, `${name} should render a result name`)
  53. assert.match(source, /class="device-meta"/u, `${name} should render a result meta line`)
  54. }
  55. })
  56. test('clustering preserves every valid input point in the rendered marker count', async () => {
  57. const { mergeMapPoints } = await import('../src/utils/gisMapClustering.js')
  58. const points = [
  59. { id: 'a', lng: 110.3, lat: 28.4 },
  60. { id: 'b', lng: 110.3004, lat: 28.4003 },
  61. { id: 'c', lng: 110.9, lat: 28.9 }
  62. ]
  63. const project = ({ lng, lat }) => ({ x: lng * 1000, y: lat * 1000 })
  64. const merged = mergeMapPoints(points, project, 48)
  65. const renderedCount = merged.reduce((sum, item) => sum + (item.type === 'cluster' ? item.count : 1), 0)
  66. assert.equal(renderedCount, points.length)
  67. })
  68. test('the shared metric card neutralises the global absolute metric icon rule', async () => {
  69. const shell = await readFile(shellPath, 'utf8')
  70. assert.match(shell, /\.metric-grid :deep\(\.metric\) \{\s*position: static;/u)
  71. assert.match(shell, /\.metric-grid :deep\(\.metric-icon\) \{\s*position: static;/u)
  72. })
  73. test('all three GIS maps render the shared shell inside the same map card and drawer layout', async () => {
  74. for (const [name, path] of Object.entries(pages)) {
  75. const source = await readFile(path, 'utf8')
  76. assert.match(source, /class="water-panel map-card"/u, `${name} should render the shared map card`)
  77. assert.match(source, /<WaterGisPageShell\s+embedded/u, `${name} should embed the shared shell in the card`)
  78. assert.match(source, /<section class="detail-hero">/u, `${name} should use the shared drawer hero`)
  79. assert.match(source, /<section class="detail-section">/u, `${name} should use the shared drawer section`)
  80. }
  81. })
  82. test('all three GIS maps share the page heading and filter card chrome', async () => {
  83. for (const [name, path] of Object.entries(pages)) {
  84. const source = await readFile(path, 'utf8')
  85. assert.match(source, /class="page-heading"/u, `${name} should render the page heading`)
  86. assert.match(source, /class="page-title-icon"/u, `${name} should render the page title icon`)
  87. assert.match(source, /class="water-panel filter-card"/u, `${name} should render the filter card`)
  88. assert.match(source, /class="filter-form"/u, `${name} should render the inline filter form`)
  89. assert.match(source, />查询<\/el-button>/u, `${name} should expose a query action`)
  90. assert.match(source, />重置<\/el-button>/u, `${name} should expose a reset action`)
  91. }
  92. })
  93. test('the GIS page chrome theme is shared by the facility and device maps', async () => {
  94. const theme = await readFile('src/views/subSystem/waterSupply/components/waterGisPageTheme.scss', 'utf8')
  95. const facility = await readFile(pages.facility, 'utf8')
  96. const device = await readFile(pages.device, 'utf8')
  97. assert.match(theme, /\.gis-page \.page-heading/u)
  98. assert.match(theme, /\.gis-page \.page-title-icon/u)
  99. assert.match(theme, /\.gis-page \.filter-form \.el-form-item/u)
  100. assert.match(theme, /\.gis-page \.card-heading/u)
  101. assert.match(facility, /@use '\.\.\/components\/waterGisPageTheme\.scss'/u)
  102. assert.match(device, /@use '\.\/waterGisPageTheme\.scss'/u)
  103. })
  104. test('every GIS map renders its points from the filtered layer state', async () => {
  105. const facility = await readFile(pages.facility, 'utf8')
  106. const device = await readFile(pages.device, 'utf8')
  107. const alarm = await readFile(pages.alarm, 'utf8')
  108. assert.match(facility, /const visibleMapModel = computed\(\(\) => filterFacilityMapModel\(fullMapModel\.value, layerState\)\)/u)
  109. assert.match(facility, /const mapPoints = computed\(\(\) => visibleMapModel\.value\.points\.map/u)
  110. assert.match(device, /const mapPoints = computed\(\(\) => filteredDevices\.value\.map/u)
  111. assert.match(alarm, /const gisMapPoints = computed\(\(\) => filteredAlarmPoints\.value\.map/u)
  112. })