## ADDED Requirements

### Requirement: E2E 测试基础设施

前端 E2E 测试 SHALL 自动启动隔离的 Go 后端实例，提供真实 API 交互能力。

#### Scenario: 双 webServer 自动启动

- **WHEN** 执行 `bun run test:e2e`
- **THEN** Playwright SHALL 按 webServer 数组顺序先启动 Go 后端（端口 19026），再启动 Vite 前端（端口 5173）
- **THEN** Go 后端 SHALL 使用临时目录中的独立数据库文件和日志目录
- **THEN** Go 后端启动命令 SHALL 包含 `--server-port 19026`、`--database-path`、`--log-path`、`--log-level warn` 参数
- **THEN** Go 后端 cwd SHALL 指向 `backend/` 目录
- **THEN** webServer 等待超时 SHALL 为 60000ms
- **THEN** 两个 webServer SHALL 设置 `reuseExistingServer: false`

#### Scenario: 环境变量传递

- **WHEN** Playwright 配置加载
- **THEN** `playwright.config.ts` SHALL 设置 `process.env.NEX_BACKEND_PORT` 为 `'19026'`
- **THEN** `process.env.NEX_E2E_TEMP_DIR` SHALL 设置为临时目录路径
- **THEN** Vite dev server 子进程 SHALL 自动继承上述环境变量
- **THEN** E2E 测试文件 SHALL 通过 `process.env` 读取后端端口和临时目录信息

#### Scenario: Vite proxy 动态化

- **WHEN** `vite.config.ts` 配置 proxy target
- **THEN** proxy target SHALL 读取 `process.env.NEX_BACKEND_PORT`，回退到 `'9826'`
- **THEN** 日常 `bun run dev` 时（无环境变量）proxy target SHALL 为 `http://localhost:9826`
- **THEN** E2E 测试时 proxy target SHALL 为 `http://localhost:19026`

### Requirement: 临时文件隔离

E2E 测试 SHALL 使用固定临时目录隔离所有文件，确保主进程与 worker 进程使用同一路径，测试结束后自动清理。

#### Scenario: 临时目录创建

- **WHEN** Playwright 配置加载
- **THEN** SHALL 使用固定路径 `path.join(os.tmpdir(), 'nex-e2e')` 作为临时目录
- **THEN** SHALL 先执行 `fs.rmSync(tempDir, { recursive: true, force: true })` 清理残留
- **THEN** SHALL 执行 `fs.mkdirSync` 创建 `log/` 子目录
- **THEN** 临时目录 SHALL 包含 `test.db`（数据库）和 `log/`（日志）子路径

#### Scenario: 临时目录清理

- **WHEN** Playwright 所有测试完成
- **THEN** `globalTeardown` SHALL 使用固定路径 `path.join(os.tmpdir(), 'nex-e2e')` 定位临时目录
- **THEN** SHALL 使用 `fs.rmSync(dir, { recursive: true, force: true })` 删除临时目录
- **THEN** 清理 SHALL 在 webServer 进程关闭之后执行

### Requirement: 统计数据 seed

E2E 测试 SHALL 能通过 sql.js 直接操作 SQLite 文件 seed 历史统计数据。

#### Scenario: SQLite 统计数据插入

- **WHEN** 统计页面测试需要历史数据
- **THEN** SHALL 使用 sql.js 打开临时数据库文件
- **THEN** SHALL 通过 `INSERT INTO usage_stats` 插入指定日期和请求量的统计数据
- **THEN** SHALL 将修改后的数据库写回文件（`db.export()` → `fs.writeFileSync`）
- **THEN** seed 操作 SHALL 在 API 创建供应商和模型之后串行执行
- **THEN** seed 完成后 SHALL 关闭 sql.js 数据库连接

### Requirement: E2E 共享工具模块

E2E 测试 SHALL 提供共享工具模块（`e2e/fixtures.ts`）封装常用操作。

#### Scenario: 共享常量

- **WHEN** 测试文件需要后端 API 地址
- **THEN** fixtures SHALL export `API_BASE` 常量，值为 `http://localhost:${process.env.NEX_BACKEND_PORT}`

#### Scenario: API seed 工具

- **WHEN** 测试需要通过 API 准备数据
- **THEN** fixtures SHALL export `seedProvider(request, data)` 函数，通过 `request.post` 创建供应商
- **THEN** fixtures SHALL export `seedModel(request, data)` 函数，通过 `request.post` 创建模型

#### Scenario: SQLite seed 工具

- **WHEN** 测试需要 seed 统计数据
- **THEN** fixtures SHALL export `seedUsageStats(statsData)` 函数，通过 sql.js 插入 `usage_stats` 记录

### Requirement: 供应商管理 E2E 测试

E2E 测试 SHALL 验证供应商的完整 CRUD 用户流程。

#### Scenario: 创建供应商并验证

- **WHEN** 用户通过 UI 填写供应商表单并提交
- **THEN** 对话框 SHALL 关闭
- **THEN** 新供应商 SHALL 出现在表格中
- **THEN** 表格 SHALL 显示正确的 id、name、base_url、协议、enabled 状态

#### Scenario: 编辑供应商并验证

- **WHEN** 用户点击编辑按钮、修改名称并提交
- **THEN** 对话框 SHALL 关闭
- **THEN** 表格中该供应商的名称 SHALL 更新为新值

#### Scenario: 删除供应商并验证

