ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SurfSense 的 Playwright 测试套件结构:从配置、E2E 到 API Mocking 与视觉回归的完整实践

SurfSense 的 Playwright 测试套件结构:从配置、E2E 到 API Mocking 与视觉回归的完整实践 SurfSense 的 Playwright 测试套件结构从配置、E2E 到 API Mocking 与视觉回归的完整实践【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense本文基于 SurfSense 仓库中playwright-testing技能库的核心参考文档系统讲解 Playwright 测试套件的组成结构与工程实践如何初始化套件、编写基础配置、组织 E2E/组件/API/视觉回归四类测试、设计目录结构、用标签过滤测试用例并指出常见反模式。读完之后你可以参照 SurfSense 真实的 playwright.config.ts 与 E2E 套件为自己项目搭建一套结构清晰、CI 友好的 Playwright 测试体系。一、文档定位SurfSense 的 playwright-testing 技能库SurfSense 仓库在 .cursor/skills/playwright-testing/ 目录下内置了一套面向 AI 协作开发的 Playwright 测试知识库按core/核心概念、testing-patterns/测试模式、advanced/进阶主题、debugging/排障、infrastructure-ci-cd/基础设施等模块组织。本文的主文档 test-suite-structure.md 属于core/模块是整个套件中如何组织测试代码与配置的架构级参考SKILL.md 的决策树将编写 E2E 测试搭建测试架构重构维护等绝大多数活动都指向了它。二、项目初始化套件初始化使用官方脚手架命令一条命令生成配置模板与基础测试npm init playwrightlatestSurfSense 前端surfsense_web实际使用 pnpm 管理依赖package.json 中声明playwright/test: ^1.59.1作为devDependency并封装了一组脚本test:e2e: playwright test, test:e2e:prod: cross-env CI1 playwright test, test:e2e:ui: playwright test --ui, test:e2e:headed: playwright test --headed, test:e2e:debug: playwright test --debug, test:e2e:report: playwright show-report, test:e2e:install: playwright install --with-deps chromium其中test:e2e:prod通过CI1环境变量触发 CI 分支配置详见下文 webServer 部分与 CI 流水线行为完全对齐。三、基础配置playwright.config.ts主文档给出的必备配置基线如下覆盖测试目录、并行、CI 防护、重试、报告器与 webServer// playwright.config.ts import { defineConfig, devices } from playwright/test; export default defineConfig({ testDir: ./tests, fullyParallel: true, forbidOnly: !!process.env.CI, // CI 上禁止遗留 test.only retries: process.env.CI ? 2 : 0, // CI 失败重试 2 次 workers: process.env.CI ? 1 : undefined, // CI 串行避免状态污染 reporter: [[html], [list]], use: { baseURL: http://localhost:3000, trace: on-first-retry, // 首次重试时采集 trace screenshot: only-on-failure, // 仅失败时截图 }, projects: [ { name: setup, testMatch: /.*\.setup\.ts/ }, { name: chromium, use: { ...devices[Desktop Chrome] }, dependencies: [setup], // chromium 依赖 setup 先执行 }, ], webServer: { command: npm run dev, url: http://localhost:3000, reuseExistingServer: !process.env.CI, // 本地复用已有 dev server }, });SurfSense 的真实配置一份可直接借鉴的完整实现SurfSense 的 surfsense_web/playwright.config.ts 在上述基线上做了多处生产级扩展值得逐项对照import { defineConfig, devices } from playwright/test; const PORT process.env.PORT || 3000; const BACKEND_PORT process.env.BACKEND_PORT || 8000; const ZERO_CACHE_PORT process.env.ZERO_CACHE_PORT || 4848; const baseURL process.env.PLAYWRIGHT_BASE_URL || http://localhost:${PORT}; const useProxyOrigin process.env.PLAYWRIGHT_USE_PROXY_ORIGIN true; const backendURL useProxyOrigin ? baseURL : http://localhost:${BACKEND_PORT}; process.env.PLAYWRIGHT_TEST_EMAIL ?? e2e-testsurfsense.net; process.env.PLAYWRIGHT_TEST_PASSWORD ?? E2eTestPassword123!; export default defineConfig({ testDir: ./tests, timeout: 30_000, // 单条测试 30s 超时 expect: { timeout: 15_000 }, // 断言 15s 超时 fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 1 : 0, workers: 1, // 恒定单 worker测试共享后端状态 reporter: process.env.CI ? [[html, { open: never }], [github], [list]] // CI 输出 GitHub 注解 : [[html, { open: on-failure }], [list]], use: { baseURL, trace: on-first-retry, screenshot: only-on-failure, video: process.env.CI ? off : retain-on-failure, extraHTTPHeaders: { x-playwright-test: true }, // 后端识别测试流量 }, projects: [ { name: setup, testMatch: /.*\.setup\.ts/ }, { name: chromium, dependencies: [setup], use: { ...devices[Desktop Chrome], storageState: playwright/.auth/user.json, // 复用 setup 的登录态 }, }, ], webServer: process.env.PLAYWRIGHT_NO_WEB_SERVER ? undefined : { // 本地用 webpack devTurbopack 在 E2E 下有 stale-lock 问题 command: process.env.CI ? pnpm build pnpm start : pnpm exec next dev, url: http://localhost:${PORT}, reuseExistingServer: !process.env.CI, timeout: process.env.CI ? 300_000 : 180_000, // 构建启动预留 5 分钟 env: { /* 透传后端/认证相关环境变量 */ }, }, });从源码结构看该配置体现了三个工程决策setup/chromium 两项目 storageStatesetup项目匹配*.setup.ts即 tests/auth.setup.ts执行一次API 换取 token → 写入会话 Cookie → 播种 localStorage 屏蔽公告/引导浮层 → 持久化storageState之后chromium项目中每条测试都以已登录状态启动避免重复走 UI 登录流程。CI 与本地的 webServer 双轨CI 上执行pnpm build pnpm start生产构建与线上行为一致本地执行pnpm exec next dev快速迭代PLAYWRIGHT_NO_WEB_SERVER可以整体关闭 webServer 托管。测试永不进入生产产物配置文件头部注释明确说明——playwright.config.ts与tests/均列入.dockerignoreElectron 打包只携带.next/standalone/且playwright/test是 devDependency生产pnpm install --prod会直接跳过它。四、E2E 测试完整用户旅程E2E 测试通过真实浏览器验证完整用户旅程。主文档给出一个典型的结账流程结构示例// tests/e2e/checkout.spec.ts import { test, expect } from playwright/test; test.describe(Checkout Flow, () { test.beforeEach(async ({ page }) { await page.goto(/products); }); test(complete purchase as guest, async ({ page }) { // Add to cart await page.getByRole(button, { name: Add to Cart }).first().click(); await expect(page.getByTestId(cart-count)).toHaveText(1); // Go to checkout await page.getByRole(link, { name: Cart }).click(); await page.getByRole(button, { name: Checkout }).click(); // Fill shipping await page.getByLabel(Email).fill(guestexample.com); await page.getByLabel(Address).fill(123 Test St); await page.getByRole(button, { name: Continue }).click(); // Payment await page.getByLabel(Card Number).fill(4242424242424242); await page.getByRole(button, { name: Pay Now }).click(); // Confirmation await expect(page.getByRole(heading)).toHaveText(Order Confirmed); }); test(apply discount code, async ({ page }) { await page.getByRole(button, { name: Add to Cart }).first().click(); await page.getByRole(link, { name: Cart }).click(); await page.getByLabel(Discount Code).fill(SAVE10); await page.getByRole(button, { name: Apply }).click(); await expect(page.getByText(10% discount applied)).toBeVisible(); }); });主文档给出的 E2E 最佳实践只测关键用户旅程critical user journeys测试之间保持独立不依赖执行顺序使用真实感的数据在 teardown 中清理测试数据。SurfSense 的 E2E 实战薄浏览器断言 API 驱动配置SurfSense 的 E2E 套件覆盖 Next.js FastAPI Celery Postgres Redis 全栈见 tests/README.md。以 Composio Drive 旅程测试 为例它验证OAuth 夹具 → 文件选择持久化 → 索引 → 存储 source_markdown → 编辑器内容获取 → 聊天问答的完整链路test(user connects Drive, selects a file, indexes it, and chats with the canary token, async ({ page, request, apiToken, workspace, composioDriveConnector, chatThread, }) { test.setTimeout(240_000); // worker 冷启动 Docling 解析 摘要 embedding 分块 await page.goto(/dashboard/${workspace.id}/new-chat, { waitUntil: domcontentloaded }); await expectImportConnectorAvailable(page, Google Drive); // 唯一的薄浏览器断言 // 通过 API 驱动配置与索引helpers/api/connectors.ts await updateConnectorConfig(request, apiToken, composioDriveConnector.id, { /* ... */ }); await triggerIndex(request, apiToken, composioDriveConnector.id, workspace.id, { /* ... */ }); await waitForIndexingComplete(request, apiToken, composioDriveConnector.id, workspace.id, { timeoutMs: 240_000, intervalMs: 1_500, minDocuments: 2, }); // 断言索引产物canary token 必须出现在编辑器内容与聊天回答中 const editor await getEditorContent(request, apiToken, workspace.id, canaryDoc.id); expect(editor.source_markdown).toContain(CANARY_TOKENS.driveCanaryFile); const chat await streamChatToCompletion(request, apiToken, { workspaceId: workspace.id, threadId: chatThread.id, query: What is in my e2e-canary.txt Drive file?, }); expect(chat.assistantText).toContain(CANARY_TOKENS.driveCanaryFile); });这里刻意选择薄浏览器断言 API 驱动配置的策略tests/README.md 给出的理由避免等待 UI 动画/React 水合/Next.js 编译带来的不确定性deterministicAPI 走的是 UI 最终调用的同一条后端代码路径昂贵的 E2E 断言聚焦在只有 E2E 才能证明的东西——connector → Celery → 索引 → DB 的跨进程接缝。此外该套件还有三层防线防止测试意外触达真实外部服务独立的 E2E 入口脚本在导入应用前劫持 SDK 模块并替换严格 fake、fake 对未知接口抛出NotImplementedError、CI 设置HTTPS_PROXYhttp://127.0.0.1:1加哨兵 API key 使任何泄漏的出站调用直接失败。测试运行流程详见 tests/README.md。五、组件测试组件测试使用 Playwright Component Testing 在隔离环境中测试单个组件npm init playwrightlatest -- --ct组件测试涵盖挂载、props、事件、slots、mocking 及各框架示例的完整模式参见主文档指向的 component-testing.md。六、API 测试与 API Mocking 模式API 测试可以不经过浏览器直接测后端而在 E2E 场景中用page.routemock API 响应是隔离后端依赖的标准手段。主文档给出四种模式// Mock 单个端点 test(displays mocked users, async ({ page }) { await page.route(**/api/users, (route) route.fulfill({ status: 200, json: [{ id: 1, name: Test User }], }) ); await page.goto(/users); await expect(page.getByText(Test User)).toBeVisible(); }); // Mock 不同响应错误场景 test(handles API errors, async ({ page }) { await page.route(**/api/users, (route) route.fulfill({ status: 500, json: { error: Server error }, }) ); await page.goto(/users); await expect(page.getByText(Server error)).toBeVisible(); }); // 条件 mock按请求方法分流 test(mocks based on request, async ({ page }) { await page.route(**/api/users, (route, request) { if (request.method() GET) { route.fulfill({ json: [{ id: 1, name: User }] }); } else { route.continue(); } }); }); // Mock 延迟模拟慢网络 test(handles slow API, async ({ page }) { await page.route(**/api/data, (route) route.fulfill({ json: { data: test }, delay: 2000, // 2 秒延迟 }) ); await page.goto(/dashboard); await expect(page.getByText(Loading...)).toBeVisible(); await expect(page.getByText(test)).toBeVisible(); });SurfSense 在这条思路上更进一步由于 E2E 目标是验证真实后端链路它不做 UI 级 route mock而是通过 tests/helpers/api/ 下的 API 客户端auth.ts、connectors.ts、documents.ts、workspaces.ts等直接驱动真实后端仅在 OAuth 弹窗这类第三方环节使用 mock如 tests/helpers/mocks/composio-oauth.ts配合后端的 fake SDK 层完成确定性隔离。更进阶的网络拦截模式GraphQL mocking、HAR 录制回放、请求修改、网络限速参见 network-advanced.md。七、视觉回归测试视觉回归测试通过截图对比发现视觉变化。基本视觉测试// tests/visual/homepage.spec.ts import { test, expect } from playwright/test; test(homepage visual, async ({ page }) { await page.goto(/); await expect(page).toHaveScreenshot(homepage.png); }); test(component visual, async ({ page }) { await page.goto(/components); const button page.getByRole(button, { name: Primary }); await expect(button).toHaveScreenshot(primary-button.png); });视觉测试选项test(dashboard visual, async ({ page }) { await page.goto(/dashboard); await expect(page).toHaveScreenshot(dashboard.png, { fullPage: true, // 截取整个可滚动页面 maxDiffPixels: 100, // 允许最多 100 个差异像素 maxDiffPixelRatio: 0.01, // 或允许 1% 差异比例 threshold: 0.2, // 像素比较阈值 animations: disabled, // 禁用动画 mask: [page.getByTestId(date)], // 遮罩动态内容 }); });处理动态内容test(page with dynamic content, async ({ page }) { await page.goto(/profile); // 遮罩会变化的元素 await expect(page).toHaveScreenshot(profile.png, { mask: [ page.getByTestId(timestamp), page.getByTestId(avatar), page.getByRole(img), ], }); }); // 或者用 CSS 隐藏动态元素 test(page hiding dynamic elements, async ({ page }) { await page.goto(/profile); await page.addStyleTag({ content: .dynamic-content { visibility: hidden !important; } [data-testidad-banner] { display: none !important; } , }); await expect(page).toHaveScreenshot(profile-stable.png); });动态内容时间戳、头像、广告位是视觉回归误报的最大来源mask选项与 CSS 隐藏两种手段应优先使用。SurfSense 的auth.setup.ts采用了同一思想的前置消除策略在 setup 阶段向 localStorage 写入surfsense_announcements_state与surfsense-tour-userId标志位让公告弹窗和新用户引导永不出现在后续旅程里。视觉测试全局配置// playwright.config.ts export default defineConfig({ expect: { toHaveScreenshot: { maxDiffPixels: 50, animations: disabled, }, }, projects: [ { name: visual-chrome, use: { ...devices[Desktop Chrome], viewport: { width: 1280, height: 720 }, // 固定视口保证截图可比 }, testMatch: /.*visual.*\.spec\.ts/, }, ], });固定视口如上例 1280×720是视觉测试项目配置的要点之一避免不同屏幕尺寸导致的像素级差异。更新快照# 更新全部快照 npx playwright test --update-snapshots # 更新指定测试的快照 npx playwright test homepage.spec.ts --update-snapshots八、目录结构主文档推荐按测试类型与支撑设施分层组织tests/ ├── e2e/ # 端到端测试 │ ├── auth.spec.ts │ ├── checkout.spec.ts │ └── dashboard.spec.ts ├── component/ # 组件测试 │ ├── Button.spec.tsx │ └── Modal.spec.tsx ├── api/ # API 测试 │ ├── users.spec.ts │ └── products.spec.ts ├── visual/ # 视觉回归测试 │ └── homepage.spec.ts ├── fixtures/ # 自定义 fixtures │ ├── auth.fixture.ts │ └── api.fixture.ts └── pages/ # 页面对象 ├── login.page.ts └── dashboard.page.tsSurfSense 的实际 tests/ 目录是这一骨架在多连接器产品上的具体化变体按业务域connectors/vendor/service/journey.spec.ts、documents/file-upload/、smoke/组织 spec 文件同时保留fixtures/连接器夹具如 composio-drive.fixture.ts 与工作区/聊天线程夹具和helpers/api/客户端、ui/页面对象、waits/等待工具、mocks/mock。tests/README.md 还约定了新连接器的接入清单后端 fake → 模块劫持 → 后端集成测试 → fixture 文件 → 唯一一条 journey spec → 更新 README 目录图并明确过期 OAuth state、重复连接器等边界情况应放在后端测试而非 Playwright这正是一个连接器一条最小用户旅程原则的落地。九、应避免的反模式主文档列出的反模式对照表反模式问题解决方案过长的测试文件难维护、难导航按功能拆分使用 POM测试依赖执行顺序flaky、难调试保持测试独立一个测试测多个功能失败难定位一个测试只覆盖一个功能SurfSense 配置中workers: 1恒定单 worker、fullyParallel: true但测试间无状态依赖的组合正是对测试依赖执行顺序这一反模式的防御性回应——从 tests/README.md 的确定性设计原则可以看出该套件将隔离与独立性视为一等约束。十、标签与过滤使用标签test(user login smoke auth, async ({ page }) { // ... }); test(checkout flow e2e critical, async ({ page }) { // ... }); test.describe(API tests api, () { test(create user, async ({ request }) { // ... }); });运行带标签的测试# 运行冒烟测试 npx playwright test --grep smoke # 运行除慢测试外的全部测试 npx playwright test --grep-invert slow # 组合标签正则或 npx playwright test --grep smoke|critical标签smoke、critical、slow配合--grep/--grep-invert可以实现PR 上跑快速冒烟、夜间跑全量的分层执行策略基于项目的过滤project级testMatch、dependencies与标签过滤可叠加使用。十一、相关参考组件测试完整模式component-testing.md项目依赖与基于项目的过滤projects-dependencies.md页面对象模型POM组织页面交互page-object-model.md测试数据与 fixturesfixtures-hooks.md网络拦截进阶GraphQL、HAR、限速network-advanced.md技能库总入口与活动决策树SKILL.mdSurfSense 真实 E2E 套件说明本地/CI 运行、确定性防线、新连接器接入清单surfsense_web/tests/README.md小结主文档给出的套件结构——统一配置文件、setup/chromium 项目依赖、四类测试分层目录、API mocking 四种模式、视觉回归的 mask/固定视口、标签化过滤与反模式清单——是 Playwright 工程化的最小完备骨架。对照 SurfSense 的 playwright.config.ts 与 tests/ 目录可以看到这套骨架在真实全栈产品Next.js 前端 FastAPI 后端 Celery 异步任务中落地时关键增量在于storageState 复用登录态、CI/本地双轨 webServer、API 驱动的薄浏览器断言以及贯穿始终的测试隔离与确定性设计。【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表