yeg
2026-08-28 96614054b2b70fc1bcbebc967d0094633f6f2336
修复:确保浏览器验收进程完整清理

同时检查退出码与终止信号,等待后端进程关闭后再决定是否强制终止。提取设备登记公共流程,减少检测柜和采集器验收步骤重复。
2个文件已修改
72 ■■■■■ 已修改文件
tests/e2e/device-registration.spec.ts 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/support/ticket04-stack.ts 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tests/e2e/device-registration.spec.ts
@@ -14,33 +14,46 @@
  await expect(page).toHaveURL(/\/dashboard$/)
}
async function registerCabinet(page: Page, name: string) {
  await page.goto('/device-cabinet/new')
interface RegisterDeviceOptions {
  createPath: string
  listPath: RegExp
  nameLabel: string
  name: string
  fillAdditionalFields?: () => Promise<void>
}
async function registerDevice(page: Page, options: RegisterDeviceOptions) {
  await page.goto(options.createPath)
  await page.getByRole('button', { name: '选择设备' }).click()
  const row = page.locator('.device-table tbody tr').filter({ has: page.getByRole('button', { name: '选择' }) }).first()
  await expect(row).toBeVisible()
  const code = (await row.locator('td').first().textContent())?.trim() || ''
  await row.getByRole('button', { name: '选择' }).click()
  await page.locator('label').filter({ hasText: '检测柜名称' }).locator('input').fill(name)
  await page.locator('label').filter({ hasText: '规格/容量' }).locator('input').fill('120')
  await page.locator('label').filter({ hasText: options.nameLabel }).locator('input').fill(options.name)
  await options.fillAdditionalFields?.()
  await page.getByRole('button', { name: '保存', exact: true }).click()
  await expect(page).toHaveURL(/\/device-cabinet$/)
  await expect(page).toHaveURL(options.listPath)
  await expect(page.locator('tbody tr').filter({ hasText: code })).toBeVisible()
  return code
}
async function registerCabinet(page: Page, name: string) {
  return registerDevice(page, {
    createPath: '/device-cabinet/new',
    listPath: /\/device-cabinet$/,
    nameLabel: '检测柜名称',
    name,
    fillAdditionalFields: () => page.locator('label').filter({ hasText: '规格/容量' }).locator('input').fill('120'),
  })
}
async function registerCollector(page: Page, name: string) {
  await page.goto('/device-collector/new')
  await page.getByRole('button', { name: '选择设备' }).click()
  const row = page.locator('.device-table tbody tr').filter({ has: page.getByRole('button', { name: '选择' }) }).first()
  await expect(row).toBeVisible()
  const code = (await row.locator('td').first().textContent())?.trim() || ''
  await row.getByRole('button', { name: '选择' }).click()
  await page.locator('label').filter({ hasText: '采集器名称' }).locator('input').fill(name)
  await page.getByRole('button', { name: '保存', exact: true }).click()
  await expect(page).toHaveURL(/\/device-collector$/)
  await expect(page.locator('tbody tr').filter({ hasText: code })).toBeVisible()
  return code
  return registerDevice(page, {
    createPath: '/device-collector/new',
    listPath: /\/device-collector$/,
    nameLabel: '采集器名称',
    name,
  })
}
async function deleteRegisteredDevice(page: Page, path: string, code: string) {
tests/e2e/support/ticket04-stack.ts
@@ -63,6 +63,21 @@
  throw new Error(message)
}
function waitForProcessClose(child: ChildProcess, timeoutMs: number) {
  if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true)
  return new Promise<boolean>((resolvePromise) => {
    const onClose = () => {
      clearTimeout(timer)
      resolvePromise(true)
    }
    const timer = setTimeout(() => {
      child.off('close', onClose)
      resolvePromise(false)
    }, timeoutMs)
    child.once('close', onClose)
  })
}
export class Ticket04Stack {
  private docker!: DockerCommand
  private backend: ChildProcess | null = null
@@ -180,18 +195,18 @@
  private async stopBackend() {
    const backend = this.backend
    this.backend = null
    if (!backend || backend.exitCode !== null) return
    if (!backend || backend.exitCode !== null || backend.signalCode !== null) return
    const gracefulClose = waitForProcessClose(backend, 15_000)
    backend.kill('SIGTERM')
    await Promise.race([
      new Promise<void>(resolvePromise => backend.once('exit', () => resolvePromise())),
      new Promise<void>(resolvePromise => setTimeout(resolvePromise, 15_000)),
    ])
    if (backend.exitCode === null) {
    if (await gracefulClose) return
    if (backend.exitCode === null && backend.signalCode === null) {
      const forcedClose = waitForProcessClose(backend, 15_000)
      if (process.platform === 'win32') {
        spawnSync('taskkill.exe', ['/pid', String(backend.pid), '/t', '/f'], { windowsHide: true })
      } else {
        backend.kill('SIGKILL')
      }
      await forcedClose
    }
  }
}