Files
bililive-go/tests/e2e/ffmpeg-status.spec.ts
kira1928andClaude Fable 5 9811e0431b fix: 处理第二轮 code review 意见并修复 e2e 基础设施问题
后端:
- ffmpeg parser 找不到 FFmpeg 且异步初始化仍在 checking/downloading 时
  挂起等待终态(可被 ctx 取消或 Stop 中断),等待结束后立即重查并开始
  录制,不再进入"找不到 FFmpeg → 5 秒重试"失败循环(Codex P1)
- isFFmpegAvailableWithoutRemotetools 与 utils.GetFFmpegPath 语义对齐:
  配置了 ffmpeg_path 时以该路径为准不回退 PATH;补 exec.ErrDot → ./ffmpeg
  兜底(Copilot / Codex P2)
- WaitForToolsInit 在 Init() 重试进行中不再立即返回旧错误,等待本轮
  结束后读取最新结果;Init 的 defer 先发布结果再更新状态机(Gemini / Copilot)
- 新增 tools.FFmpegStatusChanged() 状态变化通知 channel

前端:
- ffmpeg-banner / update-banner 从 fixed 定位改为普通文档流,自然向下
  堆叠且把内容推开,不再互相重叠或遮挡页面顶部(Gemini high / Codex P3)
- SSEEventType 补充遗漏的 memory_warning 并注册监听

测试基础设施:
- mock server 新增 -speed 启动参数,限速在 bgo 启动前即生效(Codex P2)
- mock server 下载循环每个 chunk 前重新读取限速,运行时调速立即生效
  (Copilot / Codex P2)
- 修复 mock zip 结构:模拟真实发布包的单一顶层目录,避免 remotetools
  解压剥层后 pathToEntry 对不上导致 install 后找不到工具
- 新增 dev 专用 POST /api/debug/ffmpeg/reinit;下载失败用例先清工具缓存
  再重新触发初始化,解决用例间状态污染(Codex P2)
- e2e 等待逻辑去掉会被 SSE 长连接卡死的 networkidle(Codex P2)
- playwright.ffmpeg.config.ts 的清理副作用限定主进程执行,避免 worker
  重新加载配置时删掉下载中的工具目录
- CI 接入 ffmpeg 下载 e2e 并上传报告;package.json 加 test:e2e:ffmpeg
  (Codex P3)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:52:46 +09:00

