首页
/ Langflow E2E 自定义 Fixture 详解:让 Playwright 测试自动捕获 API 错误与流程执行失败

Langflow E2E 自定义 Fixture 详解:让 Playwright 测试自动捕获 API 错误与流程执行失败

2026-09-06 13:45:41作者:毕习沙Eudora

Langflow 的前端 E2E 测试套件在 Playwright 默认 test / expect 之上封装了一层自定义 fixture(fixtures.ts),它会自动监控所有 /api/ 响应,在后端返回 5xx、或流程构建/运行流中出现 Python 异常时将测试判为失败。本文基于 Custom Test Fixtures 参考文档 展开,结合 fixture 的源码实现,讲解这套“静默失败拦截”机制的检测范围、预期错误声明方式、超时与脱敏行为,以及全局清理流程。

为什么需要自定义 Fixture

Langflow 的 E2E 场景是:前端画布操作触发后端构建(build)或运行(run),结果往往通过 SSE/NDJSON 流返回。如果只用原生 Playwright,测试很容易出现“绿了但其实是坏的”情况:

  • 后端返回了 500 Internal Server Error,但测试只断言了 UI 文案,照样通过;
  • 一次 flow 构建因 Python 异常静默失败,但测试只检查了按钮状态;
  • API 调用返回 404(资源已被删除),测试根本没有检查响应体。

自定义 fixture 的核心思路是:在测试运行期间拦截页面发出的所有 /api/ 请求响应,把上述静默失败变成确定性的测试失败。这样开发者无需在每个用例里手写响应断言。

Fixture 入口与导入规则

