yeg
2026-08-29 fa5d4591e44edb2e9c3b2d66d6de7d074c5baf03
补充采集器采样浏览器验收

新增 Ticket06 真实前后端黑盒流程,验证采集器登记、独立实时与历史采样及重启恢复。自动化栈使用隔离模拟器端口,并修复 Ticket05 失效的 Element Plus 定位器。
1个文件已添加
3个文件已修改
84 ■■■■■ 已修改文件
package.json 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/collector-sampling.spec.ts 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/shared-fields-and-battery-groups.spec.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/support/ticket04-stack.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -8,7 +8,8 @@
    "build": "vue-tsc -b && vite build",
    "preview": "vite preview",
    "test:e2e:ticket04": "playwright test tests/e2e/device-registration.spec.ts",
    "test:e2e:ticket05": "playwright test tests/e2e/shared-fields-and-battery-groups.spec.ts"
    "test:e2e:ticket05": "playwright test tests/e2e/shared-fields-and-battery-groups.spec.ts",
    "test:e2e:ticket06": "playwright test tests/e2e/collector-sampling.spec.ts"
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.3.2",
tests/e2e/collector-sampling.spec.ts
New file
@@ -0,0 +1,67 @@
import { expect, test, type Page } from '@playwright/test'
import { Ticket04Stack } from './support/ticket04-stack'
const stack = new Ticket04Stack()
test.beforeAll(async () => stack.start())
test.afterAll(async () => stack.stop())
async function login(page: Page) {
  await page.goto('/login')
  await page.locator('input[autocomplete="username"]').fill('admin')
  await page.locator('input[autocomplete="current-password"]').fill('admin123')
  await page.getByRole('button', { name: '登录', exact: true }).click()
  await expect(page).toHaveURL(/\/dashboard$/)
}
test('采集器登记后可观察独立实时与分页历史采样', async ({ page }) => {
  await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
  page.once('dialog', dialog => dialog.accept())
  await page.getByRole('button', { name: '重置模拟器' }).click()
  let collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
  await expect(collector.getByText('S-01', { exact: true })).toBeVisible()
  await expect(collector.getByText('S-04', { exact: true })).toBeVisible()
  await collector.locator('label').filter({ hasText: '采样间隔(毫秒)' }).locator('input').fill('200')
  await collector.getByRole('button', { name: '保存配置' }).click()
  collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
  await collector.getByLabel('S-01 测量项').fill([
    'temperature=28.6 ℃',
    'positive_potential=3.812 V',
    'negative_potential=-0.116 V',
    'dynamic_pressure=101.3 kPa',
  ].join('\n'))
  await collector.getByRole('button', { name: '保存测量值' }).first().click()
  collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
  await expect(collector.getByText(/原始历史 [1-9]\d* 条/)).toBeVisible()
  await collector.getByRole('button', { name: '查看实时' }).click()
  await expect(collector.locator('.collector-output')).toContainText('dev-sim-collector-1')
  await expect(collector.locator('.collector-output')).toContainText('S-01 dynamic_pressure=101.3 kPa')
  await expect(collector.locator('.collector-output')).toContainText('设备 2026-')
  await expect(collector.locator('.collector-output')).toContainText('/ 接收 2026-')
  await collector.getByRole('button', { name: '下载历史' }).click()
  await expect(collector.locator('.collector-output')).toContainText(/历史 \d+ 条 \/ 本页 \d+ 条/)
  await login(page)
  await page.goto('/device-collector/new')
  await page.getByRole('button', { name: '选择设备' }).click()
  const candidate = page.locator('.device-table tbody tr').filter({ hasText: 'dev-sim-collector-1' })
  await expect(candidate).toBeVisible()
  await candidate.getByRole('button', { name: '选择' }).click()
  await page.locator('label').filter({ hasText: '采集器名称' }).locator('input').fill('Ticket06 验收采集器')
  await page.getByRole('button', { name: '保存', exact: true }).click()
  await expect(page.locator('tbody tr').filter({ hasText: 'dev-sim-collector-1' })).toBeVisible()
  await stack.restartBackend()
  await page.goto(`${stack.backendUrl}/dev/bts-simulator.html`)
  collector = page.locator('article.device').filter({ has: page.getByRole('heading', { name: 'dev-sim 采集器 1' }) })
  await expect(collector.locator('label').filter({ hasText: '采样间隔(毫秒)' }).locator('input')).toHaveValue('200')
  await expect(collector.getByLabel('S-01 测量项')).toHaveValue(/dynamic_pressure=101\.3 kPa/)
  await collector.getByRole('button', { name: '下载历史' }).click()
  await expect(collector.locator('.collector-output')).toContainText('dev-sim-collector-1')
  const commands = await page.locator('#requests .request-command').allTextContents()
  expect(commands.every(command => command === 'getdevinfo' || command === 'inquire')).toBe(true)
})
tests/e2e/shared-fields-and-battery-groups.spec.ts
@@ -24,14 +24,14 @@
  await page.goto('/battery-group/fields')
  await page.getByRole('button', { name: '新增字段' }).click()
  await page.locator('.field-dialog label').filter({ hasText: '字段名称' }).locator('input').fill(fieldName)
  await page.locator('.field-dialog .el-select').click()
  await page.locator('.field-dialog label').filter({ hasText: '字段类型' }).locator('.el-select__wrapper').click()
  await page.getByRole('option', { name: '数字', exact: true }).click()
  await page.getByRole('button', { name: '保存', exact: true }).last().click()
  await expect(page.locator('tbody tr').filter({ hasText: fieldName })).toBeVisible()
  await page.goto('/battery-group/new')
  await page.getByRole('button', { name: '编辑当前模板' }).click()
  await page.getByRole('checkbox', { name: fieldName, exact: true }).check()
  await page.locator('.field-checkbox-grid .el-checkbox').filter({ hasText: fieldName }).click()
  await page.getByRole('button', { name: '另存模板' }).click()
  await page.locator('.el-message-box__input input').fill(templateName)
  await page.getByRole('button', { name: '保存', exact: true }).last().click()
