Langflow 前端 E2E 测试套件详解:Playwright 配置、共享工具函数与标签规范
本篇基于仓库内 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.ts、testIds.ts、timeouts.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 运行所需的最小栈:
- loopback OpenAI 兼容服务:
node tests/fixtures/openai-compatible-server.mjs,健康检查地址http://127.0.0.1:8787/health,reuseExistingServer: true。它提供http://127.0.0.1:8787/v1的假 OpenAI API,让集成测试无需真实 API Key 即可跑通 LLM 调用链路。 - Langflow 后端:
uv run uvicorn --factory langflow.main:create_app --host localhost --port 7860 --loop asyncio --log-level error --no-access-log,等待 7860 端口就绪,启动超时设置为120 * 750ms。其环境变量为测试环境量身定制:LANGFLOW_DATABASE_URL=sqlite:///./temp——每次运行使用独立的临时 SQLite 库;LANGFLOW_AUTO_LOGIN=true配合LANGFLOW_SUPERUSER=langflow/LANGFLOW_SUPERUSER_PASSWORD=test-superuser-password——自动登录,免去 spec 逐个实现登录流程;LANGFLOW_DEACTIVATE_TRACING=true、LANGFLOW_LOG_LEVEL=ERROR、DO_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。
- 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 场景)、selectStarterTemplate 与 waitForFlowEditorReady——最后一步等待编辑器就绪,保证 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 Input与saved_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 处以上各自为政的实现(sendMessage、sendMessageAndWait、sendAndWaitForResponse 等)。它支持两个维度:
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.ts 中
TIMEOUTS.standard/TIMEOUTS.buildComplete)。注释特别提到这些默认超时对齐了"最宽松的前身实现"(Windows CI 上 chat 输入框可见性 60s),以吸收不同平台的性能差异。
setupDeploymentMocks:基于 LIFO 的部署 API mock
deployment-mocks.ts 为部署(deployment)相关 spec 提供一套完整的 API 路由 mock 与 mock 数据:PROVIDER/PROVIDERS_MOCK、DEPLOYMENT/DEPLOYMENTS_MOCK、ATTACHMENTS_MOCK、LLMS_MOCK、CONFIGS_MOCK、FLOWS_MOCK、FLOW_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 数据刻意保留
name、provider_account_id等旧字段(注释:avoid breaking any tests still reading them),与真实 API 的字段演进保持兼容。
fixtures.ts:自定义 page fixture 的隐式失败检测
所有 spec 统一从 tests/fixtures.ts 导入 test 与 expect,而不是直接来自 @playwright/test。这个封装通过 base.extend 重写了 page fixture,为每个测试附加了三类"环境级断言":
- API 5xx 错误契约(server-error contract):
page.on("response")监听所有/api/响应,状态码 ≥ 500 的响应会被记入契约(4xx 则收集为api-4xx-responses附件用于诊断)。测试结束(teardown)时,如果出现未被预期的 5xx、或预期声明但未观察到的 5xx,测试确定性失败,并提示"Register intentional failures withpage.expectServerError({ method, path, status, count })"——即故意触发错误响应的测试必须事先注册预期。底层实现在 utils/server-error-contract.mjs。 - 未落定 API 请求检测:通过
request/response/requestfinished/requestfailed四个事件追踪在途请求,测试边界处先冻结生命周期集合再 drain(API_REQUEST_DRAIN_TIMEOUT_MS = 2000),仍有未解决的 API 请求则报错。源码注释解释了为什么长连接流(SSE/轮询)按content-type提前落定,避免把合法长流误判为挂起请求。 - Flow 执行错误检测:对 200 状态码的
/events?event_delivery=、/build/、/run/响应,在 bounded 超时内解析响应体(跳过text/event-stream等流类型),逐行 JSON 解析查找build_data.params以Error开头或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.frontend 中 test_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/frontend下npx playwright test tests/core/features/playground.spec.ts --project=chromium,或npx playwright test --grep "@starter-projects" --project=chromium。 - 无障碍扫描(三档,对应不同的严格度):
make test_frontend_a11y_scan:RUN_A11Y=true RUN_A11Y_ASSERT=false,只扫描收集、不阻断;make test_frontend_a11y_scan_blocking:RUN_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_frontend是 Jest 单元测试入口,与 E2E 的make tests_frontend相差一个 "s",不要混用。
小结:新增一条 spec 的完整姿势
把 README 的两条规则落到实操,新增一条 Langflow 前端 E2E 测试应遵循:
- 文件放入对应目录(
core/features、extended/features、core/integrations等),从 tests/fixtures.ts 导入test/expect,以获得 5xx 契约、flow 错误检测与 a11y 扫描能力; - 用
awaitBootstrapTest(page)进入应用;需要模板则openStarterProject(page, "模板名");加组件用addComponentFromSidebar;发消息用sendPlaygroundMessage;涉及部署页面则setupDeploymentMocks(page, folderId, ...)先行 mock; - 定位优先使用 utils/constants/testIds.ts 与 selectors.ts 中的常量,超时使用 utils/constants/timeouts.ts 的
TIMEOUTS,不要在 spec 里散落魔法数字; - 故意触发 4xx/5xx 或 flow 错误的场景,记得
page.expectServerError({...})/page.allowFlowErrors(); - 选项参数写
{ tag: ["@release", "@<领域标签>"] },标签必须来自五个允许集合、且必含@release; - 依赖真实 LLM Provider 的用例放入
tests/live/(默认被testIgnore排除),需要 loopback 能力的用例则依赖配置里自动拉起的 mock OpenAI 服务,不要自行发明网络依赖。
遵循以上约定,新的 spec 才能被 release 覆盖稳定收集、被领域标签正确切分,并且不会给套件再添一份本可复用 helper 消除的样板。
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 StartedRust0623
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