// 独立视觉/交互测试:只在此测试服务器替换接口与地图,不改变生产页面的数据源。 // PLAYWRIGHT_MODULE 指向已安装 playwright 的 index.mjs,BROWSER_EXECUTABLE 可指定浏览器。 import { mkdtemp } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'node:path' import { pathToFileURL } from 'node:url' import assert from 'node:assert/strict' import { createServer } from 'vite' import vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' const { chromium } = await import(process.env.PLAYWRIGHT_MODULE ? pathToFileURL(process.env.PLAYWRIGHT_MODULE).href : 'playwright') const output = await mkdtemp(path.join(tmpdir(), 'alarm-gis-visual-')) const samples = [ { id: 'a1', equipmentId: 'e1', deviceCode: 'PS-DEMO-001', deviceName: '城北路液位监测点', equipmentLocation: '城北路与人民路交叉口东侧', alarmLevel: 1, actualValue: null, longitude: '110.40', latitude: '28.46' }, { id: 'a2', equipmentId: 'e1', deviceCode: 'PS-DEMO-001', deviceName: '城北路液位监测点', equipmentLocation: '城北路与人民路交叉口东侧', alarmLevel: 2, actualValue: 0, longitude: '110.40', latitude: '28.46' }, { id: 'b1', equipmentId: 'e2', deviceCode: 'PS-DEMO-002', deviceName: '沿江路流量监测点', equipmentLocation: '沿江路排水管网检查井', alarmLevel: 2, actualValue: 2.36, longitude: '110.41', latitude: '28.47' }, { id: 'c1', equipmentId: 'e3', deviceCode: 'PS-DEMO-003', deviceName: '人民路井盖监测点', equipmentLocation: '人民路北侧步行道', alarmLevel: 3, actualValue: null, longitude: null, latitude: null }, { id: 'd1', equipmentId: 'e4', deviceCode: 'PS-DEMO-004', deviceName: '建设路液位监测点', equipmentLocation: '建设路与文化路交叉口', alarmLevel: 4, actualValue: 1.26, longitude: '110.42', latitude: '28.48' } ].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] })) const apiModule = ` const samples = ${JSON.stringify(samples)}; const response = data => Promise.resolve({ data }); export const getAlarmOptions = () => response({ alarmLevels: ['一级报警','二级报警','三级报警','四级报警'].map((label,i)=>({value:i+1,label})), monitorStatuses:[{value:0,label:'异常'},{value:1,label:'已恢复'}] }); 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)))); }; export const getAlarms = params => { window.__gisHistoryRequests.push(params); const rows=samples.filter(row=>row.deviceCode===params.deviceCode); return response({records:rows,total:rows.length}); }; export const getAlarmAnalysis = id => { window.__gisAnalysisRequests.push(id); return response({ alarm:samples.find(row=>row.id===id), supported:true, samples:[], message:'测试数据:暂无区间采样' }); }; export const getAlarm = id => response(samples.find(row=>row.id===id)); export const getAlarmTimeline = () => response([]); ` 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:()=>{}}}` 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)));}}` const entry = ` 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'; import Page from '/src/views/subSystem/alarm/drainage/alarmGIS.vue'; import Pagination from '/src/components/Pagination/index.vue'; window.__gisRequests=[];window.__gisHistoryRequests=[];window.__gisAnalysisRequests=[];window.__gisMapFails=true; const app=createApp(Page);app.use(ElementPlus,{locale:zhCn});app.component('pagination',Pagination);app.mount('#app'); ` const fixture = { name: 'alarm-gis-visual-fixture', enforce: 'pre', resolveId(id) { if (id === 'virtual:gis-entry') return '\0gis-entry' if (id === 'virtual:gis-api') return '\0gis-api' if (id.endsWith('/useAlarmScope') || id.endsWith('/useAlarmScope.js')) return '\0gis-scope' if (id === 'virtual:gis-map') return '\0gis-map' }, load(id) { return ({ '\0gis-entry': entry, '\0gis-api': apiModule, '\0gis-scope': scopeModule, '\0gis-map': mapModule })[id] }, configureServer(server) { server.middlewares.use((req, res, next) => { if (req.url !== '/__alarm-gis-fixture') return next() res.setHeader('Content-Type', 'text/html; charset=utf-8') res.end('
') }) } } const server = await createServer({ configFile: false, root: process.cwd(), plugins: [fixture, vue(), AutoImport({ imports: ['vue'], dts: false })], cacheDir: path.join(output, 'vite-cache'), optimizeDeps: { entries: [], include: ['vue', 'element-plus', '@element-plus/icons-vue', 'echarts'] }, resolve: { alias: [{find:'@/api/alarm',replacement:'virtual:gis-api'},{find:'@/components/GisMapCanvas.vue',replacement:'virtual:gis-map'},{find:'@',replacement:path.resolve('src')}] }, server: { host: '127.0.0.1', port: 0 }, logLevel: 'error' }) let browser try { await server.listen() browser = await chromium.launch({ headless: true, executablePath: process.env.BROWSER_EXECUTABLE || undefined }) const page = await browser.newPage({ viewport: { width: 1366, height: 1000 }, deviceScaleFactor: 1 }) const pageErrors = [] page.on('pageerror', error => pageErrors.push(error.message)) await page.route('**/*', route => route.request().url().startsWith('http://127.0.0.1:') ? route.continue() : route.abort()) await page.goto('http://127.0.0.1:' + server.httpServer.address().port + '/__alarm-gis-fixture') await page.getByRole('heading', {name:'当前报警 GIS 一张图'}).waitFor() await page.locator('.gis-device-item').nth(3).waitFor() assert.equal(await page.locator('.gis-level-metric').count(),4) assert.deepEqual(await page.locator('.gis-level-metric > strong').allTextContents(),['1台','1台','1台','1台']) assert.equal(await page.getByRole('button',{name:'放大地图'}).isDisabled(),true) await page.screenshot({path:path.join(output,'desktop.png'),fullPage:true}) const mapBefore = await page.locator('.gis-map-stage').boundingBox() await page.getByLabel('报警设备列表').getByRole('button',{name:'城北路液位监测点',exact:true}).click() await page.locator('.alarm-gis-drawer').waitFor() await page.getByRole('tab',{name:'监测曲线',exact:true}).waitFor() await page.waitForFunction(()=>window.__gisAnalysisRequests.includes('a2')) assert.ok(!(await page.evaluate(()=>window.__gisAnalysisRequests)).includes('a1')) assert.equal((await page.locator('.gis-map-stage').boundingBox()).width,mapBefore.width) await page.getByRole('tab',{name:'报警记录',exact:true}).click() assert.equal(await page.locator('.gis-history-item').count(),2) assert.equal(await page.locator('.gis-history-actions').getByRole('button',{name:'查看曲线'}).count(),1) assert.equal(await page.locator('.pagination-container').isVisible(),true) await page.screenshot({path:path.join(output,'drawer-history.png')}) await page.getByRole('tab',{name:'设备信息',exact:true}).click() assert.equal(await page.locator('.gis-detail-grid').last().getByText('110.4',{exact:true}).count(),1) await page.keyboard.press('Escape') await page.locator('.alarm-gis-drawer').waitFor({state:'hidden'}) await page.getByRole('button',{name:'人民路井盖监测点',exact:true}).click() assert.equal(await page.getByRole('tab',{name:'报警记录',exact:true}).getAttribute('aria-selected'),'true') await page.getByRole('tab',{name:'监测曲线',exact:true}).click() await page.getByText('暂无有实际值的报警指标',{exact:true}).waitFor() await page.keyboard.press('Escape') await page.locator('.alarm-gis-drawer').waitFor({state:'hidden'}) await page.evaluate(()=>window.__gisMapFails=false) await page.getByRole('button',{name:'重试底图',exact:true}).click() await page.getByText('地图已就绪',{exact:true}).waitFor() assert.equal(await page.getByRole('button',{name:'放大地图'}).isDisabled(),false) assert.equal(await page.getByTestId('map-marker').count(),3) await page.locator('.gis-device-item').first().getByRole('button',{name:'定位',exact:true}).click() assert.equal(await page.evaluate(()=>window.__gisLocated),'PS-DEMO-001') await page.getByPlaceholder('搜索设备编码、名称或安装位置').fill('无匹配设备') await page.getByRole('button',{name:'查询',exact:true}).click() await page.getByText('暂无匹配的报警设备',{exact:true}).waitFor() await page.getByRole('button',{name:'重置',exact:true}).click() await page.locator('.gis-device-item').nth(3).waitFor() await page.setViewportSize({width:390,height:844}) assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth <= window.innerWidth),true) await page.screenshot({path:path.join(output,'mobile.png'),fullPage:true}) await page.getByLabel('报警设备列表').getByRole('button',{name:'城北路液位监测点',exact:true}).click() await page.getByRole('tab',{name:'报警记录',exact:true}).click() await page.screenshot({path:path.join(output,'mobile-drawer.png')}) assert.equal(await page.locator('.alarm-gis-drawer').evaluate(el=>el.scrollWidth <= el.clientWidth),true) assert.deepEqual(pageErrors,[]) console.log('GIS visual/interaction checks passed. Screenshots: ' + output) } finally { await browser?.close() await server.close() }