首页
/ Langflow 前端 E2E 测试套件详解:Playwright 配置、共享工具函数与标签规范

Langflow 前端 E2E 测试套件详解:Playwright 配置、共享工具函数与标签规范

2026-09-06 14:37:06作者:虞亚竹Luna

本篇基于仓库内 src/frontend/tests/README.md 展开,系统讲解 Langflow 前端 Playwright E2E 测试套件的运行方式、共享 fixtures 与工具函数、以及规格文件(spec)的标签(tag)规范。读完本文,你不仅能直接通过 make tests_frontend 运行整套 E2E 测试,还能理解每个 spec 背后的服务编排(mock LLM、uvicorn 后端、Vite 前端)、tests/fixtures.ts 自定义 page fixture 的隐式失败检测机制,以及 CI 如何通过标签过滤测试集,从而按同样的规范为 Langflow UI 编写高质量、低样板的新测试。

套件结构与运行入口

Langflow 的前端 E2E 测试全部位于 src/frontend/tests/ 目录下,统一使用 Playwright 编写。从目录结构看,套件按测试性质分成了几个清晰的子目录:

目录 内容
tests/core/features/ 核心功能回归(部署、文件夹、全局变量、playground、发布流程等)
tests/core/integrations/ 基于 starter 模板的端到端集成测试(Basic Prompting、Simple Agent、Memory Chatbot 等)
tests/core/regression/ 历史 bug 回归用例(duplicateDomIds、session 数据泄漏等)
tests/core/unit/ 依赖真实页面环境的组件级 E2E 用例
tests/extended/ 扩展集(auto-login、drag-and-drop、MCP server、sticky notes 等,含分片 shard 文件)
tests/a11y/ 无障碍(IBM accessibility-checker)专项扫描,含 baselines/ 基线
tests/live/ 需要真实 LLM Provider 的冒烟测试,默认被排除在 CI 套件之外
tests/assets/ 测试夹具数据(flow JSON、音频、图片、A2A agent 定义等)
tests/fixtures/ 进程级 mock 服务:openai-compatible-server.mjs(loopback OpenAI 兼容服务)与 mcp-loopback-server.py(loopback MCP 服务)
tests/utils/ 共享工具函数、常量(selectors.tstestIds.tstimeouts.ts)与各类 helper

套件最外层还有两个关键文件:tests/fixtures.ts(自定义 Playwright test/page fixture,见后文)与 tests/globalTeardown.ts(在 playwright.config.ts 中通过 globalTeardown 注册,用于测试结束后的全局清理)。

README 给出的运行入口是一行命令:

make tests_frontend

该 target 定义在 Makefile.frontend 中(tests_frontend 目标),其逻辑是:当环境变量 UI=true 时进入 Playwright 的交互式调试模式,否则以无头方式执行 chromium 项目:

# 等价于 make tests_frontend
cd src/frontend && npx playwright test --project=chromium

# 带 Playwright UI 调试器运行
make tests_frontend UI=true
# 等价于:cd src/frontend && npx playwright test --ui --project=chromium

需要注意 --project=chromium 这一限定:当前配置只启用了 chromium 一个浏览器项目(firefox/safari 项目在配置中被注释掉),因此显式指定项目名也是必要的。

测试编排:三层 webServer 与服务依赖

