|
- import type { APIRequestContext, Locator, Page, TestInfo } from '@playwright/test'
-
- import { attachJson, captureScreenshot, expect, runPrefix, test } from './fixtures'
- import { authHeaders, confirmMessageBox, envelopeData, loginAsAdmin } from './helpers'
-
- type JsonRecord = Record<string, unknown>
- type Headers = Record<string, string>
-
- interface Activity {
- module: string
- action: string
- result: 'PASS' | 'CLEANED' | 'CLEANUP_FAILED'
- resourceId?: string
- detail?: string
- }
-
- const MATERIAL_PNG = Buffer.from(
- 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
- 'base64',
- )
-
- const asRecord = (value: unknown): JsonRecord => value && typeof value === 'object' ? value as JsonRecord : {}
- const asRecords = (value: unknown): JsonRecord[] => Array.isArray(value) ? value.map(asRecord) : []
-
- async function apiData(response: Awaited<ReturnType<APIRequestContext['get']>>, expected = 200) {
- expect(response.status()).toBe(expected)
- return asRecord(envelopeData(await response.json()))
- }
-
- async function projectDetail(request: APIRequestContext, headers: Headers, id: string) {
- const response = await request.get(`/api/v1/video-projects/${encodeURIComponent(id)}`, { headers })
- return { response, data: response.status() === 200 ? await apiData(response) : {} }
- }
-
- async function videoDetail(request: APIRequestContext, headers: Headers, id: string) {
- const response = await request.get(`/api/v1/videos/${encodeURIComponent(id)}`, { headers })
- return { response, data: response.status() === 200 ? await apiData(response) : {} }
- }
-
- async function findFolder(request: APIRequestContext, headers: Headers, name: string) {
- const response = await request.get('/api/v1/video-folders', { headers })
- const data = await apiData(response)
- return asRecords(data.items).find((item) => String(item.name ?? '') === name) || null
- }
-
- async function findByKeyword(request: APIRequestContext, headers: Headers, endpoint: string, keyword: string, name: string) {
- const response = await request.get(endpoint, { headers, params: { keyword, page: '1', pageSize: '100' } })
- const data = await apiData(response)
- return asRecords(data.items).find((item) => String(item.name ?? item.title ?? '') === name) || null
- }
-
- async function cleanupVideo(request: APIRequestContext, headers: Headers, id: string, activities: Activity[], errors: string[]) {
- try {
- let detail = await videoDetail(request, headers, id)
- if (detail.response.status() === 404) return
- if (detail.response.status() !== 200) throw new Error(`HTTP ${detail.response.status()}`)
- if (String(detail.data.shareStatus ?? '') === 'active') {
- await request.delete(`/api/v1/videos/${encodeURIComponent(id)}/share`, {
- headers,
- data: { reason: 'APE2E failure cleanup' },
- })
- detail = await videoDetail(request, headers, id)
- }
- const removed = await request.delete(`/api/v1/videos/${encodeURIComponent(id)}`, {
- headers,
- params: { dataVersion: String(Number(detail.data.dataVersion ?? 0)) },
- })
- if (![200, 404].includes(removed.status())) throw new Error(`HTTP ${removed.status()}`)
- activities.push({ module: '成片管理', action: '失败兜底清理成片', result: 'CLEANED', resourceId: id })
- } catch (error) {
- const message = `成片 ${id} 清理失败:${error instanceof Error ? error.message : String(error)}`
- errors.push(message)
- activities.push({ module: '成片管理', action: '失败兜底清理成片', result: 'CLEANUP_FAILED', resourceId: id, detail: message })
- }
- }
-
- async function cleanupFolder(request: APIRequestContext, headers: Headers, id: string, activities: Activity[], errors: string[]) {
- try {
- const response = await request.get('/api/v1/video-folders', { headers })
- const data = await apiData(response)
- const folder = asRecords(data.items).find((item) => String(item.id ?? '') === id)
- if (!folder) return
- const removed = await request.delete(`/api/v1/video-folders/${encodeURIComponent(id)}`, {
- headers,
- params: { dataVersion: String(Number(folder.dataVersion ?? 0)) },
- })
- if (![200, 404].includes(removed.status())) throw new Error(`HTTP ${removed.status()}`)
- activities.push({ module: '成片管理', action: '失败兜底清理文件夹', result: 'CLEANED', resourceId: id })
- } catch (error) {
- const message = `文件夹 ${id} 清理失败:${error instanceof Error ? error.message : String(error)}`
- errors.push(message)
- activities.push({ module: '成片管理', action: '失败兜底清理文件夹', result: 'CLEANUP_FAILED', resourceId: id, detail: message })
- }
- }
-
- async function cleanupProject(request: APIRequestContext, headers: Headers, id: string, activities: Activity[], errors: string[]) {
- try {
- const detail = await projectDetail(request, headers, id)
- if (detail.response.status() === 404) return
- if (detail.response.status() !== 200) throw new Error(`HTTP ${detail.response.status()}`)
- const removed = await request.delete(`/api/v1/video-projects/${encodeURIComponent(id)}`, {
- headers,
- params: { dataVersion: String(Number(detail.data.dataVersion ?? 0)) },
- })
- if (![200, 404].includes(removed.status())) throw new Error(`HTTP ${removed.status()}`)
- activities.push({ module: '视频制作', action: '清理页面创建的视频工程', result: 'CLEANED', resourceId: id })
- } catch (error) {
- const message = `视频工程 ${id} 清理失败:${error instanceof Error ? error.message : String(error)}`
- errors.push(message)
- activities.push({ module: '视频制作', action: '清理页面创建的视频工程', result: 'CLEANUP_FAILED', resourceId: id, detail: message })
- }
- }
-
- async function selectFirstEnabledOption(select: Locator, preferredLabel: RegExp) {
- const options = await select.locator('option').evaluateAll((nodes) => nodes.map((node) => ({
- value: (node as HTMLOptionElement).value,
- label: (node.textContent || '').trim(),
- disabled: (node as HTMLOptionElement).disabled,
- })))
- const selected = options.find((item) => !item.disabled && item.value && preferredLabel.test(item.label))
- || options.find((item) => !item.disabled && item.value)
- expect(selected, '页面必须加载至少一个可选的真实语音能力').toBeTruthy()
- await select.selectOption(selected!.value)
- return selected!
- }
-
- async function expectPublicShareRejected(page: Page, url: string) {
- await page.goto(url)
- await expect(page.getByText('视频需要验证或链接暂不可用')).toBeVisible()
- }
-
- test.describe.configure({ mode: 'serial' })
- // 成片为 H.264/AAC MP4;Playwright 自带的开源 Chromium 不包含完整专利编解码器。
- // 本用例用系统 Chrome 完成真实播放验收,其余 E2E 仍使用项目 Chromium。
- test.use({ trace: 'off', video: 'off', channel: 'chrome' })
-
- test('视频制作与成片管理全部通过页面提交并真实生成成片', async ({ page, context, request }, testInfo: TestInfo) => {
- test.setTimeout(15 * 60_000)
- const activities: Activity[] = []
- const cleanupErrors: string[] = []
- const headers = await authHeaders(request)
- const projectName = `${runPrefix}-液压安全口播`
- const editedVideoName = `${runPrefix}-液压安全成片`
- const folderName = `${runPrefix}-视频测试文件夹`
- const renamedFolderName = `${runPrefix}-安全培训成片`
- const scriptText = '检修前必须停机、断电、卸压并完成上锁挂牌。'
- const materialName = `${runPrefix}-检修要点.png`
- const sharePassword = 'Ape2eVideo#2026'
- const rotatedPassword = 'Ape2eRotate#2026'
- let projectId = ''
- let videoId = ''
- let folderId = ''
- let primaryError: unknown
-
- try {
- await loginAsAdmin(page)
- await page.goto('/videos/create?mode=offline')
- await expect(page.getByRole('heading', { name: '视频制作', exact: true })).toBeVisible()
- await expect(page.locator('.studio-avatar-list button:not([disabled])').first()).toBeVisible()
- await expect(page.getByText('渲染就绪', { exact: true })).toBeVisible({ timeout: 30_000 })
- await captureScreenshot(page, testInfo, '01-video-create-nonempty-assets')
-
- await page.getByRole('button', { name: /横屏(1920 × 1080)/ }).click()
- const avatar = page.locator('.studio-avatar-list button:not([disabled])').first()
- const avatarName = (await avatar.locator('b').textContent() || '').trim()
- expect(avatarName, '必须从页面选择真实已就绪形象').not.toBe('')
- await avatar.click()
-
- await page.locator('.editor-tool-rail button').filter({ hasText: '音频' }).click()
- const voiceCard = page.locator('.editor-resource-list article').filter({ hasText: '温和女声' }).first()
- await expect(voiceCard).toBeVisible()
- await voiceCard.locator('label').click()
- await expect(voiceCard.locator('input[type="radio"]')).toBeChecked()
-
- await page.locator('.editor-tool-rail button').filter({ hasText: '背景' }).click()
- await page.getByRole('button', { name: '实训教室', exact: true }).click()
- await page.locator('.editor-tool-rail button').filter({ hasText: '元素' }).click()
- await page.getByRole('button', { name: /检查标记/ }).click()
- await expect(page.locator('.stage-overlay').filter({ hasText: '已完成检查' })).toBeVisible()
-
- await page.locator('.editor-tool-rail button').filter({ hasText: '素材' }).click()
- await page.locator('.media-library input[type="file"]').setInputFiles({
- name: materialName,
- mimeType: 'image/png',
- buffer: MATERIAL_PNG,
- })
- await expect(page.locator('.uploaded-media-card')).toContainText(materialName)
- await expect(page.locator('.stage-media-picture-in-picture img')).toBeVisible()
-
- await page.getByRole('button', { name: '文本', exact: true }).last().click()
- await page.getByLabel('项目名称').fill(projectName)
- await page.locator('.script-field textarea').fill(scriptText)
- const capabilitySelect = page.getByLabel('语音能力资产')
- const selectedCapability = await selectFirstEnabledOption(capabilitySelect, /演示实训播报语音/)
- await page.getByLabel('字幕', { exact: true }).check()
- await page.getByRole('button', { name: '字幕样式', exact: true }).click()
- await page.getByLabel('字体').selectOption('Source Han Sans SC')
- await page.getByLabel('字号').selectOption('48')
- await page.getByLabel('字幕位置').selectOption('center')
- await captureScreenshot(page, testInfo, '02-video-project-ui-configured')
-
- await page.getByRole('button', { name: '保存工程', exact: true }).click()
- await expect(page.getByText('工程与四轨时间线已保存').last()).toBeVisible({ timeout: 60_000 })
- await expect.poll(() => new URL(page.url()).searchParams.get('project')).not.toBeNull()
- projectId = new URL(page.url()).searchParams.get('project') || ''
- expect(projectId).not.toBe('')
-
- let persistedProject = await projectDetail(request, headers, projectId)
- expect(persistedProject.response.status()).toBe(200)
- expect(persistedProject.data.name).toBe(projectName)
- expect(persistedProject.data.scriptText).toBe(scriptText)
- expect(persistedProject.data.orientation).toBe('landscape')
- expect(persistedProject.data.backgroundType).toBe('training')
- expect(persistedProject.data.subtitleEnabled).toBe(true)
- expect(persistedProject.data.subtitleFont).toBe('Source Han Sans SC')
- expect(persistedProject.data.subtitleFontSize).toBe(48)
- expect(persistedProject.data.subtitlePosition).toBe('center')
- expect(String(persistedProject.data.avatarId ?? '')).not.toBe('')
- expect(String(persistedProject.data.voiceCapabilityId ?? '')).not.toBe('')
- const tracks = asRecords(asRecord(persistedProject.data.timeline).tracks)
- expect(tracks.map((track) => String(track.code))).toEqual(['AVATAR', 'MEDIA', 'AUDIO', 'SUBTITLE'])
- const mediaClips = asRecords(tracks.find((track) => track.code === 'MEDIA')?.clips)
- expect(mediaClips.some((clip) => clip.type === 'MEDIA' && String(clip.fileId ?? ''))).toBe(true)
- expect(mediaClips.some((clip) => clip.type === 'CHECK')).toBe(true)
- const audioClip = asRecords(tracks.find((track) => track.code === 'AUDIO')?.clips)[0] || {}
- expect(asRecord(audioClip.style).speaker).toBe(2)
- expect(String(audioClip.capabilityId ?? '')).toBe(String(persistedProject.data.voiceCapabilityId ?? ''))
- expect(asRecords(tracks.find((track) => track.code === 'SUBTITLE')?.clips)[0]?.text).toBe(scriptText)
- activities.push({ module: '视频制作', action: '页面保存横屏工程、真实形象音色、素材、元素、字幕和四轨时间线', result: 'PASS', resourceId: projectId })
- await captureScreenshot(page, testInfo, '03-video-project-saved-four-tracks')
-
- await page.reload()
- await expect(page.getByText('工程已保存', { exact: true })).toBeVisible({ timeout: 30_000 })
- await expect(page.getByLabel('项目名称')).toHaveValue(projectName)
- await expect(page.locator('.script-field textarea')).toHaveValue(scriptText)
- await expect(page.getByRole('button', { name: /横屏(1920 × 1080)/ })).toHaveClass(/active/)
- await page.locator('.editor-tool-rail button').filter({ hasText: '素材' }).click()
- await expect(page.locator('.uploaded-media-card')).toContainText(materialName)
- await expect(page.locator('.stage-overlay').filter({ hasText: '已完成检查' })).toBeVisible()
- await expect(page.getByLabel('语音能力资产')).toHaveValue(selectedCapability.value)
- await expect(page.locator('.timeline-clip.media')).toContainText(materialName)
- await expect(page.locator('.timeline-clip.subtitle')).toContainText(scriptText)
- await page.locator('.editor-tool-rail button').filter({ hasText: '音频' }).click()
- await expect(page.locator('.editor-resource-list article').filter({ hasText: '温和女声' }).locator('input[type="radio"]')).toBeChecked()
- activities.push({ module: '视频制作', action: '刷新 project 深链并核验全部配置持久化', result: 'PASS', resourceId: projectId })
- await captureScreenshot(page, testInfo, '04-video-project-deeplink-persisted')
-
- await expect(page.getByText('渲染就绪', { exact: true })).toBeVisible({ timeout: 30_000 })
- await page.getByRole('button', { name: '生成视频', exact: true }).click()
- await expect(page.locator('.editor-render-progress')).toBeVisible({ timeout: 20_000 })
- await captureScreenshot(page, testInfo, '05-real-video-render-running')
- const succeeded = page.locator('.editor-render-progress.is-succeeded')
- await expect(succeeded).toContainText('视频生成完成', { timeout: 8 * 60_000 })
- await expect(succeeded).toContainText('100%')
- activities.push({ module: '视频制作', action: '页面发起真实 TTS、浏览器录制、FFmpeg 合成与成片归档', result: 'PASS', resourceId: projectId })
- await captureScreenshot(page, testInfo, '06-real-video-render-succeeded')
-
- await succeeded.getByRole('button', { name: '查看作品' }).click()
- await expect(page).toHaveURL(/\/videos\/manage\?video=/)
- videoId = new URL(page.url()).searchParams.get('video') || ''
- expect(videoId).not.toBe('')
- let detail = page.locator('.video-detail-overlay')
- await expect(detail).toBeVisible({ timeout: 30_000 })
- await expect(detail).toContainText(projectName)
- const video = detail.locator('video')
- await expect(video).toHaveAttribute('src', /.+/)
- await detail.getByRole('button', { name: '播放视频', exact: true }).click()
- await expect(detail.getByRole('button', { name: '暂停视频', exact: true })).toBeVisible()
- await expect.poll(() => video.evaluate((element: HTMLVideoElement) => !element.paused)).toBe(true)
- await detail.getByRole('button', { name: '暂停视频', exact: true }).click()
- await expect(detail.getByRole('button', { name: '播放视频', exact: true })).toBeVisible()
- const downloadPromise = page.waitForEvent('download', { timeout: 60_000 })
- await detail.getByRole('button', { name: '下载视频', exact: true }).click()
- const download = await downloadPromise
- expect(download.suggestedFilename()).toContain(projectName)
- expect(await download.path()).not.toBeNull()
- let persistedVideo = await videoDetail(request, headers, videoId)
- expect(persistedVideo.response.status()).toBe(200)
- expect(persistedVideo.data.status).toBe('ready')
- expect(String(persistedVideo.data.videoFileCode ?? '')).not.toBe('')
- expect(Number(persistedVideo.data.size ?? 0)).toBeGreaterThan(10_000)
- persistedProject = await projectDetail(request, headers, projectId)
- expect(persistedProject.data.lastVideoId).toBe(videoId)
- activities.push({ module: '成片管理', action: '页面打开生成成片详情并实际播放、下载', result: 'PASS', resourceId: videoId })
- await captureScreenshot(page, testInfo, '07-generated-video-played-downloaded')
-
- await detail.getByRole('button', { name: '关闭' }).click()
- await page.locator('.folder-create-button').click()
- let dialog = page.locator('.el-dialog:visible').last()
- await dialog.getByLabel('文件夹名称').fill(folderName)
- const createFolderResponsePromise = page.waitForResponse((response) => (
- response.request().method() === 'POST' && new URL(response.url()).pathname === '/api/v1/video-folders'
- ))
- const saveFolderButton = dialog.getByRole('button', { name: '保存文件夹', exact: true })
- await expect(saveFolderButton).toBeEnabled()
- await saveFolderButton.click()
- const createFolderResponse = await createFolderResponsePromise
- expect(
- createFolderResponse.status(),
- `页面新建文件夹请求失败:${await createFolderResponse.text()}`,
- ).toBe(201)
- await expect(page.getByText('文件夹已创建').last()).toBeVisible()
- let folderArticle = page.locator('.video-folder-strip article').filter({ hasText: folderName })
- await expect(folderArticle).toBeVisible()
- let folder = await findFolder(request, headers, folderName)
- expect(folder).not.toBeNull()
- folderId = String(folder?.id ?? '')
- activities.push({ module: '成片管理', action: '页面新建成片文件夹', result: 'PASS', resourceId: folderId })
- await captureScreenshot(page, testInfo, '08-video-folder-created')
-
- await folderArticle.getByRole('button', { name: '重命名文件夹' }).click()
- dialog = page.locator('.el-dialog:visible').last()
- await dialog.getByLabel('文件夹名称').fill(renamedFolderName)
- await dialog.getByRole('button', { name: '保存文件夹', exact: true }).click()
- await expect(page.getByText('文件夹已更新').last()).toBeVisible()
- folderArticle = page.locator('.video-folder-strip article').filter({ hasText: renamedFolderName })
- await expect(folderArticle).toBeVisible()
-
- await page.getByPlaceholder('搜索标题、简介或标签').fill(projectName)
- await page.getByRole('button', { name: '搜索', exact: true }).click()
- await page.getByLabel('分类').selectOption({ label: '口播视频' })
- await page.getByLabel('可见范围').selectOption('private')
- await page.getByLabel('归属').selectOption('mine')
- await page.getByLabel('排序').selectOption('title')
- await page.locator('.sort-direction').click()
- let card = page.locator('.video-library-card').filter({ hasText: projectName })
- await expect(card).toBeVisible()
- activities.push({ module: '成片管理', action: '页面重命名文件夹并组合搜索、分类、可见范围、归属与排序', result: 'PASS', resourceId: folderId })
- await captureScreenshot(page, testInfo, '09-video-folder-renamed-and-filtered')
-
- await card.getByRole('button', { name: '查看详情', exact: true }).click()
- detail = page.locator('.video-detail-overlay')
- await detail.getByRole('button', { name: '编辑', exact: true }).click()
- await detail.getByLabel('标题').fill(editedVideoName)
- await detail.getByLabel('简介').fill('液压系统检修前安全确认与上锁挂牌教学成片。')
- await detail.getByLabel('所在文件夹').selectOption({ label: renamedFolderName })
- await detail.getByRole('textbox', { name: '分类', exact: true }).fill('安全培训')
- await detail.getByLabel('标签').fill('液压系统、上锁挂牌、APE2E')
- await detail.getByLabel('可见范围').selectOption('public')
- await detail.getByRole('button', { name: '保存修改', exact: true }).click()
- await expect(page.getByText('视频资料已更新').last()).toBeVisible()
- await expect(detail).toContainText(editedVideoName)
- await expect(detail).toContainText(renamedFolderName)
- await expect(detail).toContainText('内网分享链接')
- persistedVideo = await videoDetail(request, headers, videoId)
- expect(persistedVideo.data.title).toBe(editedVideoName)
- expect(persistedVideo.data.folderId).toBe(folderId)
- expect(persistedVideo.data.visibility).toBe('public')
- expect(persistedVideo.data.tags).toEqual(['液压系统', '上锁挂牌', 'APE2E'])
- activities.push({ module: '成片管理', action: '页面编辑标题、简介、文件夹、分类、标签和可见范围', result: 'PASS', resourceId: videoId })
- await captureScreenshot(page, testInfo, '10-video-metadata-folder-visibility-updated')
-
- await detail.getByRole('button', { name: '创建分享链接', exact: true }).click()
- dialog = page.locator('.el-dialog:visible').last()
- await dialog.getByLabel('有效时长(小时)').fill('1')
- await dialog.getByLabel('访问密码(可选)').fill(sharePassword)
- await dialog.getByLabel('最大访问次数').fill('5')
- await dialog.getByRole('button', { name: '生成分享链接', exact: true }).click()
- await expect(dialog.getByText('分享链接已生成')).toBeVisible()
- await expect(dialog).toContainText('最多 5 次访问')
- const firstShareInput = dialog.getByLabel('本次视频分享链接')
- const firstShareUrl = await firstShareInput.inputValue()
- expect(new URL(firstShareUrl).pathname).toMatch(/^\/share\/videos\/[^/]+$/)
- await captureScreenshot(page, testInfo, '11-video-share-password-limit-issued', [firstShareInput])
- await dialog.getByRole('button', { name: '关闭', exact: true }).click()
-
- const sharedPage = await context.newPage()
- await sharedPage.goto(firstShareUrl)
- await expect(sharedPage.getByText('视频需要验证或链接暂不可用')).toBeVisible()
- await sharedPage.getByLabel('访问密码').fill(sharePassword)
- await sharedPage.getByRole('button', { name: '验证并播放' }).click()
- await expect(sharedPage.getByRole('heading', { name: editedVideoName })).toBeVisible()
- await expect(sharedPage.locator('video')).toBeVisible()
- await captureScreenshot(sharedPage, testInfo, '12-password-protected-share-verified')
- await sharedPage.close()
- activities.push({ module: '成片管理', action: '页面签发密码与限次分享并在公开页验证', result: 'PASS', resourceId: videoId })
-
- await detail.getByRole('button', { name: '轮换分享链接', exact: true }).click()
- dialog = page.locator('.el-dialog:visible').last()
- await dialog.getByLabel('有效时长(小时)').fill('2')
- await dialog.getByLabel('访问密码(可选)').fill(rotatedPassword)
- await dialog.getByLabel('最大访问次数').fill('3')
- await dialog.getByRole('button', { name: '确认轮换', exact: true }).click()
- await expect(dialog.getByText('分享链接已生成')).toBeVisible()
- const rotatedShareInput = dialog.getByLabel('本次视频分享链接')
- const rotatedShareUrl = await rotatedShareInput.inputValue()
- expect(rotatedShareUrl).not.toBe(firstShareUrl)
- await captureScreenshot(page, testInfo, '13-video-share-rotated', [rotatedShareInput])
- await dialog.getByRole('button', { name: '关闭', exact: true }).click()
- const revokedOldSharePage = await context.newPage()
- await expectPublicShareRejected(revokedOldSharePage, firstShareUrl)
- await revokedOldSharePage.close()
-
- await detail.getByRole('button', { name: '停止分享', exact: true }).click()
- await confirmMessageBox(page, '停止分享')
- await expect(page.getByText('分享已停止').last()).toBeVisible()
- persistedVideo = await videoDetail(request, headers, videoId)
- expect(persistedVideo.data.shareStatus).not.toBe('active')
- const revokedRotatedSharePage = await context.newPage()
- await expectPublicShareRejected(revokedRotatedSharePage, rotatedShareUrl)
- await revokedRotatedSharePage.close()
- activities.push({ module: '成片管理', action: '页面轮换与停止分享,新旧链接均按预期失效', result: 'PASS', resourceId: videoId })
- await captureScreenshot(page, testInfo, '14-video-share-stopped')
-
- await detail.getByRole('button', { name: '关闭' }).click()
- folderArticle = page.locator('.video-folder-strip article').filter({ hasText: renamedFolderName })
- await folderArticle.getByRole('button', { name: '删除文件夹' }).click()
- await confirmMessageBox(page, '确认删除')
- await expect(page.getByText('文件夹已删除,视频已移至未分类').last()).toBeVisible()
- folderId = ''
- await page.getByPlaceholder('搜索标题、简介或标签').fill(editedVideoName)
- await page.getByLabel('分类').selectOption('')
- await page.getByLabel('可见范围').selectOption('public')
- await page.getByRole('button', { name: '搜索', exact: true }).click()
- card = page.locator('.video-library-card').filter({ hasText: editedVideoName })
- await expect(card).toBeVisible()
- await card.getByRole('button', { name: '查看详情', exact: true }).click()
- detail = page.locator('.video-detail-overlay')
- await expect(detail).toContainText('未分类')
- persistedVideo = await videoDetail(request, headers, videoId)
- expect(persistedVideo.data.folderId).toBeNull()
- activities.push({ module: '成片管理', action: '页面删除含成片文件夹并核验成片移至未分类', result: 'PASS', resourceId: videoId })
- await captureScreenshot(page, testInfo, '15-video-folder-deleted-video-unfiled')
-
- await detail.getByRole('button', { name: '删除', exact: true }).click()
- await confirmMessageBox(page, '确认删除')
- await expect(page.getByText('视频资源已删除').last()).toBeVisible()
- await expect(page.locator('.video-detail-overlay')).toHaveCount(0)
- const deletedVideo = await videoDetail(request, headers, videoId)
- expect(deletedVideo.response.status()).toBe(404)
- activities.push({ module: '成片管理', action: '页面永久删除成片与关联文件', result: 'PASS', resourceId: videoId })
- videoId = ''
- await captureScreenshot(page, testInfo, '16-generated-video-deleted-baseline-remains')
- } catch (error) {
- primaryError = error
- } finally {
- if (!videoId) {
- const leakedVideo = await findByKeyword(request, headers, '/api/v1/videos', runPrefix, editedVideoName).catch(() => null)
- || await findByKeyword(request, headers, '/api/v1/videos', runPrefix, projectName).catch(() => null)
- videoId = String(leakedVideo?.id ?? '')
- }
- if (!folderId) {
- const leakedFolder = await findFolder(request, headers, renamedFolderName).catch(() => null)
- || await findFolder(request, headers, folderName).catch(() => null)
- folderId = String(leakedFolder?.id ?? '')
- }
- if (!projectId) {
- const urlProjectId = new URL(page.url()).searchParams.get('project') || ''
- const leakedProject = urlProjectId
- ? null
- : await findByKeyword(request, headers, '/api/v1/video-projects', runPrefix, projectName).catch(() => null)
- projectId = urlProjectId || String(leakedProject?.id ?? '')
- }
- if (videoId) await cleanupVideo(request, headers, videoId, activities, cleanupErrors)
- if (folderId) await cleanupFolder(request, headers, folderId, activities, cleanupErrors)
- if (projectId) await cleanupProject(request, headers, projectId, activities, cleanupErrors)
- await attachJson(testInfo, '视频制作与成片管理-UI生命周期', {
- runPrefix,
- activities,
- cleanupComplete: cleanupErrors.length === 0,
- cleanupErrors,
- security: '分享密码和一次性分享令牌未写入附件,包含令牌的截图区域已遮罩。',
- })
- }
-
- if (primaryError) throw primaryError
- expect(cleanupErrors, '视频 UI 生命周期测试数据必须清理干净').toEqual([])
- })
|