import { test, expect, type Page } from '@playwright/test' // 测试账号配置 const TEST_USERNAME = process.env.TEST_USERNAME || 'admin' const TEST_PASSWORD = process.env.TEST_PASSWORD || '123456' // 登录辅助函数 async function login(page: Page) { await page.goto('/login') await page.evaluate(() => { localStorage.clear() document.cookie.split(';').forEach((c) => { document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`) }) }) await page.reload() await page.getByPlaceholder('用户名').fill(TEST_USERNAME) await page.getByPlaceholder('密码').fill(TEST_PASSWORD) await page.getByRole('button', { name: '登录' }).click() await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 }) } test.describe('LiveStream 管理 - 搜索功能测试', () => { /** * 按 Stream SN 搜索 */ test('按 Stream SN 搜索 - 验证参数传递', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 设置 API 拦截来验证参数 let requestBody: any = null await page.route('**/admin/live-stream/list', async (route) => { const request = route.request() if (request.method() === 'POST') { requestBody = request.postDataJSON() } await route.continue() }) // 在 stream sn 搜索框输入 await page.getByPlaceholder('stream sn').fill('stream_123') // 点击查询 await page.getByRole('button', { name: '查询' }).click() await page.waitForTimeout(1000) // 验证请求参数中包含 streamSn expect(requestBody).not.toBeNull() expect(requestBody.streamSn).toBe('stream_123') }) /** * 按 Name 搜索 */ test('按 Name 搜索 - 验证参数传递', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 设置 API 拦截 let requestBody: any = null await page.route('**/admin/live-stream/list', async (route) => { const request = route.request() if (request.method() === 'POST') { requestBody = request.postDataJSON() } await route.continue() }) // 在 name 搜索框输入 await page.getByPlaceholder('名称').fill('测试流') // 点击查询 await page.getByRole('button', { name: '查询' }).click() await page.waitForTimeout(1000) // 验证请求参数中包含 name expect(requestBody).not.toBeNull() expect(requestBody.name).toBe('测试流') }) /** * 按 LSS 搜索 */ test('按 LSS 搜索 - 验证参数传递', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 设置 API 拦截 let requestBody: any = null await page.route('**/admin/live-stream/list', async (route) => { const request = route.request() if (request.method() === 'POST') { requestBody = request.postDataJSON() } await route.continue() }) // 点击 LSS 下拉框 await page.locator('.el-select').filter({ hasText: 'LSS' }).click() await page.waitForTimeout(300) // 选择第一个 LSS 选项(如果有) const firstOption = page.locator('.el-select-dropdown__item').first() if (await firstOption.isVisible()) { const lssValue = await firstOption.textContent() await firstOption.click() // 点击查询 await page.getByRole('button', { name: '查询' }).click() await page.waitForTimeout(1000) // 验证请求参数中包含 lssId expect(requestBody).not.toBeNull() expect(requestBody.lssId).toBe(lssValue?.trim()) } }) /** * 按设备ID搜索 */ test('按设备ID搜索 - 验证参数传递', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 设置 API 拦截 let requestBody: any = null await page.route('**/admin/live-stream/list', async (route) => { const request = route.request() if (request.method() === 'POST') { requestBody = request.postDataJSON() } await route.continue() }) // 在设备ID搜索框输入 await page.getByPlaceholder('设备ID').fill('EEE1') // 点击查询 await page.getByRole('button', { name: '查询' }).click() await page.waitForTimeout(1000) // 验证请求参数中包含 cameraId expect(requestBody).not.toBeNull() expect(requestBody.cameraId).toBe('EEE1') }) /** * 组合搜索 - 所有字段 */ test('组合搜索 - 多字段同时搜索', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 设置 API 拦截 let requestBody: any = null await page.route('**/admin/live-stream/list', async (route) => { const request = route.request() if (request.method() === 'POST') { requestBody = request.postDataJSON() } await route.continue() }) // 填入所有搜索条件 await page.getByPlaceholder('stream sn').fill('stream_001') await page.getByPlaceholder('名称').fill('测试') await page.getByPlaceholder('设备ID').fill('EEE1') // 点击查询 await page.getByRole('button', { name: '查询' }).click() await page.waitForTimeout(1000) // 验证请求参数包含所有字段 expect(requestBody).not.toBeNull() expect(requestBody.streamSn).toBe('stream_001') expect(requestBody.name).toBe('测试') expect(requestBody.cameraId).toBe('EEE1') }) /** * 重置搜索条件 */ test('重置搜索条件 - 清空所有输入', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 填入搜索条件 await page.getByPlaceholder('stream sn').fill('test-sn') await page.getByPlaceholder('名称').fill('test-name') await page.getByPlaceholder('设备ID').fill('test-device') // 点击重置 await page.getByRole('button', { name: '重置' }).click() await page.waitForTimeout(300) // 验证搜索条件已清空 await expect(page.getByPlaceholder('stream sn')).toHaveValue('') await expect(page.getByPlaceholder('name')).toHaveValue('') await expect(page.getByPlaceholder('设备ID')).toHaveValue('') }) }) test.describe('LiveStream 管理 - BUG 回归测试', () => { /** * BUG 回归测试:按设备ID搜索应该只返回匹配的记录 * * 问题描述: * - 前端已修复:searchForm.cameraId 参数现在会正确传递给 API * - 后端待修复:API 目前未实现 cameraId 过滤,返回所有数据 * * 预期行为: * - 搜索设备ID "EEE1" 应该只返回 1 条记录 * - 该记录的设备ID列应该显示 "EEE1" * * 当前状态:测试会失败,等待后端实现过滤 */ test('BUG: 按设备ID搜索 EEE1 应该只返回 1 条匹配记录', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) await page.waitForTimeout(500) // 在设备ID搜索框输入 "EEE1" await page.getByPlaceholder('设备ID').fill('EEE1') // 点击查询按钮 await page.getByRole('button', { name: '查询' }).click() // 等待搜索结果加载 await page.waitForTimeout(1000) // 验证:应该只有 1 条记录 const tableRows = page.locator('tbody tr') const rowCount = await tableRows.count() expect(rowCount).toBe(1) // 验证:该记录的设备ID应该是 "EEE1" const firstRowDeviceId = await tableRows.first().locator('td').nth(3).textContent() expect(firstRowDeviceId?.trim()).toBe('EEE1') // 验证:分页显示 Total 1 await expect(page.locator('.el-pagination')).toContainText('Total 1') }) }) test.describe('LiveStream 管理 - 页面功能测试', () => { test('LiveStream 管理页面正确显示', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 验证页面标题 await expect(page.locator('text=LiveStream 管理')).toBeVisible() // 验证搜索表单元素 await expect(page.getByPlaceholder('stream sn')).toBeVisible() await expect(page.getByPlaceholder('name')).toBeVisible() await expect(page.getByPlaceholder('设备ID')).toBeVisible() await expect(page.getByRole('button', { name: '查询' })).toBeVisible() await expect(page.getByRole('button', { name: '重置' })).toBeVisible() await expect(page.getByRole('button', { name: '新增' })).toBeVisible() // 验证表头 await expect(page.locator('th:has-text("Stream SN"), th:has-text("stream sn")')).toBeVisible() await expect(page.locator('th:has-text("Name"), th:has-text("名称")')).toBeVisible() await expect(page.locator('th:has-text("LSS")')).toBeVisible() await expect(page.locator('th:has-text("Device ID"), th:has-text("设备ID")')).toBeVisible() }) test('打开新增 LiveStream 抽屉', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 点击新增按钮 await page.getByRole('button', { name: '新增' }).click() // 验证抽屉打开 const drawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' }) await expect(drawer).toBeVisible({ timeout: 5000 }) // 验证表单元素 await expect(drawer.locator('label:has-text("名称")')).toBeVisible() await expect(drawer.locator('label:has-text("LSS 节点")')).toBeVisible() await expect(drawer.locator('label:has-text("摄像头")')).toBeVisible() // 关闭抽屉 await drawer.getByRole('button', { name: '取消' }).click() await expect(drawer).not.toBeVisible({ timeout: 5000 }) }) test('分页功能正常', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForTimeout(1000) // 验证分页组件存在 const pagination = page.locator('.el-pagination') await expect(pagination).toBeVisible() // 验证 Total 显示 await expect(pagination.locator('text=/Total \\d+/')).toBeVisible() }) test('从侧边栏导航到 LiveStream 管理', async ({ page }) => { await login(page) // 点击侧边栏 LiveStream 管理菜单项 await page.getByText('LiveStream 管理').first().click() // 验证跳转到 LiveStream 管理页面 await expect(page).toHaveURL(/\/live-stream-manage\/list/) await expect(page.locator('text=LiveStream 管理')).toBeVisible() }) }) test.describe('LiveStream 管理 - CodeEditor 组件测试 (Bash模式)', () => { /** * 测试 CodeEditor Bash 模式 - 头部显示 */ test('CodeEditor Bash模式 - 验证头部显示Bash Script标签和复制按钮', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 点击命令模板列的"查看"链接 const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")') await expect(viewLink).toBeVisible({ timeout: 5000 }) await viewLink.click() // 等待命令模板弹窗打开 const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' }) await expect(dialog).toBeVisible({ timeout: 5000 }) // 验证 CodeEditor 头部显示 Bash Script 标签 const codeEditor = dialog.locator('.code-editor') await expect(codeEditor).toBeVisible() await expect(codeEditor.locator('.editor-header')).toBeVisible() await expect(codeEditor.locator('.file-type')).toContainText('Bash Script') // 验证 Copy 按钮存在 await expect(codeEditor.locator('button:has-text("复制"), button:has-text("Copy")')).toBeVisible() // 验证 Bash 模式没有格式化按钮 await expect(codeEditor.locator('button:has-text("格式化")')).not.toBeVisible() }) /** * 测试 CodeEditor Bash 模式 - 复制功能 */ test('CodeEditor Bash模式 - 复制按钮功能', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 点击命令模板列的"查看"链接 const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")') await viewLink.click() // 等待命令模板弹窗打开 const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' }) await expect(dialog).toBeVisible({ timeout: 5000 }) // 点击复制按钮 const copyButton = dialog.locator('.code-editor button:has-text("复制"), .code-editor button:has-text("Copy")') await copyButton.click() // 验证复制成功提示 await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 3000 }) }) /** * 测试 CodeEditor Bash 模式 - 编辑器可编辑 */ test('CodeEditor Bash模式 - 编辑器可编辑', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 点击命令模板列的"查看"链接 const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")') await viewLink.click() // 等待命令模板弹窗打开 const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' }) await expect(dialog).toBeVisible({ timeout: 5000 }) // 验证编辑器存在并可以编辑 const codeEditor = dialog.locator('.code-editor') const editorContent = codeEditor.locator('.cm-content') await expect(editorContent).toBeVisible() // 尝试在编辑器中输入内容 await editorContent.click() await page.keyboard.press('End') await page.keyboard.type('\n# Test comment') // 验证内容已添加 await expect(editorContent).toContainText('# Test comment') }) /** * 测试 CodeEditor Bash 模式 - 更新按钮存在 (Bug #4628: "关闭" changed to "取消", Bug #4629: dialog changed to drawer) */ test('CodeEditor Bash模式 - 抽屉包含取消和更新按钮', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 点击命令模板列的"查看"链接 const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")') await viewLink.click() // 等待命令模板弹窗打开 const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' }) await expect(dialog).toBeVisible({ timeout: 5000 }) // 验证关闭和更新按钮存在 await expect(dialog.locator('button:has-text("取消"), button:has-text("Cancel")')).toBeVisible() await expect(dialog.locator('button:has-text("更新"), button:has-text("Update")')).toBeVisible() // 点击关闭按钮 await dialog.locator('button:has-text("取消"), button:has-text("Cancel")').click() await expect(dialog).not.toBeVisible({ timeout: 5000 }) }) /** * 测试 CodeEditor Bash 模式 - 图标颜色正确(绿色) */ test('CodeEditor Bash模式 - 图标显示为绿色', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 点击命令模板列的"查看"链接 const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")') await viewLink.click() // 等待命令模板弹窗打开 const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' }) await expect(dialog).toBeVisible({ timeout: 5000 }) // 验证图标有 icon-bash 类(对应绿色) const codeEditor = dialog.locator('.code-editor') await expect(codeEditor.locator('.icon-bash')).toBeVisible() }) /** * 测试 CodeEditor Bash 模式 - 更新命令模板并验证保存成功 */ test('CodeEditor Bash模式 - 更新命令模板并验证保存成功', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 点击命令模板列的"查看"链接 const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")') await viewLink.click() // 等待命令模板弹窗打开 const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' }) await expect(dialog).toBeVisible({ timeout: 5000 }) // 获取编辑器 const codeEditor = dialog.locator('.code-editor') const editorContent = codeEditor.locator('.cm-content') // 生成唯一标识用于验证更新 const timestamp = Date.now() const testComment = `# Test update at ${timestamp}` // 在编辑器末尾添加测试注释 await editorContent.click() await page.keyboard.press('Meta+End') await page.keyboard.type(`\n${testComment}`) // 点击更新按钮 const updateButton = dialog.locator('button:has-text("更新"), button:has-text("Update")') await updateButton.click() // 等待更新成功提示 await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 }) // 等待弹窗关闭 await expect(dialog).not.toBeVisible({ timeout: 5000 }) // 重新打开命令模板弹窗验证内容已保存 await page.waitForTimeout(500) await viewLink.click() // 等待弹窗重新打开 const dialogReopened = page.locator('.el-drawer').filter({ hasText: '命令模板' }) await expect(dialogReopened).toBeVisible({ timeout: 5000 }) // 验证内容包含我们添加的测试注释 const editorContentReopened = dialogReopened.locator('.cm-content') await expect(editorContentReopened).toContainText(timestamp.toString()) }) /** * 测试 CodeEditor Bash 模式 - 修改全部内容并验证保存 */ test('CodeEditor Bash模式 - 替换全部命令模板并验证保存', async ({ page }) => { await login(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 点击命令模板列的"查看"链接 const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")') await viewLink.click() // 等待命令模板弹窗打开 const dialog = page.locator('.el-drawer').filter({ hasText: '命令模板' }) await expect(dialog).toBeVisible({ timeout: 5000 }) // 获取编辑器 const codeEditor = dialog.locator('.code-editor') const editorContent = codeEditor.locator('.cm-content') // 保存原始内容以便恢复 const originalContent = await editorContent.textContent() // 生成唯一标识用于验证更新 const timestamp = Date.now() const newScript = `#!/bin/bash # Updated script at ${timestamp} echo "Test script" ffmpeg -i {RTSP_URL} -c copy output.mp4` // 全选并替换内容 await editorContent.click() await page.keyboard.press('Meta+a') await page.keyboard.type(newScript) // 点击更新按钮 const updateButton = dialog.locator('button:has-text("更新"), button:has-text("Update")') await updateButton.click() // 等待更新成功提示 await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 }) // 等待弹窗关闭 await expect(dialog).not.toBeVisible({ timeout: 5000 }) // 重新打开验证内容 await page.waitForTimeout(500) await viewLink.click() const dialogReopened = page.locator('.el-drawer').filter({ hasText: '命令模板' }) await expect(dialogReopened).toBeVisible({ timeout: 5000 }) const editorContentReopened = dialogReopened.locator('.cm-content') await expect(editorContentReopened).toContainText(`Updated script at ${timestamp}`) await expect(editorContentReopened).toContainText('echo "Test script"') // 恢复原始内容 if (originalContent) { await editorContentReopened.click() await page.keyboard.press('Meta+a') await page.keyboard.type(originalContent) await dialogReopened.locator('button:has-text("更新"), button:has-text("Update")').click() await page.waitForTimeout(1000) } }) }) test.describe('LiveStream 管理 - 从 LSS 页面创建流程', () => { /** * 测试带 action=create 参数时自动打开新增抽屉 */ test('带 action=create 参数访问时自动打开新增抽屉', async ({ page }) => { await login(page) // 直接导航到带有 action=create 参数的页面 await page.goto('/live-stream?action=create') // 等待页面加载 await page.waitForTimeout(1000) // 验证新增抽屉已打开 const drawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' }) await expect(drawer).toBeVisible({ timeout: 5000 }) // 验证抽屉标题 await expect(drawer.locator('.drawer-header')).toContainText('新增 Live Stream') // 验证表单字段存在 await expect(drawer.locator('label:has-text("名称")')).toBeVisible() await expect(drawer.locator('label:has-text("LSS 节点")')).toBeVisible() await expect(drawer.locator('label:has-text("摄像头")')).toBeVisible() }) /** * 测试带 cameraId 和 action=create 参数时自动填充摄像头信息 */ test('带 cameraId 和 action=create 参数访问时尝试填充摄像头信息', async ({ page }) => { await login(page) // 模拟 API 响应,返回带有 lssId 的摄像头数据 await page.route('**/admin/camera/list*', async (route) => { const request = route.request() const url = request.url() // 检查是否是查询特定 cameraId 的请求 if (url.includes('cameraId=TEST_CAM_001')) { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, errCode: 0, data: { list: [ { id: 1, cameraId: 'TEST_CAM_001', cameraName: '测试摄像头', lssId: 'LSS_001', status: 'active' } ], total: 1 } }) }) } else { await route.continue() } }) // 导航到带有 cameraId 和 action=create 参数的页面 await page.goto('/live-stream?cameraId=TEST_CAM_001&action=create') // 等待页面和抽屉加载 await page.waitForTimeout(1500) // 验证新增抽屉已打开 const drawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' }) await expect(drawer).toBeVisible({ timeout: 5000 }) // 等待表单自动填充 await page.waitForTimeout(1000) // 验证抽屉中的表单元素可见(LSS 节点选择器) // 由于是 mock 数据,这里主要验证流程不会出错,抽屉能正常打开 await expect(drawer.locator('label:has-text("LSS")')).toBeVisible() }) /** * 测试从 LSS 页面点击未创建 Stream 的摄像头时显示提示对话框 * 注:这个测试需要在 LSS 页面进行,但这里测试的是对话框确认后的跳转结果 */ test('取消新增抽屉后返回列表页面', async ({ page }) => { await login(page) // 导航到创建页面 await page.goto('/live-stream?action=create') // 等待抽屉打开 const drawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' }) await expect(drawer).toBeVisible({ timeout: 5000 }) // 点击取消按钮 await drawer.locator('button:has-text("取消"), button:has-text("Cancel")').click() // 验证抽屉关闭 await expect(drawer).not.toBeVisible({ timeout: 3000 }) // 验证仍在 live-stream 页面 await expect(page).toHaveURL(/\/live-stream-manage\/list/) }) /** * 测试 URL 参数中的 cameraId 同时作为搜索条件 */ test('cameraId 参数同时作为搜索条件', async ({ page }) => { await login(page) // 设置 API 拦截来验证参数 let listRequestBody: any = null await page.route('**/admin/live-stream/list', async (route) => { const request = route.request() if (request.method() === 'POST') { listRequestBody = request.postDataJSON() } await route.continue() }) // 导航到带有 cameraId 参数的页面(不带 action=create) await page.goto('/live-stream?cameraId=CAM_SEARCH_TEST') // 等待列表请求完成 await page.waitForTimeout(1500) // 验证搜索框中已填入 cameraId const cameraIdInput = page.getByPlaceholder('设备ID') await expect(cameraIdInput).toHaveValue('CAM_SEARCH_TEST') // 验证 API 请求中包含 cameraId expect(listRequestBody).not.toBeNull() expect(listRequestBody.cameraId).toBe('CAM_SEARCH_TEST') }) }) test.describe('LiveStream 管理 - Bug修复验证测试', () => { // 登录辅助函数 async function loginHelper(page: Page) { await page.goto('/login') await page.evaluate(() => { localStorage.clear() document.cookie.split(';').forEach((c) => { document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`) }) }) await page.reload() await page.getByPlaceholder('用户名').fill(TEST_USERNAME) await page.getByPlaceholder('密码').fill(TEST_PASSWORD) await page.getByRole('button', { name: '登录' }).click() await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 }) } /** * Bug #4627: 启动时间和关闭时间显示用户当前时区 */ test('Bug #4627 - 时间列正确格式化显示', async ({ page }) => { await loginHelper(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 验证启动时间列存在 await expect(page.locator('th:has-text("启动时间")')).toBeVisible() await expect(page.locator('th:has-text("关闭时间")')).toBeVisible() // 验证时间格式正确 (YYYY-MM-DD HH:mm:ss) const startedAtCell = page.locator('tbody tr').first().locator('td').nth(7) const startedAtText = await startedAtCell.textContent() // 时间格式应该是 YYYY-MM-DD HH:mm:ss 或 "-" if (startedAtText && startedAtText.trim() !== '-') { expect(startedAtText).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/) } }) /** * Bug #4628: 命令模板按钮文字从"关闭"改为"取消" */ test('Bug #4628 - 命令模板抽屉按钮显示"取消"而非"关闭"', async ({ page }) => { await loginHelper(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 点击命令模板列的"查看"链接 const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")') await expect(viewLink).toBeVisible({ timeout: 5000 }) await viewLink.click() // 等待命令模板抽屉打开 const drawer = page.locator('.el-drawer').filter({ hasText: '命令模板' }) await expect(drawer).toBeVisible({ timeout: 5000 }) // 验证有"取消"按钮而非"关闭"按钮 await expect(drawer.locator('button:has-text("取消")')).toBeVisible() await expect(drawer.locator('button:has-text("关闭")')).not.toBeVisible() }) /** * Bug #4629: 命令模板使用右到左抽屉显示 */ test('Bug #4629 - 命令模板使用抽屉而非对话框', async ({ page }) => { await loginHelper(page) await page.goto('/live-stream-manage/list') // 等待表格加载 await page.waitForSelector('tbody tr', { timeout: 10000 }) // 点击命令模板列的"查看"链接 const viewLink = page.locator('tbody tr').first().locator('a:has-text("查看"), a:has-text("View")') await expect(viewLink).toBeVisible({ timeout: 5000 }) await viewLink.click() // 等待命令模板抽屉打开 const drawer = page.locator('.el-drawer').filter({ hasText: '命令模板' }) await expect(drawer).toBeVisible({ timeout: 5000 }) // 验证是抽屉(el-drawer)而非对话框(el-dialog) const dialog = page.locator('.el-dialog').filter({ hasText: '命令模板' }) await expect(dialog).not.toBeVisible() }) /** * Bug #4630: 搜索框placeholder从"name"改为"名称" */ test('Bug #4630 - 搜索框placeholder显示"名称"而非"name"', async ({ page }) => { await loginHelper(page) await page.goto('/live-stream-manage/list') // 验证名称搜索框的placeholder是"名称" const nameInput = page.getByPlaceholder('名称') await expect(nameInput).toBeVisible() // 验证没有placeholder为"name"的输入框 const nameInputEnglish = page.getByPlaceholder('name') await expect(nameInputEnglish).not.toBeVisible() }) /** * Bug #4541 (通用): 重置按钮使用灰底白字 */ test('Bug #4541 - 重置按钮使用灰底白字', async ({ page }) => { await loginHelper(page) await page.goto('/live-stream-manage/list') // 获取重置按钮 const resetButton = page.getByRole('button', { name: '重置' }) await expect(resetButton).toBeVisible() // 验证按钮有 el-button--info 类(灰色按钮) await expect(resetButton).toHaveClass(/el-button--info/) }) })