playwright.config.ts 中最重要的部分是 webServer 数组——Playwright 会在测试启动前自动拉起三个相互依赖的服务,构成一次完整 E2E 运行所需的最小栈:

  1. loopback OpenAI 兼容服务node tests/fixtures/openai-compatible-server.mjs,健康检查地址 http://127.0.0.1:8787/healthreuseExistingServer: true。它提供 http://127.0.0.1:8787/v1 的假 OpenAI API,让集成测试无需真实 API Key 即可跑通 LLM 调用链路。
  2. Langflow 后端uv run uvicorn --factory langflow.main:create_app --host localhost --port 7860 --loop asyncio --log-level error --no-access-log,等待 7860 端口就绪,启动超时设置为 120 * 750 ms。其环境变量为测试环境量身定制:
    • LANGFLOW_DATABASE_URL=sqlite:///./temp——每次运行使用独立的临时 SQLite 库;
    • LANGFLOW_AUTO_LOGIN=true 配合 LANGFLOW_SUPERUSER=langflow / LANGFLOW_SUPERUSER_PASSWORD=test-superuser-password——自动登录,免去 spec 逐个实现登录流程;
    • LANGFLOW_DEACTIVATE_TRACING=trueLANGFLOW_LOG_LEVEL=ERRORDO_NOT_TRACK=true——关闭遥测与噪音日志;
    • OPENAI_API_KEY=langflow-loopback-test-key + OPENAI_BASE_URL=http://127.0.0.1:8787/v1——把 OpenAI 指向第 1 个 loopback 服务;
    • LANGFLOW_A2A_ENABLED=true——开放 A2A 发现与 JSON-RPC 端点,供 Agent tab 相关测试(如 a2a-agent-tab.spec.ts)发布并运行真实 agent。
    • Windows CI 下 stdout 使用 pipe 以避免平台差异,其余平台为 ignore
  3. Langflow 前端npm start,等待 3000 端口(PORT || 3000),并注入 VITE_PROXY_TARGET=http://localhost:7860 让前端 dev server 把 API 请求代理到后端。

三个服务都设置了 reuseExistingServer: true,意味着本地开发时如果这些服务已经在运行(比如你本来就在跑 Langflow),Playwright 会直接复用而不重复拉起——这也是本地调试时可以先手动起服务、再跑单条 spec 的原因。

配置中其余关键参数:

  • testDir: "./tests"testIgnore: "**/live/**"——tests/live/ 下的真实 Provider 冒烟测试默认被排除,避免 CI 依赖外部 API;
  • fullyParallel: true + workers: 2——文件级全并行、限制 2 个 worker 控制内存与端口压力;
  • retries: 1 + trace: "on-first-retry"——仅在失败重试时采集 trace,兼顾诊断信息与产物体积;
  • forbidOnly: !!process.env.CI——CI 中若误留 test.only 直接构建失败;
  • 超时体系:全局 timeout: 5 * 60 * 1000(5 分钟/条)、actionTimeout: 20000
  • 报告器:CI 用 blob,本地用 list + html(输出到 playwright-report/open: "never");
  • baseURL: http://localhost:${PORT || 3000}/——spec 中 page.goto("/") 即落到前端 dev server。

共享工具函数:不要手写 setup

README 的第一条强约束是:新 spec 必须复用共享 fixtures 和 helper,而不是把 bootstrap、侧边栏拖拽、模板打开、playground 发消息、路由 mock 等代码块内联重复实现。原文明确指出这种重复是整套用例中最大的样板来源,reviewer 应标记任何对已有 helper 覆盖范围的手写复制。核心 helper 对照表如下(左列为仓库 README 原表内容):

需求 使用 文件位置
登录并进入应用 awaitBootstrapTest(page) utils/await-bootstrap-test.ts
打开一个 starter 模板 openStarterProject(page, name) utils/flow/open-starter-project.ts
从侧边栏添加组件 addComponentFromSidebar(page, ...) utils/flow/add-component-from-sidebar.ts
发送 playground 消息 sendPlaygroundMessage(page, msg) utils/playground/send-playground-message.ts
mock 部署相关 API 路由 setupDeploymentMocks(page, ...) utils/deployment-mocks.ts

下面结合源码看这几个 helper 各自封装了什么。

awaitBootstrapTest:标准化的进入应用路径

await-bootstrap-test.ts 封装了"打开首页 → 等待主界面就绪 → 必要时播种一个 flow → 打开模板面板"的完整仪式:

export const awaitBootstrapTest = async (
  page: Page,
  options?: {
    skipGoto?: boolean;          // 调用方已经 goto 过则跳过
    skipModal?: boolean;        // 不需要打开模板面板则跳过
    seedFlowIfEmpty?: boolean;  // 默认为 true:空库时先造一个 flow
  },
) => { ... }

内部先 page.goto("/"),再对 data-testid="mainpage_title" 做 30 秒等待,随后调用 seedFlowIfEmpty(空环境播种 flow,避免各用例自行处理首次运行的空态)与 waitForNewProjectButton,最后默认打开模板模态框(openTemplatesModal)。这种"幂等进入"设计让后续任何 helper 都可以无条件假设应用已就绪。

