首页
/ Langflow 前端 E2E 测试实践:Playwright 配置、自定义 Fixtures 与 data-testid 选择器体系

Langflow 前端 E2E 测试实践:Playwright 配置、自定义 Fixtures 与 data-testid 选择器体系

2026-09-05 11:03:25作者:苗圣禹Peter

Langflow 的前端 E2E 测试体系基于 Playwright 构建,核心是一套"自动错误感知"的自定义 fixture、以 data-testid 为主的选择器目录,以及覆盖画布操作、Playground 会话、模板流程等完整用户路径的 spec 组织方式。本文以仓库中的 E2E 测试技能文档 SKILL.md 为骨架,结合 playwright.config.tsfixtures.ts 等源码逐一展开,读完你可以掌握:如何运行、过滤和调试 Langflow 的 E2E 测试,如何编写符合规范的 spec(标签、fixture 导入、选择器策略),以及底层响应监控、错误契约、teardown 清理的实现原理。

一、适用范围与技术栈

这套技能文档明确划定了 E2E 测试的适用边界(引自 SKILL.md 的 "When to Apply" 一节):

  • 为功能或流程编写 E2E 测试时适用;
  • 修复失败的 E2E 测试时适用;
  • 评审 E2E 测试覆盖度时适用;
  • 修改组件中的 data-testid 属性时适用(可能破坏既有测试);
  • 修改 src/frontend/tests/utils/ 下的测试工具时适用。

不适用的场景:单元测试(Jest,前端 *.test.tsx 文件)和后端测试(pytest)。

技能文档给出的技术栈如下表。需要说明的是,版本号以当前仓库实际依赖为准:

工具 技能文档记录 当前仓库实际 用途
Playwright 1.59.1 1.60.0 E2E 测试运行器 + 浏览器自动化
Chromium (内置) (内置) 默认浏览器,Firefox/Safari 被禁用
自定义 fixtures tests/fixtures.ts 同左 自动检测 API 错误与 flow 执行失败

当前 package.json"@playwright/test": "1.60.0",且 devDependencies 中的 playwright 同为 1.60.0。配置文件中 Firefox/Safari 项目均被注释掉,只启用 Chromium 项目。

二、运行命令与 WebServer 自启动机制

2.1 常用命令

# 运行全部 E2E 测试
npx playwright test

# 按标签过滤运行
npx playwright test --grep "@release"
npx playwright test --grep "@workspace"
npx playwright test --grep "@starter-projects"

# 运行单个测试文件
npx playwright test tests/core/features/run-flow.spec.ts

# 调试模式(headed 浏览器 + 单步)
npx playwright test --debug

# 查看 HTML 报告
npx playwright show-report

# 更新快照(如有使用)
npx playwright test --update-snapshots

