drainageVideo.live.cjs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. // 使用用户正常登录的独立测试浏览器;不读取/输出Cookie、Token或密码。
  2. const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'C:/Users/Administrator/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
  3. const assert = require('node:assert/strict')
  4. async function main() {
  5. const browser = await chromium.connectOverCDP('http://127.0.0.1:9225')
  6. try {
  7. const pages = browser.contexts()[0].pages()
  8. const page = pages.find(p => /^http:\/\/(127\.0\.0\.1|localhost)(\/|:)/.test(p.url()))
  9. if (!page) throw new Error('测试浏览器中没有本机前端页面')
  10. console.log('当前页面:', new URL(page.url()).pathname)
  11. if (process.argv[2] === 'inspect') {
  12. console.log((await page.locator('body').innerText()).slice(0,1800))
  13. return
  14. }
  15. if (process.argv[2] === 'final') {
  16. const result=await page.evaluate(async()=>{
  17. const api=await import('/src/api/drainageVideo.js')
  18. const rows=(await api.getDrainageVideoMap()).data
  19. const tree=(await api.getDrainageVideoTree()).data
  20. return {videos:rows.length,remainingFixtures:rows.filter(r=>r.videoId.startsWith('TEST_DV_')).length,hasFavoritesNode:tree[0]?.id==='favorites'}
  21. })
  22. assert.equal(result.videos,2);assert.equal(result.remainingFixtures,0);assert.equal(result.hasFavoritesNode,true)
  23. const anonymous=await page.request.get('http://127.0.0.1:8301/api/drainage/video/resources')
  24. assert.equal((await anonymous.json()).code,401)
  25. await page.goto('http://127.0.0.1/subSystem/drainage/subSystem/drainaged/spjc/spfzgl')
  26. await page.waitForFunction(()=>document.querySelectorAll('.el-table__row').length===2,{},{timeout:15000})
  27. console.log('最终8301认证接口和两条真实视频可用,匿名请求拒绝,TEST_DV_资源已全部清理;窗口已刷新到视频分组。')
  28. return
  29. }
  30. if (process.argv[2] === 'routes') {
  31. if(new URL(page.url()).pathname==='/portal')await page.getByText('进入后台',{exact:true}).click()
  32. await page.waitForTimeout(1500)
  33. console.log(await page.evaluate(async()=>{
  34. const request=(await import('/src/utils/request.js')).default
  35. const data=(await request({url:'/getRouters'})).data
  36. const found=[]
  37. 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)}}
  38. walk(data);return found
  39. }))
  40. return
  41. }
  42. if (process.argv[2] === 'gis-fixture') {
  43. const {execFileSync}=require('node:child_process')
  44. const sql=statement=>{
  45. 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()}
  46. catch{throw Error('GIS测试数据SQL执行失败;未输出媒体地址或凭据,请检查独立测试记录')}
  47. }
  48. const fixture='TEST_DV_VIDEO_1'
  49. const fixtureSessions=[]
  50. const track=async response=>{
  51. if(response.url().includes(`/api/video/devices/${fixture}/live`)){
  52. try{const data=await response.json();if(data.data?.sessionId)fixtureSessions.push(data.data.sessionId)}catch{}
  53. }
  54. }
  55. page.on('response',track)
  56. const old=JSON.parse(sql(`SELECT row_to_json(v) FROM app_user.equipment_video_binding v WHERE equipment_id='${fixture}';`))
  57. const quoted=value=>value==null?'NULL':`'${String(value).replace(/'/g,"''")}'`
  58. const live=await page.evaluate(async()=>{
  59. const api=await import('/src/api/drainageVideo.js'),video=await import('/src/api/video/index.js')
  60. const row=(await api.getDrainageVideoMap()).data.find(r=>!r.videoId.startsWith('TEST_DV_'))
  61. const data=(await video.startLive(row.videoId)).data
  62. return {sessionId:data.sessionId,url:data.ws_flv||data.flv||data.url}
  63. })
  64. try {
  65. assert.ok(live.url)
  66. // 本轮临时图上测试资源引用已获授权的实时媒体,仅写测试记录,不改真实设备位置或接入信息。
  67. 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}';`)
  68. await page.goto('http://127.0.0.1/subSystem/drainage/subSystem/drainaged/spjc/jcspGIS')
  69. await page.waitForFunction(()=>document.querySelector('.gis-map-canvas canvas'),{},{timeout:30000})
  70. await page.getByText('国标视频',{exact:true}).click()
  71. await page.waitForTimeout(500)
  72. assert.equal(await page.locator('.resource').count(),1)
  73. await page.getByText('地址视频',{exact:true}).click()
  74. assert.equal(await page.locator('.resource').count(),0)
  75. await page.getByText('地址视频',{exact:true}).click()
  76. await page.locator('.resource').getByRole('button',{name:'TEST_DV_排水视频1',exact:true}).click()
  77. await page.waitForTimeout(500)
  78. await page.locator('.gis-marker-label-text').filter({hasText:'TEST_DV_排水视频1'}).first().click({force:true})
  79. await page.waitForFunction(()=>document.querySelector('.live-tile[data-playing="true"]'),{},{timeout:45000})
  80. const fs=require('node:fs'),path=require('node:path');const report=path.resolve('tests/e2e/reports/drainage-video');fs.mkdirSync(report,{recursive:true})
  81. await page.screenshot({path:path.join(report,'gis-controlled-live.png')})
  82. await Promise.all([page.waitForResponse(r=>/\/api\/video\/sessions\/[^/]+\/stop/.test(r.url())),page.locator('.el-dialog__headerbtn:visible').click()])
  83. await page.goto('http://127.0.0.1/subSystem/drainage/subSystem/drainaged/spjc/spfzgl')
  84. await page.waitForTimeout(1000)
  85. assert.equal(Number(sql(`SELECT count(*) FROM app_user.video_play_session WHERE video_id='${fixture}' AND status='ACTIVE';`)),0)
  86. console.log('GIS两类图层切换、地图标记点击、真实媒体解码及关闭会话通过(独立测试位置,不代表真实摄像机位置)。')
  87. } finally {
  88. await page.goto('http://127.0.0.1/subSystem/drainage/subSystem/drainaged/spjc/spfzgl')
  89. for(const session of fixtureSessions)await page.evaluate(async session=>{const video=await import('/src/api/video/index.js');await video.stopVideo(session)},session)
  90. await page.evaluate(async session=>{const video=await import('/src/api/video/index.js');await video.stopVideo(session)},live.sessionId)
  91. 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}';`)
  92. }
  93. page.off('response',track)
  94. return
  95. }
  96. if (process.argv[2] === 'ui') {
  97. const failures=[]
  98. page.on('pageerror',e=>failures.push(e.message))
  99. const root='/subSystem/drainage/subSystem/drainaged/'
  100. const fs=require('node:fs'),path=require('node:path')
  101. const report=path.resolve('tests/e2e/reports/drainage-video')
  102. fs.mkdirSync(report,{recursive:true})
  103. 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())
  104. await api(async api=>{
  105. for(const row of (await api.getDrainageVideoMap()).data.filter(r=>r.videoId.startsWith('TEST_DV_'))){
  106. if(row.pointId)await api.associateDrainageVideo(row.videoId,null)
  107. if(row.favorite)await api.favoriteDrainageVideo(row.videoId,false)
  108. }
  109. })
  110. // 点位关联从页面操作,API仅用于断言。
  111. await page.goto('http://127.0.0.1'+root+'spcj/spgngl')
  112. await page.getByPlaceholder('设备名称/编码').fill('TEST_DV_CODE_1')
  113. await page.getByRole('button',{name:'查询',exact:true}).click()
  114. const row=page.locator('.el-table__row').filter({has:page.getByText('TEST_DV_CODE_1',{exact:true})})
  115. await row.getByRole('button',{name:'关联点位',exact:true}).click()
  116. await page.locator('.el-dialog:visible .el-select__wrapper').click()
  117. await page.getByRole('option').filter({hasText:'TEST_DV_易积水点A'}).click()
  118. 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()])
  119. assert.equal((await saved.json()).code,200)
  120. await page.waitForTimeout(600)
  121. assert.equal(await api(async api=>(await api.getDrainageVideoMap()).data.find(r=>r.videoId==='TEST_DV_VIDEO_1').pointId),'TEST_DV_POINT_A')
  122. await page.screenshot({path:path.join(report,'association.png')})
  123. await row.getByRole('button',{name:'解除关联',exact:true}).click()
  124. await page.getByRole('button',{name:'确定',exact:true}).click()
  125. await page.waitForTimeout(600)
  126. assert.equal(await api(async api=>(await api.getDrainageVideoMap()).data.find(r=>r.videoId==='TEST_DV_VIDEO_1').pointId),null)
  127. // 分组收藏从页面操作,再切换我的收藏、刷新确认持久化。
  128. await page.goto('http://127.0.0.1'+root+'spjc/spfzgl')
  129. await page.getByPlaceholder('名称/编码').fill('TEST_DV_CODE_1')
  130. await page.getByRole('button',{name:'查询',exact:true}).click()
  131. const groupRow=page.locator('.el-table__row').filter({has:page.getByText('TEST_DV_CODE_1',{exact:true})})
  132. await groupRow.getByRole('button',{name:'收藏',exact:true}).click()
  133. await page.getByText(/我的收藏(1)/).first().click()
  134. await page.waitForTimeout(500)
  135. assert.equal(await page.locator('.el-table__row').count(),1)
  136. await page.getByRole('button',{name:'刷新',exact:true}).click()
  137. await page.waitForTimeout(500)
  138. await page.screenshot({path:path.join(report,'groups.png')})
  139. await page.getByRole('button',{name:'取消收藏',exact:true}).click()
  140. await page.waitForFunction(()=>document.querySelectorAll('.el-table__row').length===0,{},{timeout:15000})
  141. assert.equal(await page.locator('.el-table__row').count(),0)
  142. // 宫格布局及翻页用12个独立未注册测试设备,不伪造画面。
  143. await page.goto('http://127.0.0.1'+root+'spjc/spplgl')
  144. await page.getByRole('button',{name:'选择全部',exact:true}).click()
  145. await page.waitForTimeout(500)
  146. assert.equal(await page.locator('.cell').count(),4)
  147. await page.getByRole('button',{name:'下一页',exact:true}).click()
  148. await page.waitForTimeout(500)
  149. assert.match(await page.locator('.pager').innerText(),/当前 2\//)
  150. await page.getByText('9宫格',{exact:true}).click()
  151. assert.equal(await page.locator('.cell').count(),9)
  152. await page.screenshot({path:path.join(report,'batch-nine.png')})
  153. await page.getByText('1宫格',{exact:true}).click()
  154. assert.equal(await page.locator('.cell').count(),1)
  155. await page.getByRole('button',{name:'清空',exact:true}).click()
  156. // 两台真实视频同时解码成功后截图。
  157. const realRows=await api(async api=>(await api.getDrainageVideoMap()).data.filter(r=>!r.videoId.startsWith('TEST_DV_')))
  158. await page.getByText('4宫格',{exact:true}).click()
  159. for(const video of realRows.slice(0,2)){
  160. await page.locator('.batch-page .el-select__wrapper').click()
  161. await page.getByRole('option').filter({hasText:`${video.deviceName}(${video.districtName})`}).click()
  162. await page.keyboard.press('Escape')
  163. }
  164. await page.waitForFunction(()=>document.querySelectorAll('.live-tile[data-playing="true"]').length>=2,{},{timeout:45000})
  165. await page.screenshot({path:path.join(report,'batch-real-live.png')})
  166. // GIS真实地图加载、分类切换、点击视频资源播放。
  167. await page.goto('http://127.0.0.1'+root+'spjc/jcspGIS')
  168. await page.waitForFunction(()=>document.querySelector('.gis-map-canvas canvas'),{},{timeout:30000})
  169. await page.getByText('国标视频',{exact:true}).click()
  170. await page.waitForTimeout(300)
  171. assert.equal(await page.locator('.resource').count(),0)
  172. await page.getByText('国标视频',{exact:true}).click()
  173. await page.waitForTimeout(300)
  174. const marker=page.locator('img[src^="data:image/svg+xml"]').first()
  175. const hasMarker=await marker.count()
  176. if(hasMarker)await marker.click({force:true})
  177. else await page.locator('.resource').first().getByRole('button',{name:'查看视频',exact:true}).click()
  178. // 若图标选中的是离线测试设备,关闭再选真实视频;仍验证图标打开了弹窗。
  179. await page.waitForTimeout(500)
  180. const dialogs=page.locator('.el-dialog:visible')
  181. assert.equal(await dialogs.count(),1)
  182. await page.locator('.el-dialog__headerbtn:visible').click()
  183. const realLocated=realRows.find(r=>r.longitude!=null&&r.latitude!=null&&!(Number(r.longitude)===0&&Number(r.latitude)===0))
  184. if(realLocated){
  185. await page.locator('.resource').filter({hasText:realLocated.deviceName}).getByRole('button',{name:'查看视频',exact:true}).click()
  186. await page.waitForFunction(()=>document.querySelector('.live-tile[data-playing="true"]'),{},{timeout:45000})
  187. await page.screenshot({path:path.join(report,'gis-real-live.png')})
  188. await page.locator('.el-dialog__headerbtn:visible').click()
  189. }else console.log('两台真实摄像机没有有效坐标,真实位置上图验收待补齐;独立GIS媒体测试另行执行。')
  190. await page.goto('http://127.0.0.1'+root+'spjc/spfzgl')
  191. assert.deepEqual(failures,[])
  192. console.log('页面关联/解除、个人收藏持久化、1/4/9宫格分页、两路真实解码、GIS图层和点击播放通过;截图已保存。')
  193. return
  194. }
  195. const result = await page.evaluate(async () => {
  196. const api = await import('/src/api/drainageVideo.js')
  197. const video = await import('/src/api/video/index.js')
  198. const request = (await import('/src/utils/request.js')).default
  199. const rows = (await api.getDrainageVideoMap()).data
  200. const testRows = rows.filter(r => r.videoId.startsWith('TEST_DV_'))
  201. const checks = { fixtureVideos: testRows.length, leakedSourceFields: rows.some(r => 'videoUrl' in r || 'maintainerPhone' in r), api: [] }
  202. const point = 'TEST_DV_POINT_A', other = 'TEST_DV_POINT_B', id = 'TEST_DV_VIDEO_1'
  203. await api.associateDrainageVideo(id, point)
  204. await api.associateDrainageVideo('TEST_DV_VIDEO_4', point)
  205. checks.api.push('一个点位关联多个视频')
  206. const linked = await api.listDrainageVideos({ pageNum:1,pageSize:100,query:'TEST_DV_',unbound:false })
  207. checks.linked = linked.rows.length
  208. const unbound = await api.listDrainageVideos({ pageNum:1,pageSize:100,query:'TEST_DV_',unbound:true })
  209. checks.unbound = unbound.rows.length
  210. const first = await api.listDrainageVideos({ pageNum:1,pageSize:5,query:'TEST_DV_' })
  211. const second = await api.listDrainageVideos({ pageNum:2,pageSize:5,query:'TEST_DV_' })
  212. checks.pages = [first.rows.length,second.rows.length,first.total]
  213. const tree=(await api.getDrainageVideoTree()).data
  214. 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')))
  215. try { await api.associateDrainageVideo(id, other);checks.conflictRejected=false } catch { checks.conflictRejected=true }
  216. try { await api.associateDrainageVideo('TEST_DV_SENSOR', point);checks.nonVideoRejected=false } catch { checks.nonVideoRejected=true }
  217. await api.favoriteDrainageVideo(id,true)
  218. await request({ url:`/api/drainage/video/devices/${id}/favorite`,method:'put',data:{enabled:true},headers:{repeatSubmit:false} })
  219. checks.favoriteIdempotent=(await api.listDrainageVideos({query:'TEST_DV_',favoriteOnly:true})).rows.filter(row=>row.videoId===id).length===1
  220. await api.favoriteDrainageVideo(id,false)
  221. checks.favoriteRemoved=!(await api.listDrainageVideos({query:'TEST_DV_',favoriteOnly:true})).rows.some(row=>row.videoId===id)
  222. await api.associateDrainageVideo(id,null)
  223. const after=(await api.getDrainageVideoMap()).data
  224. checks.unboundCleared=after.find(row=>row.videoId===id)?.pointId==null
  225. // 只对当前真实视频创建本用户自己的会话,并在finally中停止;不操作其他用户会话。
  226. checks.realStreams=[]
  227. const sessions=[]
  228. try {
  229. for(const row of rows.filter(r=>!r.videoId.startsWith('TEST_DV_')).slice(0,2)) {
  230. try {
  231. const live=(await video.startLive(row.videoId)).data
  232. sessions.push(live.sessionId)
  233. await video.heartbeatVideo(live.sessionId)
  234. checks.realStreams.push({validSession:Boolean(live.sessionId),playableAddress:Boolean(live.url||live.ws_flv||live.flv||live.wss_flv||live.https_flv)})
  235. } catch(e) { checks.realStreams.push({error:e.message || '播放失败'}) }
  236. }
  237. } finally { for(const session of sessions)await video.stopVideo(session) }
  238. await api.associateDrainageVideo('TEST_DV_VIDEO_4',null)
  239. return checks
  240. })
  241. assert.equal(result.fixtureVideos,12)
  242. assert.equal(result.leakedSourceFields,false)
  243. assert.equal(result.linked,2)
  244. assert.equal(result.unbound,10)
  245. assert.deepEqual(result.pages,[5,5,12])
  246. for(const key of ['treeMatches','conflictRejected','nonVideoRejected','favoriteIdempotent','favoriteRemoved','unboundCleared'])assert.equal(result[key],true,key)
  247. console.log(JSON.stringify(result,null,2))
  248. } finally { await browser.close() }
  249. }
  250. main().catch(e=>{console.error(e.stack || e.message);process.exitCode=1})