import fs from 'node:fs' import type { APIRequestContext, Locator, Response as BrowserResponse, TestInfo } from '@playwright/test' import { attachJson, captureScreenshot, expect, runId, runPrefix, test } from './fixtures' import { authHeaders, chooseSelectOption, confirmMessageBox, envelopeData, fillFormItem, loginAsAdmin, tableRowByText, visibleDialog, } from './helpers' test.use({ trace: 'off', video: 'off' }) type JsonRecord = Record type Headers = Record type ResourceKind = 'sensitive-words' | 'hot-words' | 'wake-words' | 'tools' interface Activity { module: string action: string result: 'PASS' | 'FAIL' | 'CLEANED' | 'CLEANUP_FAILED' httpStatus?: number resourceId?: string detail?: string } interface JsonResponse { status(): number json(): Promise } const suffix = (runId.replace(/[^A-Za-z0-9]/g, '').slice(-8) || 'MANUAL').toUpperCase() const asRecord = (value: unknown): JsonRecord => value && typeof value === 'object' && !Array.isArray(value) ? value as JsonRecord : {} const asRecords = (value: unknown): JsonRecord[] => Array.isArray(value) ? value.map(asRecord) : [] async function responseData(response: JsonResponse, expected: number | number[], label: string) { const statuses = Array.isArray(expected) ? expected : [expected] expect(statuses, `${label}:HTTP ${response.status()}`).toContain(response.status()) return asRecord(envelopeData(await response.json())) } async function expectSuccessMessage(page: { locator: (selector: string) => Locator }, text: string | RegExp) { await expect(page.locator('.el-message--success').filter({ hasText: text }).last()).toBeVisible() } async function filterByKeyword(page: import('@playwright/test').Page, placeholder: string, keyword: string) { const input = page.getByPlaceholder(placeholder) await input.fill(keyword) await page.waitForTimeout(350) } async function findResource( request: APIRequestContext, headers: Headers, kind: ResourceKind, keyword: string, predicate: (item: JsonRecord) => boolean, ) { const response = await request.get(`/api/v1/${kind}`, { headers, params: { keyword, page: '1', pageSize: '200' }, }) const data = await responseData(response, 200, `读取 ${kind} 列表`) return asRecords(data.items).find(predicate) || null } async function cleanupBySuffix( request: APIRequestContext, headers: Headers, kind: ResourceKind, activities: Activity[], cleanupErrors: string[], ) { try { const response = await request.get(`/api/v1/${kind}`, { headers, params: { keyword: suffix, page: '1', pageSize: '200' }, }) const data = await responseData(response, 200, `清理前读取 ${kind}`) for (const item of asRecords(data.items)) { const haystack = [item.name, item.word, item.term, item.phrase, item.category] .map((value) => String(value ?? '').toUpperCase()) .join(' ') if (!haystack.includes(suffix)) continue const id = String(item.id ?? '') const dataVersion = Number(item.dataVersion) if (!id || !Number.isInteger(dataVersion)) continue const removed = await request.delete(`/api/v1/${kind}/${encodeURIComponent(id)}`, { headers, params: { dataVersion: String(dataVersion) }, }) if (![200, 404].includes(removed.status())) throw new Error(`${id} 删除失败:HTTP ${removed.status()}`) activities.push({ module: kind, action: '失败兜底清理', result: 'CLEANED', httpStatus: removed.status(), resourceId: id }) } } catch (error) { const message = `${kind} 兜底清理异常:${error instanceof Error ? error.message : String(error)}` cleanupErrors.push(message) activities.push({ module: kind, action: '失败兜底清理', result: 'CLEANUP_FAILED', detail: message }) } } async function attachLifecycleReport( testInfo: TestInfo, title: string, activities: Activity[], cleanupErrors: string[], ) { await attachJson(testInfo, `${title}-页面生命周期与清理`, { runId, runPrefix, title, activities, cleanupComplete: cleanupErrors.length === 0, cleanupErrors, security: '全程使用真实 API;报告未记录登录密码、访问令牌、下载对象地址或外部服务凭证。', }) } async function cardByText(page: import('@playwright/test').Page, text: string) { const card = page.locator('.tool-card').filter({ hasText: text }).first() await expect(card).toBeVisible() return card } test.describe('原型运营页面真实交互生命周期', () => { test('会话记录:组合筛选、无结果空态、日期范围校验与重置', async ({ page, request }, testInfo) => { const module = '会话记录' const activities: Activity[] = [] const cleanupErrors: string[] = [] const headers = await authHeaders(request) await loginAsAdmin(page) await page.goto('/agents/chat-history') await expect(page.getByRole('heading', { name: '会话记录' })).toBeVisible() const listResponsePromise = page.waitForResponse((response) => response.request().method() === 'GET' && response.url().includes('/api/v1/chat-sessions?') && response.url().includes(encodeURIComponent(`NO-SUCH-${suffix}`))) await page.getByPlaceholder('搜索会话编号、访客、智能体或聊天内容').fill(`NO-SUCH-${suffix}`) const listResponse = await listResponsePromise const listData = await responseData(listResponse, 200, '页面会话关键词筛选') expect(listData.total).toBe(0) await expect(page.getByText('没有符合条件的聊天记录')).toBeVisible() activities.push({ module, action: '页面关键词筛选并展示真实空态', result: 'PASS', httpStatus: listResponse.status() }) const selects = page.locator('.filter-row .el-select') await selects.nth(1).click() await page.locator('.el-select-dropdown:visible .el-select-dropdown__item').filter({ hasText: '网页端' }).click() await selects.nth(2).click() await page.locator('.el-select-dropdown:visible .el-select-dropdown__item').filter({ hasText: '正常结束' }).click() await page.waitForTimeout(350) await expect(page.getByText('没有符合条件的聊天记录')).toBeVisible() activities.push({ module, action: '页面渠道与会话结果组合筛选', result: 'PASS' }) const startDate = page.getByPlaceholder('开始日期') const endDate = page.getByPlaceholder('结束日期') await startDate.fill('2026-08-16') await startDate.press('Enter') await endDate.fill('2026-08-15') await endDate.press('Enter') await expect(page.getByRole('alert')).toContainText('开始日期不能晚于结束日期') activities.push({ module, action: '页面拦截无效日期范围', result: 'PASS' }) await captureScreenshot(page, testInfo, '00-chat-history-filter-empty-validation') await page.getByRole('button', { name: '重置' }).click() await expect(page.getByRole('alert')).toHaveCount(0) await expect(page.getByPlaceholder('搜索会话编号、访客、智能体或聊天内容')).toHaveValue('') const direct = await request.get('/api/v1/chat-sessions', { headers, params: { page: '1', pageSize: '20' } }) await responseData(direct, 200, '重置后读取会话列表') activities.push({ module, action: '页面重置全部筛选条件', result: 'PASS', httpStatus: direct.status() }) await attachLifecycleReport(testInfo, module, activities, cleanupErrors) }) test('敏感词:单条增改启停、批量预览提交、筛选导出与公式注入防护', async ({ page, request }, testInfo) => { const module = '敏感词库' const activities: Activity[] = [] const cleanupErrors: string[] = [] const headers = await authHeaders(request) const manualWord = `违规拆卸-${suffix}` const editedReply = `已拦截 ${suffix} 测试规则,请遵循安全作业规程。` const formulaWord = `=2+2-${suffix}` const importedWord = `带电测试-${suffix}` let primaryError: unknown await cleanupBySuffix(request, headers, 'sensitive-words', activities, cleanupErrors) try { await loginAsAdmin(page) await page.goto('/knowledge/sensitive-words') await expect(page.getByRole('heading', { name: '敏感词库' })).toBeVisible() await page.getByRole('button', { name: '新增敏感词' }).click() let dialog = visibleDialog(page) await fillFormItem(dialog, '敏感词或表达式', manualWord) await fillFormItem(dialog, '所属分类', `E2E安全-${suffix}`) await chooseSelectOption(page, dialog, '风险等级', '高风险') await fillFormItem(dialog, '安全回复', '请立即停止该操作,并联系现场安全负责人。') await fillFormItem(dialog, '管理备注', `页面创建 ${suffix}`) await dialog.getByRole('button', { name: '保存规则' }).click() await expectSuccessMessage(page, '敏感词规则已创建') await filterByKeyword(page, '搜索敏感词、安全回复或备注', manualWord) let row = tableRowByText(page, manualWord) await expect(row).toBeVisible() await captureScreenshot(page, testInfo, '01-sensitive-created') const created = await findResource(request, headers, 'sensitive-words', manualWord, (item) => item.word === manualWord) expect(created, '页面创建的敏感词必须真实落库').not.toBeNull() activities.push({ module, action: '页面新增并刷新列表', result: 'PASS', resourceId: String(created?.id ?? '') }) await row.getByRole('button', { name: '编辑' }).click() dialog = visibleDialog(page) await fillFormItem(dialog, '安全回复', editedReply) await fillFormItem(dialog, '管理备注', `页面编辑后持久化 ${suffix}`) await dialog.getByRole('button', { name: '保存规则' }).click() await expectSuccessMessage(page, '敏感词规则已更新') await page.reload() await filterByKeyword(page, '搜索敏感词、安全回复或备注', manualWord) row = tableRowByText(page, manualWord) await expect(row).toContainText(editedReply) activities.push({ module, action: '页面编辑并刷新验证持久化', result: 'PASS', resourceId: String(created?.id ?? '') }) const statusSwitch = row.locator('.el-switch').first() await statusSwitch.click() await expectSuccessMessage(page, '敏感词规则已停用') await page.reload() await filterByKeyword(page, '搜索敏感词、安全回复或备注', manualWord) row = tableRowByText(page, manualWord) await expect(row.locator('.el-switch').first()).not.toHaveClass(/is-checked/) activities.push({ module, action: '页面停用并刷新验证持久化', result: 'PASS', resourceId: String(created?.id ?? '') }) await page.getByRole('button', { name: '批量导入' }).click() dialog = visibleDialog(page) await fillFormItem(dialog, '词表内容', `${formulaWord}|E2E导入-${suffix}|高\n${importedWord}|E2E导入-${suffix}|中`) await expect(dialog.getByText('识别到 2 条')).toBeVisible() await expect(dialog.getByText('可导入 2 条,重复跳过 0 条')).toBeVisible() await captureScreenshot(page, testInfo, '02-sensitive-import-preview') const previewResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST' && response.url().includes('/api/v1/sensitive-words/import/preview')) const commitResponsePromise: Promise = page.waitForResponse((response) => response.request().method() === 'POST' && response.url().includes('/api/v1/sensitive-words/import/commit')).catch((error: unknown) => error instanceof Error ? error : new Error(String(error))) await dialog.getByRole('button', { name: '导入 2 条' }).click() const previewResponse = await previewResponsePromise await responseData(previewResponse, 200, '页面敏感词导入预检') const commitResponse = await commitResponsePromise if (commitResponse instanceof Error) throw commitResponse const commitData = await responseData(commitResponse, 200, '页面敏感词导入提交') expect(commitData).toMatchObject({ imported: 2, skipped: 0, total: 2 }) await expectSuccessMessage(page, /已导入 2 条敏感词/) activities.push({ module, action: '页面批量预览并提交两条真实数据', result: 'PASS', httpStatus: commitResponse.status() }) await filterByKeyword(page, '搜索敏感词、安全回复或备注', formulaWord) await expect(tableRowByText(page, formulaWord)).toBeVisible() const downloadPromise = page.waitForEvent('download') await page.getByRole('button', { name: '导出', exact: true }).click() const download = await downloadPromise const downloadedPath = await download.path() expect(downloadedPath, '敏感词导出必须生成可读取的 CSV 文件').toBeTruthy() const csv = fs.readFileSync(downloadedPath!, 'utf8').replace(/^\uFEFF/, '') expect(csv).toContain(`'${formulaWord}`) expect(csv).not.toContain(`\n${manualWord},`) activities.push({ module, action: '按页面当前筛选导出 CSV', result: 'PASS', detail: '筛选仅包含公式测试词;危险公式首字符已由服务端转义。' }) await captureScreenshot(page, testInfo, '03-sensitive-filtered-exported') await page.reload() await filterByKeyword(page, '搜索敏感词、安全回复或备注', manualWord) row = tableRowByText(page, manualWord) await row.getByRole('button', { name: '删除' }).click() await confirmMessageBox(page, '确认删除') await expectSuccessMessage(page, '敏感词已删除') await expect(tableRowByText(page, manualWord)).toHaveCount(0) activities.push({ module, action: '页面删除单条规则', result: 'PASS', resourceId: String(created?.id ?? '') }) } catch (error) { primaryError = error activities.push({ module, action: '页面生命周期', result: 'FAIL', detail: error instanceof Error ? error.message : String(error) }) } finally { await cleanupBySuffix(request, headers, 'sensitive-words', activities, cleanupErrors) const residue = await findResource(request, headers, 'sensitive-words', suffix, (item) => [item.word, item.category].some((value) => String(value ?? '').includes(suffix))) if (residue) cleanupErrors.push(`敏感词残留:${String(residue.id ?? '')}`) await attachLifecycleReport(testInfo, module, activities, cleanupErrors) } if (primaryError) throw primaryError expect(cleanupErrors).toEqual([]) }) test('热词:页面创建、别名去重、编辑、状态筛选、刷新持久化与删除', async ({ page, request }, testInfo) => { const module = '热词管理' const activities: Activity[] = [] const cleanupErrors: string[] = [] const headers = await authHeaders(request) const term = `液压泵-${suffix}` const editedAlias = `液压棒-${suffix}` let resourceId = '' let primaryError: unknown await cleanupBySuffix(request, headers, 'hot-words', activities, cleanupErrors) try { await loginAsAdmin(page) await page.goto('/knowledge/hot-words') await expect(page.getByRole('heading', { name: '热词管理' })).toBeVisible() await page.getByRole('button', { name: '新增热词' }).click() let dialog = visibleDialog(page) await fillFormItem(dialog, '标准热词', term) await fillFormItem(dialog, '所属分类', `液压系统-${suffix}`) await fillFormItem(dialog, '同音词 / 易错词', `液压蹦-${suffix}\n液压崩-${suffix}\n液压蹦-${suffix}`) await fillFormItem(dialog, '识别说明', '装备维修语音识别纠偏页面测试。') await dialog.getByRole('button', { name: '保存热词' }).click() await expectSuccessMessage(page, '热词已创建') await filterByKeyword(page, '搜索热词、同音词、分类、智能体或说明', term) let row = tableRowByText(page, term) await expect(row).toContainText(`液压蹦-${suffix}`) await expect(row.locator('.alias-list span')).toHaveCount(2) const created = await findResource(request, headers, 'hot-words', term, (item) => item.term === term) expect(created).not.toBeNull() resourceId = String(created?.id ?? '') expect(created?.aliases).toEqual([`液压蹦-${suffix}`, `液压崩-${suffix}`]) activities.push({ module, action: '页面创建并由服务端持久化去重别名', result: 'PASS', resourceId }) await captureScreenshot(page, testInfo, '04-hotword-created-aliases') await row.getByRole('button', { name: '编辑' }).click() dialog = visibleDialog(page) await fillFormItem(dialog, '同音词 / 易错词', `${editedAlias}\n泵体异响-${suffix}`) await fillFormItem(dialog, '识别说明', '页面编辑后的专业术语与易错词。') await dialog.getByRole('button', { name: '保存热词' }).click() await expectSuccessMessage(page, '热词已更新') await page.reload() await filterByKeyword(page, '搜索热词、同音词、分类、智能体或说明', term) row = tableRowByText(page, term) await expect(row).toContainText(editedAlias) activities.push({ module, action: '页面编辑并刷新验证别名持久化', result: 'PASS', resourceId }) await row.locator('.el-switch').click() await expectSuccessMessage(page, '热词已停用') const statusSelect = page.locator('.toolbar .el-select').filter({ hasText: /全部状态|已停用/ }).last() await statusSelect.click() await page.locator('.el-select-dropdown:visible .el-select-dropdown__item').filter({ hasText: '已停用' }).click() await page.waitForTimeout(350) row = tableRowByText(page, term) await expect(row).toBeVisible() await page.reload() await filterByKeyword(page, '搜索热词、同音词、分类、智能体或说明', term) row = tableRowByText(page, term) await expect(row.locator('.el-switch')).not.toHaveClass(/is-checked/) activities.push({ module, action: '页面停用、状态筛选并刷新验证', result: 'PASS', resourceId }) await captureScreenshot(page, testInfo, '05-hotword-disabled-filtered') await row.getByRole('button', { name: '详情' }).click() dialog = visibleDialog(page) await expect(dialog).toContainText(editedAlias) await captureScreenshot(page, testInfo, '06-hotword-detail-persisted') await dialog.locator('.el-dialog__headerbtn').click() row = tableRowByText(page, term) await row.getByRole('button', { name: '删除' }).click() await confirmMessageBox(page, '确认删除') await expectSuccessMessage(page, '热词已删除') activities.push({ module, action: '页面详情读取与删除', result: 'PASS', resourceId }) resourceId = '' } catch (error) { primaryError = error activities.push({ module, action: '页面生命周期', result: 'FAIL', detail: error instanceof Error ? error.message : String(error) }) } finally { await cleanupBySuffix(request, headers, 'hot-words', activities, cleanupErrors) await attachLifecycleReport(testInfo, module, activities, cleanupErrors) } if (primaryError) throw primaryError expect(cleanupErrors).toEqual([]) }) test('唤醒词:自定义时段页面 CRUD、唤醒测试、启停、刷新持久化与清理', async ({ page, request }, testInfo) => { const module = '唤醒词库' const activities: Activity[] = [] const cleanupErrors: string[] = [] const headers = await authHeaders(request) const name = `早晚班巡检-${suffix}` const phrase = `设备巡检${suffix.slice(-4)}` const reply = `您好,${suffix} 设备巡检流程已启动。` let resourceId = '' let primaryError: unknown await cleanupBySuffix(request, headers, 'wake-words', activities, cleanupErrors) try { await loginAsAdmin(page) await page.goto('/agents/wake-words') await expect(page.getByRole('heading', { name: '唤醒词库' })).toBeVisible() await page.getByRole('button', { name: '新增唤醒词' }).click() let dialog = visibleDialog(page) await fillFormItem(dialog, '规则名称', name) await fillFormItem(dialog, '唤醒短语', phrase) await fillFormItem(dialog, '生效时段', '06:00-22:00') await fillFormItem(dialog, '首句回复', reply) await dialog.getByRole('button', { name: '保存配置' }).click() await expectSuccessMessage(page, '唤醒词配置已创建') await filterByKeyword(page, '搜索名称、唤醒短语、智能体或回复内容', phrase) let row = tableRowByText(page, phrase) await expect(row).toContainText('06:00-22:00') const created = await findResource(request, headers, 'wake-words', phrase, (item) => item.phrase === phrase) expect(created).not.toBeNull() resourceId = String(created?.id ?? '') expect(created?.activePeriod).toBe('06:00-22:00') activities.push({ module, action: '页面创建自定义 06:00-22:00 时段并真实落库', result: 'PASS', resourceId }) await captureScreenshot(page, testInfo, '07-wakeword-custom-period-created') await row.getByRole('button', { name: '详情' }).click() dialog = visibleDialog(page) await expect(dialog).toContainText('06:00-22:00') await dialog.getByRole('button', { name: '测试唤醒' }).click() const alert = page.locator('.el-message-box:visible').last() await expect(alert).toContainText(reply) await expect(alert).toContainText(phrase) await alert.getByRole('button', { name: '知道了' }).click() await expect(alert).toBeHidden() await dialog.locator('.el-dialog__headerbtn').click() await expect(dialog).toBeHidden() activities.push({ module, action: '页面详情与真实唤醒测试', result: 'PASS', resourceId }) row = tableRowByText(page, phrase) await row.getByRole('button', { name: '编辑' }).click() dialog = visibleDialog(page) await fillFormItem(dialog, '首句回复', `${reply} 请按更新后的步骤执行。`) await dialog.getByRole('button', { name: '保存配置' }).click() await expectSuccessMessage(page, '唤醒词配置已更新') await page.reload() await filterByKeyword(page, '搜索名称、唤醒短语、智能体或回复内容', phrase) row = tableRowByText(page, phrase) await expect(row).toContainText('06:00-22:00') await expect(row).toContainText('请按更新后的步骤执行') activities.push({ module, action: '页面编辑并刷新验证自定义时段未丢失', result: 'PASS', resourceId }) await row.locator('.el-switch').click() await expectSuccessMessage(page, '唤醒词已停用') await page.reload() await filterByKeyword(page, '搜索名称、唤醒短语、智能体或回复内容', phrase) row = tableRowByText(page, phrase) await expect(row.locator('.el-switch')).not.toHaveClass(/is-checked/) activities.push({ module, action: '页面停用并刷新验证', result: 'PASS', resourceId }) await captureScreenshot(page, testInfo, '08-wakeword-disabled-persisted') await row.getByRole('button', { name: '删除' }).click() await confirmMessageBox(page, '确认删除') await expectSuccessMessage(page, '唤醒词已删除') activities.push({ module, action: '页面删除自定义时段唤醒词', result: 'PASS', resourceId }) resourceId = '' } catch (error) { primaryError = error activities.push({ module, action: '页面生命周期', result: 'FAIL', detail: error instanceof Error ? error.message : String(error) }) } finally { await cleanupBySuffix(request, headers, 'wake-words', activities, cleanupErrors) await attachLifecycleReport(testInfo, module, activities, cleanupErrors) } if (primaryError) throw primaryError expect(cleanupErrors).toEqual([]) }) test('工具管理:SSRF 拒绝、安全地址 CRUD/启动、条件可用性检测和状态筛选', async ({ page, request }, testInfo) => { const module = '工具管理' const activities: Activity[] = [] const cleanupErrors: string[] = [] const headers = await authHeaders(request) const name = `维修工单-${suffix}` const editedName = `${name}-修订` const safePath = `/ape2e/${suffix.toLowerCase()}` let resourceId = '' let primaryError: unknown await cleanupBySuffix(request, headers, 'tools', activities, cleanupErrors) try { await loginAsAdmin(page) await page.goto('/tools') await expect(page.getByRole('heading', { name: '工具管理' })).toBeVisible() await page.getByRole('button', { name: '新增业务工具' }).first().click() let dialog = visibleDialog(page) await fillFormItem(dialog, '工具名称', name) await fillFormItem(dialog, '用途说明', '装备维修工单与作业进度协同入口。') await dialog.getByText('高级接入参数(技术人员)').first().click() await fillFormItem(dialog, '原始工具地址', 'http://127.0.0.1:8001') await fillFormItem(dialog, '入口路径', safePath) await chooseSelectOption(page, dialog, '打开方式', '新窗口打开') await dialog.getByRole('button', { name: '保存工具' }).click() await expect(page.locator('.el-message--error').last()).toContainText(/本机|私有|内网|地址|禁止/) const unsafeLookup = await findResource(request, headers, 'tools', name, (item) => item.name === name) expect(unsafeLookup, '被 SSRF 策略拒绝的本机地址不得落库').toBeNull() activities.push({ module, action: '页面提交本机地址并由服务端 SSRF 策略拒绝', result: 'PASS', httpStatus: 400 }) await fillFormItem(dialog, '原始工具地址', 'https://example.com') await dialog.getByRole('button', { name: '保存工具' }).click() await expectSuccessMessage(page, '工具已加入市场') let card = await cardByText(page, name) const created = await findResource(request, headers, 'tools', name, (item) => item.name === name) expect(created).not.toBeNull() resourceId = String(created?.id ?? '') expect(created?.domain).toBe('https://example.com') activities.push({ module, action: '页面创建安全 HTTPS 工具', result: 'PASS', resourceId }) await captureScreenshot(page, testInfo, '09-tool-safe-url-created') await card.getByRole('button', { name: '编辑' }).click() dialog = visibleDialog(page) await fillFormItem(dialog, '工具名称', editedName) await fillFormItem(dialog, '用途说明', '装备维修工单、审批与进度协同入口(已修订)。') await dialog.getByRole('button', { name: '保存工具' }).click() await expectSuccessMessage(page, '工具配置已更新') await page.reload() await filterByKeyword(page, '搜索工具名称、分类或用途', editedName) card = await cardByText(page, editedName) await expect(card).toContainText('已修订') activities.push({ module, action: '页面编辑并刷新验证持久化', result: 'PASS', resourceId }) const popupPromise = page.waitForEvent('popup') await card.getByRole('button', { name: '打开工具' }).click() const popup = await popupPromise await popup.waitForLoadState('domcontentloaded').catch(() => undefined) expect(popup.url()).toContain(`example.com${safePath}`) await popup.close() activities.push({ module, action: '页面通过后端 launch 地址安全启动新窗口', result: 'PASS', resourceId }) const checkResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST' && response.url().includes(`/api/v1/tools/${encodeURIComponent(resourceId)}/check`)) await card.getByRole('button', { name: '测试可用性' }).click() const checkApiResponse = await checkResponsePromise await responseData(checkApiResponse, 200, '页面工具可用性检测') const checkMessage = page.locator('.el-message').last() await expect(checkMessage).toBeVisible({ timeout: 20_000 }) const checkedResponse = await request.get(`/api/v1/tools/${encodeURIComponent(resourceId)}`, { headers }) const checked = await responseData(checkedResponse, 200, '读取工具检测结果') expect(['available', 'unavailable']).toContain(String(checked.connectionStatus)) const conditionLabel = checked.connectionStatus === 'available' ? '可正常使用' : '需要检查' const statusSelect = page.locator('.toolbar .el-select').last() await statusSelect.click() await page.locator('.el-select-dropdown:visible .el-select-dropdown__item').filter({ hasText: conditionLabel }).click() await page.waitForTimeout(350) card = await cardByText(page, editedName) activities.push({ module, action: '页面发起真实外部可用性检测并按实际结果筛选', result: 'PASS', resourceId, detail: `外部网络结果为“${conditionLabel}”;测试不把第三方网络可达性固定为成功条件。`, }) await captureScreenshot(page, testInfo, '10-tool-conditional-check-filter') await card.locator('footer .el-switch').click() await expectSuccessMessage(page, '工具入口已停用') await page.reload() await filterByKeyword(page, '搜索工具名称、分类或用途', editedName) card = await cardByText(page, editedName) await expect(card).toHaveClass(/disabled/) activities.push({ module, action: '页面停用并刷新验证', result: 'PASS', resourceId }) await card.getByRole('button', { name: '删除' }).click() await confirmMessageBox(page, '确认删除') await expectSuccessMessage(page, '工具已删除') activities.push({ module, action: '页面删除工具', result: 'PASS', resourceId }) resourceId = '' } catch (error) { primaryError = error activities.push({ module, action: '页面生命周期', result: 'FAIL', detail: error instanceof Error ? error.message : String(error) }) } finally { await cleanupBySuffix(request, headers, 'tools', activities, cleanupErrors) await attachLifecycleReport(testInfo, module, activities, cleanupErrors) } if (primaryError) throw primaryError expect(cleanupErrors).toEqual([]) }) })