仓库还提供了一键脚本 run-tests.sh:它负责安装 Playwright 浏览器、通过 make frontend 启动前端、用 poetry run langflow run --backend-only --port 7860 启动后端(设置 LANGFLOW_DATABASE_URL=sqlite:///./tempLANGFLOW_AUTO_LOGIN=True),最后执行 npx playwright test tests/core --project=chromium,并在退出时通过 trap 清理 7860/3000 端口与临时数据库。tests/README.md 则建议日常开发用 make tests_frontend 入口运行。

2.2 Playwright 自动拉起的服务栈

playwright.config.tswebServer 数组看,Playwright 会按顺序自动启动三个服务:

  1. OpenAI 兼容 mock 服务node tests/fixtures/openai-compatible-server.mjs,健康检查地址 http://127.0.0.1:8787/health。这使得 E2E 测试中的 LLM 调用不依赖真实 OpenAI 凭证。

  2. 后端 API(uvicorn,7860 端口)

    uv run uvicorn --factory langflow.main:create_app \
      --host localhost --port 7860 --loop asyncio \
      --log-level error --no-access-log
    

    注入的关键环境变量(见配置文件 L124-L137):

    环境变量 作用
    LANGFLOW_DATABASE_URL sqlite:///./temp 使用临时 SQLite 数据库,测试后清理
    LANGFLOW_AUTO_LOGIN true 跳过登录页,配合 awaitBootstrapTest
    LANGFLOW_SUPERUSER / LANGFLOW_SUPERUSER_PASSWORD langflow / 测试口令 提供固定超级用户
    LANGFLOW_DEACTIVATE_TRACING true 关闭追踪,减少噪声
    LANGFLOW_LOG_LEVEL ERROR 降低日志量
    OPENAI_API_KEY / OPENAI_BASE_URL 本地回环 key / http://127.0.0.1:8787/v1 把模型调用指向上面的 mock 服务
    LANGFLOW_A2A_ENABLED true 暴露 A2A 发现 + JSON-RPC 端点,供 Agent 标签页测试发布并调用真实 agent

    后端启动超时设为 120 * 750 毫秒量级的宽裕值(timeout: 120 * 750),并允许复用已存在的服务器(reuseExistingServer: true),避免重复冷启动。

  3. 前端(npm start,即 Vite,3000 端口):通过 VITE_PROXY_TARGET=http://localhost:7860/api 请求代理到后端。

2.3 关键配置项

来自 playwright.config.ts 的实际取值:

配置 说明
fullyParallel true 测试文件并行执行
timeout 5 * 60 * 1000(5 分钟) Flow 构建可能较慢,防止误报超时
retries 1 当前配置固定 1 次重试(技能文档曾记录为本地 3 次/CI 2 次,以当前配置为准)
workers 2 平衡速度与资源占用
actionTimeout 20000(20 秒) 单个 action(click、fill 等)的超时
trace on-first-retry 首次重试时采集 trace,便于事后调试
baseURL http://localhost:${PORT || 3000}/ 指向 Vite dev server
forbidOnly CI 时强制开启 防止 test.only 被意外提交
testIgnore **/live/** 排除 live 目录(连接真实外部服务的测试)
reporter CI 用 blob;本地用 list + html 本地报告输出到 playwright-report/

Chromium 项目额外授予了 clipboard-read / clipboard-write 权限,用于覆盖剪贴板相关交互。

三、目录结构与文件命名

src/frontend/tests/
├── fixtures.ts                     # 自定义 fixture:错误检测 + a11y 扫描钩子
├── globalTeardown.ts               # 收尾:删除临时数据库
├── a11y/                            # 可访问性扫描辅助
├── assets/                          # 测试用文件资源(uploadFile 的来源目录)
├── live/                            # 连接真实外部服务的测试(默认被忽略)
├── core/
│   ├── features/                   # 主功能测试(run-flow、playground 等)
│   ├── integrations/               # Starter project / 模板测试
│   ├── regression/                 # Bug 回归测试
│   └── unit/                       # 组件级 Playwright 测试
└── utils/                           # 37+ 个共享辅助函数

extended/ 目录下平行组织 features/integrations/regression/ 三类"扩展"测试(MCP、auto-save 等较新特性)。

文件命名约定:

  • kebab-case + .spec.ts 后缀:run-flow.spec.tsplayground.spec.tsflow-lock.spec.ts
  • 模板测试允许带空格的文件名:Document QA.spec.tsSocial Media Agent.spec.ts
  • 分片并行测试:chatInputOutputUser-shard-0.spec.ts
  • E2E 用 .spec.ts(Playwright 约定),单元测试用 .test.tsx(Jest 约定),两者不可混用

四、测试编写范式(Test Anatomy)

以下四种范式直接继承自 SKILL.md,并标注了当前源码状态。

4.1 基础测试

import { expect, test } from "../../fixtures";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";

test(
  "user should be able to run a flow successfully",
  { tag: ["@release", "@workspace"] },
  async ({ page }) => {
    await awaitBootstrapTest(page);

    // Arrange: 创建 flow
    await page.getByTestId("blank-flow").click();

    // Act: 添加组件并运行
    await page.getByTestId("sidebar-search-input").fill("Chat Output");
    // ... setup ...

    // Assert: 验证构建成功
    await expect(page.getByTestId("build-status-success")).toBeVisible({ timeout: 30000 });
  },
);

awaitBootstrapTest 是当前每个测试的强制入口。从 await-bootstrap-test.ts 的源码看,它做了三件事:page.goto("/") 打开应用,等待 [data-testid="mainpage_title"] 出现(30 秒超时,seedFlowIfEmpty 默认开启,为空工作区播种 flow),然后打开模板选择弹窗(可用 skipModal: true 跳过)。它存在的原因是:没有这一步,测试会与前端初始化竞争——组件可能未渲染、store 可能未水合、API 调用可能未完成。技能文档中几乎每条"元素找不到"的偶发失败都归因于缺少这一步。

4.2 使用 test.describe 组织

test.describe("Flow Lock Feature", () => {
  test(
    "should lock and unlock a flow",
    { tag: ["@release", "@api"] },
    async ({ page }) => { /* ... */ },
  );

  test(
    "should prevent editing when locked",
    { tag: ["@release"] },
    async ({ page }) => { /* ... */ },
  );
});