- **WHEN** 用户点击删除按钮并确认
- **THEN** 确认框 SHALL 消失
- **THEN** 该供应商 SHALL 从表格中消失

### Requirement: 模型管理 E2E 测试

E2E 测试 SHALL 验证模型的完整 CRUD 用户流程，断言 SHALL 精确匹配以避免多列文本干扰。

#### Scenario: 前置数据准备

- **WHEN** 模型测试开始前
- **THEN** SHALL 通过 API seed 一个供应商（不通过 UI）

#### Scenario: 创建模型并验证

- **WHEN** 用户展开供应商行、填写模型表单并提交
- **THEN** 对话框 SHALL 关闭
- **THEN** 新模型 SHALL 出现在展开行的模型表格中
- **THEN** 模型表格 SHALL 显示统一模型 ID（`provider_id/model_name` 格式）
- **THEN** 验证断言 SHALL 使用 `{ exact: true }` 精确匹配模型名称文本，避免匹配到"统一模型 ID"列

#### Scenario: 编辑模型并验证

- **WHEN** 用户点击模型编辑按钮、修改并提交
- **THEN** 对话框 SHALL 关闭
- **THEN** 模型表格 SHALL 显示更新后的数据
- **THEN** 验证断言 SHALL 使用 `{ exact: true }` 精确匹配模型名称文本

#### Scenario: 删除模型并验证

- **WHEN** 用户点击模型删除按钮并确认
- **THEN** 该模型 SHALL 从模型表格中消失
- **THEN** 验证断言 SHALL 使用 `{ exact: true }` 精确匹配模型名称文本

### Requirement: 统计页面 E2E 测试

E2E 测试 SHALL 验证统计页面的数据展示和筛选功能。所有测试用例 SHALL NOT 被跳过。

#### Scenario: 统计数据准备

- **WHEN** 统计测试开始前
- **THEN** SHALL 通过 API seed 供应商和模型
- **THEN** SHALL 通过 sql.js seed 多日历史统计数据（覆盖不同供应商、不同模型、不同日期）

#### Scenario: 统计概览验证

- **WHEN** 加载统计页面
- **THEN** 统计摘要卡片 SHALL 显示正确的总请求量
- **THEN** 统计摘要卡片 SHALL 显示正确的活跃模型数和活跃供应商数
- **THEN** 统计表格 SHALL 显示 seed 的数据行
- **THEN** 页面 SHALL 渲染趋势图表区域（标题包含"请求趋势"）

#### Scenario: 统计筛选验证

- **WHEN** 页面加载完成
- **THEN** 页面 SHALL 显示筛选栏（供应商下拉选择和模型名称输入框）
- **WHEN** 用户选择供应商筛选条件
- **THEN** 统计表格 SHALL 只显示该供应商的数据
- **WHEN** 用户输入模型名称筛选条件
- **THEN** 统计表格 SHALL 只显示匹配模型的数据

### Requirement: 导航 E2E 测试

E2E 测试 SHALL 验证页面导航和侧边栏功能。

#### Scenario: 侧边栏渲染

- **WHEN** 加载任意页面
- **THEN** 侧边栏 SHALL 显示应用名称和导航菜单项

#### Scenario: 页面切换

- **WHEN** 用户点击侧边栏导航菜单项
- **THEN** 页面 SHALL 切换到对应路由并显示正确内容
- **THEN** 当前菜单项 SHALL 高亮

#### Scenario: URL 持久化

- **WHEN** 用户在统计页面刷新浏览器
- **THEN** 页面 SHALL 保持在统计页面

### Requirement: 表单验证 E2E 测试

E2E 测试 SHALL 验证表单的校验行为。

#### Scenario: 供应商表单必填校验

- **WHEN** 用户打开添加供应商对话框并直接点击保存
- **THEN** SHALL 显示所有必填字段的验证错误提示

#### Scenario: URL 格式校验

- **WHEN** 用户在 base_url 字段输入无效 URL 并提交
- **THEN** SHALL 显示 URL 格式错误提示

#### Scenario: 对话框行为

- **WHEN** 用户打开对话框后点击取消
- **THEN** 对话框 SHALL 关闭且表单重置
- **WHEN** 用户再次打开对话框
- **THEN** 表单字段 SHALL 为空

### Requirement: E2E 测试文件组织

E2E 测试 SHALL 按 7 个 spec 文件组织。

#### Scenario: 文件结构

- **WHEN** 查看 `e2e/` 目录
- **THEN** SHALL 包含以下测试文件：`providers.spec.ts`、`models.spec.ts`、`stats.spec.ts`、`navigation.spec.ts`、`validation.spec.ts`
- **THEN** SHALL 包含以下辅助文件：`fixtures.ts`、`global-setup.ts`、`global-teardown.ts`
- **THEN** SHALL NOT 包含已删除的文件：`stats-cards.spec.ts`、`sidebar.spec.ts`、`crud.spec.ts`

### Requirement: 新增依赖

E2E 测试 SHALL 引入 sql.js 作为 devDependency。

#### Scenario: sql.js 依赖

- **WHEN** 安装前端依赖
- **THEN** `package.json` 的 devDependencies SHALL 包含 `sql.js`
- **THEN** `package.json` 的 devDependencies SHALL 包含 `@types/sql.js`
- **THEN** sql.js SHALL 仅在 E2E 测试的 seed 逻辑中使用，不影响生产代码
