Langflow E2E 自定义 Fixture 详解:让 Playwright 测试自动捕获 API 错误与流程执行失败
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 导入 test 与 expect,而不是从 @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/(流程运行)。
处理流程是:
- 跳过流式内容:若响应头
content-type属于text/event-stream、application/grpc、application/octet-stream、application/x-ndjson中的流式提示(fixtures.ts#L220-L231),说明响应体尚未完整,直接返回,不做正文解析; - 带超时读取正文:通过
readResponseBodyWithTimeout在 2 秒内读取响应体(下文详述),超时或读取失败则记录诊断信息并跳过; - 逐行解析 JSON:把响应体按行拆分,对每一行尝试
JSON.parse,命中以下任一条件即判定为流程错误(fixtures.ts#L252-L280):json.data.build_data.params以"Error"开头(组件构建错误);json.data.error === true或json.error === true(注意是布尔true,而非false),错误文案取自error_message字段;
- 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-L130 与 fixtures.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 中:
- 注册时强校验(
expectServerError,server-error-contract.mjs#L239-L257):path必须以/开头且不含查询串,status必须是 500–599 的整数,count必须是 ≥1 的整数,否则立即抛错; - 匹配规则(
observeServerError,server-error-contract.mjs#L259-L276):按method + path + status三元组配对,且同一预期只能被认领count次; - 双向失败(
getServerErrorContractFailures):未认领的 5xx(unexpected)或声明了却没出现/出现次数不符(missing)都会让测试失败。也就是说,契约既是“白名单”也是“断言”。
错误上报:失败消息长什么样
fixture 采用“测试函数执行期间收集、测试函数结束(use(page) 之后)统一清算”的策略。清算顺序大致是:
- 用 2 秒宽限期(
API_REQUEST_DRAIN_TIMEOUT_MS = 2000)等待在途 API 请求收敛(fixtures.ts#L393-L411),确保“响应触发的后续请求”也能被观察到; - 将 4xx 诊断挂为
api-4xx-responses附件; - 检查服务端错误契约,若有意外/缺失的 5xx 或超 2 秒仍未解决的请求,抛出
Server-error contract failed并逐条列出method path -> status,同时提示“Register intentional failures with page.expectServerError(...)”(fixtures.ts#L490-L502); - 若存在未被豁免的流程错误,抛出形如下面的消息(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_key、token、password、authorization 等敏感字段的值替换为 [REDACTED],并截断到 1000 字符,避免把密钥泄漏进测试报告。
超时行为:为什么 fixture 不会卡死长流
文档中提到“读取响应体使用 2 秒超时”,其落地实现是 server-error-contract.mjs 中的 readResponseBodyWithTimeout(server-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 秒的清算上限,超时的检查任务会被记录为诊断错误。此外,shouldSettleApiRequestOnResponse(server-error-contract.mjs#L205-L233)区分了“长生命周期流”(text/event-stream、gRPC,或 event_delivery=streaming/direct 的 NDJSON)与有限响应——前者在响应头到达后即可结算,后者要一直跟踪到 requestfinished / requestfailed,保证由响应触发的后续请求在测试收尾时仍然可见。
全局清理:globalTeardown 删除临时数据库
playwright.config.ts 的 webServer 配置会为 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 相关要点可以浓缩为:
- 一律
import { expect, test } from "../../fixtures",从pagefixture 解构出的page即LangflowPage; - 任何意外 5xx 与未被豁免的流程错误都会让测试失败,无需手写响应断言;4xx 会以附件形式出现在报告中辅助定位;
- 故意测试错误处理时,测试开头调用
page.allowFlowErrors()(仅限流程错误,5xx 不受豁免); - 故意让后端返回 5xx 时,用
page.expectServerError({ method, path, status, count })注册精确契约,出现次数必须与声明一致; - 长流与慢响应有 2 秒级的超时保护,fixture 不会拖垮测试;测试结束后临时数据库由
globalTeardown统一清理。
这套机制让 Langflow 的前端 E2E 测试从“只看 UI 表象”升级为“UI 断言 + 后端健康兜底”,是 e2e-testing 技能文档 中推荐的工作方式之一。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00