4.3 串行模式(依赖顺序的测试)

test.describe.configure({ mode: "serial" });

test("step 1: create flow", async ({ page }) => { /* ... */ });
test("step 2: edit flow", async ({ page }) => { /* ... */ });
test("step 3: delete flow", async ({ page }) => { /* ... */ });

4.4 事件投递模式包装器(注意当前实现已简化)

技能文档描述 withEventDeliveryModes 会把测试在 streaming / polling / direct 三种事件投递模式下各跑一遍(通过拦截 /api/v1/config 路由自动配置)。但阅读当前源码 withEventDeliveryModes.ts 可以看到,该包装器现在是一个兼容性 no-op shim:其注释明确说明 "v2 workflows endpoint replaced the three modes with a single AG-UI SSE path",即 v2 workflows 端点已把三种投递模式统一为单一的 AG-UI SSE 通道,包装器目前只注册一次测试,保留签名以兼容所有既有调用点:

export function withEventDeliveryModes(
  title: string,
  config: TestConfig,
  testFn: TestFunction,
) {
  test(title, config, async ({ page }) => {
    await testFn({ page });
  });
}

因此从源码结构看,新测试继续按原有签名调用它不会出错,但不要再依赖它产生 3 倍用例;三种模式的差异覆盖逻辑已由后端统一的 SSE 通道取代。

五、标签(Tag)体系

tests/README.md 与技能文档一致地规定:每个测试必须携带 @release 标签——release 运行按该标签 grep 过滤,未打标签或标签拼错的 spec 会静默地从发布覆盖中消失(文档特别提醒:是 @starter-projects,不是 @starter-projectss)。除此之外只允许以下六个标签,不得自创:

标签 用途 适用场景
@release 属于发布运行(每个 spec 必需) 所有测试
@workspace 工作区/flow 管理 创建、编辑、删除 flow
@api 依赖 API 的功能 调用后端端点的测试
@database 数据库操作 涉及持久化的测试
@components 组件级测试 单个组件的行为
@starter-projects 模板/起步项目测试 预置 flow 模板
// 正确:打标签
test("my feature test", { tag: ["@release", "@workspace"] }, async ({ page }) => { ... });

// 错误:无标签,无法被过滤
test("my feature test", async ({ page }) => { ... });

六、自定义 Fixture:自动错误感知

这是 Langflow E2E 体系最有价值的部分。所有 spec 必须从 ../../fixtures 导入 testexpect,而不是从 @playwright/test 导入——后者会绕过全部错误检测:

// 正确
import { expect, test } from "../../fixtures";

// 错误——绕过错误检测,可能产生"静默通过"
import { expect, test } from "@playwright/test";

6.1 为什么需要它

没有自定义 fixture 时,测试可能在以下情况下依然"通过":

  • 后端返回 500,但测试只检查了 UI 文案;
  • flow 构建因 Python 异常静默失败,但测试只检查了按钮状态;
  • 资源被删除后 API 返回 404,测试根本没检查响应。

6.2 检测行为(对照 fixtures.ts 源码)

fixtures.ts 通过 base.extend 重写了 page fixture(L86-L525),在每个测试的 page 生命周期内挂接 request / response / requestfinished / requestfailed 监听器,核心逻辑 inspectResponse(L186-L335)分两条线工作:

(1)HTTP 状态码监控:凡是 URL 包含 /api/ 且状态码 ≥ 400 的响应都会被记录。4xx 进入 clientErrors 列表(附 method/path/status/脱敏后的响应体)并在测试结束后作为 api-4xx-responses 附件写入报告;5xx 则进入"服务端错误契约"(server error contract,实现见 server-error-contract.mjs):

状态码 含义 失败原因
400 Bad Request 客户端发送了非法数据——通常是前端 bug
404 Not Found 资源不存在——通常是过期 ID 或缺少前置步骤
422 Validation Error Pydantic 校验失败——通常是 schema 不匹配
500 Internal Server Error 后端崩溃——永远是 bug

未声明的 5xx、以及已声明但未观察到的 5xx(例如用 page.expectServerError({ method, path, status, count }) 注册了预期却未发生)都会让 fixture 在 teardown 阶段抛出 Server-error contract failed 错误,并列出每一笔未匹配项。

(2)流式/执行类响应的错误解析:对状态为 200 且路径包含 /events?event_delivery=/build//run/ 的响应,fixture 先排除 text/event-streamapplication/grpcapplication/octet-streamapplication/x-ndjson 等"真流式"内容(这些由别的机制跟踪),然后对有限响应体逐行尝试 JSON 解析,命中以下任一情况即记为 flow 执行错误:

  • json.data.build_data.paramsError 开头(构建错误);
  • json.data.error === truejson.error === true(记录 error_message);
  • 原始文本匹配 Python 异常模式:NameError:TypeError:ValueError:AttributeError:ImportError:KeyError:An error occured ...

响应体读取有 2 秒超时RESPONSE_BODY_READ_TIMEOUT_MS = 2000,见 L42-L44):流式响应仍在写入时读取会超时,此时跳过 body 解析并打警告,避免 fixture 被长流卡死。teardown 阶段还有 2 秒的"在途请求排空"窗口(API_REQUEST_DRAIN_TIMEOUT_MS),确保跟随请求(如 404 后的重试)的响应仍可被观察到。

6.3 允许预期错误

测试 error handling 本身时需要显式放行。fixture 在 page 上注入 allowFlowErrors 方法(L128-L130):

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

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

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

注意边界:allowFlowErrors() 只抑制 flow 执行错误,HTTP 5xx 仍然会失败——服务端崩溃不属于"预期行为"。

错误报告是聚合式的:测试函数结束后,fixture 把收集到的错误拼成描述性消息抛出,形如:

Test failed due to 2 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.

这样开发者不必在每个测试里手写响应断言就能定位根因。

6.4 附带能力:a11y 扫描与 CPU 节流

同一 fixture 还提供两个源码级能力:

  • 可访问性扫描钩子 page.runA11yScan(label, options?):由 RUN_A11Y=true 开启(使用 accessibility-checker 包),RUN_A11Y_ASSERT=true 时进一步断言新增违规数为 0,扫描摘要会作为附件写入报告(L139-L183)。配套聚合脚本见 npm run a11y:report / a11y:html-report / a11y:job-summary
  • CPU 节流LF_CPU_THROTTLE=<rate> 环境变量可让 Chromium 通过 CDP Emulation.setCPUThrottlingRate 降速,用于复现慢机器(如 Windows CI)上的竞态条件(L76-L108)。

七、选择器策略与 data-testid 目录

7.1 优先级

  1. getByTestId — 最稳定,占 Langflow E2E 使用量的绝大多数;
  2. getByRole — 按钮、标题、表单元素;
  3. getByText — 可见文本内容;
  4. waitForSelector — CSS 选择器与动态元素;
  5. locator — 复杂选择器(CSS、XPath)。

7.2 data-testid 命名约定

完整目录见 selectors.md。核心前缀约定(kebab-case + 类型前缀):

前缀 元素类型 示例
input- 文本输入 input-chat-playgroundinput-flow-name
button- / button_ 动作按钮 button-sendbutton_run_chat output
icon- 图标按钮 icon-Globeicon-Lockicon-ChevronLeft
popover-anchor-input- 组件参数字段 popover-anchor-input-openai_api_key
add-component-button- 拖拽添加按钮 add-component-button-chat-output
card- flow/组件卡片 card-my-flow-name
title- 画布节点标题 title-OpenAItitle-Chat Output
handle- 连接 handle handle-{component}-{shownode}-{field}-{direction}
show 字段可见性开关 showmodel_nameshowtemperature

