live-stream.spec.ts 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285
  1. import { test, expect, type Page } from '@playwright/test'
  2. // 测试账号配置
  3. const TEST_USERNAME = process.env.TEST_USERNAME || 'admin'
  4. const TEST_PASSWORD = process.env.TEST_PASSWORD || '123456'
  5. // 登录辅助函数
  6. async function login(page: Page) {
  7. await page.goto('/login')
  8. await page.evaluate(() => {
  9. localStorage.clear()
  10. document.cookie.split(';').forEach((c) => {
  11. document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`)
  12. })
  13. })
  14. await page.reload()
  15. await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
  16. await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
  17. await page.getByRole('button', { name: '登录' }).click()
  18. await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 })
  19. }
  20. test.describe('LiveStream 管理 - 搜索功能测试', () => {
  21. /**
  22. * 按 Stream SN 搜索
  23. */
  24. test('按 Stream SN 搜索 - 验证参数传递', async ({ page }) => {
  25. await login(page)
  26. await page.goto('/live-stream-manage/list')
  27. // 等待表格加载
  28. await page.waitForSelector('tbody tr', { timeout: 10000 })
  29. // 设置 API 拦截来验证参数
  30. let requestBody: any = null
  31. await page.route('**/admin/live-stream/list', async (route) => {
  32. const request = route.request()
  33. if (request.method() === 'POST') {
  34. requestBody = request.postDataJSON()
  35. }
  36. await route.continue()
  37. })
  38. // 在 stream sn 搜索框输入
  39. await page.getByPlaceholder('stream sn').fill('stream_123')
  40. // 点击查询
  41. await page.getByRole('button', { name: '查询' }).click()
  42. await page.waitForTimeout(1000)
  43. // 验证请求参数中包含 streamSn
  44. expect(requestBody).not.toBeNull()
  45. expect(requestBody.streamSn).toBe('stream_123')
  46. })
  47. /**
  48. * 按 Name 搜索
  49. */
  50. test('按 Name 搜索 - 验证参数传递', async ({ page }) => {
  51. await login(page)
  52. await page.goto('/live-stream-manage/list')
  53. // 等待表格加载
  54. await page.waitForSelector('tbody tr', { timeout: 10000 })
  55. // 设置 API 拦截
  56. let requestBody: any = null
  57. await page.route('**/admin/live-stream/list', async (route) => {
  58. const request = route.request()
  59. if (request.method() === 'POST') {
  60. requestBody = request.postDataJSON()
  61. }
  62. await route.continue()
  63. })
  64. // 在 name 搜索框输入
  65. await page.getByPlaceholder('名称').fill('测试流')
  66. // 点击查询
  67. await page.getByRole('button', { name: '查询' }).click()
  68. await page.waitForTimeout(1000)
  69. // 验证请求参数中包含 name
  70. expect(requestBody).not.toBeNull()
  71. expect(requestBody.name).toBe('测试流')
  72. })
  73. /**
  74. * 按 LSS 搜索
  75. */
  76. test('按 LSS 搜索 - 验证参数传递', async ({ page }) => {
  77. await login(page)
  78. await page.goto('/live-stream-manage/list')
  79. // 等待表格加载
  80. await page.waitForSelector('tbody tr', { timeout: 10000 })
  81. // 设置 API 拦截
  82. let requestBody: any = null
  83. await page.route('**/admin/live-stream/list', async (route) => {
  84. const request = route.request()
  85. if (request.method() === 'POST') {
  86. requestBody = request.postDataJSON()
  87. }
  88. await route.continue()
  89. })
  90. // 点击 LSS 下拉框
  91. await page.locator('.el-select').filter({ hasText: 'LSS' }).click()
  92. await page.waitForTimeout(300)
  93. // 选择第一个 LSS 选项(如果有)
  94. const firstOption = page.locator('.el-select-dropdown__item').first()
  95. if (await firstOption.isVisible()) {
  96. const lssValue = await firstOption.textContent()
  97. await firstOption.click()
  98. // 点击查询
  99. await page.getByRole('button', { name: '查询' }).click()
  100. await page.waitForTimeout(1000)
  101. // 验证请求参数中包含 lssId
  102. expect(requestBody).not.toBeNull()
  103. expect(requestBody.lssId).toBe(lssValue?.trim())
  104. }
  105. })
  106. /**
  107. * 按设备ID搜索
  108. */
  109. test('按设备ID搜索 - 验证参数传递', async ({ page }) => {
  110. await login(page)
  111. await page.goto('/live-stream-manage/list')
  112. // 等待表格加载
  113. await page.waitForSelector('tbody tr', { timeout: 10000 })
  114. // 设置 API 拦截
  115. let requestBody: any = null
  116. await page.route('**/admin/live-stream/list', async (route) => {
  117. const request = route.request()
  118. if (request.method() === 'POST') {
  119. requestBody = request.postDataJSON()
  120. }
  121. await route.continue()
  122. })
  123. // 在设备ID搜索框输入
  124. await page.getByPlaceholder('设备ID').fill('EEE1')
  125. // 点击查询
  126. await page.getByRole('button', { name: '查询' }).click()
  127. await page.waitForTimeout(1000)
  128. // 验证请求参数中包含 cameraId
  129. expect(requestBody).not.toBeNull()
  130. expect(requestBody.cameraId).toBe('EEE1')
  131. })
  132. /**
  133. * 组合搜索 - 所有字段
  134. */
  135. test('组合搜索 - 多字段同时搜索', async ({ page }) => {
  136. await login(page)
  137. await page.goto('/live-stream-manage/list')
  138. // 等待表格加载
  139. await page.waitForSelector('tbody tr', { timeout: 10000 })
  140. // 设置 API 拦截
  141. let requestBody: any = null
  142. await page.route('**/admin/live-stream/list', async (route) => {
  143. const request = route.request()
  144. if (request.method() === 'POST') {
  145. requestBody = request.postDataJSON()
  146. }
  147. await route.continue()
  148. })
  149. // 填入所有搜索条件
  150. await page.getByPlaceholder('stream sn').fill('stream_001')
  151. await page.getByPlaceholder('名称').fill('测试')
  152. await page.getByPlaceholder('设备ID').fill('EEE1')
  153. // 点击查询
  154. await page.getByRole('button', { name: '查询' }).click()
  155. await page.waitForTimeout(1000)
  156. // 验证请求参数包含所有字段
  157. expect(requestBody).not.toBeNull()
  158. expect(requestBody.streamSn).toBe('stream_001')
  159. expect(requestBody.name).toBe('测试')
  160. expect(requestBody.cameraId).toBe('EEE1')
  161. })
  162. /**
  163. * 重置搜索条件
  164. */
  165. test('重置搜索条件 - 清空所有输入', async ({ page }) => {
  166. await login(page)
  167. await page.goto('/live-stream-manage/list')
  168. // 填入搜索条件
  169. await page.getByPlaceholder('stream sn').fill('test-sn')
  170. await page.getByPlaceholder('名称').fill('test-name')
  171. await page.getByPlaceholder('设备ID').fill('test-device')
  172. // 点击重置
  173. await page.getByRole('button', { name: '重置' }).click()
  174. await page.waitForTimeout(300)
  175. // 验证搜索条件已清空
  176. await expect(page.getByPlaceholder('stream sn')).toHaveValue('')
  177. await expect(page.getByPlaceholder('name')).toHaveValue('')
  178. await expect(page.getByPlaceholder('设备ID')).toHaveValue('')
  179. })
  180. })
  181. test.describe('LiveStream 管理 - BUG 回归测试', () => {
  182. /**
  183. * BUG 回归测试:按设备ID搜索应该只返回匹配的记录
  184. *
  185. * 问题描述:
  186. * - 前端已修复:searchForm.cameraId 参数现在会正确传递给 API
  187. * - 后端待修复:API 目前未实现 cameraId 过滤,返回所有数据
  188. *
  189. * 预期行为:
  190. * - 搜索设备ID "EEE1" 应该只返回 1 条记录
  191. * - 该记录的设备ID列应该显示 "EEE1"
  192. *
  193. * 当前状态:测试会失败,等待后端实现过滤
  194. */
  195. test('BUG: 按设备ID搜索 EEE1 应该只返回 1 条匹配记录', async ({ page }) => {
  196. await login(page)
  197. await page.goto('/live-stream-manage/list')
  198. // 等待表格加载
  199. await page.waitForSelector('tbody tr', { timeout: 10000 })
  200. await page.waitForTimeout(500)
  201. // 在设备ID搜索框输入 "EEE1"
  202. await page.getByPlaceholder('设备ID').fill('EEE1')
  203. // 点击查询按钮
  204. await page.getByRole('button', { name: '查询' }).click()
  205. // 等待搜索结果加载
  206. await page.waitForTimeout(1000)
  207. // 验证:应该只有 1 条记录
  208. const tableRows = page.locator('tbody tr')
  209. const rowCount = await tableRows.count()
  210. expect(rowCount).toBe(1)
  211. // 验证:该记录的设备ID应该是 "EEE1"
  212. const firstRowDeviceId = await tableRows.first().locator('td').nth(3).textContent()
  213. expect(firstRowDeviceId?.trim()).toBe('EEE1')
  214. // 验证:分页显示 Total 1
  215. await expect(page.locator('.el-pagination')).toContainText('Total 1')
  216. })
  217. })
  218. test.describe('LiveStream 管理 - 页面功能测试', () => {
  219. test('LiveStream 管理页面正确显示', async ({ page }) => {
  220. await login(page)
  221. await page.goto('/live-stream-manage/list')
  222. // 验证页面标题
  223. await expect(page.locator('text=LiveStream 管理')).toBeVisible()
  224. // 验证搜索表单元素
  225. await expect(page.getByPlaceholder('stream sn')).toBeVisible()
  226. await expect(page.getByPlaceholder('name')).toBeVisible()
  227. await expect(page.getByPlaceholder('设备ID')).toBeVisible()
  228. await expect(page.getByRole('button', { name: '查询' })).toBeVisible()
  229. await expect(page.getByRole('button', { name: '重置' })).toBeVisible()
  230. await expect(page.getByRole('button', { name: '新增' })).toBeVisible()
  231. // 验证表头
  232. await expect(page.locator('th:has-text("Stream SN"), th:has-text("stream sn")')).toBeVisible()
  233. await expect(page.locator('th:has-text("Name"), th:has-text("名称")')).toBeVisible()
  234. await expect(page.locator('th:has-text("LSS")')).toBeVisible()
  235. await expect(page.locator('th:has-text("Device ID"), th:has-text("设备ID")')).toBeVisible()
  236. })
  237. test('打开新增 LiveStream 抽屉', async ({ page }) => {
  238. await login(page)
  239. await page.goto('/live-stream-manage/list')
  240. // 点击新增按钮
  241. await page.getByRole('button', { name: '新增' }).click()
  242. // 验证抽屉打开
  243. const drawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' })
  244. await expect(drawer).toBeVisible({ timeout: 5000 })
  245. // 验证表单元素
  246. await expect(drawer.locator('label:has-text("名称")')).toBeVisible()
  247. await expect(drawer.locator('label:has-text("LSS 节点")')).toBeVisible()
  248. await expect(drawer.locator('label:has-text("摄像头")')).toBeVisible()
  249. // 关闭抽屉
  250. await drawer.getByRole('button', { name: '取消' }).click()
  251. await expect(drawer).not.toBeVisible({ timeout: 5000 })
  252. })
  253. test('分页功能正常', async ({ page }) => {
  254. await login(page)
  255. await page.goto('/live-stream-manage/list')
  256. // 等待表格加载
  257. await page.waitForTimeout(1000)
  258. // 验证分页组件存在
  259. const pagination = page.locator('.el-pagination')
  260. await expect(pagination).toBeVisible()
  261. // 验证 Total 显示
  262. await expect(pagination.locator('text=/Total \\d+/')).toBeVisible()
  263. })
  264. test('从侧边栏导航到 LiveStream 管理', async ({ page }) => {
  265. await login(page)
  266. // 点击侧边栏 LiveStream 管理菜单项
  267. await page.getByText('LiveStream 管理').first().click()
  268. // 验证跳转到 LiveStream 管理页面
  269. await expect(page).toHaveURL(/\/live-stream-manage\/list/)
  270. await expect(page.locator('text=LiveStream 管理')).toBeVisible()
  271. })
  272. })
  273. test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () => {
  274. /**
  275. * 测试 CodeEditor Bash 模式 - 头部显示
  276. */
  277. test('CodeEditor Bash模式 - 验证头部显示Bash Script标签和复制按钮', async ({ page }) => {
  278. await login(page)
  279. await page.goto('/live-stream-manage/list')
  280. // 等待表格加载
  281. await page.waitForSelector('tbody tr', { timeout: 10000 })
  282. // 点击命令模板列的"查看"链接
  283. const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")')
  284. await expect(viewLink).toBeVisible({ timeout: 5000 })
  285. await viewLink.click()
  286. // 等待命令模板弹窗打开
  287. const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
  288. await expect(dialog).toBeVisible({ timeout: 5000 })
  289. // 验证 CodeEditor 头部显示 Bash Script 标签
  290. const codeEditor = dialog.locator('.code-editor')
  291. await expect(codeEditor).toBeVisible()
  292. await expect(codeEditor.locator('.editor-header')).toBeVisible()
  293. await expect(codeEditor.locator('.file-type')).toContainText('Bash Script')
  294. // 验证 Copy 按钮存在
  295. await expect(codeEditor.locator('button:has-text("复制"), button:has-text("Copy")')).toBeVisible()
  296. // 验证 Bash 模式没有格式化按钮
  297. await expect(codeEditor.locator('button:has-text("格式化")')).not.toBeVisible()
  298. })
  299. /**
  300. * 测试 CodeEditor Bash 模式 - 复制功能
  301. */
  302. test('CodeEditor Bash模式 - 复制按钮功能', async ({ page }) => {
  303. await login(page)
  304. await page.goto('/live-stream-manage/list')
  305. // 等待表格加载
  306. await page.waitForSelector('tbody tr', { timeout: 10000 })
  307. // 点击命令模板列的"查看"链接
  308. const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")')
  309. await viewLink.click()
  310. // 等待命令模板弹窗打开
  311. const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
  312. await expect(dialog).toBeVisible({ timeout: 5000 })
  313. // 点击复制按钮
  314. const copyButton = dialog.locator('.code-editor button:has-text("复制"), .code-editor button:has-text("Copy")')
  315. await copyButton.click()
  316. // 验证复制成功提示
  317. await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 3000 })
  318. })
  319. /**
  320. * 测试 CodeEditor Bash 模式 - 编辑器可编辑
  321. */
  322. test('CodeEditor Bash模式 - 编辑器可编辑', async ({ page }) => {
  323. await login(page)
  324. await page.goto('/live-stream-manage/list')
  325. // 等待表格加载
  326. await page.waitForSelector('tbody tr', { timeout: 10000 })
  327. // 点击命令模板列的"查看"链接
  328. const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")')
  329. await viewLink.click()
  330. // 等待命令模板弹窗打开
  331. const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
  332. await expect(dialog).toBeVisible({ timeout: 5000 })
  333. // 验证编辑器存在并可以编辑
  334. const codeEditor = dialog.locator('.code-editor')
  335. const editorContent = codeEditor.locator('.cm-content')
  336. await expect(editorContent).toBeVisible()
  337. // 尝试在编辑器中输入内容
  338. await editorContent.click()
  339. await page.keyboard.press('End')
  340. await page.keyboard.type('\n# Test comment')
  341. // 验证内容已添加
  342. await expect(editorContent).toContainText('# Test comment')
  343. })
  344. /**
  345. * 测试 CodeEditor Bash 模式 - 更新按钮存在 (Bug #4628: "关闭" changed to "取消", Bug #4629: dialog changed to drawer)
  346. */
  347. test('CodeEditor Bash模式 - 抽屉包含取消和更新按钮', async ({ page }) => {
  348. await login(page)
  349. await page.goto('/live-stream-manage/list')
  350. // 等待表格加载
  351. await page.waitForSelector('tbody tr', { timeout: 10000 })
  352. // 点击命令模板列的"查看"链接
  353. const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")')
  354. await viewLink.click()
  355. // 等待命令模板弹窗打开
  356. const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
  357. await expect(dialog).toBeVisible({ timeout: 5000 })
  358. // 验证关闭和更新按钮存在
  359. await expect(dialog.locator('button:has-text("取消"), button:has-text("Cancel")')).toBeVisible()
  360. await expect(dialog.locator('button:has-text("更新"), button:has-text("Update")')).toBeVisible()
  361. // 点击关闭按钮
  362. await dialog.locator('button:has-text("取消"), button:has-text("Cancel")').click()
  363. await expect(dialog).not.toBeVisible({ timeout: 5000 })
  364. })
  365. /**
  366. * 测试 CodeEditor Bash 模式 - 图标颜色正确(绿色)
  367. */
  368. test('CodeEditor Bash模式 - 图标显示为绿色', async ({ page }) => {
  369. await login(page)
  370. await page.goto('/live-stream-manage/list')
  371. // 等待表格加载
  372. await page.waitForSelector('tbody tr', { timeout: 10000 })
  373. // 点击命令模板列的"查看"链接
  374. const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")')
  375. await viewLink.click()
  376. // 等待命令模板弹窗打开
  377. const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
  378. await expect(dialog).toBeVisible({ timeout: 5000 })
  379. // 验证图标有 icon-bash 类(对应绿色)
  380. const codeEditor = dialog.locator('.code-editor')
  381. await expect(codeEditor.locator('.icon-bash')).toBeVisible()
  382. })
  383. /**
  384. * 测试 CodeEditor Bash 模式 - 更新命令模板并验证保存成功
  385. */
  386. test('CodeEditor Bash模式 - 更新命令模板并验证保存成功', async ({ page }) => {
  387. await login(page)
  388. await page.goto('/live-stream-manage/list')
  389. // 等待表格加载
  390. await page.waitForSelector('tbody tr', { timeout: 10000 })
  391. // 点击命令模板列的"查看"链接
  392. const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")')
  393. await viewLink.click()
  394. // 等待命令模板弹窗打开
  395. const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
  396. await expect(dialog).toBeVisible({ timeout: 5000 })
  397. // 获取编辑器
  398. const codeEditor = dialog.locator('.code-editor')
  399. const editorContent = codeEditor.locator('.cm-content')
  400. // 生成唯一标识用于验证更新
  401. const timestamp = Date.now()
  402. const testComment = `# Test update at ${timestamp}`
  403. // 在编辑器末尾添加测试注释
  404. await editorContent.click()
  405. await page.keyboard.press('Meta+End')
  406. await page.keyboard.type(`\n${testComment}`)
  407. // 点击更新按钮
  408. const updateButton = dialog.locator('button:has-text("更新"), button:has-text("Update")')
  409. await updateButton.click()
  410. // 等待更新成功提示
  411. await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 })
  412. // 等待弹窗关闭
  413. await expect(dialog).not.toBeVisible({ timeout: 5000 })
  414. // 重新打开命令模板弹窗验证内容已保存
  415. await page.waitForTimeout(500)
  416. await viewLink.click()
  417. // 等待弹窗重新打开
  418. const dialogReopened = page.locator('.el-drawer').filter({ hasText: '命令模板' })
  419. await expect(dialogReopened).toBeVisible({ timeout: 5000 })
  420. // 验证内容包含我们添加的测试注释
  421. const editorContentReopened = dialogReopened.locator('.cm-content')
  422. await expect(editorContentReopened).toContainText(timestamp.toString())
  423. })
  424. /**
  425. * 测试 CodeEditor Bash 模式 - 修改全部内容并验证保存
  426. */
  427. test('CodeEditor Bash模式 - 替换全部命令模板并验证保存', async ({ page }) => {
  428. await login(page)
  429. await page.goto('/live-stream-manage/list')
  430. // 等待表格加载
  431. await page.waitForSelector('tbody tr', { timeout: 10000 })
  432. // 点击命令模板列的"查看"链接
  433. const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")')
  434. await viewLink.click()
  435. // 等待命令模板弹窗打开
  436. const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' })
  437. await expect(dialog).toBeVisible({ timeout: 5000 })
  438. // 获取编辑器
  439. const codeEditor = dialog.locator('.code-editor')
  440. const editorContent = codeEditor.locator('.cm-content')
  441. // 保存原始内容以便恢复
  442. const originalContent = await editorContent.textContent()
  443. // 生成唯一标识用于验证更新
  444. const timestamp = Date.now()
  445. const newScript = `#!/bin/bash
  446. # Updated script at ${timestamp}
  447. echo "Test script"
  448. ffmpeg -i {RTSP_URL} -c copy output.mp4`
  449. // 全选并替换内容
  450. await editorContent.click()
  451. await page.keyboard.press('Meta+a')
  452. await page.keyboard.type(newScript)
  453. // 点击更新按钮
  454. const updateButton = dialog.locator('button:has-text("更新"), button:has-text("Update")')
  455. await updateButton.click()
  456. // 等待更新成功提示
  457. await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 })
  458. // 等待弹窗关闭
  459. await expect(dialog).not.toBeVisible({ timeout: 5000 })
  460. // 重新打开验证内容
  461. await page.waitForTimeout(500)
  462. await viewLink.click()
  463. const dialogReopened = page.locator('.el-drawer').filter({ hasText: '命令模板' })
  464. await expect(dialogReopened).toBeVisible({ timeout: 5000 })
  465. const editorContentReopened = dialogReopened.locator('.cm-content')
  466. await expect(editorContentReopened).toContainText(`Updated script at ${timestamp}`)
  467. await expect(editorContentReopened).toContainText('echo "Test script"')
  468. // 恢复原始内容
  469. if (originalContent) {
  470. await editorContentReopened.click()
  471. await page.keyboard.press('Meta+a')
  472. await page.keyboard.type(originalContent)
  473. await dialogReopened.locator('button:has-text("更新"), button:has-text("Update")').click()
  474. await page.waitForTimeout(1000)
  475. }
  476. })
  477. })
  478. test.describe('LiveStream 管理 - 从 LSS 页面创建流程', () => {
  479. /**
  480. * 测试带 action=create 参数时自动打开新增抽屉
  481. */
  482. test('带 action=create 参数访问时自动打开新增抽屉', async ({ page }) => {
  483. await login(page)
  484. // 直接导航到带有 action=create 参数的页面
  485. await page.goto('/live-stream?action=create')
  486. // 等待页面加载
  487. await page.waitForTimeout(1000)
  488. // 验证新增抽屉已打开
  489. const drawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' })
  490. await expect(drawer).toBeVisible({ timeout: 5000 })
  491. // 验证抽屉标题
  492. await expect(drawer.locator('.drawer-header')).toContainText('新增 Live Stream')
  493. // 验证表单字段存在
  494. await expect(drawer.locator('label:has-text("名称")')).toBeVisible()
  495. await expect(drawer.locator('label:has-text("LSS 节点")')).toBeVisible()
  496. await expect(drawer.locator('label:has-text("摄像头")')).toBeVisible()
  497. })
  498. /**
  499. * 测试带 cameraId 和 action=create 参数时自动填充摄像头信息
  500. */
  501. test('带 cameraId 和 action=create 参数访问时尝试填充摄像头信息', async ({ page }) => {
  502. await login(page)
  503. // 模拟 API 响应,返回带有 lssId 的摄像头数据
  504. await page.route('**/admin/camera/list*', async (route) => {
  505. const request = route.request()
  506. const url = request.url()
  507. // 检查是否是查询特定 cameraId 的请求
  508. if (url.includes('cameraId=TEST_CAM_001')) {
  509. await route.fulfill({
  510. status: 200,
  511. contentType: 'application/json',
  512. body: JSON.stringify({
  513. success: true,
  514. errCode: 0,
  515. data: {
  516. list: [
  517. {
  518. id: 1,
  519. cameraId: 'TEST_CAM_001',
  520. cameraName: '测试摄像头',
  521. lssId: 'LSS_001',
  522. status: 'active'
  523. }
  524. ],
  525. total: 1
  526. }
  527. })
  528. })
  529. } else {
  530. await route.continue()
  531. }
  532. })
  533. // 导航到带有 cameraId 和 action=create 参数的页面
  534. await page.goto('/live-stream?cameraId=TEST_CAM_001&action=create')
  535. // 等待页面和抽屉加载
  536. await page.waitForTimeout(1500)
  537. // 验证新增抽屉已打开
  538. const drawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' })
  539. await expect(drawer).toBeVisible({ timeout: 5000 })
  540. // 等待表单自动填充
  541. await page.waitForTimeout(1000)
  542. // 验证抽屉中的表单元素可见(LSS 节点选择器)
  543. // 由于是 mock 数据,这里主要验证流程不会出错,抽屉能正常打开
  544. await expect(drawer.locator('label:has-text("LSS")')).toBeVisible()
  545. })
  546. /**
  547. * 测试从 LSS 页面点击未创建 Stream 的摄像头时显示提示对话框
  548. * 注:这个测试需要在 LSS 页面进行,但这里测试的是对话框确认后的跳转结果
  549. */
  550. test('取消新增抽屉后返回列表页面', async ({ page }) => {
  551. await login(page)
  552. // 导航到创建页面
  553. await page.goto('/live-stream?action=create')
  554. // 等待抽屉打开
  555. const drawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' })
  556. await expect(drawer).toBeVisible({ timeout: 5000 })
  557. // 点击取消按钮
  558. await drawer.locator('button:has-text("取消"), button:has-text("Cancel")').click()
  559. // 验证抽屉关闭
  560. await expect(drawer).not.toBeVisible({ timeout: 3000 })
  561. // 验证仍在 live-stream 页面
  562. await expect(page).toHaveURL(/\/live-stream-manage\/list/)
  563. })
  564. /**
  565. * 测试 URL 参数中的 cameraId 同时作为搜索条件
  566. */
  567. test('cameraId 参数同时作为搜索条件', async ({ page }) => {
  568. await login(page)
  569. // 设置 API 拦截来验证参数
  570. let listRequestBody: any = null
  571. await page.route('**/admin/live-stream/list', async (route) => {
  572. const request = route.request()
  573. if (request.method() === 'POST') {
  574. listRequestBody = request.postDataJSON()
  575. }
  576. await route.continue()
  577. })
  578. // 导航到带有 cameraId 参数的页面(不带 action=create)
  579. await page.goto('/live-stream?cameraId=CAM_SEARCH_TEST')
  580. // 等待列表请求完成
  581. await page.waitForTimeout(1500)
  582. // 验证搜索框中已填入 cameraId
  583. const cameraIdInput = page.getByPlaceholder('设备ID')
  584. await expect(cameraIdInput).toHaveValue('CAM_SEARCH_TEST')
  585. // 验证 API 请求中包含 cameraId
  586. expect(listRequestBody).not.toBeNull()
  587. expect(listRequestBody.cameraId).toBe('CAM_SEARCH_TEST')
  588. })
  589. })
  590. test.describe('LiveStream 管理 - Bug修复验证测试', () => {
  591. // 登录辅助函数
  592. async function loginHelper(page: Page) {
  593. await page.goto('/login')
  594. await page.evaluate(() => {
  595. localStorage.clear()
  596. document.cookie.split(';').forEach((c) => {
  597. document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`)
  598. })
  599. })
  600. await page.reload()
  601. await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
  602. await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
  603. await page.getByRole('button', { name: '登录' }).click()
  604. await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 })
  605. }
  606. /**
  607. * Bug #4627: 启动时间和关闭时间显示用户当前时区
  608. */
  609. test('Bug #4627 - 时间列正确格式化显示', async ({ page }) => {
  610. await loginHelper(page)
  611. await page.goto('/live-stream-manage/list')
  612. // 等待表格加载
  613. await page.waitForSelector('tbody tr', { timeout: 10000 })
  614. // 验证启动时间列存在
  615. await expect(page.locator('th:has-text("启动时间")')).toBeVisible()
  616. await expect(page.locator('th:has-text("关闭时间")')).toBeVisible()
  617. // 验证时间格式正确 (YYYY-MM-DD HH:mm:ss)
  618. const startedAtCell = page.locator('tbody tr').first().locator('td').nth(7)
  619. const startedAtText = await startedAtCell.textContent()
  620. // 时间格式应该是 YYYY-MM-DD HH:mm:ss 或 "-"
  621. if (startedAtText && startedAtText.trim() !== '-') {
  622. expect(startedAtText).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)
  623. }
  624. })
  625. /**
  626. * Bug #4628: 命令模板按钮文字从"关闭"改为"取消"
  627. */
  628. test('Bug #4628 - 命令模板抽屉按钮显示"取消"而非"关闭"', async ({ page }) => {
  629. await loginHelper(page)
  630. await page.goto('/live-stream-manage/list')
  631. // 等待表格加载
  632. await page.waitForSelector('tbody tr', { timeout: 10000 })
  633. // 点击命令模板列的"查看"链接
  634. const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")')
  635. await expect(viewLink).toBeVisible({ timeout: 5000 })
  636. await viewLink.click()
  637. // 等待命令模板抽屉打开
  638. const drawer = page.locator('.el-drawer').filter({ hasText: '命令模板' })
  639. await expect(drawer).toBeVisible({ timeout: 5000 })
  640. // 验证有"取消"按钮而非"关闭"按钮
  641. await expect(drawer.locator('button:has-text("取消")')).toBeVisible()
  642. await expect(drawer.locator('button:has-text("关闭")')).not.toBeVisible()
  643. })
  644. /**
  645. * Bug #4629: 命令模板使用右到左抽屉显示
  646. */
  647. test('Bug #4629 - 命令模板使用抽屉而非对话框', async ({ page }) => {
  648. await loginHelper(page)
  649. await page.goto('/live-stream-manage/list')
  650. // 等待表格加载
  651. await page.waitForSelector('tbody tr', { timeout: 10000 })
  652. // 点击命令模板列的"查看"链接
  653. const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")')
  654. await expect(viewLink).toBeVisible({ timeout: 5000 })
  655. await viewLink.click()
  656. // 等待命令模板抽屉打开
  657. const drawer = page.locator('.el-drawer').filter({ hasText: '命令模板' })
  658. await expect(drawer).toBeVisible({ timeout: 5000 })
  659. // 验证是抽屉(el-drawer)而非对话框(el-dialog)
  660. const dialog = page.locator('.el-dialog').filter({ hasText: '命令模板' })
  661. await expect(dialog).not.toBeVisible()
  662. })
  663. /**
  664. * Bug #4630: 搜索框placeholder从"name"改为"名称"
  665. */
  666. test('Bug #4630 - 搜索框placeholder显示"名称"而非"name"', async ({ page }) => {
  667. await loginHelper(page)
  668. await page.goto('/live-stream-manage/list')
  669. // 验证名称搜索框的placeholder是"名称"
  670. const nameInput = page.getByPlaceholder('名称')
  671. await expect(nameInput).toBeVisible()
  672. // 验证没有placeholder为"name"的输入框
  673. const nameInputEnglish = page.getByPlaceholder('name')
  674. await expect(nameInputEnglish).not.toBeVisible()
  675. })
  676. /**
  677. * Bug #4541 (通用): 重置按钮使用灰底白字
  678. */
  679. test('Bug #4541 - 重置按钮使用灰底白字', async ({ page }) => {
  680. await loginHelper(page)
  681. await page.goto('/live-stream-manage/list')
  682. // 获取重置按钮
  683. const resetButton = page.getByRole('button', { name: '重置' })
  684. await expect(resetButton).toBeVisible()
  685. // 验证按钮有 el-button--info 类(灰色按钮)
  686. await expect(resetButton).toHaveClass(/el-button--info/)
  687. })
  688. })
  689. test.describe('LiveStream 管理 - 播放功能与 PTZ 控制测试', () => {
  690. /**
  691. * 测试播放按钮打开抽屉并加载 PTZ 预置位和能力信息
  692. */
  693. test('点击播放按钮打开抽屉并加载 PTZ 数据', async ({ page }) => {
  694. await login(page)
  695. await page.goto('/live-stream-manage/list')
  696. // 等待表格加载
  697. await page.waitForSelector('tbody tr', { timeout: 10000 })
  698. // 记录 API 调用
  699. let presetListCalled = false
  700. let capabilitiesCalled = false
  701. let presetListCameraId = ''
  702. let capabilitiesCameraId = ''
  703. // 拦截 PTZ 预置位列表请求
  704. await page.route('**/camera/control/*/preset/list', async (route) => {
  705. presetListCalled = true
  706. const url = route.request().url()
  707. const match = url.match(/camera\/control\/([^/]+)\/preset\/list/)
  708. if (match) {
  709. presetListCameraId = match[1]
  710. }
  711. // Mock 返回预置位数据
  712. await route.fulfill({
  713. status: 200,
  714. contentType: 'application/json',
  715. body: JSON.stringify({
  716. code: 200,
  717. msg: 'success',
  718. data: [
  719. { id: '1', name: 'Preset 1' },
  720. { id: '2', name: 'Preset 2' }
  721. ]
  722. })
  723. })
  724. })
  725. // 拦截 PTZ 能力请求
  726. await page.route('**/camera/control/*/ptz/capabilities', async (route) => {
  727. capabilitiesCalled = true
  728. const url = route.request().url()
  729. const match = url.match(/camera\/control\/([^/]+)\/ptz\/capabilities/)
  730. if (match) {
  731. capabilitiesCameraId = match[1]
  732. }
  733. // Mock 返回能力数据
  734. await route.fulfill({
  735. status: 200,
  736. contentType: 'application/json',
  737. body: JSON.stringify({
  738. code: 200,
  739. msg: 'success',
  740. data: {
  741. maxPresetNum: 255,
  742. controlProtocol: {
  743. options: ['ISAPI'],
  744. current: 'ISAPI'
  745. },
  746. absoluteZoom: {
  747. min: 1,
  748. max: 30
  749. },
  750. support3DPosition: true,
  751. supportPtzLimits: true
  752. }
  753. })
  754. })
  755. })
  756. // 找到包含 cameraId 的行并点击播放按钮
  757. const rows = page.locator('tbody tr')
  758. const rowCount = await rows.count()
  759. // 找到有 cameraId 的行
  760. let targetRow = null
  761. for (let i = 0; i < rowCount; i++) {
  762. const row = rows.nth(i)
  763. const cameraIdCell = await row.locator('td').nth(3).textContent()
  764. if (cameraIdCell && cameraIdCell.trim() !== '-' && cameraIdCell.trim() !== '') {
  765. targetRow = row
  766. break
  767. }
  768. }
  769. if (targetRow) {
  770. // 点击播放按钮 (data-id="live-stream-play-btn")
  771. const playButton = targetRow.locator('[data-id="live-stream-play-btn"]')
  772. await expect(playButton).toBeVisible()
  773. await playButton.click()
  774. // 等待抽屉打开
  775. const drawer = page.locator('.el-drawer')
  776. await expect(drawer).toBeVisible({ timeout: 5000 })
  777. // 验证播放 tab 被选中
  778. const playTab = drawer.locator('.el-tabs__item').filter({ hasText: '播放' })
  779. await expect(playTab).toHaveClass(/is-active/)
  780. // 等待 API 调用完成
  781. await page.waitForTimeout(2000)
  782. // 验证 PTZ 预置位和能力 API 被调用
  783. expect(presetListCalled).toBe(true)
  784. expect(capabilitiesCalled).toBe(true)
  785. // 验证抽屉中显示 PTZ 控制面板
  786. await expect(drawer.locator('text=PTZ')).toBeVisible()
  787. // 验证抽屉中显示预置位面板
  788. await expect(drawer.locator('text=预置位')).toBeVisible()
  789. // 验证抽屉中显示摄像头信息面板
  790. await expect(drawer.locator('text=摄像头信息')).toBeVisible()
  791. }
  792. })
  793. /**
  794. * 测试 PTZ 方向控制按钮
  795. */
  796. test('PTZ 方向控制按钮存在且可交互', async ({ page }) => {
  797. await login(page)
  798. await page.goto('/live-stream-manage/list')
  799. // 等待表格加载
  800. await page.waitForSelector('tbody tr', { timeout: 10000 })
  801. // Mock PTZ API 响应
  802. await page.route('**/camera/control/*/preset/list', async (route) => {
  803. await route.fulfill({
  804. status: 200,
  805. contentType: 'application/json',
  806. body: JSON.stringify({ code: 200, msg: 'success', data: [] })
  807. })
  808. })
  809. await page.route('**/camera/control/*/ptz/capabilities', async (route) => {
  810. await route.fulfill({
  811. status: 200,
  812. contentType: 'application/json',
  813. body: JSON.stringify({ code: 200, msg: 'success', data: { maxPresetNum: 255 } })
  814. })
  815. })
  816. // 找到有 cameraId 的行并点击播放按钮
  817. const rows = page.locator('tbody tr')
  818. const rowCount = await rows.count()
  819. for (let i = 0; i < rowCount; i++) {
  820. const row = rows.nth(i)
  821. const cameraIdCell = await row.locator('td').nth(3).textContent()
  822. if (cameraIdCell && cameraIdCell.trim() !== '-' && cameraIdCell.trim() !== '') {
  823. const playButton = row.locator('[data-id="live-stream-play-btn"]')
  824. await playButton.click()
  825. break
  826. }
  827. }
  828. // 等待抽屉打开
  829. const drawer = page.locator('.el-drawer')
  830. await expect(drawer).toBeVisible({ timeout: 5000 })
  831. // 展开 PTZ 控制面板
  832. const ptzHeader = drawer.locator('.el-collapse-item__header').filter({ hasText: 'PTZ' })
  833. if (await ptzHeader.isVisible()) {
  834. // 验证 PTZ 控制面板
  835. const ptzGrid = drawer.locator('.ptz-grid')
  836. await expect(ptzGrid).toBeVisible()
  837. // 验证 9 个方向按钮存在
  838. const ptzButtons = drawer.locator('.ptz-btn')
  839. await expect(ptzButtons).toHaveCount(9)
  840. // 验证缩放按钮存在
  841. const zoomButtons = drawer.locator('.zoom-buttons button')
  842. await expect(zoomButtons).toHaveCount(2)
  843. // 验证速度滑块存在
  844. const speedSlider = drawer.locator('.speed-slider')
  845. await expect(speedSlider).toBeVisible()
  846. }
  847. })
  848. /**
  849. * 测试预置位列表显示
  850. */
  851. test('预置位列表正确显示', async ({ page }) => {
  852. await login(page)
  853. await page.goto('/live-stream-manage/list')
  854. // 等待表格加载
  855. await page.waitForSelector('tbody tr', { timeout: 10000 })
  856. // Mock PTZ API 响应 - 返回预置位列表
  857. await page.route('**/camera/control/*/preset/list', async (route) => {
  858. await route.fulfill({
  859. status: 200,
  860. contentType: 'application/json',
  861. body: JSON.stringify({
  862. code: 200,
  863. msg: 'success',
  864. data: [
  865. { id: '1', name: '门口' },
  866. { id: '2', name: '窗户' },
  867. { id: '3', name: '走廊' }
  868. ]
  869. })
  870. })
  871. })
  872. await page.route('**/camera/control/*/ptz/capabilities', async (route) => {
  873. await route.fulfill({
  874. status: 200,
  875. contentType: 'application/json',
  876. body: JSON.stringify({ code: 200, msg: 'success', data: { maxPresetNum: 255 } })
  877. })
  878. })
  879. // 找到有 cameraId 的行并点击播放按钮
  880. const rows = page.locator('tbody tr')
  881. const rowCount = await rows.count()
  882. for (let i = 0; i < rowCount; i++) {
  883. const row = rows.nth(i)
  884. const cameraIdCell = await row.locator('td').nth(3).textContent()
  885. if (cameraIdCell && cameraIdCell.trim() !== '-' && cameraIdCell.trim() !== '') {
  886. const playButton = row.locator('[data-id="live-stream-play-btn"]')
  887. await playButton.click()
  888. break
  889. }
  890. }
  891. // 等待抽屉打开
  892. const drawer = page.locator('.el-drawer')
  893. await expect(drawer).toBeVisible({ timeout: 5000 })
  894. // 等待预置位加载
  895. await page.waitForTimeout(1500)
  896. // 展开预置位面板
  897. const presetHeader = drawer.locator('.el-collapse-item__header').filter({ hasText: '预置位' })
  898. await expect(presetHeader).toBeVisible()
  899. // 验证预置位列表中有 3 个项目
  900. const presetItems = drawer.locator('.preset-item')
  901. await expect(presetItems).toHaveCount(3)
  902. // 验证预置位名称显示正确
  903. await expect(drawer.locator('.preset-name:has-text("门口")')).toBeVisible()
  904. await expect(drawer.locator('.preset-name:has-text("窗户")')).toBeVisible()
  905. await expect(drawer.locator('.preset-name:has-text("走廊")')).toBeVisible()
  906. })
  907. /**
  908. * 测试摄像头信息显示
  909. */
  910. test('摄像头能力信息正确显示', async ({ page }) => {
  911. await login(page)
  912. await page.goto('/live-stream-manage/list')
  913. // 等待表格加载
  914. await page.waitForSelector('tbody tr', { timeout: 10000 })
  915. // Mock PTZ API 响应
  916. await page.route('**/camera/control/*/preset/list', async (route) => {
  917. await route.fulfill({
  918. status: 200,
  919. contentType: 'application/json',
  920. body: JSON.stringify({ code: 200, msg: 'success', data: [] })
  921. })
  922. })
  923. await page.route('**/camera/control/*/ptz/capabilities', async (route) => {
  924. await route.fulfill({
  925. status: 200,
  926. contentType: 'application/json',
  927. body: JSON.stringify({
  928. code: 200,
  929. msg: 'success',
  930. data: {
  931. maxPresetNum: 255,
  932. controlProtocol: {
  933. options: ['ISAPI', 'ONVIF'],
  934. current: 'ISAPI'
  935. },
  936. absoluteZoom: {
  937. min: 1,
  938. max: 30
  939. },
  940. support3DPosition: true,
  941. supportPtzLimits: true
  942. }
  943. })
  944. })
  945. })
  946. // 找到有 cameraId 的行并点击播放按钮
  947. const rows = page.locator('tbody tr')
  948. const rowCount = await rows.count()
  949. for (let i = 0; i < rowCount; i++) {
  950. const row = rows.nth(i)
  951. const cameraIdCell = await row.locator('td').nth(3).textContent()
  952. if (cameraIdCell && cameraIdCell.trim() !== '-' && cameraIdCell.trim() !== '') {
  953. const playButton = row.locator('[data-id="live-stream-play-btn"]')
  954. await playButton.click()
  955. break
  956. }
  957. }
  958. // 等待抽屉打开
  959. const drawer = page.locator('.el-drawer')
  960. await expect(drawer).toBeVisible({ timeout: 5000 })
  961. // 等待能力信息加载
  962. await page.waitForTimeout(1500)
  963. // 展开摄像头信息面板
  964. const cameraInfoHeader = drawer.locator('.el-collapse-item__header').filter({ hasText: '摄像头信息' })
  965. await expect(cameraInfoHeader).toBeVisible()
  966. // 验证能力信息显示
  967. const cameraInfoContent = drawer.locator('.camera-info-content')
  968. await expect(cameraInfoContent).toBeVisible()
  969. // 验证最大预置位数显示
  970. await expect(cameraInfoContent.locator('text=255')).toBeVisible()
  971. // 验证控制协议显示
  972. await expect(cameraInfoContent.locator('text=ISAPI')).toBeVisible()
  973. // 验证变焦倍数显示
  974. await expect(cameraInfoContent.locator('text=/1.*30/')).toBeVisible()
  975. })
  976. /**
  977. * 测试播放器控制按钮
  978. */
  979. test('播放器控制按钮存在', async ({ page }) => {
  980. await login(page)
  981. await page.goto('/live-stream-manage/list')
  982. // 等待表格加载
  983. await page.waitForSelector('tbody tr', { timeout: 10000 })
  984. // Mock PTZ API 响应
  985. await page.route('**/camera/control/**', async (route) => {
  986. await route.fulfill({
  987. status: 200,
  988. contentType: 'application/json',
  989. body: JSON.stringify({ code: 200, msg: 'success', data: null })
  990. })
  991. })
  992. // 点击第一行的播放按钮
  993. const playButton = page.locator('[data-id="live-stream-play-btn"]').first()
  994. await playButton.click()
  995. // 等待抽屉打开
  996. const drawer = page.locator('.el-drawer')
  997. await expect(drawer).toBeVisible({ timeout: 5000 })
  998. // 验证播放器控制按钮存在
  999. const controls = drawer.locator('.player-controls')
  1000. await expect(controls).toBeVisible()
  1001. // 验证各个控制按钮
  1002. await expect(controls.locator('button:has-text("播放")')).toBeVisible()
  1003. await expect(controls.locator('button:has-text("暂停")')).toBeVisible()
  1004. await expect(controls.locator('button:has-text("停止")')).toBeVisible()
  1005. await expect(controls.locator('button:has-text("截图")')).toBeVisible()
  1006. await expect(controls.locator('button:has-text("全屏")')).toBeVisible()
  1007. // 验证静音开关存在
  1008. await expect(controls.locator('.el-switch')).toBeVisible()
  1009. })
  1010. /**
  1011. * 测试时间轴组件
  1012. */
  1013. test('巡航时间轴组件存在', async ({ page }) => {
  1014. await login(page)
  1015. await page.goto('/live-stream-manage/list')
  1016. // 等待表格加载
  1017. await page.waitForSelector('tbody tr', { timeout: 10000 })
  1018. // Mock PTZ API 响应
  1019. await page.route('**/camera/control/**', async (route) => {
  1020. await route.fulfill({
  1021. status: 200,
  1022. contentType: 'application/json',
  1023. body: JSON.stringify({ code: 200, msg: 'success', data: null })
  1024. })
  1025. })
  1026. // 点击播放按钮
  1027. const playButton = page.locator('[data-id="live-stream-play-btn"]').first()
  1028. await playButton.click()
  1029. // 等待抽屉打开
  1030. const drawer = page.locator('.el-drawer')
  1031. await expect(drawer).toBeVisible({ timeout: 5000 })
  1032. // 验证时间轴容器存在
  1033. const timeline = drawer.locator('.timeline-container')
  1034. await expect(timeline).toBeVisible()
  1035. // 验证时间轴头部
  1036. await expect(timeline.locator('text=巡航时间轴')).toBeVisible()
  1037. // 验证时间选择器
  1038. await expect(timeline.locator('.el-select')).toBeVisible()
  1039. // 验证添加点按钮
  1040. await expect(timeline.locator('button:has-text("添加点")')).toBeVisible()
  1041. // 验证播放巡航按钮
  1042. await expect(timeline.locator('button:has-text("播放巡航")')).toBeVisible()
  1043. // 验证时间轴轨道
  1044. await expect(timeline.locator('.timeline-track')).toBeVisible()
  1045. })
  1046. })