Files
bililive-go/playwright.config.ts
kira1928andClaude Sonnet 4.6 13c32ff68d test: 添加 FFmpeg 异步初始化 e2e 测试,并修复 code review 发现的两个 Bug
## e2e 测试基础设施

### 测试辅助工具
- test/fake-ffmpeg/main.go: 极简 FFmpeg 替身程序,响应 -version 参数
- test/ffmpeg-mock-server/main.go: 模拟 FFmpeg 下载服务器
  - 启动时自动编译 fake-ffmpeg 并打包成 zip(含 bin/ffmpeg.exe 路径)
  - GET /ffmpeg.zip: 支持 ?speed=<字节/秒> 限速和 ?fail=true 失败模拟
  - POST /control: 运行时动态修改限速/失败行为
  - GET /health: 等待 zip 就绪后返回 200(供 Playwright 做启动检查)

### 测试 Fixture
- tests/e2e/fixtures/test-tools-config.json: remotetools 测试配置,将所有
  平台的 FFmpeg 下载 URL 重定向至 127.0.0.1:8890/ffmpeg.zip
- tests/e2e/fixtures/ffmpeg-test-config.template.yml: bgo 测试配置
  - 端口 8085(避免与正常测试冲突)
  - ffmpeg_path 指向不存在路径(强制走 remotetools 下载流程)
  - 独立 app_data_path(避免与正常测试的缓存互相干扰)

### 后端调试注入端点(dev 构建专有)
- src/servers/debug_routes_dev.go: POST /api/debug/ffmpeg/force-state
  供 e2e 测试注入任意 FFmpeg 状态,无需实际触发下载
- src/servers/debug_routes_stub.go: 非 dev 构建的空实现
- src/servers/server.go: 注册 registerDevDebugRoutes(apiRoute)
- src/tools/tools.go: 导出 ForceFFmpegStatus() 供调试端点调用
- src/tools/dev_config.go: 支持 REMOTETOOLS_CONFIG 环境变量覆盖
  remotetools 配置路径,使下载流程测试可指向 mock server

### E2E 测试文件
- tests/e2e/ffmpeg-status.spec.ts: 9 条状态注入测试(在正常配置下运行)
  - 验证 ready/checking/downloading/not_found/error 各状态的 Banner 显示
  - 验证状态切换后 Banner 正确消失
  - 验证 GET /api/ffmpeg/status 返回格式和 Alert 类型
- tests/e2e/ffmpeg-download.spec.ts: 3 条完整下载流程测试(需 mock server)
  - 慢速下载时出现 downloading Banner,加速后 Banner 消失
  - 下载失败时显示 error Banner
  - 页面刷新后正确恢复当前状态
- playwright.ffmpeg.config.ts: 下载流程测试专用配置
  - 运行 ffmpeg-mock-server 和使用独立 bgo 实例(端口 8085)
  - 通过 REMOTETOOLS_CONFIG 环境变量注入测试用 remotetools 配置
  - 每次运行前清空 external_tools 目录,确保测试从干净状态开始
- playwright.config.ts: 新增 testIgnore 排除 ffmpeg-download.spec.ts
  (该文件需要 mock server,仅在 playwright.ffmpeg.config.ts 下运行)

## Bug 修复(对应 PR #1150 code review)

### Fix 1: WaitForToolsInit 错误分支死代码(tools.go)
原代码将 WaitForToolsInit 返回的所有非 nil 错误都当作 ctx 取消处理,
但 WaitForToolsInit 在 Init() 失败时也返回该错误,导致:
- FFmpeg 状态永久停留在 "checking"
- not_found/error 状态永远不会被设置
修复:通过 ctx.Err() != nil 区分 ctx 取消和 Init 失败,失败时
正确调用 setFFmpegStatus({State: "not_found"})

### Fix 2: 前端竞态条件(ffmpeg-banner/index.tsx)
原代码将 handleSSEMessage 定义在 useEffect 外部(useCallback),
导致:HTTP 初始化请求如果晚于 SSE 推送到达,会用旧状态覆盖新状态,
使 Banner 重新出现并永久卡在旧状态;且组件卸载后 HTTP 回调仍会
调用 setStatus 造成内存泄漏。
修复:
- 将 handler 移入 useEffect,添加 active flag 防止卸载后更新
- 添加 sseReceived flag,HTTP 响应到达时若已收到 SSE 则忽略
- 移除不再需要的 useCallback import

