| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- // 使用用户正常登录的独立测试浏览器;不读取/输出Cookie、Token或密码。
- const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'C:/Users/Administrator/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
- const assert = require('node:assert/strict')
- async function main() {
- const browser = await chromium.connectOverCDP('http://127.0.0.1:9225')
- try {
- const pages = browser.contexts()[0].pages()
- const page = pages.find(p => /^http:\/\/(127\.0\.0\.1|localhost)(\/|:)/.test(p.url()))
- if (!page) throw new Error('测试浏览器中没有本机前端页面')
- console.log('当前页面:', new URL(page.url()).pathname)
- if (process.argv[2] === 'inspect') {
- console.log((await page.locator('body').innerText()).slice(0,1800))
- return
- }
- if (process.argv[2] === 'final') {
- const result=await page.evaluate(async()=>{
- const api=await import('/src/api/drainageVideo.js')
- const rows=(await api.getDrainageVideoMap()).data
- const tree=(await api.getDrainageVideoTree()).data
- return {videos:rows.length,remainingFixtures:rows.filter(r=>r.videoId.startsWith('TEST_DV_')).length,hasFavoritesNode:tree[0]?.id==='favorites'}
- })
- assert.equal(result.videos,2);assert.equal(result.remainingFixtures,0);assert.equal(result.hasFavoritesNode,true)
- const anonymous=await page.request.get('http://127.0.0.1:8301/api/drainage/video/resources')
- assert.equal((await anonymous.json()).code,401)
- await page.goto('http://127.0.0.1/subSystem/drainage/subSystem/drainaged/spjc/spfzgl')
- await page.waitForFunction(()=>document.querySelectorAll('.el-table__row').length===2,{},{timeout:15000})
- console.log('最终8301认证接口和两条真实视频可用,匿名请求拒绝,TEST_DV_资源已全部清理;窗口已刷新到视频分组。')
- return
- }
- if (process.argv[2] === 'routes') {
- if(new URL(page.url()).pathname==='/portal')await page.getByText('进入后台',{exact:true}).click()
- await page.waitForTimeout(1500)
- console.log(await page.evaluate(async()=>{
- const request=(await import('/src/utils/request.js')).default
- const data=(await request({url:'/getRouters'})).data
- const found=[]
- function walk(nodes,parent=''){for(const r of nodes){const path=r.path.startsWith('/')?r.path:`${parent}/${r.path}`;if(/spjc/.test(r.component || ''))found.push({path,name:r.name,component:r.component});walk(r.children || [],path)}}
- walk(data);return found
- }))
- return
- }
- if (process.argv[2] === 'gis-fixture') {
- const {execFileSync}=require('node:child_process')
- const sql=statement=>{
- try{return execFileSync('psql',['-h','192.168.110.30','-p','54321','-U','app_user','-d','pipi_ner','-v','ON_ERROR_STOP=1','-q','-t','-A'],{input:statement,encoding:'utf8',env:process.env,stdio:['pipe','pipe','pipe']}).trim()}
- catch{throw Error('GIS测试数据SQL执行失败;未输出媒体地址或凭据,请检查独立测试记录')}
- }
- const fixture='TEST_DV_VIDEO_1'
- const fixtureSessions=[]
- const track=async response=>{
- if(response.url().includes(`/api/video/devices/${fixture}/live`)){
- try{const data=await response.json();if(data.data?.sessionId)fixtureSessions.push(data.data.sessionId)}catch{}
- }
- }
- page.on('response',track)
- const old=JSON.parse(sql(`SELECT row_to_json(v) FROM app_user.equipment_video_binding v WHERE equipment_id='${fixture}';`))
- const quoted=value=>value==null?'NULL':`'${String(value).replace(/'/g,"''")}'`
- const live=await page.evaluate(async()=>{
- const api=await import('/src/api/drainageVideo.js'),video=await import('/src/api/video/index.js')
- const row=(await api.getDrainageVideoMap()).data.find(r=>!r.videoId.startsWith('TEST_DV_'))
- const data=(await video.startLive(row.videoId)).data
- return {sessionId:data.sessionId,url:data.ws_flv||data.flv||data.url}
- })
- try {
- assert.ok(live.url)
- // 本轮临时图上测试资源引用已获授权的实时媒体,仅写测试记录,不改真实设备位置或接入信息。
- sql(`UPDATE app_user.equipment_video_binding SET source_type='MANUAL',gb_device_id=NULL,gb_channel_id=NULL,video_url=${quoted(live.url)} WHERE equipment_id='${fixture}';`)
- await page.goto('http://127.0.0.1/subSystem/drainage/subSystem/drainaged/spjc/jcspGIS')
- await page.waitForFunction(()=>document.querySelector('.gis-map-canvas canvas'),{},{timeout:30000})
- await page.getByText('国标视频',{exact:true}).click()
- await page.waitForTimeout(500)
- assert.equal(await page.locator('.resource').count(),1)
- await page.getByText('地址视频',{exact:true}).click()
- assert.equal(await page.locator('.resource').count(),0)
- await page.getByText('地址视频',{exact:true}).click()
- await page.locator('.resource').getByRole('button',{name:'TEST_DV_排水视频1',exact:true}).click()
- await page.waitForTimeout(500)
- await page.locator('.gis-marker-label-text').filter({hasText:'TEST_DV_排水视频1'}).first().click({force:true})
- await page.waitForFunction(()=>document.querySelector('.live-tile[data-playing="true"]'),{},{timeout:45000})
- const fs=require('node:fs'),path=require('node:path');const report=path.resolve('tests/e2e/reports/drainage-video');fs.mkdirSync(report,{recursive:true})
- await page.screenshot({path:path.join(report,'gis-controlled-live.png')})
- await Promise.all([page.waitForResponse(r=>/\/api\/video\/sessions\/[^/]+\/stop/.test(r.url())),page.locator('.el-dialog__headerbtn:visible').click()])
- await page.goto('http://127.0.0.1/subSystem/drainage/subSystem/drainaged/spjc/spfzgl')
- await page.waitForTimeout(1000)
- assert.equal(Number(sql(`SELECT count(*) FROM app_user.video_play_session WHERE video_id='${fixture}' AND status='ACTIVE';`)),0)
- console.log('GIS两类图层切换、地图标记点击、真实媒体解码及关闭会话通过(独立测试位置,不代表真实摄像机位置)。')
- } finally {
- await page.goto('http://127.0.0.1/subSystem/drainage/subSystem/drainaged/spjc/spfzgl')
- for(const session of fixtureSessions)await page.evaluate(async session=>{const video=await import('/src/api/video/index.js');await video.stopVideo(session)},session)
- await page.evaluate(async session=>{const video=await import('/src/api/video/index.js');await video.stopVideo(session)},live.sessionId)
- sql(`UPDATE app_user.equipment_video_binding SET source_type=${quoted(old.source_type)},gb_device_id=${quoted(old.gb_device_id)},gb_channel_id=${quoted(old.gb_channel_id)},video_url=${quoted(old.video_url)} WHERE equipment_id='${fixture}';`)
- }
- page.off('response',track)
- return
- }
- if (process.argv[2] === 'ui') {
- const failures=[]
- page.on('pageerror',e=>failures.push(e.message))
- const root='/subSystem/drainage/subSystem/drainaged/'
- const fs=require('node:fs'),path=require('node:path')
- const report=path.resolve('tests/e2e/reports/drainage-video')
- fs.mkdirSync(report,{recursive:true})
- const api=fn=>page.evaluate(async fnText=>{const api=await import('/src/api/drainageVideo.js');return await new Function('api',`return (${fnText})(api)`)(api)},fn.toString())
- await api(async api=>{
- for(const row of (await api.getDrainageVideoMap()).data.filter(r=>r.videoId.startsWith('TEST_DV_'))){
- if(row.pointId)await api.associateDrainageVideo(row.videoId,null)
- if(row.favorite)await api.favoriteDrainageVideo(row.videoId,false)
- }
- })
- // 点位关联从页面操作,API仅用于断言。
- await page.goto('http://127.0.0.1'+root+'spcj/spgngl')
- await page.getByPlaceholder('设备名称/编码').fill('TEST_DV_CODE_1')
- await page.getByRole('button',{name:'查询',exact:true}).click()
- const row=page.locator('.el-table__row').filter({has:page.getByText('TEST_DV_CODE_1',{exact:true})})
- await row.getByRole('button',{name:'关联点位',exact:true}).click()
- await page.locator('.el-dialog:visible .el-select__wrapper').click()
- await page.getByRole('option').filter({hasText:'TEST_DV_易积水点A'}).click()
- const [saved]=await Promise.all([page.waitForResponse(r=>r.url().includes('/devices/TEST_DV_VIDEO_1/waterlogging')&&r.request().method()==='PUT'),page.getByRole('button',{name:'保存',exact:true}).click()])
- assert.equal((await saved.json()).code,200)
- await page.waitForTimeout(600)
- assert.equal(await api(async api=>(await api.getDrainageVideoMap()).data.find(r=>r.videoId==='TEST_DV_VIDEO_1').pointId),'TEST_DV_POINT_A')
- await page.screenshot({path:path.join(report,'association.png')})
- await row.getByRole('button',{name:'解除关联',exact:true}).click()
- await page.getByRole('button',{name:'确定',exact:true}).click()
- await page.waitForTimeout(600)
- assert.equal(await api(async api=>(await api.getDrainageVideoMap()).data.find(r=>r.videoId==='TEST_DV_VIDEO_1').pointId),null)
- // 分组收藏从页面操作,再切换我的收藏、刷新确认持久化。
- await page.goto('http://127.0.0.1'+root+'spjc/spfzgl')
- await page.getByPlaceholder('名称/编码').fill('TEST_DV_CODE_1')
- await page.getByRole('button',{name:'查询',exact:true}).click()
- const groupRow=page.locator('.el-table__row').filter({has:page.getByText('TEST_DV_CODE_1',{exact:true})})
- await groupRow.getByRole('button',{name:'收藏',exact:true}).click()
- await page.getByText(/我的收藏(1)/).first().click()
- await page.waitForTimeout(500)
- assert.equal(await page.locator('.el-table__row').count(),1)
- await page.getByRole('button',{name:'刷新',exact:true}).click()
- await page.waitForTimeout(500)
- await page.screenshot({path:path.join(report,'groups.png')})
- await page.getByRole('button',{name:'取消收藏',exact:true}).click()
- await page.waitForFunction(()=>document.querySelectorAll('.el-table__row').length===0,{},{timeout:15000})
- assert.equal(await page.locator('.el-table__row').count(),0)
- // 宫格布局及翻页用12个独立未注册测试设备,不伪造画面。
- await page.goto('http://127.0.0.1'+root+'spjc/spplgl')
- await page.getByRole('button',{name:'选择全部',exact:true}).click()
- await page.waitForTimeout(500)
- assert.equal(await page.locator('.cell').count(),4)
- await page.getByRole('button',{name:'下一页',exact:true}).click()
- await page.waitForTimeout(500)
- assert.match(await page.locator('.pager').innerText(),/当前 2\//)
- await page.getByText('9宫格',{exact:true}).click()
- assert.equal(await page.locator('.cell').count(),9)
- await page.screenshot({path:path.join(report,'batch-nine.png')})
- await page.getByText('1宫格',{exact:true}).click()
- assert.equal(await page.locator('.cell').count(),1)
- await page.getByRole('button',{name:'清空',exact:true}).click()
- // 两台真实视频同时解码成功后截图。
- const realRows=await api(async api=>(await api.getDrainageVideoMap()).data.filter(r=>!r.videoId.startsWith('TEST_DV_')))
- await page.getByText('4宫格',{exact:true}).click()
- for(const video of realRows.slice(0,2)){
- await page.locator('.batch-page .el-select__wrapper').click()
- await page.getByRole('option').filter({hasText:`${video.deviceName}(${video.districtName})`}).click()
- await page.keyboard.press('Escape')
- }
- await page.waitForFunction(()=>document.querySelectorAll('.live-tile[data-playing="true"]').length>=2,{},{timeout:45000})
- await page.screenshot({path:path.join(report,'batch-real-live.png')})
- // GIS真实地图加载、分类切换、点击视频资源播放。
- await page.goto('http://127.0.0.1'+root+'spjc/jcspGIS')
- await page.waitForFunction(()=>document.querySelector('.gis-map-canvas canvas'),{},{timeout:30000})
- await page.getByText('国标视频',{exact:true}).click()
- await page.waitForTimeout(300)
- assert.equal(await page.locator('.resource').count(),0)
- await page.getByText('国标视频',{exact:true}).click()
- await page.waitForTimeout(300)
- const marker=page.locator('img[src^="data:image/svg+xml"]').first()
- const hasMarker=await marker.count()
- if(hasMarker)await marker.click({force:true})
- else await page.locator('.resource').first().getByRole('button',{name:'查看视频',exact:true}).click()
- // 若图标选中的是离线测试设备,关闭再选真实视频;仍验证图标打开了弹窗。
- await page.waitForTimeout(500)
- const dialogs=page.locator('.el-dialog:visible')
- assert.equal(await dialogs.count(),1)
- await page.locator('.el-dialog__headerbtn:visible').click()
- const realLocated=realRows.find(r=>r.longitude!=null&&r.latitude!=null&&!(Number(r.longitude)===0&&Number(r.latitude)===0))
- if(realLocated){
- await page.locator('.resource').filter({hasText:realLocated.deviceName}).getByRole('button',{name:'查看视频',exact:true}).click()
- await page.waitForFunction(()=>document.querySelector('.live-tile[data-playing="true"]'),{},{timeout:45000})
- await page.screenshot({path:path.join(report,'gis-real-live.png')})
- await page.locator('.el-dialog__headerbtn:visible').click()
- }else console.log('两台真实摄像机没有有效坐标,真实位置上图验收待补齐;独立GIS媒体测试另行执行。')
- await page.goto('http://127.0.0.1'+root+'spjc/spfzgl')
- assert.deepEqual(failures,[])
- console.log('页面关联/解除、个人收藏持久化、1/4/9宫格分页、两路真实解码、GIS图层和点击播放通过;截图已保存。')
- return
- }
- const result = await page.evaluate(async () => {
- const api = await import('/src/api/drainageVideo.js')
- const video = await import('/src/api/video/index.js')
- const request = (await import('/src/utils/request.js')).default
- const rows = (await api.getDrainageVideoMap()).data
- const testRows = rows.filter(r => r.videoId.startsWith('TEST_DV_'))
- const checks = { fixtureVideos: testRows.length, leakedSourceFields: rows.some(r => 'videoUrl' in r || 'maintainerPhone' in r), api: [] }
- const point = 'TEST_DV_POINT_A', other = 'TEST_DV_POINT_B', id = 'TEST_DV_VIDEO_1'
- await api.associateDrainageVideo(id, point)
- await api.associateDrainageVideo('TEST_DV_VIDEO_4', point)
- checks.api.push('一个点位关联多个视频')
- const linked = await api.listDrainageVideos({ pageNum:1,pageSize:100,query:'TEST_DV_',unbound:false })
- checks.linked = linked.rows.length
- const unbound = await api.listDrainageVideos({ pageNum:1,pageSize:100,query:'TEST_DV_',unbound:true })
- checks.unbound = unbound.rows.length
- const first = await api.listDrainageVideos({ pageNum:1,pageSize:5,query:'TEST_DV_' })
- const second = await api.listDrainageVideos({ pageNum:2,pageSize:5,query:'TEST_DV_' })
- checks.pages = [first.rows.length,second.rows.length,first.total]
- const tree=(await api.getDrainageVideoTree()).data
- checks.treeMatches=tree.some(root=>root.label==='城南' && root.children?.some(p=>p.pointId===point && p.videoIds.includes(id) && p.videoIds.includes('TEST_DV_VIDEO_4')))
- try { await api.associateDrainageVideo(id, other);checks.conflictRejected=false } catch { checks.conflictRejected=true }
- try { await api.associateDrainageVideo('TEST_DV_SENSOR', point);checks.nonVideoRejected=false } catch { checks.nonVideoRejected=true }
- await api.favoriteDrainageVideo(id,true)
- await request({ url:`/api/drainage/video/devices/${id}/favorite`,method:'put',data:{enabled:true},headers:{repeatSubmit:false} })
- checks.favoriteIdempotent=(await api.listDrainageVideos({query:'TEST_DV_',favoriteOnly:true})).rows.filter(row=>row.videoId===id).length===1
- await api.favoriteDrainageVideo(id,false)
- checks.favoriteRemoved=!(await api.listDrainageVideos({query:'TEST_DV_',favoriteOnly:true})).rows.some(row=>row.videoId===id)
- await api.associateDrainageVideo(id,null)
- const after=(await api.getDrainageVideoMap()).data
- checks.unboundCleared=after.find(row=>row.videoId===id)?.pointId==null
- // 只对当前真实视频创建本用户自己的会话,并在finally中停止;不操作其他用户会话。
- checks.realStreams=[]
- const sessions=[]
- try {
- for(const row of rows.filter(r=>!r.videoId.startsWith('TEST_DV_')).slice(0,2)) {
- try {
- const live=(await video.startLive(row.videoId)).data
- sessions.push(live.sessionId)
- await video.heartbeatVideo(live.sessionId)
- checks.realStreams.push({validSession:Boolean(live.sessionId),playableAddress:Boolean(live.url||live.ws_flv||live.flv||live.wss_flv||live.https_flv)})
- } catch(e) { checks.realStreams.push({error:e.message || '播放失败'}) }
- }
- } finally { for(const session of sessions)await video.stopVideo(session) }
- await api.associateDrainageVideo('TEST_DV_VIDEO_4',null)
- return checks
- })
- assert.equal(result.fixtureVideos,12)
- assert.equal(result.leakedSourceFields,false)
- assert.equal(result.linked,2)
- assert.equal(result.unbound,10)
- assert.deepEqual(result.pages,[5,5,12])
- for(const key of ['treeMatches','conflictRejected','nonVideoRejected','favoriteIdempotent','favoriteRemoved','unboundCleared'])assert.equal(result[key],true,key)
- console.log(JSON.stringify(result,null,2))
- } finally { await browser.close() }
- }
- main().catch(e=>{console.error(e.stack || e.message);process.exitCode=1})
|