openStarterProject:一行替代三步仪式

open-starter-project.ts 的注释说明它替代了"在 core/integrations/ 中出现 50 多次的 3 步仪式":

// 被替代的手工写法:
await awaitBootstrapTest(page);
await page.getByTestId("side_nav_options_all-templates").click();
await page.getByRole("heading", { name: "<模板名>" }).click();

现在等价于一行:

await openStarterProject(page, "Basic Prompting"); // 第三个参数 { skipBootstrap?: boolean }

实现上它内部调用 awaitBootstrapTest(可用 skipBootstrap: true 跳过,适合 AUTO_LOGIN=off 场景)、selectStarterTemplatewaitForFlowEditorReady——最后一步等待编辑器就绪,保证 spec 后续拖拽、发消息等操作不会与页面加载竞争。

addComponentFromSidebar:搜索、拖拽、内联 "+" 三合一

add-component-from-sidebar.ts 替代的是"在套件中出现 60 多次的 5 行仪式":点击侧边栏搜索框、填入关键词、等待组件行出现、dragTo 拖到画布。其参数对象值得逐条理解:

type AddComponentOpts = {
  search: string;        // 输入侧边栏搜索框的关键词
  testId: string;       // 组件行的精确 data-testid,如 "input_outputChat Output"
  position?: { x: number; y: number }; // 提供则拖拽到该画布坐标(默认 {x:200,y:200})
  hoverAdd?: boolean;   // 提供则改为 hover 后点击行内 "+" 按钮,而非拖拽
  addButtonSlug?: string; // 行内 "+" 按钮的 slug,默认由行 testId 反推
};

有两个实现细节很有参考价值:

  • 行 testId 到按钮 slug 的反向推导:生产 UI 中行内按钮使用 add-component-button-<slug> 这样的 testid(slug 由显示名经 convertTestName 生成)。helper 中的 rowTestIdToAddButtonSlug 用正则 /^([a-z_]+)([A-Z0-9].*)$/ 把形如 input_outputChat Output 的行 testId 拆出显示名(类别前缀全小写,显示名从第一个大写字母/数字开始),再套用与生产相同的 slug 规则(空格转连字符、转小写),得到 chat-output
  • strict-mode 安全的定位:侧边栏可能同时在多个行上渲染同一个 add-component-button-<slug>(例如 input_outputChat Inputsaved_componentsChat Input 共用 add-component-button-chat-input),顶层 page.getByTestId(...) 会触发 Playwright 严格模式报错。因此 helper 先定位目标行,再用 row.locator("xpath=..") 把按钮查询限定在该行的父容器内。

无论拖拽还是 hoverAdd,函数结束前都会断言画布节点数(.react-flow__node)恰好 +1,把"组件确实被加上"这一事实内建进 helper,而不是留给调用方各自检查。

sendPlaygroundMessage:统一发消息与构建等待

send-playground-message.ts 合并了套件中 7 处以上各自为政的实现(sendMessagesendMessageAndWaitsendAndWaitForResponse 等)。它支持两个维度:

type SendOpts = {
  surface?: "canvas" | "shareable"; // 常规 playground 面板 或 已发布(可分享)页面
  sendBy?: "button" | "enter";      // 点击发送按钮 或 按 Enter 键
};
  • canvas(默认):等待 data-testid 为 chat playground 输入框的元素出现后 fill 消息;shareable:改为按 placeholder 文本定位已发布页面的输入框。
  • 发送后统一以 Stop 按钮的出现再消失 作为"构建完成"的判据:先等 Stop 可见(30s 量级),再等它隐藏(120s 量级,见 utils/constants/timeouts.tsTIMEOUTS.standard / TIMEOUTS.buildComplete)。注释特别提到这些默认超时对齐了"最宽松的前身实现"(Windows CI 上 chat 输入框可见性 60s),以吸收不同平台的性能差异。

setupDeploymentMocks:基于 LIFO 的部署 API mock

deployment-mocks.ts 为部署(deployment)相关 spec 提供一套完整的 API 路由 mock 与 mock 数据:PROVIDER/PROVIDERS_MOCKDEPLOYMENT/DEPLOYMENTS_MOCKATTACHMENTS_MOCKLLMS_MOCKCONFIGS_MOCKFLOWS_MOCKFLOW_VERSIONS_MOCK,以及快照(snapshots)与运行(run)响应的多种形态(空快照、重名快照、deploying/running/completed 状态机)。

