Browse Source

feat: enhance machine management interface with data attributes and new link component

- Add ElLink component to the machine name column for better navigation
- Introduce data-id attributes to buttons and form elements for improved testability
- Implement loading state for delete operations to prevent multiple submissions
- Update E2E tests to utilize data-id selectors for more reliable element targeting
yb 3 weeks ago
parent
commit
018a4b3d5b
4 changed files with 92 additions and 65 deletions
  1. 1 0
      src/components.d.ts
  2. 25 15
      src/views/machine/index.vue
  3. 15 7
      tests/e2e/auth.spec.ts
  4. 51 43
      tests/e2e/machine.spec.ts

+ 1 - 0
src/components.d.ts

@@ -35,6 +35,7 @@ declare module 'vue' {
     ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
+    ElLink: typeof import('element-plus/es')['ElLink']
     ElMain: typeof import('element-plus/es')['ElMain']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']

+ 25 - 15
src/views/machine/index.vue

@@ -2,15 +2,19 @@
   <div class="page-container">
     <!-- 操作按钮 -->
     <div class="table-actions">
-      <el-button type="primary" :icon="Plus" @click="handleAdd">新增机器</el-button>
-      <el-button type="success" :icon="Refresh" @click="getList">刷新列表</el-button>
+      <el-button type="primary" :icon="Plus" data-id="btn-add-machine" @click="handleAdd">新增机器</el-button>
+      <el-button type="success" :icon="Refresh" data-id="btn-refresh" @click="getList">刷新列表</el-button>
     </div>
 
     <!-- 数据表格 -->
-    <el-table v-loading="loading" :data="machineList" border>
+    <el-table v-loading="loading" :data="machineList" border data-id="machine-table">
       <el-table-column type="index" label="序号" width="60" align="center" />
       <el-table-column prop="machineId" label="机器ID" min-width="120" show-overflow-tooltip />
-      <el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
+      <el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip>
+        <template #default="{ row }">
+          <el-link type="primary" :data-id="`link-edit-${row.machineId}`" @click="handleEdit(row)">{{ row.name }}</el-link>
+        </template>
+      </el-table-column>
       <el-table-column prop="location" label="位置" min-width="120" show-overflow-tooltip />
       <el-table-column prop="description" label="描述" min-width="150" show-overflow-tooltip />
       <el-table-column prop="cameraCount" label="摄像头数" width="100" align="center">
@@ -28,34 +32,34 @@
       <el-table-column prop="createdAt" label="创建时间" width="170" align="center" />
       <el-table-column label="操作" width="150" align="center" fixed="right">
         <template #default="{ row }">
-          <el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
-          <el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
+          <el-button type="primary" link :icon="Edit" :data-id="`btn-edit-${row.machineId}`" @click="handleEdit(row)">编辑</el-button>
+          <el-button type="danger" link :icon="Delete" :disabled="deleteLoading" :data-id="`btn-delete-${row.machineId}`" @click="handleDelete(row)">删除</el-button>
         </template>
       </el-table-column>
     </el-table>
 
     <!-- 新增/编辑弹窗 -->
-    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close>
-      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close data-id="dialog-machine">
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px" data-id="form-machine">
         <el-form-item label="机器ID" prop="machineId">
-          <el-input v-model="form.machineId" placeholder="请输入机器ID" :disabled="isEdit" />
+          <el-input v-model="form.machineId" placeholder="请输入机器ID" :disabled="isEdit" data-id="input-machine-id" />
         </el-form-item>
         <el-form-item label="名称" prop="name">
-          <el-input v-model="form.name" placeholder="请输入名称" />
+          <el-input v-model="form.name" placeholder="请输入名称" data-id="input-name" />
         </el-form-item>
         <el-form-item label="位置" prop="location">
-          <el-input v-model="form.location" placeholder="请输入位置" />
+          <el-input v-model="form.location" placeholder="请输入位置" data-id="input-location" />
         </el-form-item>
         <el-form-item label="描述" prop="description">
-          <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入描述" />
+          <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入描述" data-id="input-description" />
         </el-form-item>
         <el-form-item v-if="isEdit" label="启用状态">
-          <el-switch v-model="form.enabled" />
+          <el-switch v-model="form.enabled" data-id="switch-enabled" />
         </el-form-item>
       </el-form>
       <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
+        <el-button data-id="btn-cancel" @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitLoading" data-id="btn-submit" @click="handleSubmit">确定</el-button>
       </template>
     </el-dialog>
   </div>
@@ -70,6 +74,7 @@ import type { MachineDTO, MachineAddRequest, MachineUpdateRequest } from '@/type
 
 const loading = ref(false)
 const submitLoading = ref(false)
+const deleteLoading = ref(false)
 const machineList = ref<MachineDTO[]>([])
 const dialogVisible = ref(false)
 const formRef = ref<FormInstance>()
@@ -134,10 +139,13 @@ function handleEdit(row: MachineDTO) {
 }
 
 async function handleDelete(row: MachineDTO) {
+  if (deleteLoading.value) return
+
   try {
     await ElMessageBox.confirm(`确定要删除机器 "${row.name}" 吗?`, '提示', {
       type: 'warning'
     })
+    deleteLoading.value = true
     const res = await deleteMachine(row.id)
     if (res.code === 200) {
       ElMessage.success('删除成功')
@@ -147,6 +155,8 @@ async function handleDelete(row: MachineDTO) {
     if (error !== 'cancel') {
       console.error('删除失败', error)
     }
+  } finally {
+    deleteLoading.value = false
   }
 }
 

+ 15 - 7
tests/e2e/auth.spec.ts

@@ -34,10 +34,14 @@ test.describe('登录登出测试', () => {
     // 输入登录信息
     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 })
+    // 等待登录按钮可用并点击
+    const loginBtn = page.getByRole('button', { name: '登 录' })
+    await expect(loginBtn).toBeEnabled()
+    await loginBtn.click()
+
+    // 等待登录 API 响应并跳转
+    await page.waitForURL(/^(?!.*\/login).*$/, { timeout: 15000 })
 
     // 验证用户名显示为 admin
     await expect(page.locator('.username')).toBeVisible({ timeout: 10000 })
@@ -48,8 +52,10 @@ test.describe('登录登出测试', () => {
     // 先登录
     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 })
+    const loginBtn = page.getByRole('button', { name: '登 录' })
+    await expect(loginBtn).toBeEnabled()
+    await loginBtn.click()
+    await page.waitForURL(/^(?!.*\/login).*$/, { timeout: 15000 })
 
     // 点击用户下拉菜单
     await page.locator('.user-info').click()
@@ -66,8 +72,10 @@ test.describe('登录登出测试', () => {
     // 先登录
     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 })
+    const loginBtn = page.getByRole('button', { name: '登 录' })
+    await expect(loginBtn).toBeEnabled()
+    await loginBtn.click()
+    await page.waitForURL(/^(?!.*\/login).*$/, { timeout: 15000 })
 
     // 点击用户下拉菜单
     await page.locator('.user-info').click()

+ 51 - 43
tests/e2e/machine.spec.ts

@@ -1,4 +1,4 @@
-import { test, expect } from '@playwright/test'
+import { test, expect, type Page } from '@playwright/test'
 
 // 测试账号配置
 const TEST_USERNAME = process.env.TEST_USERNAME || 'admin'
@@ -7,9 +7,12 @@ const TEST_PASSWORD = process.env.TEST_PASSWORD || '123456'
 // 生成唯一的测试机器ID
 const generateMachineId = () => `TEST_${Date.now()}`
 
+// data-id 选择器辅助函数
+const byDataId = (id: string) => `[data-id="${id}"]`
+
 test.describe('机器管理 CRUD 测试', () => {
   // 登录辅助函数
-  async function login(page: any) {
+  async function login(page: Page) {
     await page.goto('/login')
     await page.evaluate(() => {
       localStorage.clear()
@@ -30,9 +33,9 @@ test.describe('机器管理 CRUD 测试', () => {
     await page.goto('/machine')
 
     // 验证页面元素
-    await expect(page.getByRole('button', { name: '新增机器' })).toBeVisible()
-    await expect(page.getByRole('button', { name: '刷新列表' })).toBeVisible()
-    await expect(page.locator('.el-table')).toBeVisible()
+    await expect(page.locator(byDataId('btn-add-machine'))).toBeVisible()
+    await expect(page.locator(byDataId('btn-refresh'))).toBeVisible()
+    await expect(page.locator(byDataId('machine-table'))).toBeVisible()
 
     // 验证表头
     await expect(page.getByText('机器ID')).toBeVisible()
@@ -45,16 +48,16 @@ test.describe('机器管理 CRUD 测试', () => {
     await page.goto('/machine')
 
     // 等待表格加载
-    await expect(page.locator('.el-table')).toBeVisible()
+    await expect(page.locator(byDataId('machine-table'))).toBeVisible()
 
     // 点击刷新按钮
-    await page.getByRole('button', { name: '刷新列表' }).click()
+    await page.locator(byDataId('btn-refresh')).click()
 
     // 验证加载状态(可能很快消失)
     await page.waitForTimeout(500)
 
     // 表格应该仍然可见
-    await expect(page.locator('.el-table')).toBeVisible()
+    await expect(page.locator(byDataId('machine-table'))).toBeVisible()
   })
 
   test('新增机器完整流程', async ({ page }) => {
@@ -65,10 +68,10 @@ test.describe('机器管理 CRUD 测试', () => {
     const machineName = `测试机器_${Date.now()}`
 
     // 点击新增按钮
-    await page.getByRole('button', { name: '新增机器' }).click()
+    await page.locator(byDataId('btn-add-machine')).click()
 
     // 验证弹窗打开
-    await expect(page.getByRole('dialog')).toBeVisible()
+    await expect(page.locator(byDataId('dialog-machine'))).toBeVisible()
     await expect(page.locator('.el-dialog__title')).toContainText('新增机器')
 
     // 填写表单
@@ -77,14 +80,13 @@ test.describe('机器管理 CRUD 测试', () => {
     await page.getByPlaceholder('请输入位置').fill('测试位置')
     await page.getByPlaceholder('请输入描述').fill('E2E测试创建的机器')
 
-    // 提交表单
-    await page.getByRole('dialog').getByRole('button', { name: '确定' }).click()
-
-    // 等待成功消息
-    await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 10000 })
+    // 等待提交按钮可用后点击
+    const submitBtn = page.locator(byDataId('btn-submit'))
+    await expect(submitBtn).toBeEnabled()
+    await submitBtn.click()
 
     // 验证弹窗关闭
-    await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 5000 })
+    await expect(page.locator(byDataId('dialog-machine'))).not.toBeVisible({ timeout: 5000 })
 
     // 验证新机器出现在列表中
     await expect(page.getByText(machineId)).toBeVisible({ timeout: 5000 })
@@ -96,23 +98,23 @@ test.describe('机器管理 CRUD 测试', () => {
     await page.goto('/machine')
 
     // 等待表格加载
-    await expect(page.locator('.el-table')).toBeVisible()
+    await expect(page.locator(byDataId('machine-table'))).toBeVisible()
     await page.waitForTimeout(1000)
 
-    // 找到第一行的编辑按钮
-    const firstEditButton = page.locator('.el-table__body-wrapper').getByText('编辑').first()
+    // 点击第一行的名称链接
+    const firstNameLink = page.locator('[data-id^="link-edit-"]').first()
 
     // 检查是否有数据可编辑
-    const editButtonCount = await firstEditButton.count()
-    if (editButtonCount === 0) {
+    const linkCount = await firstNameLink.count()
+    if (linkCount === 0) {
       test.skip()
       return
     }
 
-    await firstEditButton.click()
+    await firstNameLink.click()
 
     // 验证弹窗打开
-    await expect(page.getByRole('dialog')).toBeVisible()
+    await expect(page.locator(byDataId('dialog-machine'))).toBeVisible()
     await expect(page.locator('.el-dialog__title')).toContainText('编辑机器')
 
     // 修改名称
@@ -120,14 +122,13 @@ test.describe('机器管理 CRUD 测试', () => {
     await nameInput.clear()
     await nameInput.fill(`编辑测试_${Date.now()}`)
 
-    // 提交
-    await page.getByRole('dialog').getByRole('button', { name: '确定' }).click()
-
-    // 等待成功消息
-    await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 10000 })
+    // 等待提交按钮可用后点击
+    const submitBtn = page.locator(byDataId('btn-submit'))
+    await expect(submitBtn).toBeEnabled()
+    await submitBtn.click()
 
     // 验证弹窗关闭
-    await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 5000 })
+    await expect(page.locator(byDataId('dialog-machine'))).not.toBeVisible({ timeout: 15000 })
   })
 
   test('删除机器功能', async ({ page }) => {
@@ -139,16 +140,23 @@ test.describe('机器管理 CRUD 测试', () => {
     const machineName = `待删除机器_${Date.now()}`
 
     // 新增机器
-    await page.getByRole('button', { name: '新增机器' }).click()
+    await page.locator(byDataId('btn-add-machine')).click()
     await page.getByPlaceholder('请输入机器ID').fill(machineId)
     await page.getByPlaceholder('请输入名称').fill(machineName)
-    await page.getByRole('dialog').getByRole('button', { name: '确定' }).click()
-    await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 10000 })
-    await page.waitForTimeout(1000)
+
+    // 等待提交按钮可用后点击
+    const submitBtn = page.locator(byDataId('btn-submit'))
+    await expect(submitBtn).toBeEnabled()
+    await submitBtn.click()
+
+    // 等待弹窗关闭(更可靠的成功指示)
+    await expect(page.locator(byDataId('dialog-machine'))).not.toBeVisible({ timeout: 15000 })
+
+    // 等待新机器出现在列表中
+    await expect(page.getByText(machineId)).toBeVisible({ timeout: 5000 })
 
     // 找到刚创建的机器的删除按钮
-    const row = page.locator('.el-table__row').filter({ hasText: machineId })
-    await row.getByText('删除').click()
+    await page.locator(byDataId(`btn-delete-${machineId}`)).click()
 
     // 确认删除对话框
     await expect(page.locator('.el-message-box')).toBeVisible()
@@ -166,11 +174,11 @@ test.describe('机器管理 CRUD 测试', () => {
     await page.goto('/machine')
 
     // 点击新增按钮
-    await page.getByRole('button', { name: '新增机器' }).click()
-    await expect(page.getByRole('dialog')).toBeVisible()
+    await page.locator(byDataId('btn-add-machine')).click()
+    await expect(page.locator(byDataId('dialog-machine'))).toBeVisible()
 
     // 直接点击确定,不填写任何内容
-    await page.getByRole('dialog').getByRole('button', { name: '确定' }).click()
+    await page.locator(byDataId('btn-submit')).click()
 
     // 验证显示验证错误
     await expect(page.getByText('请输入机器ID')).toBeVisible()
@@ -182,17 +190,17 @@ test.describe('机器管理 CRUD 测试', () => {
     await page.goto('/machine')
 
     // 点击新增按钮
-    await page.getByRole('button', { name: '新增机器' }).click()
-    await expect(page.getByRole('dialog')).toBeVisible()
+    await page.locator(byDataId('btn-add-machine')).click()
+    await expect(page.locator(byDataId('dialog-machine'))).toBeVisible()
 
     // 填写部分内容
     await page.getByPlaceholder('请输入机器ID').fill('TEST_CANCEL')
 
     // 点击取消
-    await page.getByRole('dialog').getByRole('button', { name: '取消' }).click()
+    await page.locator(byDataId('btn-cancel')).click()
 
     // 验证弹窗关闭
-    await expect(page.getByRole('dialog')).not.toBeVisible()
+    await expect(page.locator(byDataId('dialog-machine'))).not.toBeVisible()
   })
 
   test('从侧边栏导航到机器管理', async ({ page }) => {
@@ -203,6 +211,6 @@ test.describe('机器管理 CRUD 测试', () => {
 
     // 验证跳转到机器管理页面
     await expect(page).toHaveURL(/\/machine/)
-    await expect(page.getByRole('button', { name: '新增机器' })).toBeVisible()
+    await expect(page.locator(byDataId('btn-add-machine'))).toBeVisible()
   })
 })