lss.spec.ts 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821
  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. const TEST_DATA = {
  7. name: '東京テスト環境',
  8. address: '東京都渋谷区神宮前1-2-3',
  9. // 用于恢复的原始数据
  10. originalName: '初台測試環境',
  11. originalAddress: ''
  12. }
  13. test.describe('LSS管理 CRUD 测试', () => {
  14. // 登录辅助函数
  15. async function login(page: Page) {
  16. await page.goto('/login')
  17. await page.evaluate(() => {
  18. localStorage.clear()
  19. document.cookie.split(';').forEach((c) => {
  20. document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`)
  21. })
  22. })
  23. await page.reload()
  24. await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
  25. await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
  26. await page.getByRole('button', { name: '登录' }).click()
  27. await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 })
  28. }
  29. test('LSS管理页面正确显示', async ({ page }) => {
  30. await login(page)
  31. await page.goto('/lss-manage/list')
  32. // 验证页面标题 (Bug #4537: 页面标题从 "LSS 管理" 改为 "LSS 列表")
  33. // 使用 .first() 避免匹配多个元素(侧边栏和面包屑都有)
  34. await expect(page.locator('text=LSS 列表').first()).toBeVisible()
  35. // 验证搜索表单元素
  36. await expect(page.getByPlaceholder('LSS ID')).toBeVisible()
  37. await expect(page.getByPlaceholder('名称')).toBeVisible()
  38. await expect(page.getByRole('button', { name: '查询' })).toBeVisible()
  39. await expect(page.getByRole('button', { name: '重置' })).toBeVisible()
  40. // 验证表头
  41. await expect(page.locator('th:has-text("LSS ID")')).toBeVisible()
  42. await expect(page.locator('th:has-text("名称")')).toBeVisible()
  43. await expect(page.locator('th:has-text("地址")')).toBeVisible()
  44. await expect(page.locator('th:has-text("IP")')).toBeVisible()
  45. await expect(page.locator('th:has-text("操作")')).toBeVisible()
  46. })
  47. test('编辑LSS节点 - 修改Name和Address', async ({ page }) => {
  48. await login(page)
  49. await page.goto('/lss-manage/list')
  50. // 等待表格加载
  51. await page.waitForTimeout(1000)
  52. // 找到数据行中的编辑按钮(Actions列第一个按钮)
  53. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  54. await expect(editButton).toBeVisible({ timeout: 10000 })
  55. await editButton.click()
  56. // 验证编辑对话框打开
  57. const dialog = page.locator('.el-drawer, .el-dialog').filter({ hasText: 'LSS详情' })
  58. await expect(dialog).toBeVisible({ timeout: 5000 })
  59. // 修改 Name 字段
  60. const nameInput = dialog
  61. .locator('input')
  62. .filter({ hasText: /名称|Name/ })
  63. .or(dialog.locator('label:has-text("Name")').locator('..').locator('input'))
  64. .first()
  65. // 使用更通用的方式定位 Name 输入框
  66. const nameField = dialog.getByPlaceholder('请输入名称')
  67. await nameField.clear()
  68. await nameField.fill(TEST_DATA.name)
  69. // 修改 Address 字段
  70. const addressField = dialog.getByPlaceholder('请输入地址')
  71. await addressField.clear()
  72. await addressField.fill(TEST_DATA.address)
  73. // 点击 更新 按钮保存
  74. await dialog.getByRole('button', { name: '更新' }).click()
  75. // 等待对话框关闭
  76. await expect(dialog).not.toBeVisible({ timeout: 10000 })
  77. // 验证列表中数据已更新
  78. await page.waitForTimeout(500)
  79. const firstRow = page.locator('tbody tr').first()
  80. await expect(firstRow).toContainText(TEST_DATA.name)
  81. await expect(firstRow).toContainText(TEST_DATA.address)
  82. })
  83. test('查询LSS节点', async ({ page }) => {
  84. await login(page)
  85. await page.goto('/lss-manage/list')
  86. // 等待表格加载
  87. await page.waitForTimeout(1000)
  88. // 在名称搜索框输入关键词
  89. await page.getByPlaceholder('名称').fill('東京')
  90. // 点击搜索
  91. await page.getByRole('button', { name: '查询' }).click()
  92. await page.waitForTimeout(500)
  93. // 验证搜索结果
  94. const tableRows = page.locator('tbody tr')
  95. const rowCount = await tableRows.count()
  96. if (rowCount > 0) {
  97. // 验证每行都包含搜索关键词
  98. for (let i = 0; i < rowCount; i++) {
  99. const row = tableRows.nth(i)
  100. const rowText = await row.textContent()
  101. expect(rowText).toContain('東京')
  102. }
  103. }
  104. })
  105. test('重置搜索条件', async ({ page }) => {
  106. await login(page)
  107. await page.goto('/lss-manage/list')
  108. // 填入搜索条件
  109. await page.getByPlaceholder('LSS ID').fill('test-id')
  110. await page.getByPlaceholder('名称').fill('test-name')
  111. // 点击重置
  112. await page.getByRole('button', { name: '重置' }).click()
  113. await page.waitForTimeout(300)
  114. // 验证搜索条件已清空
  115. await expect(page.getByPlaceholder('LSS ID')).toHaveValue('')
  116. await expect(page.getByPlaceholder('名称')).toHaveValue('')
  117. })
  118. // TODO: 此测试因 UI 可见性问题暂时跳过
  119. // 设备列表抽屉元素存在但 CSS visibility 为 hidden,需要检查组件实现
  120. test.skip('查看LSS节点设备列表', async ({ page }) => {
  121. await login(page)
  122. await page.goto('/lss-manage/list')
  123. // 等待表格加载
  124. await page.waitForTimeout(1000)
  125. // 找到有 LSS ID 的行的设备列表按钮(跳过空行)
  126. const rows = page.locator('tbody tr')
  127. let deviceListButton = null
  128. for (let i = 0; i < (await rows.count()); i++) {
  129. const row = rows.nth(i)
  130. const lssIdCell = await row.locator('td').first().textContent()
  131. if (lssIdCell && lssIdCell.trim()) {
  132. deviceListButton = row.locator('button').first()
  133. break
  134. }
  135. }
  136. if (!deviceListButton) {
  137. test.skip()
  138. return
  139. }
  140. await expect(deviceListButton).toBeVisible({ timeout: 10000 })
  141. await deviceListButton.click()
  142. // 等待抽屉动画完成
  143. await page.waitForTimeout(500)
  144. // 验证设备列表面板打开
  145. const devicePanel = page.locator('.device-drawer.el-drawer')
  146. await expect(devicePanel).toBeVisible({ timeout: 10000 })
  147. // 验证设备列表表头
  148. await expect(devicePanel.locator('th:has-text("设备ID")')).toBeVisible()
  149. await expect(devicePanel.locator('th:has-text("名称")')).toBeVisible()
  150. // 关闭面板
  151. await devicePanel.locator('button[aria-label*="Close"], .el-drawer__close-btn, .el-dialog__close').first().click()
  152. await expect(devicePanel).not.toBeVisible({ timeout: 5000 })
  153. })
  154. test('从侧边栏导航到LSS管理', async ({ page }) => {
  155. await login(page)
  156. // 等待侧边栏加载
  157. await page.waitForTimeout(500)
  158. // 点击展开 LSS 管理 父菜单
  159. await page.getByText('LSS 管理').first().click()
  160. await page.waitForTimeout(300)
  161. // 点击侧边栏 LSS 列表菜单项 (Bug #4537: 菜单标题从 "LSS 管理" 改为 "LSS 列表")
  162. await page.getByText('LSS 列表').first().click()
  163. // 验证跳转到 LSS 管理页面
  164. await expect(page).toHaveURL(/\/lss-manage\/list/)
  165. // 使用 .first() 避免匹配多个元素(侧边栏和面包屑都有)
  166. await expect(page.locator('text=LSS 列表').first()).toBeVisible()
  167. })
  168. })
  169. test.describe('LSS管理 - CodeEditor 组件测试 (JSON模式)', () => {
  170. // 登录辅助函数
  171. async function login(page: Page) {
  172. await page.goto('/login')
  173. await page.evaluate(() => {
  174. localStorage.clear()
  175. document.cookie.split(';').forEach((c) => {
  176. document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`)
  177. })
  178. })
  179. await page.reload()
  180. await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
  181. await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
  182. await page.getByRole('button', { name: '登录' }).click()
  183. await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 })
  184. }
  185. /**
  186. * 测试 CodeEditor JSON 模式 - 头部显示
  187. */
  188. test('CodeEditor JSON模式 - 验证头部显示JSON标签和按钮', async ({ page }) => {
  189. await login(page)
  190. await page.goto('/lss-manage/list')
  191. // 等待表格加载
  192. await page.waitForTimeout(1000)
  193. // 点击 Device List 按钮打开设备列表
  194. const deviceListButton = page.locator('tbody tr').first().locator('button').first()
  195. await expect(deviceListButton).toBeVisible({ timeout: 10000 })
  196. await deviceListButton.click()
  197. // 等待设备列表面板打开
  198. const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: '摄像头列表' })
  199. await expect(devicePanel).toBeVisible({ timeout: 5000 })
  200. // 点击 Parameter Configuration 的 View 按钮
  201. const viewButton = devicePanel.locator('tbody tr').first().locator('button:has-text("查看")').first()
  202. await expect(viewButton).toBeVisible({ timeout: 5000 })
  203. await viewButton.click()
  204. // 等待参数配置抽屉打开
  205. const paramsDrawer = page.locator('.params-drawer')
  206. await expect(paramsDrawer).toBeVisible({ timeout: 5000 })
  207. // 验证 CodeEditor 头部显示 JSON 标签
  208. const codeEditor = paramsDrawer.locator('.code-editor')
  209. await expect(codeEditor).toBeVisible()
  210. await expect(codeEditor.locator('.editor-header')).toBeVisible()
  211. await expect(codeEditor.locator('.file-type')).toContainText('JSON')
  212. // 验证 Copy 按钮存在
  213. await expect(codeEditor.locator('button:has-text("复制"), button:has-text("Copy")')).toBeVisible()
  214. // 验证 格式化 按钮存在 (JSON模式特有)
  215. await expect(codeEditor.locator('button:has-text("格式化")')).toBeVisible()
  216. })
  217. /**
  218. * 测试 CodeEditor JSON 模式 - 复制功能
  219. */
  220. test('CodeEditor JSON模式 - 复制按钮功能', async ({ page }) => {
  221. await login(page)
  222. await page.goto('/lss-manage/list')
  223. // 等待表格加载
  224. await page.waitForTimeout(1000)
  225. // 点击 Device List 按钮
  226. const deviceListButton = page.locator('tbody tr').first().locator('button').first()
  227. await deviceListButton.click()
  228. // 等待设备列表面板打开
  229. const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: '摄像头列表' })
  230. await expect(devicePanel).toBeVisible({ timeout: 5000 })
  231. // 点击 Parameter Configuration 的 View 按钮
  232. const viewButton = devicePanel.locator('tbody tr').first().locator('button:has-text("查看")').first()
  233. await viewButton.click()
  234. // 等待参数配置抽屉打开
  235. const paramsDrawer = page.locator('.params-drawer')
  236. await expect(paramsDrawer).toBeVisible({ timeout: 5000 })
  237. // 点击复制按钮
  238. const copyButton = paramsDrawer.locator(
  239. '.code-editor button:has-text("复制"), .code-editor button:has-text("Copy")'
  240. )
  241. await expect(copyButton).toBeVisible()
  242. await copyButton.click()
  243. // 验证复制按钮点击后没有报错(复制成功提示在无头浏览器中可能不显示)
  244. await page.waitForTimeout(500)
  245. await expect(paramsDrawer).toBeVisible()
  246. })
  247. /**
  248. * 测试 CodeEditor JSON 模式 - 格式化功能
  249. */
  250. test('CodeEditor JSON模式 - 格式化按钮功能', async ({ page }) => {
  251. await login(page)
  252. await page.goto('/lss-manage/list')
  253. // 等待表格加载
  254. await page.waitForTimeout(1000)
  255. // 点击 Device List 按钮
  256. const deviceListButton = page.locator('tbody tr').first().locator('button').first()
  257. await deviceListButton.click()
  258. // 等待设备列表面板打开
  259. const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: '摄像头列表' })
  260. await expect(devicePanel).toBeVisible({ timeout: 5000 })
  261. // 点击 Parameter Configuration 的 View 按钮
  262. const viewButton = devicePanel.locator('tbody tr').first().locator('button:has-text("查看")').first()
  263. await viewButton.click()
  264. // 等待参数配置抽屉打开
  265. const paramsDrawer = page.locator('.params-drawer')
  266. await expect(paramsDrawer).toBeVisible({ timeout: 5000 })
  267. // 验证格式化按钮可用(如果内容是有效JSON)
  268. const formatButton = paramsDrawer.locator('.code-editor button:has-text("格式化")')
  269. await expect(formatButton).toBeVisible()
  270. // 点击格式化按钮(如果按钮未禁用)
  271. const isDisabled = await formatButton.isDisabled()
  272. if (!isDisabled) {
  273. await formatButton.click()
  274. // 格式化后内容应该保持有效
  275. await page.waitForTimeout(300)
  276. }
  277. })
  278. /**
  279. * 测试 CodeEditor JSON 模式 - 无效JSON显示错误提示
  280. */
  281. test('CodeEditor JSON模式 - 无效JSON显示错误提示', async ({ page }) => {
  282. await login(page)
  283. await page.goto('/lss-manage/list')
  284. // 等待表格加载
  285. await page.waitForTimeout(1000)
  286. // 点击 Device List 按钮
  287. const deviceListButton = page.locator('tbody tr').first().locator('button').first()
  288. await deviceListButton.click()
  289. // 等待设备列表面板打开
  290. const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: '摄像头列表' })
  291. await expect(devicePanel).toBeVisible({ timeout: 5000 })
  292. // 点击 Parameter Configuration 的 View 按钮
  293. const viewButton = devicePanel.locator('tbody tr').first().locator('button:has-text("查看")').first()
  294. await viewButton.click()
  295. // 等待参数配置抽屉打开
  296. const paramsDrawer = page.locator('.params-drawer')
  297. await expect(paramsDrawer).toBeVisible({ timeout: 5000 })
  298. // 获取编辑器并输入无效JSON
  299. const codeEditor = paramsDrawer.locator('.code-editor')
  300. const editorContent = codeEditor.locator('.cm-content')
  301. // 清空并输入无效JSON
  302. await editorContent.click()
  303. await page.keyboard.press('Meta+a')
  304. await page.keyboard.type('{ invalid json }')
  305. // 验证错误提示显示
  306. await expect(codeEditor.locator('.validation-error')).toBeVisible({ timeout: 3000 })
  307. await expect(codeEditor.locator('.validation-error')).toContainText('JSON 格式错误')
  308. // 验证格式化按钮被禁用
  309. const formatButton = codeEditor.locator('button:has-text("格式化")')
  310. await expect(formatButton).toBeDisabled()
  311. })
  312. /**
  313. * 测试 CodeEditor JSON 模式 - 更新内容并验证保存成功
  314. * 注:此测试依赖于后端实际保存数据,在某些环境中可能不稳定
  315. */
  316. // TODO: 待后端 API 稳定后重新启用
  317. test.skip('CodeEditor JSON模式 - 更新参数配置并验证保存成功', async ({ page }) => {
  318. await login(page)
  319. await page.goto('/lss-manage/list')
  320. // 等待表格加载
  321. await page.waitForTimeout(1000)
  322. // 点击 Device List 按钮
  323. const deviceListButton = page.locator('tbody tr').first().locator('button').first()
  324. await deviceListButton.click()
  325. // 等待设备列表面板打开
  326. const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: '摄像头列表' })
  327. await expect(devicePanel).toBeVisible({ timeout: 5000 })
  328. // 点击 Parameter Configuration 的 View 按钮
  329. const viewButton = devicePanel.locator('tbody tr').first().locator('button:has-text("查看")').first()
  330. await viewButton.click()
  331. // 等待参数配置抽屉打开
  332. const paramsDrawer = page.locator('.params-drawer')
  333. await expect(paramsDrawer).toBeVisible({ timeout: 5000 })
  334. // 获取当前编辑器内容
  335. const codeEditor = paramsDrawer.locator('.code-editor')
  336. const editorContent = codeEditor.locator('.cm-content')
  337. // 生成唯一标识用于验证更新
  338. const timestamp = Date.now()
  339. const testValue = `"testUpdate": "${timestamp}"`
  340. // 修改JSON内容 - 在现有JSON中添加测试字段
  341. await editorContent.click()
  342. await page.keyboard.press('Meta+a')
  343. // 输入新的有效JSON内容
  344. await page.keyboard.type(`{\n ${testValue},\n "ip": "192.168.0.64"\n}`)
  345. // 点击更新按钮
  346. const updateButton = paramsDrawer.locator('button:has-text("更新"), button:has-text("Update")')
  347. await updateButton.click()
  348. // 等待更新成功提示
  349. await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 })
  350. // 等待抽屉关闭
  351. await expect(paramsDrawer).not.toBeVisible({ timeout: 5000 })
  352. // 重新打开参数配置抽屉验证内容已保存
  353. await page.waitForTimeout(500)
  354. await viewButton.click()
  355. // 等待抽屉重新打开
  356. const paramsDrawerReopened = page.locator('.params-drawer')
  357. await expect(paramsDrawerReopened).toBeVisible({ timeout: 5000 })
  358. // 验证内容包含我们添加的测试字段
  359. const editorContentReopened = paramsDrawerReopened.locator('.cm-content')
  360. await expect(editorContentReopened).toContainText(timestamp.toString())
  361. })
  362. /**
  363. * 测试 CodeEditor JSON 模式 - 运行参数更新并验证保存成功
  364. * 注:此测试依赖于后端实际保存数据,在某些环境中可能不稳定
  365. */
  366. // TODO: 待后端 API 稳定后重新启用
  367. test.skip('CodeEditor JSON模式 - 更新运行参数并验证保存成功', async ({ page }) => {
  368. await login(page)
  369. await page.goto('/lss-manage/list')
  370. // 等待表格加载
  371. await page.waitForTimeout(1000)
  372. // 点击 Device List 按钮
  373. const deviceListButton = page.locator('tbody tr').first().locator('button').first()
  374. await deviceListButton.click()
  375. // 等待设备列表面板打开
  376. const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: '摄像头列表' })
  377. await expect(devicePanel).toBeVisible({ timeout: 5000 })
  378. // 点击 Run Parameters 的 View 按钮(第二个 View 按钮)
  379. const viewButtons = devicePanel.locator('tbody tr').first().locator('button:has-text("查看")')
  380. const runParamsViewButton = viewButtons.nth(1)
  381. await expect(runParamsViewButton).toBeVisible({ timeout: 5000 })
  382. await runParamsViewButton.click()
  383. // 等待运行参数抽屉打开
  384. const paramsDrawer = page.locator('.el-drawer').filter({ hasText: '运行参数' })
  385. await expect(paramsDrawer).toBeVisible({ timeout: 5000 })
  386. // 获取编辑器
  387. const codeEditor = paramsDrawer.locator('.code-editor')
  388. const editorContent = codeEditor.locator('.cm-content')
  389. // 生成唯一标识用于验证更新
  390. const timestamp = Date.now()
  391. const testValue = `"runtimeTest": "${timestamp}"`
  392. // 修改JSON内容
  393. await editorContent.click()
  394. await page.keyboard.press('Meta+a')
  395. await page.keyboard.type(`{\n ${testValue}\n}`)
  396. // 点击更新按钮
  397. const updateButton = paramsDrawer.locator('button:has-text("更新"), button:has-text("Update")')
  398. await updateButton.click()
  399. // 等待更新成功提示
  400. await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 })
  401. // 等待抽屉关闭
  402. await expect(paramsDrawer).not.toBeVisible({ timeout: 5000 })
  403. // 重新打开运行参数抽屉验证内容已保存
  404. await page.waitForTimeout(500)
  405. await runParamsViewButton.click()
  406. // 等待抽屉重新打开
  407. const paramsDrawerReopened = page.locator('.el-drawer').filter({ hasText: '运行参数' })
  408. await expect(paramsDrawerReopened).toBeVisible({ timeout: 5000 })
  409. // 验证内容包含我们添加的测试字段
  410. const editorContentReopened = paramsDrawerReopened.locator('.cm-content')
  411. await expect(editorContentReopened).toContainText(timestamp.toString())
  412. })
  413. })
  414. test.describe('LSS管理 - 摄像头列表搜索测试', () => {
  415. // 登录辅助函数
  416. async function login(page: Page) {
  417. await page.goto('/login')
  418. await page.evaluate(() => {
  419. localStorage.clear()
  420. document.cookie.split(';').forEach((c) => {
  421. document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`)
  422. })
  423. })
  424. await page.reload()
  425. await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
  426. await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
  427. await page.getByRole('button', { name: '登录' }).click()
  428. await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 })
  429. }
  430. // 打开摄像头列表辅助函数
  431. async function openCameraList(page: Page) {
  432. await page.goto('/lss-manage/list')
  433. await page.waitForTimeout(1000)
  434. // 点击 Device List 按钮打开设备列表 - 点击编辑按钮进入 LSS 编辑抽屉
  435. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  436. await expect(editButton).toBeVisible({ timeout: 10000 })
  437. await editButton.click()
  438. // 等待 LSS 编辑抽屉打开
  439. const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
  440. await expect(drawer).toBeVisible({ timeout: 5000 })
  441. // 点击"摄像头列表" Tab
  442. await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
  443. await page.waitForTimeout(500)
  444. return drawer
  445. }
  446. /**
  447. * 测试摄像头搜索表单元素显示
  448. */
  449. test('摄像头列表 - 验证搜索表单元素显示', async ({ page }) => {
  450. await login(page)
  451. const drawer = await openCameraList(page)
  452. const toolbar = drawer.locator('.camera-toolbar')
  453. // 验证 Device ID 搜索框存在
  454. await expect(toolbar.getByPlaceholder('设备ID')).toBeVisible()
  455. // 验证 Name 搜索框存在
  456. await expect(toolbar.getByPlaceholder('名称')).toBeVisible()
  457. // 验证 Status 下拉框存在
  458. await expect(toolbar.locator('.el-select').first()).toBeVisible()
  459. // 验证查询按钮存在
  460. await expect(toolbar.getByRole('button', { name: /查询|Search/ })).toBeVisible()
  461. // 验证重置按钮存在
  462. await expect(toolbar.getByRole('button', { name: /重置|Reset/ })).toBeVisible()
  463. // 验证新增按钮存在
  464. await expect(toolbar.getByRole('button', { name: /新增|Add/ })).toBeVisible()
  465. })
  466. /**
  467. * 测试按设备ID搜索
  468. */
  469. test('摄像头列表 - 按设备ID搜索', async ({ page }) => {
  470. await login(page)
  471. const drawer = await openCameraList(page)
  472. const toolbar = drawer.locator('.camera-toolbar')
  473. // 等待表格数据加载
  474. await page.waitForTimeout(500)
  475. // 获取搜索前的数据数量
  476. const tableRows = drawer.locator('.tab-content-wrapper tbody tr')
  477. const initialRowCount = await tableRows.count()
  478. expect(initialRowCount).toBeGreaterThan(0)
  479. // 从第一行获取真实设备ID作为搜索关键词
  480. const firstRowDeviceId = (await tableRows.first().locator('td').first().textContent())?.trim() || ''
  481. expect(firstRowDeviceId).toBeTruthy()
  482. // 输入设备ID搜索
  483. await toolbar.getByPlaceholder('设备ID').fill(firstRowDeviceId)
  484. // 点击搜索
  485. await toolbar.getByRole('button', { name: /查询|Search/ }).click()
  486. await page.waitForTimeout(500)
  487. // 验证搜索条件已应用
  488. await expect(toolbar.getByPlaceholder('设备ID')).toHaveValue(firstRowDeviceId)
  489. // 验证搜索结果
  490. const resultRows = drawer.locator('.tab-content-wrapper tbody tr')
  491. const rowCount = await resultRows.count()
  492. // 搜索结果应该有数据
  493. expect(rowCount).toBeGreaterThan(0)
  494. // 搜索结果不超过初始数据量
  495. expect(rowCount).toBeLessThanOrEqual(initialRowCount)
  496. // 验证搜索结果中包含搜索关键词
  497. const resultDeviceId = await resultRows.first().locator('td').first().textContent()
  498. expect(resultDeviceId?.trim()).toContain(firstRowDeviceId)
  499. })
  500. /**
  501. * 测试按名称搜索
  502. */
  503. test('摄像头列表 - 按名称搜索', async ({ page }) => {
  504. await login(page)
  505. const drawer = await openCameraList(page)
  506. const toolbar = drawer.locator('.camera-toolbar')
  507. // 等待表格数据加载
  508. await page.waitForTimeout(500)
  509. // 获取搜索前的数据数量
  510. const tableRows = drawer.locator('.tab-content-wrapper tbody tr')
  511. const initialRowCount = await tableRows.count()
  512. expect(initialRowCount).toBeGreaterThan(0)
  513. // 从第一行获取真实名称作为搜索关键词
  514. const firstRowName = (await tableRows.first().locator('td').nth(1).textContent())?.trim() || ''
  515. expect(firstRowName).toBeTruthy()
  516. // 输入名称搜索
  517. await toolbar.getByPlaceholder('名称').fill(firstRowName)
  518. // 点击搜索
  519. await toolbar.getByRole('button', { name: /查询|Search/ }).click()
  520. await page.waitForTimeout(500)
  521. // 验证搜索条件已应用
  522. await expect(toolbar.getByPlaceholder('名称')).toHaveValue(firstRowName)
  523. // 验证搜索结果
  524. const resultRows = drawer.locator('.tab-content-wrapper tbody tr')
  525. const rowCount = await resultRows.count()
  526. // 搜索结果应该有数据
  527. expect(rowCount).toBeGreaterThan(0)
  528. // 搜索结果不超过初始数据量
  529. expect(rowCount).toBeLessThanOrEqual(initialRowCount)
  530. // 验证搜索结果中包含搜索关键词
  531. const resultName = await resultRows.first().locator('td').nth(1).textContent()
  532. expect(resultName?.trim()).toContain(firstRowName)
  533. })
  534. /**
  535. * 测试组合搜索 - 设备ID + 名称
  536. */
  537. test('摄像头列表 - 组合搜索设备ID和名称', async ({ page }) => {
  538. await login(page)
  539. const drawer = await openCameraList(page)
  540. const toolbar = drawer.locator('.camera-toolbar')
  541. // 等待表格数据加载
  542. await page.waitForTimeout(500)
  543. // 从第一行获取真实数据作为搜索关键词
  544. const tableRows = drawer.locator('.tab-content-wrapper tbody tr')
  545. const rowCount = await tableRows.count()
  546. expect(rowCount).toBeGreaterThan(0)
  547. const deviceId = (await tableRows.first().locator('td').first().textContent())?.trim() || ''
  548. const name = (await tableRows.first().locator('td').nth(1).textContent())?.trim() || ''
  549. // 输入设备ID
  550. await toolbar.getByPlaceholder('设备ID').fill(deviceId)
  551. // 输入名称
  552. await toolbar.getByPlaceholder('名称').fill(name)
  553. // 点击搜索
  554. await toolbar.getByRole('button', { name: /查询|Search/ }).click()
  555. await page.waitForTimeout(500)
  556. // 验证搜索条件已应用(输入框保留值)
  557. await expect(toolbar.getByPlaceholder('设备ID')).toHaveValue(deviceId)
  558. await expect(toolbar.getByPlaceholder('名称')).toHaveValue(name)
  559. })
  560. /**
  561. * 测试重置搜索条件
  562. */
  563. test('摄像头列表 - 重置搜索条件', async ({ page }) => {
  564. await login(page)
  565. const drawer = await openCameraList(page)
  566. const toolbar = drawer.locator('.camera-toolbar')
  567. // 填入搜索条件
  568. await toolbar.getByPlaceholder('设备ID').fill('test-device-id')
  569. await toolbar.getByPlaceholder('名称').fill('test-camera-name')
  570. // 验证输入值
  571. await expect(toolbar.getByPlaceholder('设备ID')).toHaveValue('test-device-id')
  572. await expect(toolbar.getByPlaceholder('名称')).toHaveValue('test-camera-name')
  573. // 点击重置
  574. await toolbar.getByRole('button', { name: /重置|Reset/ }).click()
  575. await page.waitForTimeout(300)
  576. // 验证搜索条件已清空
  577. await expect(toolbar.getByPlaceholder('设备ID')).toHaveValue('')
  578. await expect(toolbar.getByPlaceholder('名称')).toHaveValue('')
  579. })
  580. /**
  581. * 测试搜索无结果情况
  582. */
  583. test('摄像头列表 - 搜索无结果显示空状态', async ({ page }) => {
  584. await login(page)
  585. const drawer = await openCameraList(page)
  586. const toolbar = drawer.locator('.camera-toolbar')
  587. // 输入一个不存在的设备ID
  588. await toolbar.getByPlaceholder('设备ID').fill('non-existent-device-id-xyz123')
  589. // 点击搜索
  590. await toolbar.getByRole('button', { name: /查询|Search/ }).click()
  591. await page.waitForTimeout(500)
  592. // 验证显示空状态或表格无数据
  593. const tableRows = drawer.locator('.tab-content-wrapper tbody tr')
  594. const rowCount = await tableRows.count()
  595. if (rowCount === 0) {
  596. // 验证显示空状态组件
  597. await expect(drawer.locator('.el-empty')).toBeVisible()
  598. }
  599. })
  600. /**
  601. * 测试按Enter键搜索
  602. */
  603. test('摄像头列表 - 按Enter键触发搜索', async ({ page }) => {
  604. await login(page)
  605. const drawer = await openCameraList(page)
  606. const toolbar = drawer.locator('.camera-toolbar')
  607. // 等待表格数据加载
  608. await page.waitForTimeout(500)
  609. // 从第一行获取真实设备ID
  610. const tableRows = drawer.locator('.tab-content-wrapper tbody tr')
  611. expect(await tableRows.count()).toBeGreaterThan(0)
  612. const realDeviceId = (await tableRows.first().locator('td').first().textContent())?.trim() || ''
  613. // 在设备ID输入框输入并按Enter
  614. const deviceIdInput = toolbar.getByPlaceholder('设备ID')
  615. await deviceIdInput.fill(realDeviceId)
  616. await deviceIdInput.press('Enter')
  617. await page.waitForTimeout(500)
  618. // 验证搜索已执行(检查输入值保留)
  619. await expect(deviceIdInput).toHaveValue(realDeviceId)
  620. // 验证搜索结果有数据
  621. const resultRows = drawer.locator('.tab-content-wrapper tbody tr')
  622. const rowCount = await resultRows.count()
  623. expect(rowCount).toBeGreaterThan(0)
  624. })
  625. /**
  626. * 测试名称输入框按Enter键搜索
  627. */
  628. test('摄像头列表 - 名称输入框按Enter键触发搜索', async ({ page }) => {
  629. await login(page)
  630. const drawer = await openCameraList(page)
  631. const toolbar = drawer.locator('.camera-toolbar')
  632. // 等待表格数据加载
  633. await page.waitForTimeout(500)
  634. // 从第一行获取真实名称
  635. const tableRows = drawer.locator('.tab-content-wrapper tbody tr')
  636. expect(await tableRows.count()).toBeGreaterThan(0)
  637. const realName = (await tableRows.first().locator('td').nth(1).textContent())?.trim() || ''
  638. // 在名称输入框输入并按Enter
  639. const nameInput = toolbar.getByPlaceholder('名称')
  640. await nameInput.fill(realName)
  641. await nameInput.press('Enter')
  642. await page.waitForTimeout(500)
  643. // 验证搜索已执行(检查输入值保留)
  644. await expect(nameInput).toHaveValue(realName)
  645. // 验证搜索结果有数据
  646. const resultRows = drawer.locator('.tab-content-wrapper tbody tr')
  647. const rowCount = await resultRows.count()
  648. expect(rowCount).toBeGreaterThan(0)
  649. })
  650. /**
  651. * 测试表格列显示正确
  652. */
  653. test('摄像头列表 - 验证表格列显示', async ({ page }) => {
  654. await login(page)
  655. const drawer = await openCameraList(page)
  656. // 验证表头列(中英文)
  657. await expect(drawer.locator('th:has-text("设备ID"), th:has-text("Device ID")')).toBeVisible()
  658. await expect(drawer.locator('th:has-text("名称"), th:has-text("Name")')).toBeVisible()
  659. await expect(drawer.locator('th:has-text("状态"), th:has-text("Status")')).toBeVisible()
  660. await expect(drawer.locator('th:has-text("厂商"), th:has-text("Vendor")')).toBeVisible()
  661. await expect(drawer.locator('th:has-text("型号"), th:has-text("Model")')).toBeVisible()
  662. })
  663. /**
  664. * 测试设备总数显示
  665. */
  666. test('摄像头列表 - 验证设备总数显示', async ({ page }) => {
  667. await login(page)
  668. const drawer = await openCameraList(page)
  669. // 等待表格数据加载
  670. await page.waitForTimeout(500)
  671. // 获取表格行数
  672. const tableRows = drawer.locator('.tab-content-wrapper tbody tr')
  673. const rowCount = await tableRows.count()
  674. if (rowCount > 0) {
  675. // 验证总数显示(例如 "共 2 个设备" 或 "Total 2 Device")
  676. await expect(drawer.locator('.camera-count')).toBeVisible()
  677. }
  678. })
  679. })
  680. test.describe('LSS管理 - 摄像头未创建 Live Stream 对话框测试', () => {
  681. // 登录辅助函数
  682. async function login(page: Page) {
  683. await page.goto('/login')
  684. await page.evaluate(() => {
  685. localStorage.clear()
  686. document.cookie.split(';').forEach((c) => {
  687. document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`)
  688. })
  689. })
  690. await page.reload()
  691. await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
  692. await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
  693. await page.getByRole('button', { name: '登录' }).click()
  694. await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 })
  695. }
  696. // 打开摄像头列表辅助函数
  697. async function openCameraList(page: Page) {
  698. await page.goto('/lss-manage/list')
  699. await page.waitForTimeout(1000)
  700. // 点击编辑按钮进入 LSS 编辑抽屉
  701. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  702. await expect(editButton).toBeVisible({ timeout: 10000 })
  703. await editButton.click()
  704. // 等待 LSS 编辑抽屉打开
  705. const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
  706. await expect(drawer).toBeVisible({ timeout: 5000 })
  707. // 点击"摄像头列表" Tab
  708. await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
  709. await page.waitForTimeout(500)
  710. return drawer
  711. }
  712. /**
  713. * 测试点击无 streamSn 的摄像头时显示对话框
  714. */
  715. test('点击无 streamSn 的摄像头时显示"尚未建立 Live Stream"对话框', async ({ page }) => {
  716. await login(page)
  717. // Mock 摄像头列表 API 返回无 streamSn 的数据
  718. await page.route('**/admin/camera/list*', async (route) => {
  719. await route.fulfill({
  720. status: 200,
  721. contentType: 'application/json',
  722. body: JSON.stringify({
  723. success: true,
  724. errCode: 0,
  725. data: {
  726. list: [
  727. {
  728. id: 1,
  729. cameraId: 'CAM_NO_STREAM',
  730. cameraName: '无推流摄像头',
  731. lssId: 'lss-yb',
  732. status: 'active',
  733. streamSn: null // 没有 streamSn
  734. }
  735. ],
  736. total: 1
  737. }
  738. })
  739. })
  740. })
  741. const drawer = await openCameraList(page)
  742. // 等待表格数据加载
  743. await page.waitForTimeout(500)
  744. // 点击摄像头控制按钮(crosshairs-btn)
  745. const crosshairsBtn = drawer.locator('.crosshairs-btn').first()
  746. await expect(crosshairsBtn).toBeVisible({ timeout: 5000 })
  747. await crosshairsBtn.click()
  748. // 验证对话框显示
  749. const messageBox = page.locator('.el-message-box')
  750. await expect(messageBox).toBeVisible({ timeout: 5000 })
  751. // 验证对话框标题
  752. await expect(messageBox.locator('.el-message-box__title')).toContainText('尚未建立 Live Stream')
  753. // 验证对话框内容
  754. await expect(messageBox.locator('.el-message-box__message')).toContainText('请先新增 Live Stream')
  755. // 验证按钮存在
  756. await expect(messageBox.locator('button:has-text("新增 Live Stream")')).toBeVisible()
  757. await expect(messageBox.locator('button:has-text("取消")')).toBeVisible()
  758. })
  759. /**
  760. * 测试点击"取消"按钮关闭对话框
  761. */
  762. test('点击"取消"按钮关闭对话框', async ({ page }) => {
  763. await login(page)
  764. // Mock 摄像头列表 API 返回无 streamSn 的数据
  765. await page.route('**/admin/camera/list*', async (route) => {
  766. await route.fulfill({
  767. status: 200,
  768. contentType: 'application/json',
  769. body: JSON.stringify({
  770. success: true,
  771. errCode: 0,
  772. data: {
  773. list: [
  774. {
  775. id: 1,
  776. cameraId: 'CAM_NO_STREAM',
  777. cameraName: '无推流摄像头',
  778. lssId: 'lss-yb',
  779. status: 'active',
  780. streamSn: null
  781. }
  782. ],
  783. total: 1
  784. }
  785. })
  786. })
  787. })
  788. const drawer = await openCameraList(page)
  789. await page.waitForTimeout(500)
  790. // 点击摄像头控制按钮
  791. const crosshairsBtn = drawer.locator('.crosshairs-btn').first()
  792. await crosshairsBtn.click()
  793. // 等待对话框显示
  794. const messageBox = page.locator('.el-message-box')
  795. await expect(messageBox).toBeVisible({ timeout: 5000 })
  796. // 点击取消按钮
  797. await messageBox.locator('button:has-text("取消")').click()
  798. // 验证对话框关闭
  799. await expect(messageBox).not.toBeVisible({ timeout: 3000 })
  800. // 验证仍在 LSS 页面
  801. await expect(page).toHaveURL(/\/lss-manage\/list/)
  802. })
  803. /**
  804. * 测试点击"新增 Live Stream"按钮跳转到创建页面
  805. */
  806. test('点击"新增 Live Stream"按钮跳转到 live-stream 创建页面', async ({ page }) => {
  807. await login(page)
  808. const drawer = await openCameraList(page)
  809. await page.waitForTimeout(500)
  810. // 找到没有 streamSn 的摄像头(crosshairs-btn 没有 active 类的)
  811. const inactiveCrosshairsBtn = drawer.locator('.crosshairs-btn:not(.active)').first()
  812. // 如果存在无 streamSn 的摄像头
  813. if ((await inactiveCrosshairsBtn.count()) > 0) {
  814. await inactiveCrosshairsBtn.click()
  815. // 等待对话框显示
  816. const messageBox = page.locator('.el-message-box')
  817. await expect(messageBox).toBeVisible({ timeout: 5000 })
  818. // 点击"新增 Live Stream"按钮
  819. await messageBox.locator('button:has-text("新增 Live Stream")').click()
  820. // 验证跳转到 live-stream 页面并带有 action=create 参数
  821. await expect(page).toHaveURL(/\/live-stream\?cameraId=.*&action=create/, { timeout: 5000 })
  822. // 验证新增抽屉自动打开
  823. const liveStreamDrawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' })
  824. await expect(liveStreamDrawer).toBeVisible({ timeout: 5000 })
  825. } else {
  826. // 如果没有无 streamSn 的摄像头,跳过测试
  827. test.skip()
  828. }
  829. })
  830. /**
  831. * 测试有 streamSn 的摄像头直接跳转(不显示对话框)
  832. */
  833. test('有 streamSn 的摄像头直接跳转到 live-stream 页面', async ({ page }) => {
  834. await login(page)
  835. const drawer = await openCameraList(page)
  836. await page.waitForTimeout(500)
  837. // 找到有 streamSn 的摄像头(crosshairs-btn 有 active 类的)
  838. const activeCrosshairsBtn = drawer.locator('.crosshairs-btn.active').first()
  839. // 如果存在有 streamSn 的摄像头
  840. if ((await activeCrosshairsBtn.count()) > 0) {
  841. await activeCrosshairsBtn.click()
  842. // 等待一小段时间检查是否有对话框
  843. await page.waitForTimeout(500)
  844. // 验证没有显示对话框,直接跳转
  845. const messageBox = page.locator('.el-message-box')
  846. // 验证跳转到 live-stream 页面(不带 action=create)
  847. await expect(page).toHaveURL(/\/live-stream\?cameraId=/, { timeout: 5000 })
  848. await expect(page).not.toHaveURL(/action=create/)
  849. } else {
  850. // 如果没有有 streamSn 的摄像头,跳过测试
  851. test.skip()
  852. }
  853. })
  854. })
  855. test.describe.skip('LSS管理 - Bug修复验证测试', () => {
  856. // 登录辅助函数
  857. async function login(page: Page) {
  858. await page.goto('/login')
  859. await page.evaluate(() => {
  860. localStorage.clear()
  861. document.cookie.split(';').forEach((c) => {
  862. document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`)
  863. })
  864. })
  865. await page.reload()
  866. await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
  867. await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
  868. await page.getByRole('button', { name: '登录' }).click()
  869. await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 })
  870. }
  871. /**
  872. * Bug #4535: 重置按钮需要清除所有搜索条件(包括排序状态)
  873. */
  874. test('Bug #4535 - 重置按钮清除所有搜索条件包括排序状态', async ({ page }) => {
  875. await login(page)
  876. await page.goto('/lss-manage/list')
  877. // 等待表格加载
  878. await page.waitForTimeout(1000)
  879. // 填入搜索条件
  880. await page.getByPlaceholder('LSS ID').fill('test-id')
  881. await page.getByPlaceholder('名称').fill('test-name')
  882. // 点击表头进行排序
  883. const lssIdHeader = page.locator('th:has-text("LSS ID")')
  884. await lssIdHeader.click()
  885. await page.waitForTimeout(300)
  886. // 验证排序图标出现(使用 .first() 避免 strict mode 错误)
  887. await expect(lssIdHeader.locator('.caret-wrapper').first()).toBeVisible()
  888. // 点击重置
  889. await page.getByRole('button', { name: '重置' }).click()
  890. await page.waitForTimeout(300)
  891. // 验证搜索条件已清空
  892. await expect(page.getByPlaceholder('LSS ID')).toHaveValue('')
  893. await expect(page.getByPlaceholder('名称')).toHaveValue('')
  894. // TODO: 验证排序状态已重置(表头不再显示排序方向)
  895. // Bug #4535 尚未完全修复 - 重置按钮目前只清空搜索条件,不清空排序状态
  896. // 当 Bug #4535 修复后,取消下面的注释以启用排序状态验证
  897. // const headerClasses = await lssIdHeader.getAttribute('class') || ''
  898. // expect(headerClasses).not.toMatch(/\bascending\b/)
  899. // expect(headerClasses).not.toMatch(/\bdescending\b/)
  900. })
  901. /**
  902. * Bug #4536: 心跳状态显示为英文(active/hold/dead)而非中文
  903. */
  904. test('Bug #4536 - 心跳状态显示为英文格式', async ({ page }) => {
  905. await login(page)
  906. await page.goto('/lss-manage/list')
  907. // 等待表格加载
  908. await page.waitForTimeout(1000)
  909. // 获取心跳列的内容
  910. const heartbeatColumn = page.locator('tbody tr').first().locator('td').nth(4)
  911. const heartbeatText = await heartbeatColumn.textContent()
  912. // 验证心跳状态是英文格式(active/hold/dead),而非中文(活跃/待机/离线)
  913. // 心跳列格式: "status | time" 例如 "active | 2024-01-26 10:00:00"
  914. expect(heartbeatText).toMatch(/active|hold|dead/i)
  915. // 不应该包含中文状态
  916. expect(heartbeatText).not.toMatch(/活跃|待机|离线/)
  917. })
  918. /**
  919. * Bug #4537: 页面标题从"LSS 管理"改为"LSS 列表"
  920. */
  921. test('Bug #4537 - 页面标题显示为"LSS 列表"', async ({ page }) => {
  922. await login(page)
  923. await page.goto('/lss-manage/list')
  924. // 等待页面加载
  925. await page.waitForTimeout(500)
  926. // 验证侧边栏菜单显示"LSS 列表"
  927. await expect(page.locator('.el-menu-item.is-active, .el-sub-menu.is-active').first()).toContainText('LSS 列表')
  928. // 验证页面内容区域标题(如果有面包屑或页面标题)
  929. // 注意:根据实际UI调整选择器
  930. const pageTitle = page.locator('text=LSS 列表').first()
  931. await expect(pageTitle).toBeVisible()
  932. })
  933. /**
  934. * Bug #4538: 分页默认显示15条/页
  935. */
  936. test('Bug #4538 - 分页默认显示15条/页', async ({ page }) => {
  937. await login(page)
  938. await page.goto('/lss-manage/list')
  939. // 等待表格加载
  940. await page.waitForTimeout(1000)
  941. // 验证分页选择器的默认值是15
  942. const pageSizeSelector = page.locator(
  943. '.el-pagination .el-select .el-input__inner, .el-pagination .el-select-v2__placeholder'
  944. )
  945. const pageSizeText = (await pageSizeSelector.first().textContent()) || (await pageSizeSelector.first().inputValue())
  946. // 验证默认每页显示15条
  947. expect(pageSizeText).toContain('15')
  948. })
  949. /**
  950. * Bug #4538: 分页选项包含15
  951. */
  952. test('Bug #4538 - 分页选项包含15条/页选项', async ({ page }) => {
  953. await login(page)
  954. await page.goto('/lss-manage/list')
  955. // 等待表格加载
  956. await page.waitForTimeout(1000)
  957. // 点击分页选择器打开下拉菜单
  958. const pageSizeSelector = page.locator('.el-pagination .el-select')
  959. await pageSizeSelector.click()
  960. await page.waitForTimeout(300)
  961. // 验证下拉菜单中包含15选项
  962. const dropdown = page.locator('.el-select-dropdown, .el-popper')
  963. await expect(dropdown.locator('text=15')).toBeVisible()
  964. // 验证选项顺序正确: 10, 15, 20, 50, 100
  965. const options = dropdown.locator('.el-select-dropdown__item')
  966. const optionsText = await options.allTextContents()
  967. // 验证包含所有期望的选项
  968. expect(optionsText.some((t) => t.includes('10'))).toBeTruthy()
  969. expect(optionsText.some((t) => t.includes('15'))).toBeTruthy()
  970. expect(optionsText.some((t) => t.includes('20'))).toBeTruthy()
  971. expect(optionsText.some((t) => t.includes('50'))).toBeTruthy()
  972. expect(optionsText.some((t) => t.includes('100'))).toBeTruthy()
  973. })
  974. /**
  975. * Bug #4540: 查询按钮使用蓝底(409EFF)白字
  976. */
  977. test('Bug #4540 - 查询按钮使用蓝底白字', async ({ page }) => {
  978. await login(page)
  979. await page.goto('/lss-manage/list')
  980. // 等待页面加载
  981. await page.waitForTimeout(500)
  982. // 获取查询按钮
  983. const searchButton = page.locator('[data-id="btn-search"]')
  984. await expect(searchButton).toBeVisible()
  985. // 验证按钮有 el-button--primary 类(蓝色按钮)
  986. await expect(searchButton).toHaveClass(/el-button--primary/)
  987. // 验证按钮背景色接近 #409EFF
  988. const bgColor = await searchButton.evaluate((el) => {
  989. return window.getComputedStyle(el).backgroundColor
  990. })
  991. // #409EFF 的 RGB 值为 rgb(64, 158, 255)
  992. expect(bgColor).toMatch(/rgb\(64,\s*158,\s*255\)|rgba\(64,\s*158,\s*255/)
  993. })
  994. /**
  995. * Bug #4541: 重置按钮使用灰底(909399)白字
  996. */
  997. test('Bug #4541 - 重置按钮使用灰底白字', async ({ page }) => {
  998. await login(page)
  999. await page.goto('/lss-manage/list')
  1000. // 等待页面加载
  1001. await page.waitForTimeout(500)
  1002. // 获取重置按钮
  1003. const resetButton = page.locator('[data-id="btn-reset"]')
  1004. await expect(resetButton).toBeVisible()
  1005. // 验证按钮有 el-button--info 类(灰色按钮)
  1006. await expect(resetButton).toHaveClass(/el-button--info/)
  1007. // 验证按钮背景色接近 #909399
  1008. const bgColor = await resetButton.evaluate((el) => {
  1009. return window.getComputedStyle(el).backgroundColor
  1010. })
  1011. // #909399 的 RGB 值为 rgb(144, 147, 153)
  1012. expect(bgColor).toMatch(/rgb\(144,\s*147,\s*153\)|rgba\(144,\s*147,\s*153/)
  1013. })
  1014. /**
  1015. * Bug #4542: 分页当前页背景色使用蓝底(409EFF)白字
  1016. */
  1017. test('Bug #4542 - 分页当前页使用蓝底白字', async ({ page }) => {
  1018. await login(page)
  1019. await page.goto('/lss-manage/list')
  1020. // 等待表格加载
  1021. await page.waitForTimeout(1000)
  1022. // 获取分页组件中当前激活的页码按钮
  1023. const activePager = page.locator('.el-pagination .el-pager .is-active, .el-pagination .el-pager .active')
  1024. await expect(activePager).toBeVisible()
  1025. // 验证当前页背景色接近 #409EFF
  1026. const bgColor = await activePager.evaluate((el) => {
  1027. return window.getComputedStyle(el).backgroundColor
  1028. })
  1029. // #409EFF 的 RGB 值为 rgb(64, 158, 255)
  1030. expect(bgColor).toMatch(/rgb\(64,\s*158,\s*255\)|rgba\(64,\s*158,\s*255/)
  1031. })
  1032. /**
  1033. * Bug #4557: 摄像头列表状态下拉框显示 active/hold/dead
  1034. */
  1035. test('Bug #4557 - 摄像头列表状态下拉框显示英文选项', async ({ page }) => {
  1036. await login(page)
  1037. await page.goto('/lss-manage/list')
  1038. // 等待表格加载
  1039. await page.waitForTimeout(1000)
  1040. // 点击编辑按钮进入 LSS 编辑抽屉
  1041. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  1042. await expect(editButton).toBeVisible({ timeout: 10000 })
  1043. await editButton.click()
  1044. // 等待 LSS 编辑抽屉打开
  1045. const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
  1046. await expect(drawer).toBeVisible({ timeout: 5000 })
  1047. // 点击"摄像头列表" Tab
  1048. await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
  1049. await page.waitForTimeout(500)
  1050. // 点击状态下拉框
  1051. const statusSelect = drawer.locator('.camera-toolbar .el-select').first()
  1052. await statusSelect.click()
  1053. await page.waitForTimeout(300)
  1054. // 验证下拉选项包含 active/hold/dead 而非中文
  1055. const dropdown = page.locator('.el-select-dropdown, .el-popper').last()
  1056. const options = dropdown.locator('.el-select-dropdown__item')
  1057. const optionsText = await options.allTextContents()
  1058. // 验证包含英文选项
  1059. expect(optionsText.some((t) => t.includes('active'))).toBeTruthy()
  1060. expect(optionsText.some((t) => t.includes('hold'))).toBeTruthy()
  1061. expect(optionsText.some((t) => t.includes('dead'))).toBeTruthy()
  1062. // 验证不包含中文状态
  1063. expect(optionsText.some((t) => t.includes('在线'))).toBeFalsy()
  1064. expect(optionsText.some((t) => t.includes('离线'))).toBeFalsy()
  1065. })
  1066. /**
  1067. * Bug #4558: 摄像头列表没有数据时表头要显示
  1068. */
  1069. test('Bug #4558 - 摄像头列表无数据时显示表头', async ({ page }) => {
  1070. await login(page)
  1071. // Mock 摄像头列表 API 返回空数据
  1072. await page.route('**/admin/camera/list*', async (route) => {
  1073. await route.fulfill({
  1074. status: 200,
  1075. contentType: 'application/json',
  1076. body: JSON.stringify({
  1077. success: true,
  1078. errCode: 0,
  1079. data: {
  1080. list: [],
  1081. total: 0
  1082. }
  1083. })
  1084. })
  1085. })
  1086. await page.goto('/lss-manage/list')
  1087. await page.waitForTimeout(1000)
  1088. // 点击编辑按钮进入 LSS 编辑抽屉
  1089. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  1090. await expect(editButton).toBeVisible({ timeout: 10000 })
  1091. await editButton.click()
  1092. // 等待 LSS 编辑抽屉打开
  1093. const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
  1094. await expect(drawer).toBeVisible({ timeout: 5000 })
  1095. // 点击"摄像头列表" Tab
  1096. await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
  1097. await page.waitForTimeout(500)
  1098. // 验证表头可见(即使没有数据)
  1099. await expect(drawer.locator('th:has-text("设备ID")')).toBeVisible()
  1100. await expect(drawer.locator('th:has-text("名称")')).toBeVisible()
  1101. await expect(drawer.locator('th:has-text("状态")')).toBeVisible()
  1102. // 验证空状态提示显示
  1103. await expect(drawer.locator('.el-empty')).toBeVisible()
  1104. })
  1105. /**
  1106. * Bug #4593: 删除提示显示正确的设备名称而非undefined
  1107. */
  1108. test('Bug #4593 - 删除提示显示正确的设备名称', async ({ page }) => {
  1109. await login(page)
  1110. // Mock 摄像头列表 API 返回有数据
  1111. const testCameraName = '测试摄像头001'
  1112. await page.route('**/admin/camera/list*', async (route) => {
  1113. await route.fulfill({
  1114. status: 200,
  1115. contentType: 'application/json',
  1116. body: JSON.stringify({
  1117. success: true,
  1118. errCode: 0,
  1119. data: {
  1120. list: [
  1121. {
  1122. id: 1,
  1123. cameraId: 'CAM_TEST_001',
  1124. cameraName: testCameraName,
  1125. lssId: 'lss-yb',
  1126. status: 'active',
  1127. streamSn: 'STREAM_001'
  1128. }
  1129. ],
  1130. total: 1
  1131. }
  1132. })
  1133. })
  1134. })
  1135. await page.goto('/lss-manage/list')
  1136. await page.waitForTimeout(1000)
  1137. // 点击编辑按钮进入 LSS 编辑抽屉
  1138. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  1139. await expect(editButton).toBeVisible({ timeout: 10000 })
  1140. await editButton.click()
  1141. // 等待 LSS 编辑抽屉打开
  1142. const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
  1143. await expect(drawer).toBeVisible({ timeout: 5000 })
  1144. // 点击"摄像头列表" Tab
  1145. await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
  1146. await page.waitForTimeout(500)
  1147. // 点击删除按钮
  1148. const deleteButton = drawer
  1149. .locator('tbody tr')
  1150. .first()
  1151. .locator('button[type="button"]')
  1152. .filter({ hasText: '' })
  1153. .last()
  1154. // 使用 Icon 选择器更精确
  1155. const deleteIcon = drawer.locator('tbody tr').first().locator('.iconify--mdi[data-icon*="delete"]').first()
  1156. if ((await deleteIcon.count()) > 0) {
  1157. await deleteIcon.click()
  1158. } else {
  1159. // 备选方案:点击最后一个按钮(删除按钮)
  1160. await drawer.locator('tbody tr').first().locator('button').last().click()
  1161. }
  1162. // 等待确认对话框
  1163. const messageBox = page.locator('.el-message-box')
  1164. await expect(messageBox).toBeVisible({ timeout: 5000 })
  1165. // 验证对话框中显示正确的设备名称,而非 "undefined"
  1166. const messageText = await messageBox.locator('.el-message-box__message').textContent()
  1167. expect(messageText).toContain(testCameraName)
  1168. expect(messageText).not.toContain('undefined')
  1169. // 关闭对话框
  1170. await messageBox.locator('button:has-text("取消")').click()
  1171. })
  1172. /**
  1173. * Bug #4569: 编辑摄像头标题改为"摄像头详情"
  1174. */
  1175. test('Bug #4569 - 编辑摄像头抽屉标题显示"摄像头详情"', async ({ page }) => {
  1176. await login(page)
  1177. await page.goto('/lss-manage/list')
  1178. await page.waitForTimeout(1000)
  1179. // 点击编辑按钮进入 LSS 编辑抽屉
  1180. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  1181. await expect(editButton).toBeVisible({ timeout: 10000 })
  1182. await editButton.click()
  1183. // 等待 LSS 编辑抽屉打开
  1184. const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
  1185. await expect(drawer).toBeVisible({ timeout: 5000 })
  1186. // 点击"摄像头列表" Tab
  1187. await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
  1188. await page.waitForTimeout(500)
  1189. // 点击摄像头编辑按钮
  1190. const cameraEditButton = drawer.locator('tbody tr').first().locator('button').first()
  1191. if ((await cameraEditButton.count()) > 0) {
  1192. await cameraEditButton.click()
  1193. // 等待摄像头详情抽屉打开
  1194. const cameraDrawer = page.locator('.el-drawer').filter({ hasText: '摄像头详情' })
  1195. await expect(cameraDrawer).toBeVisible({ timeout: 5000 })
  1196. // 验证标题是"摄像头详情"而非"编辑摄像头"
  1197. await expect(cameraDrawer.locator('.el-drawer__header, .el-drawer__title')).toContainText('摄像头详情')
  1198. }
  1199. })
  1200. /**
  1201. * Bug #4570: 摄像头详情增加"添加时间"字段
  1202. */
  1203. test('Bug #4570 - 摄像头详情显示添加时间', async ({ page }) => {
  1204. await login(page)
  1205. // Mock 摄像头详情 API 返回带有 createdAt 的数据
  1206. await page.route('**/admin/camera/get*', async (route) => {
  1207. await route.fulfill({
  1208. status: 200,
  1209. contentType: 'application/json',
  1210. body: JSON.stringify({
  1211. success: true,
  1212. errCode: 0,
  1213. data: {
  1214. id: 1,
  1215. cameraId: 'CAM_TEST_001',
  1216. cameraName: '测试摄像头',
  1217. lssId: 'lss-yb',
  1218. status: 'active',
  1219. vendorName: 'hikvision',
  1220. model: 'DS-2CD2043G0-I',
  1221. createdAt: '2024-01-15T10:30:00Z'
  1222. }
  1223. })
  1224. })
  1225. })
  1226. await page.goto('/lss-manage/list')
  1227. await page.waitForTimeout(1000)
  1228. // 点击编辑按钮进入 LSS 编辑抽屉
  1229. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  1230. await expect(editButton).toBeVisible({ timeout: 10000 })
  1231. await editButton.click()
  1232. // 等待 LSS 编辑抽屉打开
  1233. const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
  1234. await expect(drawer).toBeVisible({ timeout: 5000 })
  1235. // 点击"摄像头列表" Tab
  1236. await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
  1237. await page.waitForTimeout(500)
  1238. // 点击摄像头编辑按钮
  1239. const cameraEditButton = drawer.locator('tbody tr').first().locator('button').first()
  1240. if ((await cameraEditButton.count()) > 0) {
  1241. await cameraEditButton.click()
  1242. // 等待摄像头详情抽屉打开
  1243. const cameraDrawer = page.locator('.el-drawer').filter({ hasText: '摄像头详情' })
  1244. await expect(cameraDrawer).toBeVisible({ timeout: 5000 })
  1245. // 验证有"添加时间"字段
  1246. await expect(cameraDrawer.locator('label:has-text("添加时间")')).toBeVisible()
  1247. // 验证添加时间值显示正确格式
  1248. const timeValue = cameraDrawer.locator('label:has-text("添加时间")').locator('..').locator('.form-value')
  1249. const timeText = await timeValue.textContent()
  1250. // 验证时间格式或包含日期
  1251. expect(timeText).toMatch(/\d{4}-\d{2}-\d{2}|\-/)
  1252. }
  1253. })
  1254. /**
  1255. * Bug #4545: 按钮大小统一,圆角框
  1256. */
  1257. test('Bug #4545 - 按钮有统一的圆角', async ({ page }) => {
  1258. await login(page)
  1259. await page.goto('/lss-manage/list')
  1260. // 等待页面加载
  1261. await page.waitForTimeout(500)
  1262. // 获取查询按钮
  1263. const searchButton = page.locator('[data-id="btn-search"]')
  1264. await expect(searchButton).toBeVisible()
  1265. // 验证按钮有圆角
  1266. const borderRadius = await searchButton.evaluate((el) => {
  1267. return window.getComputedStyle(el).borderRadius
  1268. })
  1269. // 验证圆角值不为0
  1270. expect(borderRadius).not.toBe('0px')
  1271. expect(parseInt(borderRadius)).toBeGreaterThanOrEqual(4)
  1272. })
  1273. /**
  1274. * Bug #4549: LSS列表作为「LSS 管理」的子菜单
  1275. */
  1276. test('Bug #4549 - LSS列表作为LSS管理的子菜单', async ({ page }) => {
  1277. await login(page)
  1278. // 等待侧边栏加载
  1279. await page.waitForTimeout(500)
  1280. // 验证"LSS 管理"父菜单存在
  1281. const lssManageMenu = page.locator('.layout__nav-item--parent').filter({ hasText: 'LSS 管理' })
  1282. await expect(lssManageMenu).toBeVisible()
  1283. // 点击展开子菜单
  1284. await lssManageMenu.click()
  1285. await page.waitForTimeout(300)
  1286. // 验证"LSS 列表"子菜单存在
  1287. const lssListMenu = page.locator('.layout__nav-item--child').filter({ hasText: 'LSS 列表' })
  1288. await expect(lssListMenu).toBeVisible()
  1289. // 点击子菜单导航到页面
  1290. await lssListMenu.click()
  1291. await expect(page).toHaveURL(/\/lss-manage\/list/)
  1292. })
  1293. /**
  1294. * Bug #4554 & #4555: 摄像头列表搜索字段
  1295. */
  1296. test('Bug #4554/#4555 - 摄像头列表有设备ID和设备名称搜索字段', async ({ page }) => {
  1297. await login(page)
  1298. await page.goto('/lss-manage/list')
  1299. await page.waitForTimeout(1000)
  1300. // 点击编辑按钮进入 LSS 编辑抽屉
  1301. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  1302. await expect(editButton).toBeVisible({ timeout: 10000 })
  1303. await editButton.click()
  1304. // 等待 LSS 编辑抽屉打开
  1305. const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
  1306. await expect(drawer).toBeVisible({ timeout: 5000 })
  1307. // 点击"摄像头列表" Tab
  1308. await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
  1309. await page.waitForTimeout(500)
  1310. // 验证有"设备ID"搜索框
  1311. await expect(drawer.getByPlaceholder('设备ID')).toBeVisible()
  1312. // 验证有"名称"搜索框
  1313. await expect(drawer.getByPlaceholder('名称')).toBeVisible()
  1314. // 验证没有"IP / 设备ID / 名称"混合搜索框
  1315. const mixedSearch = drawer.getByPlaceholder('IP / 设备ID / 名称')
  1316. await expect(mixedSearch).not.toBeVisible()
  1317. })
  1318. /**
  1319. * Bug #4562: 摄像头列表有分页功能
  1320. */
  1321. test('Bug #4562 - 摄像头列表有分页功能', async ({ page }) => {
  1322. await login(page)
  1323. await page.goto('/lss-manage/list')
  1324. await page.waitForTimeout(1000)
  1325. // 点击编辑按钮进入 LSS 编辑抽屉
  1326. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  1327. await expect(editButton).toBeVisible({ timeout: 10000 })
  1328. await editButton.click()
  1329. // 等待 LSS 编辑抽屉打开
  1330. const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
  1331. await expect(drawer).toBeVisible({ timeout: 5000 })
  1332. // 点击"摄像头列表" Tab
  1333. await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
  1334. await page.waitForTimeout(500)
  1335. // 验证分页组件存在
  1336. await expect(drawer.locator('.el-pagination')).toBeVisible()
  1337. })
  1338. /**
  1339. * Bug #4567: List厂商显示和详情页一样
  1340. */
  1341. test('Bug #4567 - 厂商列使用vendorName字段', async ({ page }) => {
  1342. await login(page)
  1343. // Mock 摄像头列表 API 返回带有 vendorName 的数据
  1344. await page.route('**/admin/camera/list*', async (route) => {
  1345. await route.fulfill({
  1346. status: 200,
  1347. contentType: 'application/json',
  1348. body: JSON.stringify({
  1349. success: true,
  1350. errCode: 0,
  1351. data: {
  1352. list: [
  1353. {
  1354. id: 1,
  1355. cameraId: 'CAM_001',
  1356. cameraName: '测试摄像头',
  1357. lssId: 'lss-yb',
  1358. status: 'active',
  1359. vendorName: 'hikvision',
  1360. model: 'DS-2CD2T45'
  1361. }
  1362. ],
  1363. total: 1
  1364. }
  1365. })
  1366. })
  1367. })
  1368. await page.goto('/lss-manage/list')
  1369. await page.waitForTimeout(1000)
  1370. // 点击编辑按钮进入 LSS 编辑抽屉
  1371. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  1372. await expect(editButton).toBeVisible({ timeout: 10000 })
  1373. await editButton.click()
  1374. // 等待 LSS 编辑抽屉打开
  1375. const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
  1376. await expect(drawer).toBeVisible({ timeout: 5000 })
  1377. // 点击"摄像头列表" Tab
  1378. await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
  1379. await page.waitForTimeout(500)
  1380. // 验证厂商列显示正确的格式化名称
  1381. const vendorCell = drawer.locator('tbody tr').first().locator('td').nth(5)
  1382. const vendorText = await vendorCell.textContent()
  1383. expect(vendorText).toContain('HIKVISION')
  1384. })
  1385. /**
  1386. * Bug #4588: 参数配置更新按钮蓝底白字
  1387. */
  1388. test('Bug #4588 - 参数配置更新按钮使用蓝底白字', async ({ page }) => {
  1389. await login(page)
  1390. await page.goto('/lss-manage/list')
  1391. await page.waitForTimeout(1000)
  1392. // 点击编辑按钮进入 LSS 编辑抽屉
  1393. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  1394. await expect(editButton).toBeVisible({ timeout: 10000 })
  1395. await editButton.click()
  1396. // 等待 LSS 编辑抽屉打开
  1397. const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
  1398. await expect(drawer).toBeVisible({ timeout: 5000 })
  1399. // 点击"摄像头列表" Tab
  1400. await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
  1401. await page.waitForTimeout(500)
  1402. // 点击参数配置"查看"按钮
  1403. const viewButton = drawer.locator('tbody tr').first().locator('button:has-text("查看")').first()
  1404. if ((await viewButton.count()) > 0) {
  1405. await viewButton.click()
  1406. // 等待参数配置抽屉打开
  1407. const paramsDrawer = page.locator('.params-drawer')
  1408. await expect(paramsDrawer).toBeVisible({ timeout: 5000 })
  1409. // 验证更新按钮是蓝色
  1410. const updateButton = paramsDrawer.locator('button:has-text("更新")')
  1411. await expect(updateButton).toHaveClass(/el-button--primary/)
  1412. }
  1413. })
  1414. /**
  1415. * Bug #4594: 删除确认对话框确定按钮蓝底白字
  1416. */
  1417. test('Bug #4594 - 删除确认对话框确定按钮蓝底白字', async ({ page }) => {
  1418. await login(page)
  1419. // Mock 摄像头列表 API
  1420. await page.route('**/admin/camera/list*', async (route) => {
  1421. await route.fulfill({
  1422. status: 200,
  1423. contentType: 'application/json',
  1424. body: JSON.stringify({
  1425. success: true,
  1426. errCode: 0,
  1427. data: {
  1428. list: [{ id: 1, cameraId: 'CAM_001', cameraName: '测试', status: 'active' }],
  1429. total: 1
  1430. }
  1431. })
  1432. })
  1433. })
  1434. await page.goto('/lss-manage/list')
  1435. await page.waitForTimeout(1000)
  1436. // 点击编辑按钮进入 LSS 编辑抽屉
  1437. const editButton = page.locator('tbody tr').first().locator('button').nth(1)
  1438. await expect(editButton).toBeVisible({ timeout: 10000 })
  1439. await editButton.click()
  1440. // 等待 LSS 编辑抽屉打开
  1441. const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
  1442. await expect(drawer).toBeVisible({ timeout: 5000 })
  1443. // 点击"摄像头列表" Tab
  1444. await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
  1445. await page.waitForTimeout(500)
  1446. // 点击删除按钮
  1447. await drawer.locator('tbody tr').first().locator('button').last().click()
  1448. // 等待确认对话框
  1449. const messageBox = page.locator('.el-message-box')
  1450. await expect(messageBox).toBeVisible({ timeout: 5000 })
  1451. // 验证确定按钮是蓝色
  1452. const confirmButton = messageBox.locator('button.el-button--primary')
  1453. await expect(confirmButton).toBeVisible()
  1454. const bgColor = await confirmButton.evaluate((el) => {
  1455. return window.getComputedStyle(el).backgroundColor
  1456. })
  1457. // #409EFF 的 RGB 值为 rgb(64, 158, 255)
  1458. expect(bgColor).toMatch(/rgb\(64,\s*158,\s*255\)|rgba\(64,\s*158,\s*255/)
  1459. // 关闭对话框
  1460. await messageBox.locator('button:has-text("取消")').click()
  1461. })
  1462. })