### Fix 3: FFmpegAsyncInit 过早访问 remotetools(tools.go)
原注释称"先检查系统 PATH / 配置文件中的 FFmpeg,不需要 remotetools",
但实际调用 utils.IsFFmpegExist → utils.GetFFmpegPath → remotetools.Get().GetTool(),
在 Init() 完成前访问未配置好的 remotetools,可能产生误判。
修复:新增 isFFmpegAvailableWithoutRemotetools() 仅检查配置路径
(os.Stat)和系统 PATH(exec.LookPath),不触碰 remotetools。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 01:02:30 +09:00

129 lines
4.0 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.
import { defineConfig, devices } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
// 加载本地环境变量配置(.env.local 不会被 git 跟踪)
dotenv.config({ path: path.join(__dirname, '.env.local') });
// 在测试前复制配置模板到 test-output 目录
// 这样配置文件的变化不会被 git 跟踪
const configTemplate = path.join(__dirname, 'tests/e2e/fixtures/test-config.template.yml');
const configRuntime = path.join(__dirname, 'test-output/test-config.yml');
// 确保 test-output 目录存在并复制配置
fs.mkdirSync(path.dirname(configRuntime), { recursive: true });
fs.copyFileSync(configTemplate, configRuntime);
/**
* Bililive-Go E2E 测试配置
*
* 使用 osrp-stream-tester 作为模拟直播流服务器
* 测试 Web UI 的各种功能
*/
export default defineConfig({
testDir: './tests/e2e',
// ffmpeg-download.spec.ts 需要独立的 mock server,仅在 playwright.ffmpeg.config.ts 中运行
testIgnore: ['**/ffmpeg-download.spec.ts'],
// 测试输出目录(被 .gitignore 忽略)
outputDir: 'test-results',
// 全局超时:30 秒(快速失败,节省 CI 时间)
timeout: 30 * 1000,
// 期望超时
expect: {
timeout: 5 * 1000,
},
// 由于需要共享服务器,不使用完全并行
fullyParallel: false,
// CI 环境检测
forbidOnly: !!process.env.CI,
// 重试策略(CI 中只重试 1 次)
retries: process.env.CI ? 1 : 0,
// 并发控制:由于需要共享服务器,使用单线程
workers: 1,
// 测试报告
reporter: [
['list'],
['html', { outputFolder: 'playwright-report', open: 'never' }],
],
// 共享配置
use: {
// 基础 URL(bgo Web UI)
baseURL: 'http://127.0.0.1:8080',
// 仅在失败时截图
screenshot: 'only-on-failure',
// 始终录制 trace,便于调试(可在 Trace Viewer 中查看源代码和调用栈)
trace: {
mode: 'on',
sources: true, // 在 trace 中包含源代码
snapshots: true, // 包含 DOM 快照
screenshots: true, // 包含截图用于时间线预览
},
// 仅在失败时录制视频
video: 'on-first-retry',
// 浏览器视口
viewport: { width: 1280, height: 720 },
},
// 只测试 Chromium(减少测试时间)
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
// Web 服务器配置
webServer: [
// update-mock-server 模拟版本 API 服务器
{
command: 'go run ./test/update-mock-server -version 99.0.0 -port 8889',
url: 'http://127.0.0.1:8889/health',
reuseExistingServer: !process.env.CI,
timeout: 60 * 1000,
stdout: 'pipe',
stderr: 'pipe',
},
// osrp-stream-tester 测试流服务器
{
// 本地开发可设置 OSRP_STREAM_TESTER_PATH 环境变量指向本地目录
// 例如: set OSRP_STREAM_TESTER_PATH=C:/path/to/osrp-stream-tester
// 如未设置(包括 CI),则从远程拉取最新版本
command: process.env.OSRP_STREAM_TESTER_PATH
? `powershell -Command "cd ${process.env.OSRP_STREAM_TESTER_PATH}; go run ./cmd/stream-tester serve --port 8888"`
: 'go run github.com/kira1928/osrp-stream-tester/cmd/stream-tester@latest serve --port 8888',
url: 'http://127.0.0.1:8888/health',
reuseExistingServer: !process.env.CI,
timeout: 30 * 1000,
stdout: 'pipe',
stderr: 'pipe',
},
// bililive-go 主程序(使用 dev 构建标签)
{
// 在 CI 中使用预构建的二进制,本地使用 go run
// 配置文件从模板复制到 test-output 目录(避免 git 跟踪动态变化)
command: process.env.CI
? './bin/bililive-dev --config test-output/test-config.yml'
: 'go run -tags dev ./src/cmd/bililive --config test-output/test-config.yml',
url: 'http://127.0.0.1:8080/api/info',
reuseExistingServer: !process.env.CI,
timeout: 60 * 1000,
stdout: 'pipe',
stderr: 'pipe',
},
],
});