From 5e7267db074ea090af068e3f1435f06195a19099 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Wed, 22 Apr 2026 14:32:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(e2e):=20=E4=BF=AE=E5=A4=8D=2010=20=E4=B8=AA?= =?UTF-8?q?=E8=A2=AB=20skip=20=E7=9A=84=20E2E=20=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 playwright.config.ts 的 mkdtemp 替换为固定路径,解决主进程/worker 临时目录不一致问题 - 交换后端 WAL 与迁移执行顺序,确保 sql.js 能读取到完整 schema - 修复 models.spec.ts 断言使用 exact:true 避免统一模型 ID 列干扰 - 移除全部 10 个 test.skip,26 个 E2E 测试全部通过 --- backend/cmd/server/main.go | 8 ++++---- frontend/e2e/fixtures.ts | 6 ++---- frontend/e2e/global-setup.ts | 6 ++++-- frontend/e2e/global-teardown.ts | 6 ++++-- frontend/e2e/models.spec.ts | 14 +++++++------- frontend/e2e/stats.spec.ts | 14 +++++++------- frontend/playwright.config.ts | 5 ++++- openspec/specs/e2e-testing/spec.md | 19 ++++++++++++++----- 8 files changed, 46 insertions(+), 32 deletions(-) diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index c24f41c..57bfe6a 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -142,14 +142,14 @@ func initDatabase(cfg *config.Config) (*gorm.DB, error) { return nil, err } - if err := db.Exec("PRAGMA journal_mode=WAL").Error; err != nil { - log.Printf("警告: 启用 WAL 模式失败: %v", err) - } - if err := runMigrations(db); err != nil { return nil, fmt.Errorf("数据库迁移失败: %w", err) } + if err := db.Exec("PRAGMA journal_mode=WAL").Error; err != nil { + log.Printf("警告: 启用 WAL 模式失败: %v", err) + } + sqlDB, err := db.DB() if err != nil { return nil, err diff --git a/frontend/e2e/fixtures.ts b/frontend/e2e/fixtures.ts index 475045d..104f8c7 100644 --- a/frontend/e2e/fixtures.ts +++ b/frontend/e2e/fixtures.ts @@ -1,4 +1,5 @@ import fs from 'node:fs' +import os from 'node:os' import path from 'node:path' import initSqlite from 'sql.js' @@ -76,10 +77,7 @@ export async function seedModel( } export async function seedUsageStats(statsData: SeedStatsInput[]) { - const tempDir = process.env.NEX_E2E_TEMP_DIR - if (!tempDir) { - throw new Error('NEX_E2E_TEMP_DIR not set - ensure playwright.config.ts is loaded') - } + const tempDir = path.join(os.tmpdir(), 'nex-e2e') const dbPath = path.join(tempDir, 'test.db') diff --git a/frontend/e2e/global-setup.ts b/frontend/e2e/global-setup.ts index b83bd6c..04981a4 100644 --- a/frontend/e2e/global-setup.ts +++ b/frontend/e2e/global-setup.ts @@ -1,8 +1,10 @@ import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' async function globalSetup() { - const tempDir = process.env.NEX_E2E_TEMP_DIR - if (tempDir && fs.existsSync(tempDir)) { + const tempDir = path.join(os.tmpdir(), 'nex-e2e') + if (fs.existsSync(tempDir)) { console.log(`E2E temp dir: ${tempDir}`) } } diff --git a/frontend/e2e/global-teardown.ts b/frontend/e2e/global-teardown.ts index 9fbaef0..ba8c2b5 100644 --- a/frontend/e2e/global-teardown.ts +++ b/frontend/e2e/global-teardown.ts @@ -1,8 +1,10 @@ import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' async function globalTeardown() { - const tempDir = process.env.NEX_E2E_TEMP_DIR - if (tempDir && fs.existsSync(tempDir)) { + const tempDir = path.join(os.tmpdir(), 'nex-e2e') + if (fs.existsSync(tempDir)) { await new Promise((resolve) => setTimeout(resolve, 500)) try { fs.rmSync(tempDir, { recursive: true, force: true }) diff --git a/frontend/e2e/models.spec.ts b/frontend/e2e/models.spec.ts index 4abb6aa..5fa07e6 100644 --- a/frontend/e2e/models.spec.ts +++ b/frontend/e2e/models.spec.ts @@ -43,7 +43,7 @@ test.describe('模型管理', () => { await expect(page.getByText('暂无模型,点击上方按钮添加')).toBeVisible() }) - test.skip('应能为供应商添加模型', async ({ page }) => { + test('应能为供应商添加模型', async ({ page }) => { await page.locator('.t-table__expand-box').first().click() await expect(page.locator('.t-table__expanded-row').first()).toBeVisible() @@ -58,7 +58,7 @@ test.describe('模型管理', () => { const responsePromise = page.waitForResponse(resp => resp.url().includes('/api/models') && resp.request().method() === 'POST') await inputs.saveBtn.click() await responsePromise - await expect(page.locator('.t-table__expanded-row').getByText('gpt_4_turbo')).toBeVisible({ timeout: 5000 }) + await expect(page.locator('.t-table__expanded-row').getByText('gpt_4_turbo', { exact: true })).toBeVisible({ timeout: 5000 }) }) test('应显示统一模型 ID', async ({ page, request }) => { @@ -79,7 +79,7 @@ test.describe('模型管理', () => { await expect(page.locator('.t-table__expanded-row').getByText(`${providerId}/claude_3`)).toBeVisible() }) - test.skip('应能编辑模型', async ({ page, request }) => { + test('应能编辑模型', async ({ page, request }) => { await request.post(`${API_BASE}/api/models`, { data: { provider_id: providerId, @@ -104,10 +104,10 @@ test.describe('模型管理', () => { const responsePromise = page.waitForResponse(resp => resp.url().includes('/api/models') && resp.request().method() === 'PUT') await inputs.saveBtn.click() await responsePromise - await expect(page.locator('.t-table__expanded-row').getByText('gpt_4o')).toBeVisible({ timeout: 5000 }) + await expect(page.locator('.t-table__expanded-row').getByText('gpt_4o', { exact: true })).toBeVisible({ timeout: 5000 }) }) - test.skip('应能删除模型', async ({ page, request }) => { + test('应能删除模型', async ({ page, request }) => { await request.post(`${API_BASE}/api/models`, { data: { provider_id: providerId, @@ -121,11 +121,11 @@ test.describe('模型管理', () => { await page.locator('.t-table__expand-box').first().click() await expect(page.locator('.t-table__expanded-row').first()).toBeVisible() - await expect(page.locator('.t-table__expanded-row').getByText('to_delete_model')).toBeVisible() + await expect(page.locator('.t-table__expanded-row').getByText('to_delete_model', { exact: true })).toBeVisible() await page.locator('.t-table__expanded-row button:has-text("删除")').first().click() await expect(page.getByText(/确定要删除/)).toBeVisible() await page.locator('.t-popconfirm').getByRole('button', { name: '确定' }).click() - await expect(page.locator('.t-table__expanded-row').getByText('to_delete_model')).not.toBeVisible({ timeout: 5000 }) + await expect(page.locator('.t-table__expanded-row').getByText('to_delete_model', { exact: true })).not.toBeVisible({ timeout: 5000 }) }) }) diff --git a/frontend/e2e/stats.spec.ts b/frontend/e2e/stats.spec.ts index 983abfa..45a58aa 100644 --- a/frontend/e2e/stats.spec.ts +++ b/frontend/e2e/stats.spec.ts @@ -44,22 +44,22 @@ test.describe('统计概览', () => { await expect(page.getByRole('heading', { name: '用量统计' })).toBeVisible() }) - test.skip('应显示正确的总请求量', async ({ page }) => { + test('应显示正确的总请求量', async ({ page }) => { await page.waitForTimeout(1000) await expect(page.getByText('总请求量')).toBeVisible() }) - test.skip('应显示正确的活跃模型数和活跃供应商数', async ({ page }) => { + test('应显示正确的活跃模型数和活跃供应商数', async ({ page }) => { await page.waitForTimeout(1000) await expect(page.getByText('活跃模型数')).toBeVisible() await expect(page.getByText('活跃供应商数')).toBeVisible() }) - test.skip('应显示统计数据行', async ({ page }) => { + test('应显示统计数据行', async ({ page }) => { await expect(page.locator('.t-table__body tr').first()).toBeVisible({ timeout: 5000 }) }) - test.skip('应渲染趋势图表区域', async ({ page }) => { + test('应渲染趋势图表区域', async ({ page }) => { await expect(page.getByText('请求趋势')).toBeVisible() }) }) @@ -102,7 +102,7 @@ test.describe('统计筛选', () => { await expect(page.getByRole('heading', { name: '用量统计' })).toBeVisible() }) - test.skip('按供应商筛选', async ({ page }) => { + test('按供应商筛选', async ({ page }) => { await expect(page.locator('.t-table__body tr').first()).toBeVisible({ timeout: 5000 }) const rowCountBefore = await page.locator('.t-table__body tr:not(.t-table__empty-row)').count() @@ -115,14 +115,14 @@ test.describe('统计筛选', () => { expect(rowCountAfter).toBeLessThanOrEqual(rowCountBefore) }) - test.skip('按模型名称筛选', async ({ page }) => { + test('按模型名称筛选', async ({ page }) => { await expect(page.locator('.t-table__body tr').first()).toBeVisible({ timeout: 5000 }) await page.getByPlaceholder('模型名称').fill('gpt_4') await page.waitForTimeout(1000) await expect(page.locator('.t-table__body')).toBeVisible() }) - test.skip('应显示筛选栏', async ({ page }) => { + test('应显示筛选栏', async ({ page }) => { await expect(page.locator('.t-select').first()).toBeVisible() await expect(page.getByPlaceholder('模型名称')).toBeVisible() }) diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 4a6a083..118cf03 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -8,7 +8,10 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const E2E_PORT = 19026 -const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nex-e2e-')) +const tempDir = path.join(os.tmpdir(), 'nex-e2e') +if (!fs.existsSync(path.join(tempDir, 'test.db'))) { + fs.rmSync(tempDir, { recursive: true, force: true }) +} const dbPath = path.join(tempDir, 'test.db') const logPath = path.join(tempDir, 'log') diff --git a/openspec/specs/e2e-testing/spec.md b/openspec/specs/e2e-testing/spec.md index ab0f7fb..d978b59 100644 --- a/openspec/specs/e2e-testing/spec.md +++ b/openspec/specs/e2e-testing/spec.md @@ -31,18 +31,21 @@ ### Requirement: 临时文件隔离 -E2E 测试 SHALL 使用临时目录隔离所有文件,测试结束后自动清理。 +E2E 测试 SHALL 使用固定临时目录隔离所有文件,确保主进程与 worker 进程使用同一路径,测试结束后自动清理。 #### Scenario: 临时目录创建 - **WHEN** Playwright 配置加载 -- **THEN** SHALL 使用 `fs.mkdtempSync(path.join(os.tmpdir(), 'nex-e2e-'))` 创建临时目录 +- **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 使用 `fs.rm(dir, { recursive: true, force: true })` 删除临时目录 +- **THEN** `globalTeardown` SHALL 使用固定路径 `path.join(os.tmpdir(), 'nex-e2e')` 定位临时目录 +- **THEN** SHALL 使用 `fs.rmSync(dir, { recursive: true, force: true })` 删除临时目录 - **THEN** 清理 SHALL 在 webServer 进程关闭之后执行 ### Requirement: 统计数据 seed @@ -103,7 +106,7 @@ E2E 测试 SHALL 验证供应商的完整 CRUD 用户流程。 ### Requirement: 模型管理 E2E 测试 -E2E 测试 SHALL 验证模型的完整 CRUD 用户流程。 +E2E 测试 SHALL 验证模型的完整 CRUD 用户流程,断言 SHALL 精确匹配以避免多列文本干扰。 #### Scenario: 前置数据准备 @@ -116,21 +119,24 @@ E2E 测试 SHALL 验证模型的完整 CRUD 用户流程。 - **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 验证统计页面的数据展示和筛选功能。 +E2E 测试 SHALL 验证统计页面的数据展示和筛选功能。所有测试用例 SHALL NOT 被跳过。 #### Scenario: 统计数据准备 @@ -144,9 +150,12 @@ E2E 测试 SHALL 验证统计页面的数据展示和筛选功能。 - **THEN** 统计摘要卡片 SHALL 显示正确的总请求量 - **THEN** 统计摘要卡片 SHALL 显示正确的活跃模型数和活跃供应商数 - **THEN** 统计表格 SHALL 显示 seed 的数据行 +- **THEN** 页面 SHALL 渲染趋势图表区域(标题包含"请求趋势") #### Scenario: 统计筛选验证 +- **WHEN** 页面加载完成 +- **THEN** 页面 SHALL 显示筛选栏(供应商下拉选择和模型名称输入框) - **WHEN** 用户选择供应商筛选条件 - **THEN** 统计表格 SHALL 只显示该供应商的数据 - **WHEN** 用户输入模型名称筛选条件