fixture 文件位于 src/frontend/tests/fixtures.ts。它通过 base.extend 扩展 Playwright 的 page fixture(fixtures.ts#L86-L98),并重新导出 expect

硬性规则:测试文件必须从 ../../fixtures 导入 testexpect,而不是从 @playwright/test 导入。 后者会绕过全部错误检测,让静默失败重新成为可能:

// 正确 —— 包含自动错误检测
import { expect, test } from "../../fixtures";

// 错误 —— 没有错误检测,可能出现静默失败
import { expect, test } from "@playwright/test";

导入自定义 fixture 后,page 的类型不再是原生 Page,而是 utils/types.ts 中定义的 LangflowPage,它在原生 Page 上附加了三个辅助方法:

export type LangflowPage = Page & {
  allowFlowErrors: () => void;
  expectServerError: (expectation: ExpectedServerError) => void;
  runA11yScan: (
    label: string,
    options?: A11yScanOptions,
  ) => Promise<ICheckerResult | null>;
};

其中前两个与错误检测直接相关,runA11yScan 则用于无障碍扫描(由环境变量 RUN_A11Y 控制开关)。

第一层检测:HTTP 错误响应

fixture 在 page fixture 内注册了 request / response / requestfinished / requestfailed 四类监听器(fixtures.ts#L337-L391),核心检查逻辑在 inspectResponse 中(fixtures.ts#L186-L208):

if (url.includes("/api/") && status >= 400) {
  const method = response.request().method().toUpperCase();
  const path = new URL(url).pathname;
  const observed: ObservedHttpError = {
    method,
    path,
    status,
    statusText: response.statusText(),
    responseBody: await getResponseBody(response, `${method} ${path}`),
  };

  if (status < 500) {
    if (clientErrors.length < MAX_CLIENT_ERROR_DIAGNOSTICS) {
      clientErrors.push(observed);
    }
  } else {
    observeServerError(serverErrorContract, observed);
  }
}

参考文档对每个状态码的含义解释如下:

状态码 含义 为什么值得关注
400 Bad Request 客户端发送了非法数据——大概率是前端 bug
404 Not Found 资源不存在——大概率是过期 ID 或缺少测试前置准备
422 Validation Error Pydantic 校验失败——大概率是 schema 不匹配
500 Internal Server Error 后端崩溃——一定是 bug

从源码结构看,当前实现把 4xx 与 5xx 做了区分处理:

  • 4xx 响应:记入 clientErrors 诊断列表(上限 20 条,MAX_CLIENT_ERROR_DIAGNOSTICS),测试结束时以 JSON 附件 api-4xx-responses 的形式挂到测试报告里(fixtures.ts#L441-L446),供排查时定位问题;
  • 5xx 响应:进入“服务端错误契约”(server error contract),未注册的 5xx 会直接使测试失败(见下文“声明预期错误”一节)。

另外,请求追踪有一个豁免项:shouldTrackApiRequest 会跳过 /api/v1/files/profile_pictures/ 路径(server-error-contract.mjs#L192-L203),因为头像这类静态资源在浏览器关闭时可能被取消,若计入“未完成的 API 请求”会造成假性失败。

第二层检测:流程执行错误(流式响应)

对于 HTTP 200 但内容里藏着错误的情形,fixture 专门监控三类事件/执行端点(fixtures.ts#L211-L216):

  • URL 含 /events?event_delivery=(事件投递端点,覆盖 streaming/polling/direct 三种模式);
  • URL 含 /build/(组件构建);
  • URL 含 /run/(流程运行)。

处理流程是:

  1. 跳过流式内容:若响应头 content-type 属于 text/event-streamapplication/grpcapplication/octet-streamapplication/x-ndjson 中的流式提示(fixtures.ts#L220-L231),说明响应体尚未完整,直接返回,不做正文解析;
  2. 带超时读取正文:通过 readResponseBodyWithTimeout 在 2 秒内读取响应体(下文详述),超时或读取失败则记录诊断信息并跳过;
  3. 逐行解析 JSON:把响应体按行拆分,对每一行尝试 JSON.parse,命中以下任一条件即判定为流程错误(fixtures.ts#L252-L280):
    • json.data.build_data.params"Error" 开头(组件构建错误);
    • json.data.error === truejson.error === true(注意是布尔 true,而非 false),错误文案取自 error_message 字段;
  4. Python 异常兜底:若 JSON 解析没有命中,则用一组正则匹配原始文本中的 Python 异常(fixtures.ts#L287-L295):
const exceptionPatterns = [
  /NameError: .+/,
  /TypeError: .+/,
  /ValueError: .+/,
  /AttributeError: .+/,
  /ImportError: .+/,
  /KeyError: .+/,
  /An error occured .+/,
];

命中后,错误会以 { path, status: 200, statusText: "Flow Error", type: "flow_error" } 的形式记入错误列表(上限 20 条,MAX_FLOW_ERROR_DIAGNOSTICS = 20)。

声明预期错误:allowFlowErrors 与 expectServerError

有些测试本身就是故意触发错误(验证错误处理、校验反馈等)。fixture 提供两个粒度不同的豁免/声明机制。

allowFlowErrors():豁免流程执行错误

在测试开头调用 page.allowFlowErrors(),即可让本测试中的流程执行错误不再导致失败:

test("should show error message on invalid component config", { tag: ["@release"] }, async ({ page }) => {
  page.allowFlowErrors();  // 仅对当前测试放行流程错误

  await awaitBootstrapTest(page);
  // ... 触发错误的操作 ...

  await expect(page.getByText(/error/i)).toBeVisible();
});

源码实现非常直接:fixture 内部维护一个 allowFlowErrors 布尔标志,测试结束检查错误列表时,只有 flowErrors.length > 0 && !allowFlowErrors 才抛错(fixtures.ts#L124-L130 对应实现见 fixtures.ts#L124-L130fixtures.ts#L505-L524)。

注意allowFlowErrors() 只豁免流程执行错误。HTTP 5xx 仍会失败——后端崩溃不属于预期行为,必须显式处理。仓库中真实用例可见 knowledge-bases.a11y.spec.ts 中多处 page.allowFlowErrors() 的用法。

expectServerError():为 5xx 建立“契约”

对于确实预期后端返回 5xx 的场景(例如模拟写入失败),可以在测试中先注册预期,5xx 出现时才会被“认领”而不算意外错误:

page.expectServerError({
  method: "POST",
  path: "/api/v1/variables/",
  status: 500,
  count: 1,
});

该模式在 db-providers.a11y.spec.ts#L105-L117 中有实际使用(配合 page.route 拦截并让写入接口失败)。契约的校验与匹配逻辑在 server-error-contract.mjs 中:

  • 注册时强校验expectServerErrorserver-error-contract.mjs#L239-L257):path 必须以 / 开头且不含查询串,status 必须是 500–599 的整数,count 必须是 ≥1 的整数,否则立即抛错;
  • 匹配规则observeServerErrorserver-error-contract.mjs#L259-L276):按 method + path + status 三元组配对,且同一预期只能被认领 count 次;
  • 双向失败getServerErrorContractFailures):未认领的 5xx(unexpected)或声明了却没出现/出现次数不符(missing)都会让测试失败。也就是说,契约既是“白名单”也是“断言”。

错误上报:失败消息长什么样

fixture 采用“测试函数执行期间收集、测试函数结束(use(page) 之后)统一清算”的策略。清算顺序大致是:

  1. 用 2 秒宽限期(API_REQUEST_DRAIN_TIMEOUT_MS = 2000)等待在途 API 请求收敛(fixtures.ts#L393-L411),确保“响应触发的后续请求”也能被观察到;
  2. 将 4xx 诊断挂为 api-4xx-responses 附件;
  3. 检查服务端错误契约,若有意外/缺失的 5xx 或超 2 秒仍未解决的请求,抛出 Server-error contract failed 并逐条列出 method path -> status,同时提示“Register intentional failures with page.expectServerError(...)”(fixtures.ts#L490-L502);
  4. 若存在未被豁免的流程错误,抛出形如下面的消息(fixtures.ts#L505-L524):
Test failed due to 1 flow execution error(s):
  - /api/v1/build/abc123/flow
    Traceback (most recent call last)...

If this error is expected, call page.allowFlowErrors() at the start of your test.

参考文档中给出的另一类失败示例(意外 5xx)对应的是契约失败分支:

Server-error contract failed:
  - unexpected POST /api/v1/build/abc123/flow -> 500: ...

值得注意的安全细节:所有进入失败消息的响应体片段都会先经过 sanitizeResponseExcerpt 处理(server-error-contract.mjs#L67-L79)——它会把 api_keytokenpasswordauthorization 等敏感字段的值替换为 [REDACTED],并截断到 1000 字符,避免把密钥泄漏进测试报告。

超时行为:为什么 fixture 不会卡死长流

文档中提到“读取响应体使用 2 秒超时”,其落地实现是 server-error-contract.mjs 中的 readResponseBodyWithTimeoutserver-error-contract.mjs#L81-L127):用 Promise.race 竞速 response.text() 与 2 秒定时器,超时返回诊断信息而非正文。

fixture 顶部把这组常量集中在了一起(fixtures.ts#L41-L45):

const MAX_FLOW_ERROR_DIAGNOSTICS = 20;
const API_REQUEST_DRAIN_TIMEOUT_MS = 2000;
const RESPONSE_BODY_READ_TIMEOUT_MS = API_REQUEST_DRAIN_TIMEOUT_MS;
const RESPONSE_INSPECTION_DRAIN_TIMEOUT_MS = API_REQUEST_DRAIN_TIMEOUT_MS;

设计意图是:仍在传输中的流式响应如果 2 秒内读不到完整正文,就跳过该响应的正文解析,避免 fixture 无限阻塞在长流上;同理,响应体检查任务本身也有 2 秒的清算上限,超时的检查任务会被记录为诊断错误。此外,shouldSettleApiRequestOnResponseserver-error-contract.mjs#L205-L233)区分了“长生命周期流”(text/event-stream、gRPC,或 event_delivery=streaming/direct 的 NDJSON)与有限响应——前者在响应头到达后即可结算,后者要一直跟踪到 requestfinished / requestfailed,保证由响应触发的后续请求在测试收尾时仍然可见。

全局清理:globalTeardown 删除临时数据库

playwright.config.tswebServer 配置会为 E2E 启动三组进程:一个 OpenAI 兼容的本地 mock 服务(端口 8787)、uvicorn 后端(端口 7860)与前端 npm start(端口 3000)。其中后端被注入 LANGFLOW_DATABASE_URL: "sqlite:///./temp"LANGFLOW_AUTO_LOGIN: "true" 等测试环境变量(playwright.config.ts#L114-L153),即整轮测试跑在一个位于 src/frontend/temp 的临时 SQLite 库上。

配置同时声明了全局清理钩子(playwright.config.ts#L52):

globalTeardown: require.resolve("./tests/globalTeardown.ts"),

globalTeardown.ts 在所有测试结束后删除该临时数据库目录,保证下一轮测试从干净状态开始(globalTeardown.ts#L48-L80)。实现上有两点工程细节:

  • removeWithRetry 最多重试 5 次、按 200 * 2^i 毫秒指数退避:Windows 上 uvicorn 进程可能仍持有 SQLite 文件句柄,POSIX 允许删除打开中的文件而 Win32 会报 EBUSY/EPERM
  • 若整体删除失败,则逐个子项删除(removeChildrenBestEffort)再重试,且绝不从 teardown 中抛出异常,最坏情况下留下日志提示交给 CI 工作区清理。

小结:把哪些测试写成什么样子

综合参考文档与源码,编写 Langflow E2E 用例时的 fixture 相关要点可以浓缩为:

  1. 一律 import { expect, test } from "../../fixtures",从 page fixture 解构出的 pageLangflowPage
  2. 任何意外 5xx 与未被豁免的流程错误都会让测试失败,无需手写响应断言;4xx 会以附件形式出现在报告中辅助定位;
  3. 故意测试错误处理时,测试开头调用 page.allowFlowErrors()(仅限流程错误,5xx 不受豁免);
  4. 故意让后端返回 5xx 时,用 page.expectServerError({ method, path, status, count }) 注册精确契约,出现次数必须与声明一致;
  5. 长流与慢响应有 2 秒级的超时保护,fixture 不会拖垮测试;测试结束后临时数据库由 globalTeardown 统一清理。

这套机制让 Langflow 的前端 E2E 测试从“只看 UI 表象”升级为“UI 断言 + 后端健康兜底”,是 e2e-testing 技能文档 中推荐的工作方式之一。

登录后查看全文
热门项目推荐
相关项目推荐