setupDeploymentMocks(page, folderId, snapshotsMock?, flowsMock?) 的注册顺序体现了 Playwright 路由的一个关键机制:路由按 LIFO(后注册先匹配)工作,因此它故意把宽泛的兜底路由 **/api/v1/deployments* 注册在最前,让随后注册的 **/api/v1/deployments/snapshots****/api/v1/deployments/providers****/api/v1/deployments/llms****/api/v1/deployments/configs** 等具体路由都能优先命中。几个值得留意的细节:

  • providers 路由按方法分流:GET 直接 fulfill,其他方法 route.continue() 透传到真实后端;
  • snapshots 路由在传入 SNAPSHOTS_DUPLICATE_MOCK 时会读取请求 URL 中的 names 参数并原样回显为"已存在工具",使重复名检查逻辑不受前端 tool name 命名格式影响;
  • flows 路由会把捕获到的 folderId 注入每条 mock flow 的 folder_id,让组件侧的文件夹过滤器通过;对 URL 含 /versions/ 的请求则改回 FLOW_VERSIONS_MOCK
  • mock 数据刻意保留 nameprovider_account_id 等旧字段(注释:avoid breaking any tests still reading them),与真实 API 的字段演进保持兼容。

fixtures.ts:自定义 page fixture 的隐式失败检测

所有 spec 统一从 tests/fixtures.ts 导入 testexpect,而不是直接来自 @playwright/test。这个封装通过 base.extend 重写了 page fixture,为每个测试附加了三类"环境级断言":

  1. API 5xx 错误契约(server-error contract)page.on("response") 监听所有 /api/ 响应,状态码 ≥ 500 的响应会被记入契约(4xx 则收集为 api-4xx-responses 附件用于诊断)。测试结束(teardown)时,如果出现未被预期的 5xx、或预期声明但未观察到的 5xx,测试确定性失败,并提示"Register intentional failures with page.expectServerError({ method, path, status, count })"——即故意触发错误响应的测试必须事先注册预期。底层实现在 utils/server-error-contract.mjs
  2. 未落定 API 请求检测:通过 request/response/requestfinished/requestfailed 四个事件追踪在途请求,测试边界处先冻结生命周期集合再 drain(API_REQUEST_DRAIN_TIMEOUT_MS = 2000),仍有未解决的 API 请求则报错。源码注释解释了为什么长连接流(SSE/轮询)按 content-type 提前落定,避免把合法长流误判为挂起请求。
  3. Flow 执行错误检测:对 200 状态码的 /events?event_delivery=/build//run/ 响应,在 bounded 超时内解析响应体(跳过 text/event-stream 等流类型),逐行 JSON 解析查找 build_data.paramsError 开头或 error: true 的结构,再用 NameError:/TypeError: 等 Python 异常模式兜底匹配。发现 flow 错误即收集,teardown 时若未调用 page.allowFlowErrors() 则使测试失败——这意味着构建/运行 flow 的测试默认对后端报错零容忍,预期出错的测试必须显式放行。