@@ -41,15 +41,15 @@
  await page.locator('label').filter({ hasText: '型号' }).locator('input').fill('M-TICKET05')
  await page.locator('label').filter({ hasText: '目的' }).locator('textarea').fill('Ticket05 持久化验收')
  await page.locator('.battery-row input').first().fill(`BAT-${suffix}`)
  await page.locator('label').filter({ hasText: fieldName }).locator('input').fill('72')
  await page.getByPlaceholder(`输入${fieldName}`).fill('72')
  await page.getByRole('button', { name: '保存新建电池组', exact: true }).click()
  await expect(page).toHaveURL(/\/battery-group$/)
  await expect(page.locator('tbody tr').filter({ hasText: groupName })).toBeVisible()
  await page.locator('tbody tr').filter({ hasText: groupName }).getByRole('button', { name: '编辑' }).click()
  await expect(page.locator('label').filter({ hasText: fieldName }).locator('input')).toHaveValue('72')
  await expect(page.getByPlaceholder(`输入${fieldName}`)).toHaveValue('72')
  await expect(page.locator('.battery-row input').first()).toHaveValue(`BAT-${suffix}`)
  await page.reload()
  await expect(page.locator('label').filter({ hasText: fieldName }).locator('input')).toHaveValue('72')
  await expect(page.getByPlaceholder(`输入${fieldName}`)).toHaveValue('72')
  await expect(page.locator('.battery-row input').first()).toHaveValue(`BAT-${suffix}`)
})
tests/e2e/support/ticket04-stack.ts
@@ -15,6 +15,7 @@
const apiPort = Number(process.env.TICKET04_API_PORT || 18001)
const mysqlPort = Number(process.env.TICKET04_MYSQL_PORT || 63306)
const redisPort = Number(process.env.TICKET04_REDIS_PORT || 56379)
const simulatorPort = Number(process.env.TICKET04_SIMULATOR_PORT || 15021)
const mysqlImage = process.env.TICKET04_MYSQL_IMAGE || 'public.ecr.aws/docker/library/mysql:8.0.36'
const redisImage = process.env.TICKET04_REDIS_IMAGE || 'public.ecr.aws/docker/library/redis:7.2-alpine'
const mysqlPassword = 'ticket04-mysql'
@@ -87,7 +88,7 @@
  readonly backendUrl = `http://127.0.0.1:${apiPort}`
  async start() {
    await Promise.all([mysqlPort, redisPort, apiPort].map(assertPortAvailable))
    await Promise.all([mysqlPort, redisPort, apiPort, simulatorPort].map(assertPortAvailable))
    this.docker = resolveDocker()
    mkdirSync(artifactDir, { recursive: true })
    try {
@@ -166,6 +167,7 @@
      `--spring.data.redis.password=${redisPassword}`,
      '--spring.data.redis.database=0',
      '--spring.devtools.restart.enabled=false',
      `--battery.dev-simulator.port=${simulatorPort}`,
      `--ruoyi.profile=${artifactDir}`,
    ], {
      cwd: backendRepo,