编写自定义 Node
自定义 Node 将业务操作封装为用例作者可以在 YAML 中调用的步骤。本文介绍输入定义与校验、跨 Node 数据共享,以及执行与清理。
尚未创建测试项目时,请先阅读创建和使用测试项目。平台环境、Agent 接入和运行参数见配置测试项目。
- 注册 Node:定义操作、在 YAML 中调用,并通过生成 Node 说明书验证注册。
- Node 执行参数:区分业务参数与框架的步骤配置。
- 跨 Node 共享上下文:创建、读取和重置共享测试数据。
- 进阶用法:返回结果、处理错误与取消,以及清理资源。
注册自定义业务 Node
自定义 Node 从 YAML 接收参数并执行操作。下面以创建测 试用户数据为例,将用户信息写入 JSON 文件,供测试服务或数据导入步骤加载。
1. 定义并注册 Node
创建 midscene.config.ts:
name 是 YAML 中使用的操作名称。inputSchema 定义参数,execute({ input }) 接收校验后的参数值。将 Node 加入 nodes 数组后,用例就可以调用它。扩展已有项目时,将它追加到原有的 nodes 数组即可。
inputSchema 是可选字段,但定义后,Midscene Test 会在调用 execute() 前校验参数。示例中的 z.string().min(1) 要求姓名非空,.email() 校验邮箱格式,z.strictObject() 拒绝未知字段。输入不符合要求时,会抛出 NodeInputValidationError。
TypeScript 根据 schema 推导 input 的类型。.describe() 中的字段说明会出现在生成的 Node 说明书中。
2. 在 YAML 中调用
创建 cases/user.yaml:
Midscene Test 读取 YAML,找到名为 user.create 的 Node,并调用它的 execute()。此时 input 为 { name: "Alice", email: "alice@example.com" },Node 无需自行解析 YAML 文件。
示例使用 async execute({ input }) 和 await 等待文件写入。写入失败时会抛出错误,使 Node 执行失败。这里创建的是本地测试数据文件;需要在业务系统中创建用户时,将文件写入替换为测试数据接口或数据库调用即可。
3. 生成 Node 说明书,验证注册
保存配置后,生成 Node 说明书:
命令加载项目配置,并在当前目录生成 midscene-node-spec.md。显式配置 projects 时,会为每个执行项目生成 midscene-node-spec.<project-name>.md。检查说明书是否包含:
- 可用 Node 列表中的
user.create。 - “创建测试用户数据文件。”这一操作说明。
name、email两个输入字段及其描述。
这一步验证 Node 是否已注册、输入 schema 是否可以导出,不会执行 Node 或创建用户数据。修改 Node 定义或注册配置后,可以重新生成说明书,供用例作者和 AI Agent 查阅。完整使用方法见生成当前项目的 Node 说明书。
Node 执行参数
Midscene Test 调用 execute() 时,会传入包含本次执行参数和运行信息的对象。可以通过 execute({ input, $, context }) 这样的解构写法,直接取出需要的字段。
业务参数与步骤配置
input 包含 Node 的 inputSchema 定义的业务参数。$ 包含框架处理的步骤配置,例如超时时间和发生错误后是否继续执行。
例如,为前面的 user.create 调用添加步骤配置:
框架将 $ 与业务参数分开,并规范化其中的字段名。在 execute({ input, $ }) 中,可以读取到:
inputSchema 只需声明 name 和 email,input 中不包含 $。超时和出错后是否继续执行由框架控制。continue-on-error 允许当前阶段的后续步骤在失败后继续执行,但仍受资源安全限制约束,失败的步骤仍会使整个用例失败。
可用的执行字段
execute() 接收的参数对象包含以下常用字段:
input:从 YAML 传入并经过 Zod 校验后的业务参数。$:由 Midscene Test 控制的通用 Step 属性(如规范化后的timeoutMs和continueOnError)。signal:超时或运行取消时触发的AbortSignal,在异步请求或长耗时任务中使用它以响应取消。context:fileSetup返回的文件运行环境;未配置fileSetup时,使用项目setup的返回值。onTeardown():注册当前 Node 所创建资源的清理函数,支持 attempt 级别或 Document 级,按 LIFO(后进先出)顺序执行。scope:标记当前 Node 执行的上下文边界,值为case或document。case或document:当前执行位置的详细运行信息。
其中,context 用于访问运行资源和测试状态。下一节介绍如何通过它在多个 Node 之间共享数据。
跨 Node 共享上下文
Node 的每次执行都是独立调用,框架不会自动将上一次执行的结果传入下一次调用。当一个业务流程由多个 Node 完成时,它们可能需要使用同一份测试数据。例如,订单退款用例先创建订单,再打开该订单的退款页面,最后删除测试订单。下面沿着这份数据的使用过程,说明 Node 如何协作。
创建共享上下文
下面只用 fileSetup 为每份文件创建 Browser、页面和 context,供该文件的 Node 共用。没有共享资源,无需配置 setup;需要共享 Browser 时,见资源隔离示例。
导入的 orderService 是你自己的测试数据服务,需要实现 create 和 remove 方法。应用地址也需替换为测试环境地址。
各文件的 context 独立,orderId 由后续 Node 创建订单时写入。
在 Node 中写入数据
接着创建 nodes.ts。order.prepare 使用 context 中的订单服务创建订单,再将 ID 写入 context.orderId,供后续 Node 使用:
context.orderId = order.id 修改共享对象,使后续 Node 可以读取这个 ID。返回 data 不会自动将它写入 context。
创建订单后,onTeardown() 注册清理函数,在清理时删除本次创建的订单并清除保存的 ID。回调直接使用本次调用的 order.id,因此创建与清理封装在同一个 Node 中,YAML 无需额外调用清理步骤。
在后续 Node 中读取数据
browser.openRefundPage 读取保存的 ID,打开退款页面。如果订单 ID 不存在,这个 Node 会报错。
将 fileSetup 和 Node 一起注册,框架就会把当前文件的返回对象作为 context 传给各 Node:
在 YAML 中,通过 beforeEach 调用 order.prepare,然后在用例步骤中使用保存的订单 ID:
已注册的清理函数在本次用例的 afterEach 阶段之后执行,即使 YAML 没有声明 afterEach 步骤也会执行。后续准备步骤或用例步骤失败时,框架仍会执行已注册的清理。每次重试有独立的清理范围;如果创建订单失败、尚未注册回调,则本次调用没有对应的清理函数。
Agent 接入和平台资源配置见配置测试项目。
进阶用法
以下介绍 Node 内部的执行结果、错误处理、取消和资源清理。
记录执行结果
上面的订单准备 Node 还返回了 summary 和 data。summary 是供报告展示的执行摘要,data 保存结构化输出。两者均为可选字段,Node 也可以不返回结果。这些值保存在运行结果中,与共享的 context 相互独立。
异步操作、错误与取消
异步操作使用 async execute(),并通过 await 等待完成。操作失败时应抛出错误。
Midscene Test 在执行 Node 时通过 signal 提供 AbortSignal。调用支持取消的 API 时,将它传入,例如 fetch(url, { signal })。长时间运行的循环可以在每次迭代之间调用 signal.throwIfAborted(),以响应步骤超时或运行取消。
文件并发中,Node 或 fileSetup 确认项目共享资源失效后,必须显式报告 PROJECT_RESOURCE_ERROR,才会停止该项目的其他文件。可以直接抛出 @midscene/test 的 WorkflowError 并设置此 code,也可以将该错误放在 cause 或 AggregateError.errors 中。
Browser 可能由单个文件独占,也可能由项目共享,框架不会根据关闭报错的文案推断资源归属。未标明共享资源故障的 Browser 错误按文件错误处理。影响范围见失败时如何执行。
资源的生命周期与清理
Node 创建资源后,可以通过 onTeardown() 注册清理函数。清理时机取决于 Node 所在的执行阶段。
资源清理分为以下范围:
- 在
beforeEach、用例steps或afterEach的 Node 中注册的清理,在本次用例执行的afterEach之后运行。每次重试有独立的清理范围。 - 在
beforeAll或afterAll的 Node 中注册的清理,在当前文件的afterAll之后运行。
每个范围内的清理函数按注册顺序的逆序执行,即 LIFO(后进先出)。Node 可以借此释放自己创建的资源,或完成报告生成。注册清理不会自动创建新的 Agent 或重置缓存;这些行为取决于项目实现。
如果 Node 超时后仍未退出,注册的资源清理会延后,避免释放正在使用的资源。清理回调抛错后仍会尝试剩余回调;回调一直不结束时,后续回调会等待它结束。框架限制等待清理结果的时间,超过期限后返回 pending 快照并继续隔离资源,不会把它当作已清理完成。等待期限见超时与取消机制。
测试运行被中断时,框架仍会尝试执行 afterEach、afterAll 和通过 onTeardown() 注册的清理函数。取消机制的详细说明见超时与取消机制。
项目级浏览器和 Agent 的清理见使用 setup 创建和清理共享资源。

