ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SurfSense 的 Next.js Playwright E2E 测试模式:从 webServer 配置到 storageState 认证态的实战指南

SurfSense 的 Next.js Playwright E2E 测试模式:从 webServer 配置到 storageState 认证态的实战指南 SurfSense 的 Next.js Playwright E2E 测试模式从 webServer 配置到 storageState 认证态的实战指南【免费下载链接】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 仓库中的 Next.js Testing Patterns 文档系统讲解用 Playwright 测试 Next.js 应用的完整模式webServer服务器管理、App Router 流式加载、动态路由、API 路由、Middleware、Hydration 错误检测、next/image断言以及基于storageState的认证态管理。文中每一类模式都会结合 SurfSense 前端仓库 surfsense_web 的真实配置与 E2E 用例加以印证读完后你能掌握一套可直接落地到 Next.js 项目尤其是 App Router 项目的 E2E 测试方案并了解 SurfSense 如何在 Next.js FastAPI Celery 的全栈场景下做确定性 E2E。1. 适用场景与前置知识该文档的适用判定非常明确When to use: Testing Next.js applications with App Router, Pages Router, API routes, middleware, SSR, dynamic routes, and server components.Prerequisites: configuration.md, locators.md即当你的应用涉及 App Router、Pages Router、API 路由、Middleware、SSR、动态路由或 Server Components 时应参照本篇模式组织 Playwright 测试。前置阅读是 Playwright 配置含webServer与 Locator 定位两部分文档SurfSense 仓库中同样提供了 Playwright 配置文档 与 Locator 文档 供参考。SurfSense 的前端正是 Next.js 应用package.json 中next: ^16.1.0纯 App Router 结构其tests/目录即为这些模式的真实落地本文会持续对照。2. 配置准备2.1 用 webServer 管理 Next.js 开发/生产服务器文档给出的标准配置如下核心思想是本地用 dev server 快速迭代CI 上构建生产 bundle 再跑测试通过webServer让 Playwright 自动拉起服务器并等待就绪// playwright.config.ts import { defineConfig, devices } from playwright/test; export default defineConfig({ testDir: ./tests, fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 50% : undefined, use: { baseURL: http://localhost:3000, trace: on-first-retry, screenshot: only-on-failure, }, projects: [ { name: chromium, use: { ...devices[Desktop Chrome] } }, { name: mobile, use: { ...devices[iPhone 14] } }, ], webServer: { command: process.env.CI ? npm run build npm run start : npm run dev, url: http://localhost:3000, reuseExistingServer: !process.env.CI, timeout: 120_000, env: { NODE_ENV: process.env.CI ? production : test, }, }, });各参数作用forbidOnly防止 CI 上遗留test.onlyretries在 CI 上给失败用例重试机会reuseExistingServer: !process.env.CI表示本地允许复用已在运行的 dev server避免每次测试都冷启动CI 上则强制全新启动trace: on-first-retry只在首次重试时录制 tracescreenshot: only-on-failure只在失败时截图二者都是失败留证据、成功不干扰的常用组合。SurfSense 的真实配置 playwright.config.ts 完整继承了这一骨架并做了几处值得注意的增强// surfsense_web/playwright.config.ts节选 export default defineConfig({ testDir: ./tests, timeout: 30_000, expect: { timeout: 15_000 }, fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 1 : 0, workers: 1, 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, }, }, ], webServer: process.env.PLAYWRIGHT_NO_WEB_SERVER ? undefined : { // Local stays on webpack dev (Turbopack caused stale-lock panics in E2E). 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, ... }, });从源码结构看有三点与文档范例的差异及其理由本地 dev server 刻意避开 Turbopack注释明确写着 Local stays on webpack dev (Turbopack caused stale-lock panics in E2E)。虽然 package.json 的dev脚本是next dev --turbopack但webServer.command本地分支直接调用pnpm exec next devwebpack以换取 E2E 稳定性——这是文档 Turbopack 一节见 11.2在实际项目中的反面修正。workers: 1串行执行SurfSense 的 E2E 是每连接器一条 journey的真实链路测试connector → Celery → indexing → DB单 worker 避免共享测试数据互相干扰这是典型全栈 E2E 与文档中多浏览器矩阵轻量测试在资源策略上的差异。PLAYWRIGHT_NO_WEB_SERVER逃生口当服务器由外部如 docker compose 或手工启动的 dev server管理时可整体跳过webServer对应文档 Playwright 配置 中测试已部署环境则不用webServer的决策。2.2 环境变量策略文档建议利用 Next.js 在NODE_ENVtest下加载.env.test的机制把可提交的测试变量与本地私密变量分层# .env.test (commit this) NEXT_PUBLIC_API_URLhttp://localhost:3000/api DATABASE_URLpostgresql://localhost:5432/test_db # .env.test.local (gitignored) NEXTAUTH_SECRETtest-secret-localSurfSense 采取了另一种等价策略不依赖任何.env文件而是在配置模块加载时就注入默认值。playwright.config.ts 顶部直接写了process.env.PLAYWRIGHT_TEST_EMAIL ?? e2e-testsurfsense.net; process.env.PLAYWRIGHT_TEST_PASSWORD ?? E2eTestPassword123!; process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? backendURL; process.env.SURFSENSE_BACKEND_INTERNAL_URL ?? backendURL; process.env.AUTH_TYPE ?? LOCAL; process.env.NEXT_PUBLIC_ZERO_CACHE_URL ?? zeroCacheURL;??语义保证了有环境变量就用外部的没有就落默认值E2E 测试 README 中也因此声明The E2E entrypointssetdefaultevery backend variable they need, so no.envfile is required on a fresh checkout。后端 URL 还处理了PLAYWRIGHT_USE_PROXY_ORIGIN开关为true时后端与 Zero cache 都走前端baseURL代理/zero否则直连localhost:8000与localhost:4848。3. App Router 测试模式SurfSense 前端是纯 App Router 应用app/目录、proxy.ts、next-intl路由以下三类模式直接适用。3.1 Server Component 内容Server Components 的内容在服务端渲染后随初始 HTML 到达测试时只需goto后直接断言角色与可见性test(renders server component content, async ({ page }) { await page.goto(/); await expect(page.getByRole(heading, { name: Welcome, level: 1 })).toBeVisible(); await expect(page.getByRole(navigation, { name: Main })).toBeVisible(); });3.2 流式加载中的 Loading 状态Next.js 支持 streaming SSR测试 loading state 的关键是人为拖慢数据源用page.route拦截并延迟对应 API然后按出现 → 数据到达 → 消失的三段式断言test(loading state during data streaming, async ({ page }) { await page.route(**/api/stats, async (route) { await new Promise((r) setTimeout(r, 2000)); await route.continue(); }); await page.goto(/dashboard); await expect(page.getByRole(progressbar)).toBeVisible(); await expect(page.getByRole(heading, { name: Dashboard })).toBeVisible(); await expect(page.getByRole(progressbar)).toBeHidden(); });3.3 嵌套布局的持久性嵌套 Layout 应该在子路由间导航时保持不变。通过先拿到 sidebar 的 locator 引用、再断言导航后它仍然可见验证布局没有被卸载重挂test(layouts persist across navigation, async ({ page }) { await page.goto(/dashboard/analytics); const sidebar page.getByRole(navigation, { name: Dashboard }); await expect(sidebar).toBeVisible(); await sidebar.getByRole(link, { name: Settings }).click(); await page.waitForURL(/dashboard/settings); await expect(sidebar).toBeVisible(); await expect(page.getByRole(heading, { name: Settings })).toBeVisible(); });3.4 SurfSense 的真实落地Dashboard 冒烟测试SurfSense 的冒烟测试 dashboard.spec.ts 就是一条最小化的 App Router 断言——证明整条 E2E 管线服务器可达、认证态生效、路由可渲染都通了test(dashboard loads for authenticated user, async ({ page }) { await page.goto(/dashboard); // Sidebar is aside (rolecomplementary); its visibility implies redirect auth fetch. await expect(page.getByRole(complementary).first()).toBeVisible({ timeout: 60_000 }); });注释点明了断言选择的意图sidebar 是asiderolecomplementary它可见即意味着未认证重定向 认证数据拉取整条链路完成。注意这里把可见性超时放宽到 60 秒是为了覆盖 dev server 首次编译与客户端数据加载的耗时属于文档API Through UI一节中waitForURL/自动重试断言思想在慢启动场景下的合理变体。4. Pages Router 测试模式4.1 getServerSideProps 的 SSRgetServerSideProps依赖req/res上下文因此不应直接测试该函数而是导航到页面、断言服务端取数后渲染出的结果test(page with getServerSideProps renders data, async ({ page }) { await page.goto(/blog); await expect(page.getByRole(heading, { name: Blog, level: 1 })).toBeVisible(); await expect(page.getByRole(article)).toHaveCount(10); await expect(page.getByRole(article).first()).toContainText(/\w/); });4.2 getStaticProps 的静态生成静态生成的内容在构建期已预渲染测试只验证内容存在即可同样不直接调用getStaticPropstest(static page shows pre-rendered content, async ({ page }) { await page.goto(/about); await expect(page.getByRole(heading, { name: About Us })).toBeVisible(); await expect(page.getByText(Founded in 2020)).toBeVisible(); });需要说明SurfSense 前端没有pages/目录全部走 App Router这两小节对它是参照性内容对于 App Router 项目等价的关注点是 Server Components 的数据获取与 3.1/3.2 的断言模式。5. 动态路由测试5.1 Slug 参数动态段[slug]要同时覆盖命中与未命中两条路径——正常 slug 渲染正确内容不存在的 slug 必须返回 404 状态码和 404 页面test(dynamic [slug] renders correct content, async ({ page }) { await page.goto(/blog/testing-guide); await expect(page.getByRole(heading, { level: 1 })).toContainText(Testing Guide); await expect(page.getByText(Page not found)).toBeHidden(); }); test(non-existent slug shows 404, async ({ page }) { const response await page.goto(/blog/nonexistent-post); expect(response?.status()).toBe(404); await expect(page.getByRole(heading, { name: 404 })).toBeVisible(); });注意expect(response?.status()).toBe(404)是硬断言响应码不走自动重试保证 404 语义被严格验证。5.2 Catch-All 路由Catch-all[...slug]要验证多个层级的嵌套路径都能落到同一组件并取到正确的分段数据test(catch-all handles nested paths, async ({ page }) { await page.goto(/docs/getting-started/installation); await expect(page.getByRole(heading, { name: Installation })).toBeVisible(); await page.goto(/docs/api/configuration); await expect(page.getByRole(heading, { name: Configuration })).toBeVisible(); });5.3 查询参数查询参数驱动的内容过滤/排序可以把页面数据提取出来做结构性断言如价格升序而不依赖固定文案test(query parameters filter content, async ({ page }) { await page.goto(/products?categoryelectronicssortprice-asc); await expect(page.getByRole(heading, { name: Electronics })).toBeVisible(); const prices await page.getByTestId(product-price).allTextContents(); const numericPrices prices.map((p) parseFloat(p.replace($, ))); expect(numericPrices).toEqual([...numericPrices].sort((a, b) a - b)); });6. API 路由测试6.1 直接用 request 上下文测 APIPlaywright 的requestfixture 提供与浏览器无关的 API 直测能力覆盖成功路径、创建路径与校验失败路径三个层次test(GET /api/products returns list, async ({ request }) { const response await request.get(/api/products); expect(response.ok()).toBeTruthy(); const body await response.json(); expect(body.products).toBeInstanceOf(Array); expect(body.products[0]).toHaveProperty(id); expect(body.products[0]).toHaveProperty(name); }); test(POST /api/products creates item, async ({ request }) { const response await request.post(/api/products, { data: { name: Test Product, price: 29.99 }, }); expect(response.status()).toBe(201); const body await response.json(); expect(body.product.name).toBe(Test Product); }); test(POST /api/products validates fields, async ({ request }) { const response await request.post(/api/products, { data: { name: }, }); expect(response.status()).toBe(400); const body await response.json(); expect(body.error).toContainEqual(expect.objectContaining({ field: price })); });6.2 通过 UI 走 APIUI 驱动测试验证表单 → API → 成功提示 → 跳转的完整用户旅程最终断言落在可见的成功文案与 URL 上test(form submission calls API, async ({ page }) { await page.goto(/products/new); await page.getByLabel(Product name).fill(Widget); await page.getByLabel(Price).fill(19.99); await page.getByRole(button, { name: Create product }).click(); await expect(page.getByText(Product created successfully)).toBeVisible(); await page.waitForURL(/products/**); });6.3 SurfSense 的 API-driven 策略SurfSense 对UI 测 vs API 测的分工在 E2E 测试 README 中有明确阐述journey 测试采用薄浏览器断言 API 驱动的配置/索引理由是保持测试确定性不等待 UI 动画、React hydration、Next.js 编译时间走的是 UI 最终会调用的同一后端代码路径昂贵的 E2E 断言只保留在只有 E2E 能证明的跨进程缝隙connector → Celery → indexing → DB。配套的 API helper 位于 tests/helpers/apiauth.ts、workspaces.ts、connectors.ts、documents.ts、chat.ts各连接器的 journey spec 都放在tests/connectors/vendor/service/journey.spec.ts。这正对应文档Direct API Testing模式的规模化应用同时严格遵守反模式表中的原则——只 mock 外部服务不 mock 自己的 API详见第 12 节。7. Middleware 测试7.1 认证重定向Middleware 是 Next.js 做未认证重定向的常见位置测试要点是同时验证被重定向与回跳地址被保留test(unauthenticated user redirected to login, async ({ page }) { await page.goto(/dashboard); expect(page.url()).toContain(/login); await expect(page.getByRole(heading, { name: Sign in })).toBeVisible(); }); test(redirect preserves return URL, async ({ page }) { await page.goto(/dashboard/settings); const url new URL(page.url()); expect(url.pathname).toBe(/login); expect(url.searchParams.get(callbackUrl) || url.searchParams.get(returnTo)) .toContain(/dashboard/settings); });7.2 安全响应头Middleware 常用于统一注入安全头直接对response.headers()做断言test(middleware sets security headers, async ({ page }) { const response await page.goto(/); const headers response!.headers(); expect(headers[x-frame-options]).toBe(DENY); expect(headers[x-content-type-options]).toBe(nosniff); });7.3 基于语言的重写Locale Rewrites通过context.setExtraHTTPHeaders注入Accept-Language验证 middleware 按语言 rewrite 到对应本地化内容test(middleware rewrites based on locale, async ({ page, context }) { await context.setExtraHTTPHeaders({ Accept-Language: fr-FR,fr;q0.9, }); await page.goto(/); await expect(page.getByText(Bienvenue)).toBeVisible(); });7.4 SurfSense 的 middleware 实现SurfSense 的中间层实现在 proxy.tsNext.js 16 中由middleware.ts演进而来的proxy.ts它对所有非api|auth|_next/*的请求写入一个RUNTIME_AUTH_TYPE_COOKIE_NAMEcookie值来自resolveRuntimeAuthUiMode(process.env.AUTH_TYPE, BUILD_TIME_AUTH_TYPE)secure仅在 https 下开启。playwright.config.ts 恰好将AUTH_TYPE的默认值设为LOCAL并透传给webServer.env与use.extraHTTPHeaders[x-playwright-test] true一起构成 E2E 环境识别。多语言侧则采用 next-intl 的路由前缀方案而非 rewritei18n/routing.ts 配置了locales: [en, es, pt, hi, zh, ko]且localePrefix: as-needed——默认语言不加前缀非默认语言路径形如/zh/dashboard。测试 locale 行为时可对 7.3 的模式做等价改写直接访问/zh/...前缀路径并断言本地化文案。8. Hydration 测试8.1 捕获 console 中的 Hydration 错误Hydration 错误不一定导致页面崩溃但会破坏交互。用page.on(console)收集 error 级日志过滤出与 hydration 相关的条目后断言为空test(no hydration errors in console, async ({ page }) { const consoleErrors: string[] []; page.on(console, (msg) { if (msg.type() error) { consoleErrors.push(msg.text()); } }); await page.goto(/); await page.getByRole(button, { name: Get started }).click(); const hydrationErrors consoleErrors.filter( (e) e.includes(Hydration) || e.includes(hydration) || e.includes(did not match) ); expect(hydrationErrors).toEqual([]); });关键词did not match覆盖 React 19 中 Hydration failed because the initial UI does not match what was rendered on the server 这类提示。8.2 Hydration 后交互元素可用Hydration 完成的直接证据是客户端状态可以变更test(interactive elements work after hydration, async ({ page }) { await page.goto(/); const counter page.getByTestId(counter-value); await expect(counter).toHaveText(0); await page.getByRole(button, { name: Increment }).click(); await expect(counter).toHaveText(1); });这里toHaveText是自动重试断言天然等待 hydration 完成不需要显式 sleep。SurfSense 的取舍在 E2E 测试 README 中写得很直白journey 测试刻意不等待UI animation, React hydration, or Next.js compile time把昂贵的 E2E 断言聚焦在跨进程数据链路上hydration 相关回归留给 8.1 这类控制台断言与后端集成测试承担。这是测试预算分配而非不做 hydration 测试。9. next/image 测试next/image的断言重点是响应式与懒加载行为而不是具体 URLtest(hero image loads with srcset, async ({ page }) { await page.goto(/); const heroImage page.getByRole(img, { name: Hero banner }); await expect(heroImage).toBeVisible(); const srcset await heroImage.getAttribute(srcset); expect(srcset).toBeTruthy(); expect(srcset).toContain(w); const loading await heroImage.getAttribute(loading); expect(loading).not.toBe(lazy); }); test(offscreen images lazy load, async ({ page }) { await page.goto(/gallery); const offscreenImage page.getByRole(img, { name: Gallery item 20 }); await offscreenImage.scrollIntoViewIfNeeded(); await expect(offscreenImage).toBeVisible(); const naturalWidth await offscreenImage.evaluate( (img: HTMLImageElement) img.naturalWidth ); expect(naturalWidth).toBeGreaterThan(0); });要点首图断言srcset含w描述符且不做懒加载视口外图片滚动进视口后用naturalWidth 0证明图片真实解码。对应反模式是断言精确的图片 URL——dev 与 production 的/_next/image参数如w、q、URL 编码经常不同精确匹配会频繁误报。10. 认证态管理setup 项目 storageState10.1 配置 setup 项目文档的 NextAuth.js 场景用三个 project 划分认证边界setup只跑auth.setup.tsauthenticated依赖 setup 并加载其产物storageStateunauthenticated则用文件名后缀*.unauth.spec.ts显式声明必须未登录// playwright.config.ts export default defineConfig({ projects: [ { name: setup, testMatch: /auth\.setup\.ts/ }, { name: authenticated, use: { storageState: playwright/.auth/user.json }, dependencies: [setup], }, { name: unauthenticated, testMatch: **/*.unauth.spec.ts }, ], });10.2 认证脚本setup 脚本走真实的登录表单凭据来自环境变量登录成功后把整个 context 的 cookie/localStorage 序列化到user.json// tests/auth.setup.ts import { test as setup, expect } from playwright/test; const authFile playwright/.auth/user.json; setup(authenticate via credentials, async ({ page }) { await page.goto(/login); await page.getByLabel(Email).fill(testexample.com); await page.getByLabel(Password).fill(process.env.TEST_PASSWORD!); await page.getByRole(button, { name: Sign in }).click(); await page.waitForURL(/dashboard); await expect(page.getByRole(heading, { name: Dashboard })).toBeVisible(); await page.context().storageState({ path: authFile }); });10.3 已认证用例后续所有authenticated项目的测试直接以登录态开始无需重复登录test(authenticated user sees dashboard, async ({ page }) { await page.goto(/dashboard); await expect(page.getByRole(heading, { name: Dashboard })).toBeVisible(); await expect(page.getByText(testexample.com)).toBeVisible(); });10.4 SurfSense 的 auth.setup.ts同一模式的真实工程化SurfSense 不用 NextAuth而是自研的会话 cookie JWT 方案但其 auth.setup.ts 与上述模式完全同构并展示了两个文档未展开的工程细节拿 token 而不是走表单优先调用后端专门给 E2E 的/__e2e__/auth/token接口免限流种子用户e2e-testsurfsense.net失败时回退到 desktop 登录路径helper 在 tests/helpers/api/auth.ts。拿到 access token 后直接以httpOnly: true, sameSite: Lax写入会话 cookiesurfsense_session名称可用SESSION_COOKIE_NAME覆盖。预写 localStorage 关闭新用户对用户浮层用page.addInitScript在页面脚本执行前写入两类状态——surfsense_announcements_state把 announcements 数据源 中所有公告标记为已读已提示阻断 AnnouncementSpotlight 弹层与surfsense-tour-userId从 JWT payload 解码sub得到用户 ID关闭 OnboardingTour 新手引导。这样任何浮层都不会拦截 journey 中的点击。落盘 storageState先在公开的/login页执行 init script避免与 dashboard 的认证重定向竞争写 localStorage然后page.context().storageState({ path: authFile })保存到playwright/.auth/user.json。对应地playwright.config.ts 中setup项目的testMatch: /.*\.setup\.ts/与chromium项目的dependencies: [setup]storageState: playwright/.auth/user.json即文档 10.1 配置的直接落地。11. 实用技巧Tips11.1 Dev Server vs 生产构建ScenarioCommandTrade-offLocal developmentnpm run devFast iteration, no production behaviorCI pipelinenpm run build npm run startTests real production bundleSurfSense 把这组命令映射为 npm scriptpackage.json 中test:e2e即playwright testdev servertest:e2e:prod即cross-env CI1 playwright testCI1触发pnpm build pnpm start且 reporter 增加github输出E2E 测试 README 中明确后两者matches CI exactly。本地还有test:e2e:headed、test:e2e:ui、test:e2e:debug、test:e2e:report四个调试入口。11.2 Turbopack文档给出用 Turbopack 加速本地 E2E 的写法webServer: { command: process.env.CI ? npm run build npm run start : npx next dev --turbopack, url: http://localhost:3000, reuseExistingServer: !process.env.CI, },但注意 SurfSense 的实践经验是反例其 playwright.config.ts 注释说明 Turbopack 在 E2E 中曾引发 stale-lock panic因此本地分支退回pnpm exec next devwebpack。Turbopack 是否安全取决于版本出现 E2E 环境不稳定时可优先考虑该回退。11.3 多个 webServer 条目当前端之外还有独立 API 服务时webServer可写成数组Playwright 会并行拉起并等待所有url就绪webServer: [ { command: npm run dev:api, url: http://localhost:4000/health, reuseExistingServer: !process.env.CI, }, { command: npm run dev, url: http://localhost:3000, reuseExistingServer: !process.env.CI, }, ],SurfSense 没有用数组形式而是把 FastAPI 后端放在webServer之外单独启动本地用uv run python tests/e2e/run_backend.py与run_celery.py两个入口Celery worker 无法用webServer表达URL 就绪只能手工/脚本拉起CI/容器化环境用docker compose -f docker/docker-compose.e2e.yml up -d --build --wait一步起齐见 docker/docker-compose.e2e.yml。两条路线的完整步骤起 Postgres/Redis、alembic upgrade head、curl注册 E2E 用户、再跑pnpm test:e2e:prod都写在 E2E 测试 README 中可整体照抄。12. 反模式清单文档给出的反模式表是整篇最浓缩的守则完整继承如下Dont Do ThisProblemDo This Insteadawait page.waitForTimeout(3000)Arbitrary waits are fragileawait page.waitForURL(/path)orawait expect(locator).toBeVisible()TestgetServerSidePropsdirectlyDepends on req/res contextNavigate to page and verify rendered outputMock your own API routesHides real API bugsLet real API handle requests; mock only external servicespage.goto(http://localhost:3000/path)Breaks when port changesUsepage.goto(/path)withbaseURLRunnpm run buildlocally for every testExtremely slowUsenpm run devlocally withreuseExistingServer: trueTestnext/imageby checking exact URLsPaths change between dev/prodAssert onalt, visibility,naturalWidth 0,srcsetTest server actions by calling as functionsServer actions need Next.js runtimeTrigger through UI (forms, buttons)其中Mock your own API routes / mock only external services一条与 SurfSense E2E 的三层防线sys.modules劫持严格 fake、HTTPS_PROXYhttp://127.0.0.1:1哨兵代理 sentinel API key在精神上完全一致mock 只针对第三方 SDK/外部网络自家链路Next.js → FastAPI → Celery → Postgres必须真实跑通见 E2E 测试 README 的 How the deterministic harness works。13. 相关文档本篇文档在 SurfSense 仓库内的关联材料configuration.md -- Playwright 配置详解含webServer决策与超时选择表locators.md -- Locator 策略本篇所有getByRole/getByLabel的选取依据authentication.md -- 认证 setup 与storageState的进阶用法api-testing.md -- 用request上下文测 API 路由react.md -- Next.js 客户端组件的 React 测试模式playwright.config.ts、auth.setup.ts、E2E 测试 README -- SurfSense 的完整实现与运行手册按配置第 2 节→ 页面模式第 3–5 节→ 接口与中间件第 6–7 节→ 渲染质量第 8–9 节→ 认证态第 10 节→ 环境技巧与反模式第 11–12 节的顺序落地即可把一个 Next.js 应用的 E2E 体系从零搭到 CI 可运行。【免费下载链接】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),仅供参考
返回列表