常用选择器速查:

画布与导航blank-flow(新建空白 flow)、sidebar-search-input(组件搜索)、canvas_controls_dropdown(画布控制菜单)、fit_view / zoom_out / zoom_inreact-flow-id(ReactFlow 容器,拖拽目标)、inspector-toggle

组件字段popover-anchor-input-{fieldname}(参数输入框,字段名与 name 属性一致)、input-chat-playgrounddiv-chat-message

动作add-component-button-{component}button-sendbutton_run_{component}publish-buttonsave-flow-buttonedit-fields-buttonplayground-btn-flow-io(需用 dispatchEvent("click") 关闭)。

弹窗与面板modal-titleicon-Globe(全局变量)、icon-Lock(flow 锁)、session-selectorlock-flow-switchinput-flow-nameinput-flow-description

7.3 重要陷阱:全局变量下的 badge 模式

当组件字段选中了全局变量(load_from_db: true + value: "OPENAI_API_KEY")时,字段渲染的是 badge 而不是 <input>——此时 getByTestId("popover-anchor-input-api_key") 找不到元素,因为它根本不在 DOM 中。预置了全局变量的模板:Market Research、Price Deal Finder、Research Agent;未预置(输入框正常渲染)的模板:Instagram Copywriter。这条规则同时解释了为什么 addOpenAiInputKey(page) 辅助函数在 badge 模式下会失效。

新增 data-testid 的判据(三条同时满足才加):E2E 需要交互该元素、没有稳定的 role/text 替代、元素是动态渲染需要稳定锚点。格式为 {type}-{descriptive-name},禁止 btn1inputwrapper 这类无信息量命名。

八、核心辅助函数(tests/utils/)

共享辅助函数位于 src/frontend/tests/utils/(37+ 个,逐函数详解见 helpers.md)。技能文档给出的速查表:

函数 作用 使用时机
awaitBootstrapTest(page) 等待应用完全加载 每个测试开头必调
initialGPTsetup(page) 完整流水线:adjustView → updateComponents → selectModel → addKey → adjustView → unselectNodes 需要 OpenAI 配置的测试
adjustScreenView(page, opts?) 适配视图 + 缩小 向画布添加组件之后
zoomOut(page, times) 缩小 N 次 组件过小点不中时
selectGptModel(page) 为所有 Language Model 节点选 gpt-4o-mini 依赖 GPT 的测试
addOpenAiInputKey(page) 为所有 openai_api_key 字段填入 OPENAI_API_KEY 需要 API key 的测试
enableInspectPanel(page) 打开检查面板 必须在 edit-fields-button 之前调用
disableInspectPanel(page) 关闭检查面板 清理阶段
updateOldComponents(page) 存在过期组件时点击 "Update all" 加载已保存 flow 之后
unselectNodes(page) 点击空白画布取消所有选中 节点操作之后
renameFlow(page, { flowName }) 重命名当前 flow flow 管理测试
uploadFile(page, filename) 从测试资源目录上传文件 文件上传测试
withEventDeliveryModes(...) 事件投递模式包装(当前为 no-op shim,见 4.4 节) 既有 starter project 测试

tests/README.md 额外强调了一个评审惯例:新 spec 必须复用共享辅助函数awaitBootstrapTestopenStarterProjectaddComponentFromSidebarsendPlaygroundMessagesetupDeploymentMocks),手抄 bootstrap / 侧边栏拖拽 / 模板 / Playground / 路由 mock 代码块是套件中样板代码的最大来源,评审时应直接指出。

8.1 initialGPTsetup 的可选步骤

await initialGPTsetup(page);  // 执行全部步骤

await initialGPTsetup(page, {
  skipAdjustScreenView: true,
  skipUpdateOldComponents: true,
  skipSelectGptModel: true,
});

其六步顺序有讲究:先更新过期组件再选模型,可避免模型下拉框停留在旧组件定义的陈旧列表;选 gpt-4o-mini 则是兼顾低成本与快速响应。

8.2 检查面板模式(关键约束)