此外该 fixture 还提供 page.runA11yScan(label, options):仅在环境变量 RUN_A11Y=true 时真正执行 IBM accessibility-checker 扫描(配合 RUN_A11Y_ASSERT=true 断言无新增违规),扫描摘要会作为测试附件输出。这一开关正是 Makefile.frontendtest_frontend_a11y_scan / test_frontend_a11y_scan_blocking / test_frontend_a11y_scan_update 三个 target 的运行机制:先 grep 出所有含 runA11yScan( 的 spec 再以其参数启动 Playwright,并固定 --workers=1

最后一个细节:fixture 支持 LF_CPU_THROTTLE=<rate>(如 4)通过 CDP Emulation.setCPUThrottlingRate 对页面做 CPU 节流,注释说明这是为了在本地复现 Windows CI 慢 runner 上观察到的竞态条件。

标签(tag)规范与 CI 过滤

README 的第二条规则规定了 spec 的标签体系:

test("example", async ({ page }) => { ... }, { tag: ["@release", "@api"] });
  • 唯一允许的标签是且仅是这五个领域标签@release@workspace@api@components@starter-projects@database(README 原文列举),并且禁止发明新标签、禁止拼写错误——原文特别点名了 @starter-projectss 这类拼错示例。
  • 每条 spec 必须携带 @release:它是 release 构建时 grep 的标签,任何漏标或标错的 spec 都会静默掉出 release 覆盖范围,而不会报任何错误。领域标签叠加在 @release 之上,一条 spec 可以有多个标签。

这套约定在仓库中可以找到实际印证,例如 tests/a11y/api-keys.a11y.spec.ts 中每个用例都形如 { tag: ["@release", "@api"] }。从 CI 角度看,标签就是 Playwright 的 --grep @tag 过滤依据:release 流水线用 @release 圈定"每次发版必须全绿"的最小集合,其余标签(@workspace@api@components@starter-projects@database)则用于按领域切分日常 CI 的运行面,让不同触发条件只跑相关子集。

运行、过滤与覆盖率

make tests_frontend 外,还有几个与 E2E 直接相关的操作要点(均以 Makefile.frontend 为准):

make tests_frontend UI=true   # Playwright UI 调试模式
  • 指定单条 spec / 单个标签:Playwright 原生参数即可,例如在 src/frontendnpx playwright test tests/core/features/playground.spec.ts --project=chromium,或 npx playwright test --grep "@starter-projects" --project=chromium
  • 无障碍扫描(三档,对应不同的严格度):
    • make test_frontend_a11y_scanRUN_A11Y=true RUN_A11Y_ASSERT=false,只扫描收集、不阻断;
    • make test_frontend_a11y_scan_blockingRUN_A11Y_ASSERT=true,对照 tests/a11y/baselines/ 基线做阻断式断言;
    • make test_frontend_a11y_scan_update:重新生成 coverage/accessibility-reports 并拷贝 chromium__*.json 回基线目录;
    • make test_frontend_a11y_suite:整体跑 tests/a11y/ 目录并构建 HTML 报告与 job 摘要,worker 数可用 A11Y_WORKERS(默认 5)覆盖。
  • Jest + Playwright 合并覆盖率make test_frontend_coverage_full 会先后跑 Jest(--coverage)与 Playwright(CI=1),再用 nyc 把 coverage/playwright/individual-test 合并进 coverage/combined/、与 Jest 的 coverage-final.json 合并,最终产出 coverage/final-coverage-report 的 HTML 报告。Playwright 侧的采集由 tests/playwrightCoverage.ts 在 fixtures.ts 中被 import 时注册。
  • Jest 侧注意区分make test_frontendJest 单元测试入口,与 E2E 的 make tests_frontend 相差一个 "s",不要混用。

小结:新增一条 spec 的完整姿势

把 README 的两条规则落到实操,新增一条 Langflow 前端 E2E 测试应遵循:

  1. 文件放入对应目录(core/featuresextended/featurescore/integrations 等),从 tests/fixtures.ts 导入 test/expect,以获得 5xx 契约、flow 错误检测与 a11y 扫描能力;
  2. awaitBootstrapTest(page) 进入应用;需要模板则 openStarterProject(page, "模板名");加组件用 addComponentFromSidebar;发消息用 sendPlaygroundMessage;涉及部署页面则 setupDeploymentMocks(page, folderId, ...) 先行 mock;
  3. 定位优先使用 utils/constants/testIds.tsselectors.ts 中的常量,超时使用 utils/constants/timeouts.tsTIMEOUTS,不要在 spec 里散落魔法数字;
  4. 故意触发 4xx/5xx 或 flow 错误的场景,记得 page.expectServerError({...}) / page.allowFlowErrors()
  5. 选项参数写 { tag: ["@release", "@<领域标签>"] },标签必须来自五个允许集合、且必含 @release
  6. 依赖真实 LLM Provider 的用例放入 tests/live/(默认被 testIgnore 排除),需要 loopback 能力的用例则依赖配置里自动拉起的 mock OpenAI 服务,不要自行发明网络依赖。

遵循以上约定,新的 spec 才能被 release 覆盖稳定收集、被领域标签正确切分,并且不会给套件再添一份本可复用 helper 消除的样板。

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