|
@@ -0,0 +1,79 @@
|
|
|
|
|
+import test from 'node:test'
|
|
|
|
|
+import assert from 'node:assert/strict'
|
|
|
|
|
+import { readFile } from 'node:fs/promises'
|
|
|
|
|
+
|
|
|
|
|
+const pages = {
|
|
|
|
|
+ facility: 'src/views/subSystem/waterSupply/facility/gis.vue',
|
|
|
|
|
+ device: 'src/views/subSystem/waterSupply/components/WaterDeviceGisMap.vue',
|
|
|
|
|
+ alarm: 'src/views/subSystem/waterSupply/components/WaterAlarmPage.vue'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const shellPath = 'src/components/WaterGisPageShell.vue'
|
|
|
|
|
+
|
|
|
|
|
+test('the shared shell owns one metric grid, map error and map empty styling', async () => {
|
|
|
|
|
+ const shell = await readFile(shellPath, 'utf8')
|
|
|
|
|
+
|
|
|
|
|
+ assert.match(shell, /class="metric-grid"/u)
|
|
|
|
|
+ assert.match(shell, /<slot name="metrics" \/>/u)
|
|
|
|
|
+ assert.match(shell, /\.metric-grid :deep\(\.metric\)/u)
|
|
|
|
|
+ assert.match(shell, /\.metric-grid :deep\(\.metric-icon\)/u)
|
|
|
|
|
+ assert.match(shell, /\.metric-grid :deep\(\.metric-body\)/u)
|
|
|
|
|
+ assert.match(shell, /\.metric-grid :deep\(\.metric-value\)/u)
|
|
|
|
|
+ assert.match(shell, /:deep\(\.map-error\)/u)
|
|
|
|
|
+ assert.match(shell, /:deep\(\.map-empty\)/u)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+test('all three GIS maps fill the metrics slot without nesting another metric grid', async () => {
|
|
|
|
|
+ for (const [name, path] of Object.entries(pages)) {
|
|
|
|
|
+ const source = await readFile(path, 'utf8')
|
|
|
|
|
+ const metricsSlot = source.match(/#metrics>[\s\S]*?<\/template>/u)?.[0] || ''
|
|
|
|
|
+
|
|
|
|
|
+ assert.ok(metricsSlot, `${name} should declare a #metrics slot`)
|
|
|
|
|
+ assert.doesNotMatch(metricsSlot, /class="metric-grid"/u, `${name} should not nest a metric grid`)
|
|
|
|
|
+ assert.match(metricsSlot, /class="metric"/u, `${name} should render metric cards`)
|
|
|
|
|
+ assert.match(metricsSlot, /class="metric-icon"/u, `${name} should render a metric icon`)
|
|
|
|
|
+ assert.match(metricsSlot, /class="metric-body"/u, `${name} should render a metric body`)
|
|
|
|
|
+ assert.match(metricsSlot, /class="metric-value"/u, `${name} should render a metric value row`)
|
|
|
|
|
+ assert.match(metricsSlot, /<small>/u, `${name} should render a metric unit`)
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+test('all three GIS maps keep the same refresh, empty, error and drawer contract', async () => {
|
|
|
|
|
+ for (const [name, path] of Object.entries(pages)) {
|
|
|
|
|
+ const source = await readFile(path, 'utf8')
|
|
|
|
|
+
|
|
|
|
|
+ assert.match(source, />刷新<\/el-button>/u, `${name} should expose a refresh action`)
|
|
|
|
|
+ assert.match(source, />全图<\/el-button>/u, `${name} should expose a fit-bounds action`)
|
|
|
|
|
+ assert.match(source, /class="map-error"/u, `${name} should expose a map error state`)
|
|
|
|
|
+ assert.match(source, /class="map-error"[\s\S]*?重试<\/el-button>/u, `${name} should offer a retry action on map errors`)
|
|
|
|
|
+ assert.match(source, /class="map-empty"/u, `${name} should expose a map empty state`)
|
|
|
|
|
+ assert.match(source, /<el-drawer/u, `${name} should render the shared detail drawer component`)
|
|
|
|
|
+ assert.match(source, /direction="rtl"/u, `${name} should open the detail drawer from the right`)
|
|
|
|
|
+ assert.match(source, /class="detail-drawer/u, `${name} should use the shared detail drawer class`)
|
|
|
|
|
+ assert.match(source, /420px/u, `${name} should size the map detail drawer at 420px`)
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+test('all three GIS maps share the searchable result list structure', async () => {
|
|
|
|
|
+ for (const [name, path] of Object.entries(pages)) {
|
|
|
|
|
+ const source = await readFile(path, 'utf8')
|
|
|
|
|
+
|
|
|
|
|
+ assert.match(source, /class="device-result-list"/u, `${name} should render the shared result list`)
|
|
|
|
|
+ assert.match(source, /class="device-name"/u, `${name} should render a result name`)
|
|
|
|
|
+ assert.match(source, /class="device-meta"/u, `${name} should render a result meta line`)
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+test('clustering preserves every valid input point in the rendered marker count', async () => {
|
|
|
|
|
+ const { mergeMapPoints } = await import('../src/utils/gisMapClustering.js')
|
|
|
|
|
+ const points = [
|
|
|
|
|
+ { id: 'a', lng: 110.3, lat: 28.4 },
|
|
|
|
|
+ { id: 'b', lng: 110.3004, lat: 28.4003 },
|
|
|
|
|
+ { id: 'c', lng: 110.9, lat: 28.9 }
|
|
|
|
|
+ ]
|
|
|
|
|
+ const project = ({ lng, lat }) => ({ x: lng * 1000, y: lat * 1000 })
|
|
|
|
|
+
|
|
|
|
|
+ const merged = mergeMapPoints(points, project, 48)
|
|
|
|
|
+ const renderedCount = merged.reduce((sum, item) => sum + (item.type === 'cluster' ? item.count : 1), 0)
|
|
|
|
|
+
|
|
|
|
|
+ assert.equal(renderedCount, points.length)
|
|
|
|
|
+})
|