|
- 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<string, unknown>
- type Headers = Record<string, string>
- 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<unknown>
- }
-
- 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<BrowserResponse | Error> = 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([])
- })
- })
|