// 必须先启用检查面板
await enableInspectPanel(page);

// 点击节点选中
await page.getByTestId("title-OpenAI").click();

// 打开字段编辑器
await page.getByTestId("edit-fields-button").click();

// 切换字段可见性
await page.getByTestId("showmodel_name").click();

// 关闭字段编辑器
await page.getByTestId("edit-fields-button").click();

若跳过 enableInspectPanel(page)edit-fields-button 根本不会出现在 DOM 中——检查面板未开启时该按钮不存在,这是新写测试最常见的失败点之一。

九、跳过测试与清理

// 环境变量缺失时跳过
test.skip(!process?.env?.OPENAI_API_KEY, "OPENAI_API_KEY required to run this test");

// 无条件跳过(必须给出原因)
test.skip(true, "Feature not yet implemented with new designs");

收尾清理由 globalTeardown.ts 完成:所有测试结束后删除 src/frontend/temp 临时数据库目录。源码显示它针对 Windows 做了专门处理——uvicorn 进程可能仍持有 SQLite 文件句柄,POSIX 允许删除打开中的文件而 Win32 不允许(表现为 EBUSY/EPERM),因此 teardown 采用"指数退避重试 5 次 → 逐文件兜底删除 → 永不抛错"的策略,最坏情况下留下目录交由运行器工作区清理。

十、编写高质量 E2E 测试的规范

应做(Do)

  • 每个测试都打 @release 标签(外加适用的领域标签);
  • ../../fixtures 导入,不从 @playwright/test 导入;
  • awaitBootstrapTest(page) 开头——永远如此;
  • getByTestId 获取稳定选择器;
  • 为异步操作设置显式超时waitForSelectorexpect(...).toBeVisible() 都要带 timeout
  • 测试完整用户路径:setup → action → verification;
  • 涉及 flow 执行(chat、build)的测试沿用 withEventDeliveryModes 调用约定(当前为单次执行,见 4.4 节)。

不应做(Don't)

  • 不要用 page.waitForTimeout()(除非绝对必要)——优先 waitForSelectorexpect().toBeVisible()
  • 不要硬编码 API key——从 process.env.OPENAI_API_KEY 读取(webServer 配置已注入本地回环 key);
  • 不要无理由跳过测试——test.skip() 的第二个参数永远必填;
  • 不要从 @playwright/test 导入——使用自定义 fixtures;
  • 不要忘 enableInspectPanel(page)(访问 edit-fields-button 前);
  • 不要假设输入框存在——全局变量选中时渲染的是 badge。

对抗性场景(Challenge Tests)

E2E 测试同样应覆盖对抗性输入,技能文档列出的五类:

  • 非法输入:粘贴 1 万字符、特殊字符(<script>alert(1)</script>)、空提交;
  • 网络中断:构建中途断连时的表现;
  • 权限边界:用户能否通过直接 URL 访问他人 flow;
  • 并发操作:双击删除、快速连发消息;
  • 错误恢复:500 错误后 UI 是否能优雅恢复(注意:自定义 fixture 会自动把未声明的 500 判为失败,这类测试需配合 expectServerError 声明预期)。

十一、延伸阅读(仓库内参考文件)

文件 内容
SKILL.md 本文的技能文档骨架:适用场景、命令、标签、规范
selectors.md 完整 data-testid 目录与命名规范
helpers.md 37+ 辅助函数的逐函数文档与"何时新建辅助函数"准则
fixtures.md 自定义 fixture 错误检测行为详解
playwright.config.ts 运行配置与 webServer 三服务自启动
fixtures.ts 响应监控、错误契约、a11y 钩子源码
globalTeardown.ts 临时数据库清理与 Windows 兼容处理
tests/README.md 辅助函数复用评审惯例与标签规则
run-tests.sh 一键启动前后端并跑 core 套件的脚本

整体来看,Langflow 的 E2E 体系把"测试失败原因"从人工排查变成了契约化自动检测:标签决定发布覆盖面,fixture 决定静默失败无处藏身,选择器目录决定测试的长期稳定性,而 webServer 配置 + 回环 mock 服务则保证测试栈在无需真实 LLM 凭证的前提下完整可复现。

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