alarmGIS.visual.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. // 独立视觉/交互测试:只在此测试服务器替换接口与地图,不改变生产页面的数据源。
  2. // PLAYWRIGHT_MODULE 指向已安装 playwright 的 index.mjs,BROWSER_EXECUTABLE 可指定浏览器。
  3. import { mkdtemp } from 'node:fs/promises'
  4. import { tmpdir } from 'node:os'
  5. import path from 'node:path'
  6. import { pathToFileURL } from 'node:url'
  7. import assert from 'node:assert/strict'
  8. import { createServer } from 'vite'
  9. import vue from '@vitejs/plugin-vue'
  10. import AutoImport from 'unplugin-auto-import/vite'
  11. const { chromium } = await import(process.env.PLAYWRIGHT_MODULE ? pathToFileURL(process.env.PLAYWRIGHT_MODULE).href : 'playwright')
  12. const output = await mkdtemp(path.join(tmpdir(), 'alarm-gis-visual-'))
  13. const samples = [
  14. { id: 'a1', equipmentId: 'e1', deviceCode: 'PS-DEMO-001', deviceName: '城北路液位监测点', equipmentLocation: '城北路与人民路交叉口东侧', alarmLevel: 1, actualValue: null, longitude: '110.40', latitude: '28.46' },
  15. { id: 'a2', equipmentId: 'e1', deviceCode: 'PS-DEMO-001', deviceName: '城北路液位监测点', equipmentLocation: '城北路与人民路交叉口东侧', alarmLevel: 2, actualValue: 0, longitude: '110.40', latitude: '28.46' },
  16. { id: 'b1', equipmentId: 'e2', deviceCode: 'PS-DEMO-002', deviceName: '沿江路流量监测点', equipmentLocation: '沿江路排水管网检查井', alarmLevel: 2, actualValue: 2.36, longitude: '110.41', latitude: '28.47' },
  17. { id: 'c1', equipmentId: 'e3', deviceCode: 'PS-DEMO-003', deviceName: '人民路井盖监测点', equipmentLocation: '人民路北侧步行道', alarmLevel: 3, actualValue: null, longitude: null, latitude: null },
  18. { id: 'd1', equipmentId: 'e4', deviceCode: 'PS-DEMO-004', deviceName: '建设路液位监测点', equipmentLocation: '建设路与文化路交叉口', alarmLevel: 4, actualValue: 1.26, longitude: '110.42', latitude: '28.48' }
  19. ].map((row, index) => ({ ...row, alarmNo: 'BJ20260924102000000' + index, createTime: '2026-09-24 10:20:00', alarmStatus: 0, monitorStatusName: '异常', auditStatus: 0, auditStatusName: '未审核', orderStatusName: '未转工单', warningCode: 'WARN_INSTANT_FLOW', warningType: '液位', warningUnit: 'm', maxValue: 1.8, alarmLevelName: ['一级报警', '二级报警', '三级报警', '四级报警'][row.alarmLevel - 1] }))
  20. const apiModule = `
  21. const samples = ${JSON.stringify(samples)};
  22. const response = data => Promise.resolve({ data });
  23. export const getAlarmOptions = () => response({ alarmLevels: ['一级报警','二级报警','三级报警','四级报警'].map((label,i)=>({value:i+1,label})), monitorStatuses:[{value:0,label:'异常'},{value:1,label:'已恢复'}] });
  24. export const getAlarmMap = params => { window.__gisRequests.push(params); return window.__gisDataError ? Promise.reject(new Error('测试接口异常,请重试')) : response(samples.filter(row=>(params.alarmLevel == null || row.alarmLevel===params.alarmLevel) && (params.alarmStatus == null || row.alarmStatus===params.alarmStatus) && (!params.keyword || row.deviceCode.includes(params.keyword) || row.deviceName.includes(params.keyword)))); };
  25. export const getAlarms = params => { window.__gisHistoryRequests.push(params); const rows=samples.filter(row=>row.deviceCode===params.deviceCode); return response({records:rows,total:rows.length}); };
  26. export const getAlarmAnalysis = id => { window.__gisAnalysisRequests.push(id); return response({ alarm:samples.find(row=>row.id===id), supported:true, samples:[], message:'测试数据:暂无区间采样' }); };
  27. export const getAlarm = id => response(samples.find(row=>row.id===id));
  28. export const getAlarmTimeline = () => response([]);
  29. `
  30. const scopeModule = `import { ref, computed } from 'vue'; const scope=ref({rootTypeId:2}); export function useAlarmScope(){return {scope,scopeKey:computed(()=>JSON.stringify(scope.value)),route:{query:{}},menu:()=>null,navigate:()=>{}}}`
  31. const mapModule = `import { onMounted, h } from 'vue'; export default {props:['points'],emits:['ready','error','point-click','cluster-click'],setup(props,{emit,expose}){expose({fitBounds(){},locatePoint(point){window.__gisLocated=point.deviceCode},zoomIn(){},zoomOut(){}});onMounted(()=>window.__gisMapFails?emit('error','测试环境:底图不可用,列表及详情不受影响。'):emit('ready'));return()=>h('div',{class:'gis-map-canvas','data-testid':'map-fixture',style:'height:100%;background:#f4f7fb'},window.__gisMapFails?[]:props.points.map(point=>h('button',{'data-testid':'map-marker',onClick:()=>emit('point-click',point)},point.name)));}}`
  32. const entry = `
  33. import {createApp} from 'vue'; import ElementPlus from 'element-plus'; import 'element-plus/dist/index.css'; import zhCn from 'element-plus/es/locale/lang/zh-cn';
  34. import Page from '/src/views/subSystem/alarm/drainage/alarmGIS.vue'; import Pagination from '/src/components/Pagination/index.vue';
  35. window.__gisRequests=[];window.__gisHistoryRequests=[];window.__gisAnalysisRequests=[];window.__gisMapFails=true;
  36. const app=createApp(Page);app.use(ElementPlus,{locale:zhCn});app.component('pagination',Pagination);app.mount('#app');
  37. `
  38. const fixture = {
  39. name: 'alarm-gis-visual-fixture', enforce: 'pre',
  40. resolveId(id) {
  41. if (id === 'virtual:gis-entry') return '\0gis-entry'
  42. if (id === 'virtual:gis-api') return '\0gis-api'
  43. if (id.endsWith('/useAlarmScope') || id.endsWith('/useAlarmScope.js')) return '\0gis-scope'
  44. if (id === 'virtual:gis-map') return '\0gis-map'
  45. },
  46. load(id) { return ({ '\0gis-entry': entry, '\0gis-api': apiModule, '\0gis-scope': scopeModule, '\0gis-map': mapModule })[id] },
  47. configureServer(server) {
  48. server.middlewares.use((req, res, next) => {
  49. if (req.url !== '/__alarm-gis-fixture') return next()
  50. res.setHeader('Content-Type', 'text/html; charset=utf-8')
  51. res.end('<!doctype html><html lang="zh-CN"><meta charset="utf-8"><style>body{margin:0;font-family:"Microsoft YaHei",sans-serif;background:#f5f7fa}*{box-sizing:border-box}button{font-family:inherit}</style><div id="app"></div><script type="module" src="/@id/virtual:gis-entry"></script></html>')
  52. })
  53. }
  54. }
  55. const server = await createServer({
  56. configFile: false, root: process.cwd(), plugins: [fixture, vue(), AutoImport({ imports: ['vue'], dts: false })],
  57. cacheDir: path.join(output, 'vite-cache'),
  58. optimizeDeps: { entries: [], include: ['vue', 'element-plus', '@element-plus/icons-vue', 'echarts'] },
  59. resolve: { alias: [{find:'@/api/alarm',replacement:'virtual:gis-api'},{find:'@/components/GisMapCanvas.vue',replacement:'virtual:gis-map'},{find:'@',replacement:path.resolve('src')}] },
  60. server: { host: '127.0.0.1', port: 0 }, logLevel: 'error'
  61. })
  62. let browser
  63. try {
  64. await server.listen()
  65. browser = await chromium.launch({ headless: true, executablePath: process.env.BROWSER_EXECUTABLE || undefined })
  66. const page = await browser.newPage({ viewport: { width: 1366, height: 1000 }, deviceScaleFactor: 1 })
  67. const pageErrors = []
  68. page.on('pageerror', error => pageErrors.push(error.message))
  69. await page.route('**/*', route => route.request().url().startsWith('http://127.0.0.1:') ? route.continue() : route.abort())
  70. await page.goto('http://127.0.0.1:' + server.httpServer.address().port + '/__alarm-gis-fixture')
  71. await page.getByRole('heading', {name:'当前报警 GIS 一张图'}).waitFor()
  72. await page.locator('.gis-device-item').nth(3).waitFor()
  73. assert.equal(await page.locator('.gis-level-metric').count(),4)
  74. assert.deepEqual(await page.locator('.gis-level-metric > strong').allTextContents(),['1台','1台','1台','1台'])
  75. assert.equal(await page.getByRole('button',{name:'放大地图'}).isDisabled(),true)
  76. await page.screenshot({path:path.join(output,'desktop.png'),fullPage:true})
  77. const mapBefore = await page.locator('.gis-map-stage').boundingBox()
  78. await page.getByLabel('报警设备列表').getByRole('button',{name:'城北路液位监测点',exact:true}).click()
  79. await page.locator('.alarm-gis-drawer').waitFor()
  80. await page.getByRole('tab',{name:'监测曲线',exact:true}).waitFor()
  81. await page.waitForFunction(()=>window.__gisAnalysisRequests.includes('a2'))
  82. assert.ok(!(await page.evaluate(()=>window.__gisAnalysisRequests)).includes('a1'))
  83. assert.equal((await page.locator('.gis-map-stage').boundingBox()).width,mapBefore.width)
  84. await page.getByRole('tab',{name:'报警记录',exact:true}).click()
  85. assert.equal(await page.locator('.gis-history-item').count(),2)
  86. assert.equal(await page.locator('.gis-history-actions').getByRole('button',{name:'查看曲线'}).count(),1)
  87. assert.equal(await page.locator('.pagination-container').isVisible(),true)
  88. await page.screenshot({path:path.join(output,'drawer-history.png')})
  89. await page.getByRole('tab',{name:'设备信息',exact:true}).click()
  90. assert.equal(await page.locator('.gis-detail-grid').last().getByText('110.4',{exact:true}).count(),1)
  91. await page.keyboard.press('Escape')
  92. await page.locator('.alarm-gis-drawer').waitFor({state:'hidden'})
  93. await page.getByRole('button',{name:'人民路井盖监测点',exact:true}).click()
  94. assert.equal(await page.getByRole('tab',{name:'报警记录',exact:true}).getAttribute('aria-selected'),'true')
  95. await page.getByRole('tab',{name:'监测曲线',exact:true}).click()
  96. await page.getByText('暂无有实际值的报警指标',{exact:true}).waitFor()
  97. await page.keyboard.press('Escape')
  98. await page.locator('.alarm-gis-drawer').waitFor({state:'hidden'})
  99. await page.evaluate(()=>window.__gisMapFails=false)
  100. await page.getByRole('button',{name:'重试底图',exact:true}).click()
  101. await page.getByText('地图已就绪',{exact:true}).waitFor()
  102. assert.equal(await page.getByRole('button',{name:'放大地图'}).isDisabled(),false)
  103. assert.equal(await page.getByTestId('map-marker').count(),3)
  104. await page.locator('.gis-device-item').first().getByRole('button',{name:'定位',exact:true}).click()
  105. assert.equal(await page.evaluate(()=>window.__gisLocated),'PS-DEMO-001')
  106. await page.getByPlaceholder('搜索设备编码、名称或安装位置').fill('无匹配设备')
  107. await page.getByRole('button',{name:'查询',exact:true}).click()
  108. await page.getByText('暂无匹配的报警设备',{exact:true}).waitFor()
  109. await page.getByRole('button',{name:'重置',exact:true}).click()
  110. await page.locator('.gis-device-item').nth(3).waitFor()
  111. await page.setViewportSize({width:390,height:844})
  112. assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth <= window.innerWidth),true)
  113. await page.screenshot({path:path.join(output,'mobile.png'),fullPage:true})
  114. await page.getByLabel('报警设备列表').getByRole('button',{name:'城北路液位监测点',exact:true}).click()
  115. await page.getByRole('tab',{name:'报警记录',exact:true}).click()
  116. await page.screenshot({path:path.join(output,'mobile-drawer.png')})
  117. assert.equal(await page.locator('.alarm-gis-drawer').evaluate(el=>el.scrollWidth <= el.clientWidth),true)
  118. assert.deepEqual(pageErrors,[])
  119. console.log('GIS visual/interaction checks passed. Screenshots: ' + output)
  120. } finally {
  121. await browser?.close()
  122. await server.close()
  123. }