|
|
@@ -0,0 +1,149 @@
|
|
|
+import assert from 'node:assert/strict'
|
|
|
+import { readFileSync } from 'node:fs'
|
|
|
+import vm from 'node:vm'
|
|
|
+import * as Cesium from 'cesium'
|
|
|
+import { toPipeNetwork, filterPipeNetwork } from '../../../src/modules/cockpit/domain/pipeNetwork.js'
|
|
|
+import { planModeLayers } from '../../../src/modules/cockpit/domain/modeLayerPlan.js'
|
|
|
+import { planViewMode } from '../../../src/modules/cockpit/domain/viewModes.js'
|
|
|
+import { syncPipeLayer, clearPipeLayer } from '../../../src/modules/cockpit/services/cesiumPipeLayer.js'
|
|
|
+
|
|
|
+// Offline only: real domain functions and Cesium entities; GPU picking returns controlled hits.
|
|
|
+// No network, server, browser, file mutations, or in-place mutation testing.
|
|
|
+const source = readFileSync(new URL('../../../src/modules/cockpit/services/cesiumEngine.js', import.meta.url), 'utf8')
|
|
|
+const code = source.replace(/^import\s+[^]*?from\s+['"][^'"\n]+['"][ \t]*$/gm, '')
|
|
|
+ .replace(/^import\s+['"][^'"\n]+['"][ \t]*$/gm, '')
|
|
|
+ .replace(/^export /gm, '')
|
|
|
+const context = vm.createContext({ ...Cesium, CesiumMath: Cesium.Math })
|
|
|
+vm.runInContext(`${code}\nglobalThis.engine = { pickPointMarkerAt, pickDeviceMarkerAt };`, context)
|
|
|
+const engine = context.engine
|
|
|
+
|
|
|
+let failed = 0
|
|
|
+let passed = 0
|
|
|
+async function check(name, action) {
|
|
|
+ try { await action(); passed += 1; console.log(`PASS ${name}`) }
|
|
|
+ catch (error) { failed += 1; console.error(`FAIL ${name}: ${error.message}`) }
|
|
|
+}
|
|
|
+const path = [[110.39, 28.44], [110.395, 28.445], [110.40, 28.45]]
|
|
|
+function feature(coordinates = path, properties = {}) {
|
|
|
+ return { type: 'Feature', id: 'P-1', geometry: { type: 'LineString', coordinates }, properties: {
|
|
|
+ code: 'P-1', industry: 'GAS', diameter: '400', material: '钢',
|
|
|
+ coordinateValidation: { renderable: true }, ...properties,
|
|
|
+ } }
|
|
|
+}
|
|
|
+const network = toPipeNetwork({ features: [feature()] })
|
|
|
+await check('完整路径、源管径和可选材质原样映射', () => {
|
|
|
+ assert.deepEqual(network.pipes[0].positions, path)
|
|
|
+ assert.equal(network.pipes[0].diameter, 400)
|
|
|
+ assert.equal(network.pipes[0].material, '钢')
|
|
|
+ const missing = toPipeNetwork({ features: [feature(path, { diameter: null, material: null })] }).pipes[0]
|
|
|
+ assert.equal(missing.diameter, null)
|
|
|
+ assert.equal(missing.material, '')
|
|
|
+})
|
|
|
+await check('任一坏顶点整条不上图,保留清单,不跨点重连', () => {
|
|
|
+ for (const bad of [null, [], ['110.395', 28.445], [NaN, 28.445], [110.395, Infinity]]) {
|
|
|
+ const result = toPipeNetwork({ features: [feature([path[0], bad, path[2]])] })
|
|
|
+ assert.equal(result.pipes.length, 0)
|
|
|
+ assert.equal(result.blocked.length, 1)
|
|
|
+ assert.equal(result.blocked[0].code, 'P-1')
|
|
|
+ assert.equal(result.blocked[0].reasonText, '无地图位置')
|
|
|
+ }
|
|
|
+})
|
|
|
+await check('后端未准入即不上图,不用合法结构替代后端校核', () => {
|
|
|
+ const result = toPipeNetwork({ features: [feature(path, { coordinateValidation: { renderable: false, reason: 'UNVERIFIED_COORDINATE' } })] })
|
|
|
+ assert.equal(result.pipes.length, 0)
|
|
|
+ assert.equal(result.blocked[0].reasonText, '坐标未经校验')
|
|
|
+})
|
|
|
+await check('行业筛选同样裁剪无地图位置清单,不修改原始网络', () => {
|
|
|
+ const all = toPipeNetwork({ features: [feature(), feature(path, { code: 'W', industry: 'WATER_SUPPLY' }), feature([path[0]], { code: 'X', industry: 'WATER_SUPPLY' })] })
|
|
|
+ assert.deepEqual(filterPipeNetwork(all, { industry: 'gas' }).pipes.map((p) => p.code), ['P-1'])
|
|
|
+ assert.equal(filterPipeNetwork(all, { industry: 'gas' }).blocked.length, 0)
|
|
|
+ const water = filterPipeNetwork(all, { industry: 'water' })
|
|
|
+ assert.deepEqual(water.pipes.map((p) => p.code), ['W'])
|
|
|
+ assert.equal(water.blocked.length, 1)
|
|
|
+ assert.equal(filterPipeNetwork(all, { industry: 'all' }).pipes.length, 2)
|
|
|
+ assert.equal(all.blocked.length, 1)
|
|
|
+})
|
|
|
+await check('四种模式均保留管线,引导态隐藏,切视图不裁管线', () => {
|
|
|
+ for (const mode of ['overview', 'infrastructure', 'monitoring', 'warningResponse']) {
|
|
|
+ const input = { mode, entered: true, objects: [], facilities: [], pipelines: network.pipes }
|
|
|
+ assert.equal(planModeLayers(input).pipelines, network.pipes)
|
|
|
+ assert.equal(planModeLayers({ ...input, entered: false }).pipelines.length, 0)
|
|
|
+ }
|
|
|
+ for (const mode of ['ground', 'pipeline']) {
|
|
|
+ const plan = planViewMode(mode)
|
|
|
+ assert.ok(plan.terrainAlpha > 0 && plan.tilesetAlpha > 0)
|
|
|
+ assert.equal(plan.terrainAlpha < 1, mode === 'pipeline')
|
|
|
+ assert.equal(plan.tilesetAlpha < 1, mode === 'pipeline')
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const pipes = new Cesium.CustomDataSource('cockpit-pipelines')
|
|
|
+const devices = new Cesium.CustomDataSource('cockpit-associated-device-markers')
|
|
|
+const pipe = pipes.entities.add({ id: 'pipe:P-1', properties: { kind: 'pipe', code: 'P-1' } })
|
|
|
+const component = pipes.entities.add({ id: 'component:C-1', properties: { kind: 'component', code: 'C-1' } })
|
|
|
+const point = new Cesium.Entity({ id: 'point:M', properties: { pointCode: 'M', pointKind: 'monitoring-point', pointStatus: 'NORMAL' } })
|
|
|
+const device = new Cesium.Entity({ id: 'device:D', properties: { deviceCode: 'D', monitoringPointCode: 'M', deviceStatus: 'NORMAL' } })
|
|
|
+const model = { kind: 'pipe-model-batch', key: 'batch', instances: ['pipe:P-1'] }
|
|
|
+const building = { id: 'building' }
|
|
|
+let hits = []
|
|
|
+let drills = 0
|
|
|
+let pickedPosition = null
|
|
|
+const viewer = {
|
|
|
+ dataSources: { getByName: (name) => name === pipes.name ? [pipes] : name === devices.name ? [devices] : [] },
|
|
|
+ canvas: { getBoundingClientRect: () => ({ left: 20, top: 30 }) },
|
|
|
+ scene: {
|
|
|
+ pick(position) { pickedPosition = position; return hits[0] },
|
|
|
+ drillPick() { drills += 1; return hits },
|
|
|
+ },
|
|
|
+}
|
|
|
+await check('普通点选不触发多层拾取,窗口坐标转画布坐标', () => {
|
|
|
+ hits = [{ id: point }]; drills = 0
|
|
|
+ assert.equal(engine.pickPointMarkerAt(viewer, 120, 230).code, 'M')
|
|
|
+ assert.equal(drills, 0)
|
|
|
+ assert.equal(pickedPosition.x, 100)
|
|
|
+ assert.equal(pickedPosition.y, 200)
|
|
|
+})
|
|
|
+await check('管线、构件、模型都不能吞掉监测点', () => {
|
|
|
+ for (const foreground of [pipe, component, model]) {
|
|
|
+ hits = [{ id: foreground }, { id: point }]
|
|
|
+ assert.equal(engine.pickPointMarkerAt(viewer, 120, 230).code, 'M')
|
|
|
+ }
|
|
|
+})
|
|
|
+await check('管线遮挡设备时仍回设备身份与父点位', () => {
|
|
|
+ hits = [{ id: model }, { id: pipe }, { id: device }]
|
|
|
+ const hit = engine.pickDeviceMarkerAt(viewer, 120, 230)
|
|
|
+ assert.equal(hit.code, 'D')
|
|
|
+ assert.equal(hit.pointCode, 'M')
|
|
|
+})
|
|
|
+await check('不穿透建筑/其它对象,也不把单独管线当监测点或设备', () => {
|
|
|
+ for (const list of [[{ id: building }, { id: point }], [{ id: pipe }, { id: building }, { id: point }], [{ id: model }]]) {
|
|
|
+ hits = list
|
|
|
+ assert.equal(engine.pickPointMarkerAt(viewer, 120, 230), null)
|
|
|
+ assert.equal(engine.pickDeviceMarkerAt(viewer, 120, 230), null)
|
|
|
+ }
|
|
|
+})
|
|
|
+await check('名字相似但不属于管线源的对象不能被穿透', () => {
|
|
|
+ hits = [{ id: new Cesium.Entity({ id: 'pipe:foreign' }) }, { id: point }]
|
|
|
+ assert.equal(engine.pickPointMarkerAt(viewer, 120, 230), null)
|
|
|
+})
|
|
|
+await check('无多层拾取能力时不伪造命中', () => {
|
|
|
+ hits = [{ id: pipe }, { id: point }]
|
|
|
+ const limited = { ...viewer, scene: { pick: viewer.scene.pick } }
|
|
|
+ assert.equal(engine.pickPointMarkerAt(limited, 120, 230), null)
|
|
|
+})
|
|
|
+await check('真实Cesium折线写入且相同输入不重赋值,筛空后移除', async () => {
|
|
|
+ const target = { dataSources: new Cesium.DataSourceCollection() }
|
|
|
+ const first = await syncPipeLayer(target, network)
|
|
|
+ assert.equal(first.pipes, 1)
|
|
|
+ const entity = target.dataSources.getByName(pipes.name)[0].entities.getById('pipe:P-1')
|
|
|
+ assert.equal(entity.polyline.positions.getValue().length, path.length)
|
|
|
+ const positions = entity.polyline.positions
|
|
|
+ const second = await syncPipeLayer(target, network)
|
|
|
+ assert.equal(second.unchanged, 1)
|
|
|
+ assert.equal(entity.polyline.positions, positions)
|
|
|
+ const empty = await syncPipeLayer(target, { pipes: [], components: [] })
|
|
|
+ assert.equal(empty.removed, 1)
|
|
|
+ await clearPipeLayer(target)
|
|
|
+})
|
|
|
+console.log(`T22: ${passed} passed, ${failed} failed`)
|
|
|
+process.exitCode = failed ? 1 : 0
|