|
- import type { APIRequestContext, TestInfo } from '@playwright/test'
-
- import { attachJson, captureScreenshot, expect, runId, runPrefix, test } from './fixtures'
- import { authHeaders, confirmMessageBox, envelopeData, loginAsAdmin, visibleDialog } from './helpers'
-
- test.describe.configure({ mode: 'serial' })
- test.use({ trace: 'off', video: 'off' })
-
- type JsonRecord = Record<string, unknown>
- type Headers = Record<string, string>
-
- interface Activity {
- module: string
- action: string
- result: 'PASS' | 'FAIL' | 'CLEANED' | 'CLEANUP_FAILED'
- httpStatus?: number
- resourceId?: string
- detail?: string
- }
-
- interface ResponseLike {
- status(): number
- json(): Promise<unknown>
- }
-
- interface DictionaryItem {
- id?: string
- kind: 'term' | 'homophone'
- term?: string
- wrongText?: string
- rightText?: string
- enabled: boolean
- builtIn?: boolean
- }
-
- 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: ResponseLike, 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()))
- }
-
- function addPass(
- activities: Activity[],
- module: string,
- action: string,
- response?: ResponseLike,
- resourceId?: string,
- detail?: string,
- ) {
- activities.push({ module, action, result: 'PASS', httpStatus: response?.status(), resourceId, detail })
- }
-
- 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: '报告未记录管理员密码、访问令牌或外部服务密钥。',
- })
- }
-
- async function waitForDocumentReady(
- request: APIRequestContext,
- headers: Headers,
- documentId: string,
- timeoutMs = 60_000,
- ) {
- const deadline = Date.now() + timeoutMs
- let latest: JsonRecord = {}
- while (Date.now() < deadline) {
- const response = await request.get(`/api/v1/knowledge/documents/${encodeURIComponent(documentId)}`, { headers })
- latest = await responseData(response, 200, '等待知识资料建立索引')
- const status = String(latest.status ?? '').toLowerCase()
- if (status === 'ready') return latest
- if (status === 'failed') throw new Error(`知识资料建立索引失败:${String(latest.errorMessage ?? '未返回原因')}`)
- await new Promise((resolve) => setTimeout(resolve, 500))
- }
- throw new Error(`知识资料在 ${timeoutMs}ms 内未进入 ready,最后状态:${String(latest.status ?? 'unknown')}`)
- }
-
- async function cleanupKnowledgeByPrefix(
- request: APIRequestContext,
- headers: Headers,
- activities: Activity[],
- cleanupErrors: string[],
- ) {
- try {
- const listed = await request.get('/api/v1/knowledge/bases', {
- headers,
- params: { keyword: runPrefix, page: '1', pageSize: '200' },
- })
- const page = await responseData(listed, 200, '清理前读取知识库')
- for (const base of asRecords(page.items)) {
- const baseId = String(base.id ?? '')
- if (!baseId || !String(base.name ?? '').startsWith(runPrefix)) continue
- const documentsResponse = await request.get('/api/v1/knowledge/documents', {
- headers,
- params: { knowledgeBaseId: baseId, page: '1', pageSize: '200' },
- })
- if (documentsResponse.status() === 200) {
- const documents = asRecord(envelopeData(await documentsResponse.json()))
- for (const item of asRecords(documents.items)) {
- const documentId = String(item.id ?? '')
- if (!documentId) continue
- const detailResponse = await request.get(`/api/v1/knowledge/documents/${encodeURIComponent(documentId)}`, { headers })
- if (detailResponse.status() !== 200) continue
- const detail = asRecord(envelopeData(await detailResponse.json()))
- const removed = await request.delete(`/api/v1/knowledge/documents/${encodeURIComponent(documentId)}`, {
- headers,
- params: { dataVersion: String(Number(detail.dataVersion ?? 0)) },
- })
- if (![200, 404].includes(removed.status())) throw new Error(`资料 ${documentId} 删除失败:HTTP ${removed.status()}`)
- activities.push({ module: '维修知识', action: '资料失败兜底清理', result: 'CLEANED', httpStatus: removed.status(), resourceId: documentId })
- }
- }
- const currentList = await request.get('/api/v1/knowledge/bases', {
- headers,
- params: { keyword: String(base.name ?? runPrefix), page: '1', pageSize: '200' },
- })
- const currentPage = asRecord(envelopeData(await currentList.json()))
- const current = asRecords(currentPage.items).find((item) => String(item.id) === baseId)
- if (!current) continue
- const removedBase = await request.delete(`/api/v1/knowledge/bases/${encodeURIComponent(baseId)}`, {
- headers,
- params: { dataVersion: String(Number(current.dataVersion ?? 0)) },
- })
- if (![200, 404].includes(removedBase.status())) throw new Error(`知识库 ${baseId} 删除失败:HTTP ${removedBase.status()}`)
- activities.push({ module: '维修知识', action: '知识库失败兜底清理', result: 'CLEANED', httpStatus: removedBase.status(), resourceId: baseId })
- }
- } catch (error) {
- const message = `知识库兜底清理异常:${error instanceof Error ? error.message : String(error)}`
- cleanupErrors.push(message)
- activities.push({ module: '维修知识', action: '失败兜底清理', result: 'CLEANUP_FAILED', detail: message })
- }
- }
-
- const dictionaryItem = (value: JsonRecord): DictionaryItem => ({
- id: String(value.id ?? '') || undefined,
- kind: String(value.kind) === 'homophone' ? 'homophone' : 'term',
- term: value.term == null ? undefined : String(value.term),
- wrongText: value.wrongText == null ? undefined : String(value.wrongText),
- rightText: value.rightText == null ? undefined : String(value.rightText),
- enabled: value.enabled !== false,
- builtIn: value.builtIn == null ? undefined : Boolean(value.builtIn),
- })
-
- const dictionaryKey = (item: DictionaryItem) => item.kind === 'term'
- ? `term:${item.term ?? ''}`
- : `homophone:${item.wrongText ?? ''}=>${item.rightText ?? ''}`
-
- const dictionarySummary = (items: DictionaryItem[]) => items
- .map((item) => ({
- kind: item.kind,
- term: item.term ?? '',
- wrongText: item.wrongText ?? '',
- rightText: item.rightText ?? '',
- enabled: item.enabled,
- builtIn: Boolean(item.builtIn),
- }))
- .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)))
-
- async function getDictionary(request: APIRequestContext, headers: Headers) {
- const response = await request.get('/api/v1/asr-dictionaries', { headers })
- const payload = await responseData(response, 200, '读取 ASR 词典')
- return {
- items: asRecords(payload.items).map(dictionaryItem),
- dataVersion: Number(payload.dataVersion ?? 0),
- }
- }
-
- async function saveDictionarySnapshot(
- request: APIRequestContext,
- headers: Headers,
- desired: DictionaryItem[],
- current: { items: DictionaryItem[]; dataVersion: number },
- ) {
- const ids = new Map(current.items.map((item) => [dictionaryKey(item), item.id]))
- return request.put('/api/v1/asr-dictionaries', {
- headers,
- data: {
- items: desired.map((item) => ({
- ...(ids.get(dictionaryKey(item)) ? { id: ids.get(dictionaryKey(item)) } : {}),
- kind: item.kind,
- ...(item.kind === 'term'
- ? { term: item.term }
- : { wrongText: item.wrongText, rightText: item.rightText }),
- enabled: item.enabled,
- builtIn: item.builtIn,
- })),
- dataVersion: current.dataVersion,
- },
- })
- }
-
- async function restoreDictionary(
- request: APIRequestContext,
- headers: Headers,
- snapshot: DictionaryItem[],
- ) {
- let current = await getDictionary(request, headers)
- let response = await saveDictionarySnapshot(request, headers, snapshot, current)
- await responseData(response, 200, '恢复 ASR 词典原始内容')
-
- // 页面保存会重新生成词典行。若原快照含内置标记,先让服务恢复标记,再按快照做最后一次精确裁剪。
- if (snapshot.some((item) => item.builtIn)) {
- response = await request.post('/api/v1/asr-dictionaries/restore-built-in', { headers, data: {} })
- await responseData(response, 200, '恢复 ASR 内置词条标记')
- current = await getDictionary(request, headers)
- response = await saveDictionarySnapshot(request, headers, snapshot, current)
- await responseData(response, 200, '按原快照精确裁剪 ASR 词典')
- }
- return response
- }
-
- async function cleanupCapabilityByPrefix(
- request: APIRequestContext,
- headers: Headers,
- activities: Activity[],
- cleanupErrors: string[],
- ) {
- try {
- const response = await request.get('/api/v1/capabilities', {
- headers,
- params: { capabilityType: 'ASR', keyword: runPrefix, page: '1', pageSize: '200' },
- })
- const page = await responseData(response, 200, '清理前读取 ASR 能力')
- for (const item of asRecords(page.items ?? page.records)) {
- const id = String(item.id ?? '')
- if (!id || !String(item.name ?? '').startsWith(runPrefix)) continue
- const detailResponse = await request.get(`/api/v1/capabilities/${encodeURIComponent(id)}`, { headers })
- if (detailResponse.status() === 404) continue
- const detail = await responseData(detailResponse, 200, '清理前刷新 ASR 能力版本')
- const removed = await request.delete(`/api/v1/capabilities/${encodeURIComponent(id)}`, {
- headers,
- params: { dataVersion: String(Number(detail.dataVersion ?? 0)) },
- })
- if (![200, 404].includes(removed.status())) throw new Error(`ASR 能力 ${id} 删除失败:HTTP ${removed.status()}`)
- activities.push({ module: 'ASR模型', action: '失败兜底清理', result: 'CLEANED', httpStatus: removed.status(), resourceId: id })
- }
- } catch (error) {
- const message = `ASR 能力兜底清理异常:${error instanceof Error ? error.message : String(error)}`
- cleanupErrors.push(message)
- activities.push({ module: 'ASR模型', action: '失败兜底清理', result: 'CLEANUP_FAILED', detail: message })
- }
- }
-
- test('维修知识页面:个人库、资料重建、启停、问答引用、下载与删除完整闭环', async ({ page, request }, testInfo) => {
- test.setTimeout(150_000)
- const headers = await authHeaders(request)
- const activities: Activity[] = []
- const cleanupErrors: string[] = []
- const baseName = `${runPrefix}-液压安全知识库`.slice(0, 80)
- const documentName = `${runPrefix}-液压安全规程.txt`.slice(0, 150)
- const editedDocumentName = `${runPrefix}-液压安全规程-已重建.txt`.slice(0, 150)
- const uniqueRule = `${runPrefix}液压安全口令:拆卸测试液压管路前必须先停机、断电、验电、完全卸压、挂牌上锁,并确认压力表归零。`
- let baseId = ''
- let documentId = ''
- let primaryError: unknown
-
- await cleanupKnowledgeByPrefix(request, headers, activities, cleanupErrors)
- cleanupErrors.length = 0
- activities.length = 0
-
- try {
- await loginAsAdmin(page)
- await page.goto('/knowledge/library')
- await page.getByRole('tab', { name: '用户自建' }).click()
- await expect(page.getByRole('button', { name: /新建知识库/ })).toBeVisible()
-
- await page.getByRole('button', { name: /新建知识库/ }).click()
- const createDialog = visibleDialog(page)
- await createDialog.locator('label').filter({ hasText: '知识库名称' }).locator('input').fill(baseName)
- await createDialog.locator('label').filter({ hasText: '知识库说明' }).locator('textarea').fill(`${runPrefix} 页面提交与持久化验证`)
- await createDialog.locator('label').filter({ hasText: '专业分类' }).locator('select').selectOption({ label: '液压气动' })
- await createDialog.locator('label').filter({ hasText: '标签' }).locator('input').fill('液压,安全,APE2E')
- const createResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST' && /\/api\/v1\/knowledge\/bases(?:\?|$)/.test(response.url()))
- await createDialog.getByRole('button', { name: '创建并进入' }).click()
- const createResponse = await createResponsePromise
- const createdBase = await responseData(createResponse, 201, '页面创建个人知识库')
- baseId = String(createdBase.id ?? '')
- expect(baseId).not.toBe('')
- await expect(page.getByRole('heading', { name: baseName })).toBeVisible()
- addPass(activities, '维修知识', '页面创建个人知识库并进入详情', createResponse, baseId)
- await captureScreenshot(page, testInfo, '01-personal-knowledge-base-created')
-
- const uploadResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST' && /\/api\/v1\/knowledge\/documents(?:\?|$)/.test(response.url()))
- await page.locator('input[type="file"]').setInputFiles({
- name: documentName,
- mimeType: 'text/plain',
- buffer: Buffer.from(`${uniqueRule}\n复核要求:作业完成后空载试运行并记录压力、温升和异常声响。`, 'utf8'),
- })
- const uploadResponse = await uploadResponsePromise
- const uploadPayload = await responseData(uploadResponse, 201, '页面上传知识资料')
- documentId = String(asRecord(uploadPayload.document).id ?? '')
- expect(documentId).not.toBe('')
- await waitForDocumentReady(request, headers, documentId)
- await page.reload()
- const uploadedRow = page.locator('.knowledge-document-list article').filter({ hasText: documentName })
- await expect(uploadedRow).toContainText(/个片段/)
- addPass(activities, '维修知识', 'TXT 上传、后台索引并刷新为可用', uploadResponse, documentId)
- await captureScreenshot(page, testInfo, '02-document-uploaded-ready')
-
- await uploadedRow.getByRole('button', { name: '编辑资料' }).click()
- let editor = visibleDialog(page)
- await expect(editor.getByText('正在读取资料正文…')).toHaveCount(0)
- await editor.locator('label').filter({ hasText: '资料名称' }).locator('input').fill(editedDocumentName)
- await editor.locator('label').filter({ hasText: '资料状态' }).locator('select').selectOption({ label: '禁用' })
- await editor.locator('label').filter({ hasText: '参考资料正文' }).locator('textarea').fill(`${uniqueRule}\n编辑确认:禁用资料不参与问答,重新启用后使用最新正文。`)
- let updateResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT' && response.url().includes(`/api/v1/knowledge/documents/${documentId}`))
- await editor.getByRole('button', { name: '保存并重建索引' }).click()
- let updateResponse = await updateResponsePromise
- await responseData(updateResponse, 200, '页面编辑并禁用知识资料')
- await waitForDocumentReady(request, headers, documentId)
- await page.reload()
- let documentRow = page.locator('.knowledge-document-list article').filter({ hasText: editedDocumentName })
- await expect(documentRow).toContainText('已禁用')
- addPass(activities, '维修知识', '编辑正文、重建索引并禁用', updateResponse, documentId)
- await captureScreenshot(page, testInfo, '03-document-edited-disabled')
-
- await documentRow.getByRole('button', { name: '编辑资料' }).click()
- editor = visibleDialog(page)
- await expect(editor.getByText('正在读取资料正文…')).toHaveCount(0)
- await editor.locator('label').filter({ hasText: '资料状态' }).locator('select').selectOption({ label: '启用并参与问答' })
- updateResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT' && response.url().includes(`/api/v1/knowledge/documents/${documentId}`))
- await editor.getByRole('button', { name: '保存并重建索引' }).click()
- updateResponse = await updateResponsePromise
- await responseData(updateResponse, 200, '页面重新启用知识资料')
- await waitForDocumentReady(request, headers, documentId)
- await page.reload()
- documentRow = page.locator('.knowledge-document-list article').filter({ hasText: editedDocumentName })
- await expect(documentRow).toContainText(/个片段/)
- await expect(documentRow).not.toContainText('已禁用')
- addPass(activities, '维修知识', '重新启用并刷新验证持久化', updateResponse, documentId)
- await captureScreenshot(page, testInfo, '04-document-enabled-persisted')
-
- const question = page.getByLabel('知识库问题')
- await question.fill(`${runPrefix}液压安全口令要求完成哪些步骤?`)
- const answerResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST' && response.url().includes('/api/v1/knowledge/answer'))
- await page.getByRole('button', { name: '生成有依据回答' }).click()
- const answerResponse = await answerResponsePromise
- const answer = await responseData(answerResponse, 200, '页面知识问答')
- expect(String(answer.answer ?? '')).toContain(runPrefix)
- const citations = page.locator('.knowledge-citations button:not(:disabled)')
- await expect(page.locator('.knowledge-answer')).toBeVisible()
- await expect(citations.first()).toBeVisible()
- addPass(activities, '维修知识', '真实问答命中最新正文并显示引用', answerResponse, documentId, `引用 ${asRecords(answer.results).length} 条`)
- await captureScreenshot(page, testInfo, '05-answer-with-citation')
-
- const downloadPromise = page.waitForEvent('download')
- await citations.first().click()
- const download = await downloadPromise
- expect(download.suggestedFilename()).toContain('.txt')
- addPass(activities, '维修知识', '从引用下载受保护原文', undefined, documentId)
- await captureScreenshot(page, testInfo, '06-citation-download-complete')
-
- await documentRow.getByRole('button', { name: '编辑资料' }).click()
- editor = visibleDialog(page)
- await expect(editor.getByText('正在读取资料正文…')).toHaveCount(0)
- await editor.getByRole('button', { name: '删除资料' }).click()
- const deleteDocumentResponsePromise = page.waitForResponse((response) => response.request().method() === 'DELETE' && response.url().includes(`/api/v1/knowledge/documents/${documentId}`))
- await confirmMessageBox(page, '删除')
- const deleteDocumentResponse = await deleteDocumentResponsePromise
- await responseData(deleteDocumentResponse, 200, '页面删除知识资料')
- const deletedDocumentId = documentId
- documentId = ''
- await expect(page.locator('.knowledge-document-list article').filter({ hasText: editedDocumentName })).toHaveCount(0)
- addPass(activities, '维修知识', '页面删除知识资料', deleteDocumentResponse, deletedDocumentId)
- await captureScreenshot(page, testInfo, '07-document-deleted')
-
- await page.getByRole('button', { name: /删除知识库/ }).click()
- const deleteBaseResponsePromise = page.waitForResponse((response) => response.request().method() === 'DELETE' && response.url().includes(`/api/v1/knowledge/bases/${baseId}`))
- await confirmMessageBox(page, '确认删除')
- const deleteBaseResponse = await deleteBaseResponsePromise
- await responseData(deleteBaseResponse, 200, '页面删除个人知识库')
- const deletedBaseId = baseId
- baseId = ''
- await expect(page).toHaveURL(/\/knowledge\/library(?:\?|$)/)
- await page.getByRole('tab', { name: '用户自建' }).click()
- await expect(page.locator('.knowledge-base-card').filter({ hasText: baseName })).toHaveCount(0)
- addPass(activities, '维修知识', '页面删除个人知识库并从列表消失', deleteBaseResponse, deletedBaseId)
- await captureScreenshot(page, testInfo, '08-knowledge-base-deleted')
- } catch (error) {
- primaryError = error
- activities.push({ module: '维修知识', action: '页面完整闭环', result: 'FAIL', detail: error instanceof Error ? error.message : String(error) })
- } finally {
- if (baseId || documentId) await cleanupKnowledgeByPrefix(request, headers, activities, cleanupErrors)
- await attachLifecycleReport(testInfo, '维修知识UI', activities, cleanupErrors)
- }
-
- if (primaryError) throw primaryError
- expect(cleanupErrors, '知识库测试资源必须清理干净').toEqual([])
- })
-
- test('ASR能力页面:词典保存刷新恢复与模型创建、编辑、启停、筛选、删除', async ({ page, request }, testInfo) => {
- test.setTimeout(120_000)
- const headers = await authHeaders(request)
- const activities: Activity[] = []
- const cleanupErrors: string[] = []
- const capabilityName = `${runPrefix}-本地维修ASR`.slice(0, 90)
- const testTerm = `${runPrefix}支腿阀组`.slice(0, 180)
- const wrongText = `${runPrefix}之腿阀组`.slice(0, 180)
- const rightText = `${runPrefix}支腿阀组`.slice(0, 180)
- const dictionarySnapshot = await getDictionary(request, headers)
- let capabilityId = ''
- let primaryError: unknown
- let dictionaryRestored = false
-
- await cleanupCapabilityByPrefix(request, headers, activities, cleanupErrors)
- cleanupErrors.length = 0
- activities.length = 0
-
- try {
- await loginAsAdmin(page)
- await page.goto('/assets/asr-models')
- const dictionary = page.locator('.asr-dictionary')
- await dictionary.locator('summary').click()
- await expect(dictionary.getByRole('button', { name: '保存词典' })).toBeEnabled()
- const termInput = dictionary.locator('label').filter({ hasText: '专有名词' }).locator('textarea')
- const homophoneInput = dictionary.locator('label').filter({ hasText: '同音词纠正' }).locator('textarea')
- const originalTerms = await termInput.inputValue()
- const originalHomophones = await homophoneInput.inputValue()
- await termInput.fill([originalTerms, testTerm].filter(Boolean).join('\n'))
- await homophoneInput.fill([originalHomophones, `${wrongText}=${rightText}`].filter(Boolean).join('\n'))
- const dictionarySaveResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT' && /\/api\/v1\/asr-dictionaries(?:\?|$)/.test(response.url()))
- await dictionary.getByRole('button', { name: '保存词典' }).click()
- const dictionarySaveResponse = await dictionarySaveResponsePromise
- await responseData(dictionarySaveResponse, 200, '页面保存 ASR 词典')
- await expect(page.getByText(/词典已保存/).last()).toBeVisible()
- addPass(activities, 'ASR词典', '页面追加专有名词与同音词', dictionarySaveResponse)
- await captureScreenshot(page, testInfo, '09-asr-dictionary-saved')
-
- await page.reload()
- await dictionary.locator('summary').click()
- await expect(termInput).toHaveValue(new RegExp(testTerm))
- await expect(homophoneInput).toHaveValue(new RegExp(wrongText))
- const persistedDictionary = await getDictionary(request, headers)
- expect(persistedDictionary.items.some((item) => item.kind === 'term' && item.term === testTerm)).toBeTruthy()
- expect(persistedDictionary.items.some((item) => item.kind === 'homophone' && item.wrongText === wrongText && item.rightText === rightText)).toBeTruthy()
- addPass(activities, 'ASR词典', '页面刷新与 API 均保留词典改动')
- await captureScreenshot(page, testInfo, '10-asr-dictionary-refresh-persisted')
-
- await page.getByRole('button', { name: /新增ASR模型/ }).click()
- let dialog = visibleDialog(page)
- await dialog.locator('.capability-form > label').filter({ hasText: '名称' }).locator('input').fill(capabilityName)
- await dialog.locator('.capability-form > label').filter({ hasText: '说明' }).locator('textarea').fill(`${runPrefix} 页面创建的本地识别能力`)
- await dialog.locator('label.file-field').filter({ hasText: '识别热词' }).locator('textarea').fill(`${runPrefix}液压缸\n${runPrefix}连接销`)
- await dialog.locator('.capability-form-technical summary').click()
- await dialog.locator('.capability-form-technical input').fill(`${runPrefix.toLowerCase()}-local-asr`)
- const enabledSwitch = dialog.locator('.switch-field .el-switch')
- if ((await enabledSwitch.getAttribute('class'))?.includes('is-checked')) await enabledSwitch.click()
- const createResponsePromise = page.waitForResponse((response) => response.request().method() === 'POST' && /\/api\/v1\/capabilities(?:\?|$)/.test(response.url()))
- await dialog.getByRole('button', { name: '保存', exact: true }).click()
- const createResponse = await createResponsePromise
- const created = await responseData(createResponse, 201, '页面创建 ASR 能力')
- capabilityId = String(created.id ?? '')
- expect(capabilityId).not.toBe('')
- let card = page.locator('.capability-card').filter({ hasText: capabilityName })
- await expect(card).toBeVisible()
- await expect(card.locator('.status-pill')).toHaveText('已停用')
- addPass(activities, 'ASR模型', '页面创建禁用状态的本地 ASR', createResponse, capabilityId)
- await captureScreenshot(page, testInfo, '11-asr-model-created-disabled')
-
- await card.getByRole('button', { name: '编辑', exact: true }).click()
- dialog = visibleDialog(page)
- await dialog.locator('.capability-form > label').filter({ hasText: '说明' }).locator('textarea').fill(`${runPrefix} 已编辑、刷新后应保持`)
- await dialog.locator('label.file-field').filter({ hasText: '识别热词' }).locator('textarea').fill(`${runPrefix}液压缸\n${runPrefix}连接销\n${runPrefix}蓄能器`)
- const editResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT' && response.url().includes(`/api/v1/capabilities/${capabilityId}`) && !response.url().includes('/status'))
- await dialog.getByRole('button', { name: '保存', exact: true }).click()
- const editResponse = await editResponsePromise
- const edited = await responseData(editResponse, 200, '页面编辑 ASR 能力')
- expect(asRecords(edited.hotWords).length || (Array.isArray(edited.hotWords) ? edited.hotWords.length : 0)).toBe(3)
- addPass(activities, 'ASR模型', '页面编辑说明与识别热词', editResponse, capabilityId)
-
- await page.reload()
- const search = page.getByPlaceholder(/搜索ASR模型名称/)
- await search.fill(capabilityName)
- card = page.locator('.capability-card').filter({ hasText: capabilityName })
- await expect(card).toContainText('已编辑、刷新后应保持')
- await expect(card).toContainText(`${runPrefix}蓄能器`)
- addPass(activities, 'ASR模型', '刷新后编辑内容保持', undefined, capabilityId)
- await captureScreenshot(page, testInfo, '12-asr-model-edit-persisted')
-
- const enableResponsePromise = page.waitForResponse((response) => response.request().method() === 'PUT' && response.url().includes(`/api/v1/capabilities/${capabilityId}/status`))
- await card.getByRole('button', { name: '启用', exact: true }).click()
- const enableResponse = await enableResponsePromise
- await responseData(enableResponse, 200, '页面启用 ASR 能力')
- await expect(card.locator('.status-pill')).toHaveText('可用')
- addPass(activities, 'ASR模型', '页面启用能力', enableResponse, capabilityId)
-
- await page.reload()
- await search.fill(capabilityName)
- card = page.locator('.capability-card').filter({ hasText: capabilityName })
- await expect(card.locator('.status-pill')).toHaveText('可用')
- const statusFilter = page.getByLabel('状态筛选')
- await statusFilter.selectOption('enabled')
- await expect(card).toBeVisible()
- await statusFilter.selectOption('disabled')
- await expect(card).toHaveCount(0)
- await expect(page.getByText('没有匹配的配置')).toBeVisible()
- addPass(activities, 'ASR模型', '刷新验证启用状态并完成状态筛选', undefined, capabilityId)
- await captureScreenshot(page, testInfo, '13-asr-status-filter')
-
- await statusFilter.selectOption('all')
- card = page.locator('.capability-card').filter({ hasText: capabilityName })
- await card.getByRole('button', { name: '删除', exact: true }).click()
- const deleteResponsePromise = page.waitForResponse((response) => response.request().method() === 'DELETE' && response.url().includes(`/api/v1/capabilities/${capabilityId}`))
- await confirmMessageBox(page, /确定/)
- const deleteResponse = await deleteResponsePromise
- await responseData(deleteResponse, 200, '页面删除 ASR 能力')
- const deletedCapabilityId = capabilityId
- capabilityId = ''
- await expect(page.locator('.capability-card').filter({ hasText: capabilityName })).toHaveCount(0)
- const absent = await request.get(`/api/v1/capabilities/${encodeURIComponent(deletedCapabilityId)}`, { headers })
- expect(absent.status()).toBe(404)
- addPass(activities, 'ASR模型', '页面删除并确认 API 不可读', deleteResponse, deletedCapabilityId)
- await captureScreenshot(page, testInfo, '14-asr-model-deleted')
- } catch (error) {
- primaryError = error
- activities.push({ module: 'ASR能力与词典', action: '页面完整闭环', result: 'FAIL', detail: error instanceof Error ? error.message : String(error) })
- } finally {
- if (capabilityId) await cleanupCapabilityByPrefix(request, headers, activities, cleanupErrors)
- try {
- await restoreDictionary(request, headers, dictionarySnapshot.items)
- const restored = await getDictionary(request, headers)
- expect(dictionarySummary(restored.items)).toEqual(dictionarySummary(dictionarySnapshot.items))
- dictionaryRestored = true
- activities.push({ module: 'ASR词典', action: '精确恢复运行前语义快照', result: 'CLEANED', detail: `恢复 ${restored.items.length} 条词典数据` })
- } catch (error) {
- const message = `ASR 词典恢复异常:${error instanceof Error ? error.message : String(error)}`
- cleanupErrors.push(message)
- activities.push({ module: 'ASR词典', action: '恢复原快照', result: 'CLEANUP_FAILED', detail: message })
- }
- await attachLifecycleReport(testInfo, 'ASR能力与词典UI', activities, cleanupErrors)
- }
-
- if (primaryError) throw primaryError
- expect(dictionaryRestored, 'ASR 词典必须恢复为运行前快照').toBeTruthy()
- expect(cleanupErrors, 'ASR 测试能力与词典必须清理恢复').toEqual([])
- })
|