live-stream.spec.ts 44 KB

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