import { test, expect, type Page } from '@playwright/test' // 测试账号配置 const TEST_USERNAME = process.env.TEST_USERNAME || 'admin' const TEST_PASSWORD = process.env.TEST_PASSWORD || '123456' // 日本相关测试数据 const TEST_DATA = { name: '東京テスト環境', address: '東京都渋谷区神宮前1-2-3', // 用于恢复的原始数据 originalName: '初台測試環境', originalAddress: '' } test.describe('LSS管理 CRUD 测试', () => { // 登录辅助函数 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('LSS管理页面正确显示', async ({ page }) => { await login(page) await page.goto('/lss') // 验证页面标题 await expect(page.locator('text=LSS 管理')).toBeVisible() // 验证搜索表单元素 await expect(page.getByPlaceholder('LSS ID')).toBeVisible() await expect(page.getByPlaceholder('名称')).toBeVisible() await expect(page.getByRole('button', { name: 'Search' })).toBeVisible() await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible() // 验证表头 await expect(page.locator('th:has-text("LSS ID")')).toBeVisible() await expect(page.locator('th:has-text("Name")')).toBeVisible() await expect(page.locator('th:has-text("Address")')).toBeVisible() await expect(page.locator('th:has-text("IP")')).toBeVisible() await expect(page.locator('th:has-text("Actions")')).toBeVisible() }) test('编辑LSS节点 - 修改Name和Address', async ({ page }) => { await login(page) await page.goto('/lss') // 等待表格加载 await page.waitForTimeout(1000) // 找到数据行中的编辑按钮(Actions列第一个按钮) const editButton = page.locator('tbody tr').first().locator('button').nth(1) await expect(editButton).toBeVisible({ timeout: 10000 }) await editButton.click() // 验证编辑对话框打开 const dialog = page.locator('.el-drawer, .el-dialog').filter({ hasText: 'LSS详情' }) await expect(dialog).toBeVisible({ timeout: 5000 }) // 修改 Name 字段 const nameInput = dialog .locator('input') .filter({ hasText: /名称|Name/ }) .or(dialog.locator('label:has-text("Name")').locator('..').locator('input')) .first() // 使用更通用的方式定位 Name 输入框 const nameField = dialog.getByPlaceholder('请输入名称') await nameField.clear() await nameField.fill(TEST_DATA.name) // 修改 Address 字段 const addressField = dialog.getByPlaceholder('请输入地址') await addressField.clear() await addressField.fill(TEST_DATA.address) // 点击 Update 按钮保存 await dialog.getByRole('button', { name: 'Update' }).click() // 等待对话框关闭 await expect(dialog).not.toBeVisible({ timeout: 10000 }) // 验证列表中数据已更新 await page.waitForTimeout(500) const firstRow = page.locator('tbody tr').first() await expect(firstRow).toContainText(TEST_DATA.name) await expect(firstRow).toContainText(TEST_DATA.address) }) test('查询LSS节点', async ({ page }) => { await login(page) await page.goto('/lss') // 等待表格加载 await page.waitForTimeout(1000) // 在名称搜索框输入关键词 await page.getByPlaceholder('名称').fill('東京') // 点击搜索 await page.getByRole('button', { name: 'Search' }).click() await page.waitForTimeout(500) // 验证搜索结果 const tableRows = page.locator('tbody tr') const rowCount = await tableRows.count() if (rowCount > 0) { // 验证每行都包含搜索关键词 for (let i = 0; i < rowCount; i++) { const row = tableRows.nth(i) const rowText = await row.textContent() expect(rowText).toContain('東京') } } }) test('重置搜索条件', async ({ page }) => { await login(page) await page.goto('/lss') // 填入搜索条件 await page.getByPlaceholder('LSS ID').fill('test-id') await page.getByPlaceholder('名称').fill('test-name') // 点击重置 await page.getByRole('button', { name: 'Reset' }).click() await page.waitForTimeout(300) // 验证搜索条件已清空 await expect(page.getByPlaceholder('LSS ID')).toHaveValue('') await expect(page.getByPlaceholder('名称')).toHaveValue('') }) test('查看LSS节点设备列表', async ({ page }) => { await login(page) await page.goto('/lss') // 等待表格加载 await page.waitForTimeout(1000) // 点击 Device List 列的按钮 const deviceListButton = page.locator('tbody tr').first().locator('button').first() await expect(deviceListButton).toBeVisible({ timeout: 10000 }) await deviceListButton.click() // 验证设备列表面板打开 const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: '设备列表' }) await expect(devicePanel).toBeVisible({ timeout: 5000 }) // 验证设备列表表头 await expect(devicePanel.locator('th:has-text("设备ID")')).toBeVisible() await expect(devicePanel.locator('th:has-text("名称")')).toBeVisible() // 关闭面板 await devicePanel.locator('button[aria-label*="Close"], .el-drawer__close-btn, .el-dialog__close').first().click() await expect(devicePanel).not.toBeVisible({ timeout: 5000 }) }) test('从侧边栏导航到LSS管理', async ({ page }) => { await login(page) // 点击侧边栏 LSS 管理菜单项 await page.getByText('LSS 管理').first().click() // 验证跳转到 LSS 管理页面 await expect(page).toHaveURL(/\/lss/) await expect(page.locator('text=LSS 管理')).toBeVisible() }) }) test.describe('LSS管理 - CodeEditor 组件测试 (JSON模式)', () => { // 登录辅助函数 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 }) } /** * 测试 CodeEditor JSON 模式 - 头部显示 */ test('CodeEditor JSON模式 - 验证头部显示JSON标签和按钮', async ({ page }) => { await login(page) await page.goto('/lss') // 等待表格加载 await page.waitForTimeout(1000) // 点击 Device List 按钮打开设备列表 const deviceListButton = page.locator('tbody tr').first().locator('button').first() await expect(deviceListButton).toBeVisible({ timeout: 10000 }) await deviceListButton.click() // 等待设备列表面板打开 const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: 'Camera List' }) await expect(devicePanel).toBeVisible({ timeout: 5000 }) // 点击 Parameter Configuration 的 View 按钮 const viewButton = devicePanel.locator('tbody tr').first().locator('button:has-text("View")').first() await expect(viewButton).toBeVisible({ timeout: 5000 }) await viewButton.click() // 等待参数配置抽屉打开 const paramsDrawer = page.locator('.el-drawer').filter({ hasText: '参数配置' }) await expect(paramsDrawer).toBeVisible({ timeout: 5000 }) // 验证 CodeEditor 头部显示 JSON 标签 const codeEditor = paramsDrawer.locator('.code-editor') await expect(codeEditor).toBeVisible() await expect(codeEditor.locator('.editor-header')).toBeVisible() await expect(codeEditor.locator('.file-type')).toContainText('JSON') // 验证 Copy 按钮存在 await expect(codeEditor.locator('button:has-text("复制"), button:has-text("Copy")')).toBeVisible() // 验证 格式化 按钮存在 (JSON模式特有) await expect(codeEditor.locator('button:has-text("格式化")')).toBeVisible() }) /** * 测试 CodeEditor JSON 模式 - 复制功能 */ test('CodeEditor JSON模式 - 复制按钮功能', async ({ page }) => { await login(page) await page.goto('/lss') // 等待表格加载 await page.waitForTimeout(1000) // 点击 Device List 按钮 const deviceListButton = page.locator('tbody tr').first().locator('button').first() await deviceListButton.click() // 等待设备列表面板打开 const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: 'Camera List' }) await expect(devicePanel).toBeVisible({ timeout: 5000 }) // 点击 Parameter Configuration 的 View 按钮 const viewButton = devicePanel.locator('tbody tr').first().locator('button:has-text("View")').first() await viewButton.click() // 等待参数配置抽屉打开 const paramsDrawer = page.locator('.el-drawer').filter({ hasText: '参数配置' }) await expect(paramsDrawer).toBeVisible({ timeout: 5000 }) // 点击复制按钮 const copyButton = paramsDrawer.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 JSON 模式 - 格式化功能 */ test('CodeEditor JSON模式 - 格式化按钮功能', async ({ page }) => { await login(page) await page.goto('/lss') // 等待表格加载 await page.waitForTimeout(1000) // 点击 Device List 按钮 const deviceListButton = page.locator('tbody tr').first().locator('button').first() await deviceListButton.click() // 等待设备列表面板打开 const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: 'Camera List' }) await expect(devicePanel).toBeVisible({ timeout: 5000 }) // 点击 Parameter Configuration 的 View 按钮 const viewButton = devicePanel.locator('tbody tr').first().locator('button:has-text("View")').first() await viewButton.click() // 等待参数配置抽屉打开 const paramsDrawer = page.locator('.el-drawer').filter({ hasText: '参数配置' }) await expect(paramsDrawer).toBeVisible({ timeout: 5000 }) // 验证格式化按钮可用(如果内容是有效JSON) const formatButton = paramsDrawer.locator('.code-editor button:has-text("格式化")') await expect(formatButton).toBeVisible() // 点击格式化按钮(如果按钮未禁用) const isDisabled = await formatButton.isDisabled() if (!isDisabled) { await formatButton.click() // 格式化后内容应该保持有效 await page.waitForTimeout(300) } }) /** * 测试 CodeEditor JSON 模式 - 无效JSON显示错误提示 */ test('CodeEditor JSON模式 - 无效JSON显示错误提示', async ({ page }) => { await login(page) await page.goto('/lss') // 等待表格加载 await page.waitForTimeout(1000) // 点击 Device List 按钮 const deviceListButton = page.locator('tbody tr').first().locator('button').first() await deviceListButton.click() // 等待设备列表面板打开 const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: 'Camera List' }) await expect(devicePanel).toBeVisible({ timeout: 5000 }) // 点击 Parameter Configuration 的 View 按钮 const viewButton = devicePanel.locator('tbody tr').first().locator('button:has-text("View")').first() await viewButton.click() // 等待参数配置抽屉打开 const paramsDrawer = page.locator('.el-drawer').filter({ hasText: '参数配置' }) await expect(paramsDrawer).toBeVisible({ timeout: 5000 }) // 获取编辑器并输入无效JSON const codeEditor = paramsDrawer.locator('.code-editor') const editorContent = codeEditor.locator('.cm-content') // 清空并输入无效JSON await editorContent.click() await page.keyboard.press('Meta+a') await page.keyboard.type('{ invalid json }') // 验证错误提示显示 await expect(codeEditor.locator('.validation-error')).toBeVisible({ timeout: 3000 }) await expect(codeEditor.locator('.validation-error')).toContainText('JSON 格式错误') // 验证格式化按钮被禁用 const formatButton = codeEditor.locator('button:has-text("格式化")') await expect(formatButton).toBeDisabled() }) /** * 测试 CodeEditor JSON 模式 - 更新内容并验证保存成功 */ test('CodeEditor JSON模式 - 更新参数配置并验证保存成功', async ({ page }) => { await login(page) await page.goto('/lss') // 等待表格加载 await page.waitForTimeout(1000) // 点击 Device List 按钮 const deviceListButton = page.locator('tbody tr').first().locator('button').first() await deviceListButton.click() // 等待设备列表面板打开 const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: 'Camera List' }) await expect(devicePanel).toBeVisible({ timeout: 5000 }) // 点击 Parameter Configuration 的 View 按钮 const viewButton = devicePanel.locator('tbody tr').first().locator('button:has-text("View")').first() await viewButton.click() // 等待参数配置抽屉打开 const paramsDrawer = page.locator('.el-drawer').filter({ hasText: '参数配置' }) await expect(paramsDrawer).toBeVisible({ timeout: 5000 }) // 获取当前编辑器内容 const codeEditor = paramsDrawer.locator('.code-editor') const editorContent = codeEditor.locator('.cm-content') // 生成唯一标识用于验证更新 const timestamp = Date.now() const testValue = `"testUpdate": "${timestamp}"` // 修改JSON内容 - 在现有JSON中添加测试字段 await editorContent.click() await page.keyboard.press('Meta+a') // 输入新的有效JSON内容 await page.keyboard.type(`{\n ${testValue},\n "ip": "192.168.0.64"\n}`) // 点击更新按钮 const updateButton = paramsDrawer.locator('button:has-text("更新"), button:has-text("Update")') await updateButton.click() // 等待更新成功提示 await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 }) // 等待抽屉关闭 await expect(paramsDrawer).not.toBeVisible({ timeout: 5000 }) // 重新打开参数配置抽屉验证内容已保存 await page.waitForTimeout(500) await viewButton.click() // 等待抽屉重新打开 const paramsDrawerReopened = page.locator('.el-drawer').filter({ hasText: '参数配置' }) await expect(paramsDrawerReopened).toBeVisible({ timeout: 5000 }) // 验证内容包含我们添加的测试字段 const editorContentReopened = paramsDrawerReopened.locator('.cm-content') await expect(editorContentReopened).toContainText(timestamp.toString()) }) /** * 测试 CodeEditor JSON 模式 - 运行参数更新并验证保存成功 */ test('CodeEditor JSON模式 - 更新运行参数并验证保存成功', async ({ page }) => { await login(page) await page.goto('/lss') // 等待表格加载 await page.waitForTimeout(1000) // 点击 Device List 按钮 const deviceListButton = page.locator('tbody tr').first().locator('button').first() await deviceListButton.click() // 等待设备列表面板打开 const devicePanel = page.locator('.el-drawer, .el-dialog').filter({ hasText: 'Camera List' }) await expect(devicePanel).toBeVisible({ timeout: 5000 }) // 点击 Run Parameters 的 View 按钮(第二个 View 按钮) const viewButtons = devicePanel.locator('tbody tr').first().locator('button:has-text("View")') const runParamsViewButton = viewButtons.nth(1) await expect(runParamsViewButton).toBeVisible({ timeout: 5000 }) await runParamsViewButton.click() // 等待运行参数抽屉打开 const paramsDrawer = page.locator('.el-drawer').filter({ hasText: '运行参数' }) await expect(paramsDrawer).toBeVisible({ timeout: 5000 }) // 获取编辑器 const codeEditor = paramsDrawer.locator('.code-editor') const editorContent = codeEditor.locator('.cm-content') // 生成唯一标识用于验证更新 const timestamp = Date.now() const testValue = `"runtimeTest": "${timestamp}"` // 修改JSON内容 await editorContent.click() await page.keyboard.press('Meta+a') await page.keyboard.type(`{\n ${testValue}\n}`) // 点击更新按钮 const updateButton = paramsDrawer.locator('button:has-text("更新"), button:has-text("Update")') await updateButton.click() // 等待更新成功提示 await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 }) // 等待抽屉关闭 await expect(paramsDrawer).not.toBeVisible({ timeout: 5000 }) // 重新打开运行参数抽屉验证内容已保存 await page.waitForTimeout(500) await runParamsViewButton.click() // 等待抽屉重新打开 const paramsDrawerReopened = page.locator('.el-drawer').filter({ hasText: '运行参数' }) await expect(paramsDrawerReopened).toBeVisible({ timeout: 5000 }) // 验证内容包含我们添加的测试字段 const editorContentReopened = paramsDrawerReopened.locator('.cm-content') await expect(editorContentReopened).toContainText(timestamp.toString()) }) })