137 lines
5.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ffmpeg-status.spec.ts
*
* 测试 FFmpeg 状态 Banner 的 UI 行为。
* 使用 /api/debug/ffmpeg/force-state 端点(dev 构建专有)注入各种状态,
* 验证 Banner 的显示、内容和消失逻辑是否正确。
*
* 依赖:正常 playwright.config.ts(bgo 在 8080 端口运行,带 -tags dev)
*/
import { test, expect, Page, APIRequestContext } from '@playwright/test';
const BASE_URL = 'http://127.0.0.1:8080';
/**
* 通过 debug 端点强制设置 FFmpeg 状态,并等待 UI 响应
*/
async function forceFFmpegState(
request: APIRequestContext,
state: 'checking' | 'downloading' | 'ready' | 'not_found' | 'error',
message?: string,
source?: string,
) {
const body: Record<string, string> = { state };
if (message) body['message'] = message;
if (source) body['source'] = source;
const resp = await request.post(`${BASE_URL}/api/debug/ffmpeg/force-state`, { data: body });
expect(resp.status()).toBe(200);
}
/**
* 等待并返回 FFmpeg Banner 元素(可见状态下)
*/
function ffmpegBanner(page: Page) {
return page.locator('.ffmpeg-banner');
}
test.describe('FFmpeg 状态 Banner', () => {
test.beforeEach(async ({ page, request }) => {
// 先将状态重置为 ready,确保页面初始加载时 Banner 不可见
await forceFFmpegState(request, 'ready');
await page.goto('/');
// 等待主界面渲染完成。不能用 networkidle:SSE 长连接会让 networkidle 永远等不到
await expect(page.locator('.ant-layout').first()).toBeVisible({ timeout: 15_000 });
// Banner 应为隐藏
await expect(ffmpegBanner(page)).not.toBeVisible();
});
test.afterEach(async ({ request }) => {
// 测试后恢复 ready 状态,不影响其他测试
await forceFFmpegState(request, 'ready');
});
test('ready 状态时 Banner 不显示', async ({ page }) => {
await expect(ffmpegBanner(page)).not.toBeVisible();
});
test('checking 状态时显示正在检测提示', async ({ page, request }) => {
await forceFFmpegState(request, 'checking');
await expect(ffmpegBanner(page)).toBeVisible({ timeout: 5000 });
await expect(ffmpegBanner(page)).toContainText('正在检测 FFmpeg');
});
test('downloading 状态时显示下载进度提示', async ({ page, request }) => {
await forceFFmpegState(
request,
'downloading',
'正在从 GitHub 下载 FFmpeg...',
'remotetools',
);
await expect(ffmpegBanner(page)).toBeVisible({ timeout: 5000 });
await expect(ffmpegBanner(page)).toContainText('正在下载 FFmpeg');
await expect(ffmpegBanner(page)).toContainText('正在从 GitHub 下载 FFmpeg...');
});
test('not_found 状态时显示警告提示', async ({ page, request }) => {
await forceFFmpegState(request, 'not_found', 'FFmpeg 未找到');
await expect(ffmpegBanner(page)).toBeVisible({ timeout: 5000 });
await expect(ffmpegBanner(page)).toContainText('未找到 FFmpeg');
});
test('error 状态时显示错误提示', async ({ page, request }) => {
await forceFFmpegState(request, 'error', '下载超时');
await expect(ffmpegBanner(page)).toBeVisible({ timeout: 5000 });
await expect(ffmpegBanner(page)).toContainText('FFmpeg 下载失败');
await expect(ffmpegBanner(page)).toContainText('下载超时');
});
test('从 downloading 切换到 ready 后 Banner 消失', async ({ page, request }) => {
// 先注入 downloading 状态
await forceFFmpegState(request, 'downloading', '模拟下载中...');
await expect(ffmpegBanner(page)).toBeVisible({ timeout: 5000 });
// 再切换到 ready — Banner 应消失
await forceFFmpegState(request, 'ready');
await expect(ffmpegBanner(page)).not.toBeVisible({ timeout: 5000 });
});
test('从 error 切换到 ready 后 Banner 消失', async ({ page, request }) => {
await forceFFmpegState(request, 'error', '模拟下载失败');
await expect(ffmpegBanner(page)).toBeVisible({ timeout: 5000 });
await forceFFmpegState(request, 'ready');
await expect(ffmpegBanner(page)).not.toBeVisible({ timeout: 5000 });
});
test('GET /api/ffmpeg/status 返回正确格式', async ({ request }) => {
await forceFFmpegState(request, 'downloading', '测试中', 'remotetools');
const resp = await request.get(`${BASE_URL}/api/ffmpeg/status`);
expect(resp.status()).toBe(200);
const body = await resp.json();
expect(body).toHaveProperty('state', 'downloading');
expect(body).toHaveProperty('message', '测试中');
expect(body).toHaveProperty('source', 'remotetools');
});
test('Banner 类型:downloading/checking 为 info,not_found 为 warning,error 为 error', async ({
page,
request,
}) => {
// downloading → info(蓝色横幅)
await forceFFmpegState(request, 'downloading');
await expect(ffmpegBanner(page)).toBeVisible({ timeout: 5000 });
await expect(ffmpegBanner(page)).toHaveClass(/ant-alert-info/);
// not_found → warning(黄色横幅)
await forceFFmpegState(request, 'not_found');
await expect(ffmpegBanner(page)).toHaveClass(/ant-alert-warning/, { timeout: 5000 });
// error → error(红色横幅)
await forceFFmpegState(request, 'error', '测试失败');
await expect(ffmpegBanner(page)).toHaveClass(/ant-alert-error/, { timeout: 5000 });
});
});