## Context 前端 Playwright E2E 测试当前只启动 `bun run dev`(Vite dev server),后端不参与。现有 5 个 E2E 测试文件中,所有写操作(创建/编辑/删除供应商和模型)的验证都不完整——因为 API 请求无法到达后端,表单提交后的数据持久化无法验证,大量测试在缺少已有数据时直接 `test.skip()`。 后端具备天然测试友好特性:Go CLI 参数可覆盖所有配置(`--server-port`、`--database-path`、`--log-path`、`--log-level`),SQLite 文件数据库使每次测试可用独立临时文件,首次启动自动运行 goose 迁移,有 `/health` 端点可供就绪检查。 ## Goals / Non-Goals **Goals:** - `bun run test:e2e` 一条命令自动启动隔离后端 + 前端,运行完整 E2E 测试,自动清理 - 每次测试运行使用干净的临时数据库,测试间无状态污染 - E2E 测试能验证完整 CRUD 流程:创建→验证存在→编辑→验证更新→删除→验证消失 - 统计页面可通过 seed 数据验证数字、筛选、图表渲染 - 对日常开发流程零侵入(`bun run dev` 行为不变) - Windows 原生兼容(项目在 Windows 上开发) **Non-Goals:** - 不修改后端代码(仅通过 CLI 参数启动现有二进制) - 不引入 CI 流水线配置 - 不改动 MSW 在单元测试中的使用 - 不实现随机端口分配(使用固定非默认端口 19026) - 不引入 cross-env 等跨平台环境变量工具 ## Decisions ### Decision 1: 固定测试端口 19026 vs 随机端口 **选择**: 固定端口 19026 **理由**: 随机端口需要在 `globalSetup` 中 spawn 后端进程、等待健康检查、管理进程生命周期(包括 Windows 上的 SIGTERM 问题),复杂度高。19026 是非标准端口,与开发端口 9826 不冲突,冲突概率极低。Playwright `webServer` 数组模式可直接管理两个进程的生命周期,无需手动 spawn/kill。 **备选方案**: `globalSetup` 中分配随机端口 → spawn 后端 → 等待健康检查 → `globalTeardown` 中 kill 进程。在 Windows 上 SIGTERM 不可靠,需 taskkill 强杀,进程管理复杂。 ### Decision 2: 环境变量传递方式 **选择**: `process.env` 继承,不引入 cross-env **理由**: Playwright 的 `webServer` 通过 `child_process.spawn` 启动子进程,子进程默认继承父进程 `process.env`。在 `playwright.config.ts` 模块顶层设置 `process.env.NEX_BACKEND_PORT = '19026'`,Vite dev server 子进程自动继承,`vite.config.ts` 中 `defineConfig` 函数可读取。E2E 测试文件与 config 同进程,直接读取即可。三处消费者(后端 CLI flags / Vite / 测试文件)都无需额外工具。 ### Decision 3: 统计数据 seed 方式 **选择**: sql.js 直接操作 SQLite 文件 **理由**: 统计数据(`usage_stats` 表)只能通过后端 `statsService.Record()` 按当天日期 upsert,没有 API 可以插入任意日期的历史数据。统计页面测试需要多日趋势数据来验证筛选和图表。sql.js 是纯 WASM 实现,无原生编译依赖,Windows 上零风险。通过"读文件→内存操作→写回"模式在 `beforeAll` 中 seed 数据,串行化保证无并发冲突。 **备选方案**: - better-sqlite3:需要原生编译,Windows 上可能有编译工具链问题 - 后端加测试专用 seed API:需要修改后端代码,违反"不改后端"原则 - 只测今日数据:无法验证日期筛选和趋势图表,测试覆盖不足 ### Decision 4: 临时文件管理 **选择**: `fs.mkdtempSync(os.tmpdir() + '/nex-e2e-')` 创建,`fs.rm(dir, { recursive: true, force: true })` 在 `globalTeardown` 清理 **理由**: `mkdtempSync` 跨平台安全,Windows 上生成 `%TEMP%\nex-e2e-xxxxxxxx` 路径。Go 后端的 `--database-path` 和 `--log-path` 参数接受 Windows 路径。`fs.rm` 是 Node.js 14.14+ 内置的递归删除 API,无需额外依赖(rimraf 等)。 ### Decision 5: 测试文件拆分 5→7 **选择**: 按职责拆分 | 新文件 | 来源 | 职责 | |--------|------|------| | `providers.spec.ts` | 重写 `providers.spec.ts` + `crud.spec.ts` 供应商部分 | 供应商完整 CRUD 验证 | | `models.spec.ts` | 新增,来自 `crud.spec.ts` 模型部分 | 模型管理完整验证 | | `stats.spec.ts` | 重写 `stats.spec.ts` + 合并 `stats-cards.spec.ts` | 统计页面完整验证 | | `navigation.spec.ts` | 新增,来自 `sidebar.spec.ts` + 导航测试 | 导航和侧边栏 | | `validation.spec.ts` | 新增,来自 `crud.spec.ts` 表单验证部分 | 表单校验测试 | | `fixtures.ts` | 新增 | 共享常量和 seed 工具函数 | | `global-setup.ts` / `global-teardown.ts` | 新增 | 临时目录生命周期 | ### Decision 6: 进程启动顺序 **选择**: Playwright `webServer` 数组,后端在前、前端在后 **理由**: Playwright 按数组顺序依次等待每个 webServer 的 `url` 就绪。后端先启动(`/health` 返回 200),再启动前端(Vite 默认端口 5173)。`timeout: 60000` 覆盖 Go 首次编译耗时。两个 webServer 均设 `reuseExistingServer: false`,确保每次运行都是全新进程。 ## Risks / Trade-offs - **[Go 编译缓存]** → 首次 `go run` 需要 5-10s 编译,后续利用 Go 构建缓存加速。`timeout: 60000` 提供充足缓冲。如仍嫌慢,可手动 `cd backend && go build ./cmd/server/` 预编译。 - **[端口 19026 冲突]** → 概率极低。Playwright 报明确错误,用户手动释放即可。不考虑自动重试。 - **[Ctrl+C 强退临时文件残留]** → `globalTeardown` 不执行,但下次运行创建新目录,旧目录不占端口。可接受。 - **[SQLite WAL 文件]** → 后端进程被 Playwright 强杀时可能留下 `.db-wal`/`.db-shm` 文件,但随临时目录一起删除,无影响。 - **[sql.js 串行化约束]** → seed 数据时需确保 API 操作完成后再操作 SQLite。在 `beforeAll` 中串行执行,不存在并发问题。