PageSourceSearch

https://midscenejs.com/static/js/async/route-539feb17cdaf.ec71d2ab24.js

js midscenejs.com collected 2026-10-03 23:35:05 UTC 76,674 bytes, 1 lines download raw bytes

1"use strict";(self.rspackChunkdoc=self.rspackChunkdoc||[]).push([[6348],{28316(s,e,n){n.r(e);var r=n(65723),o=n(11470);function i(s){let e={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",span:"span",ul:"ul",...(0,o.R)(),...s.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(e.h1,{id:"编写自定义-node",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#编写自定义-node",children:"#"}),"编写自定义 Node"]}),"\n",(0,r.jsx)(e.p,{children:"自定义 Node 将业务操作封装为用例作者可以在 YAML 中调用的步骤。本文介绍输入定义与校验、跨 Node 数据共享,以及执行与清理。"}),"\n",(0,r.jsxs)(e.p,{children:["尚未创建测试项目时,请先阅读",(0,r.jsx)(e.a,{href:"/zh/midscene-test/use.html",children:"创建和使用测试项目"}),"。平台环境、Agent 接入和运行参数见",(0,r.jsx)(e.a,{href:"/zh/midscene-test/configuration.html",children:"配置测试项目"}),"。"]}),"\n",(0,r.jsxs)(e.ul,{children:["\n",(0,r.jsxs)(e.li,{children:[(0,r.jsx)(e.a,{href:"#%E6%B3%A8%E5%86%8C%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%9A%E5%8A%A1-node",children:"注册 Node"}),":定义操作、在 YAML 中调用,并通过生成 Node 说明书验证注册。"]}),"\n",(0,r.jsxs)(e.li,{children:[(0,r.jsx)(e.a,{href:"#node-%E6%89%A7%E8%A1%8C%E5%8F%82%E6%95%B0",children:"Node 执行参数"}),":区分业务参数与框架的步骤配置。"]}),"\n",(0,r.jsxs)(e.li,{children:[(0,r.jsx)(e.a,{href:"#%E8%B7%A8-node-%E5%85%B1%E4%BA%AB%E4%B8%8A%E4%B8%8B%E6%96%87",children:"跨 Node 共享上下文"}),":创建、读取和重置共享测试数据。"]}),"\n",(0,r.jsxs)(e.li,{children:[(0,r.jsx)(e.a,{href:"#%E8%BF%9B%E9%98%B6%E7%94%A8%E6%B3%95",children:"进阶用法"}),":返回结果、处理错误与取消,以及清理资源。"]}),"\n"]}),"\n",(0,r.jsxs)(e.h2,{id:"注册自定义业务-node",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#注册自定义业务-node",children:"#"}),"注册自定义业务 Node"]}),"\n",(0,r.jsx)(e.p,{children:"自定义 Node 从 YAML 接收参数并执行操作。下面以创建测试用户数据为例,将用户信息写入 JSON 文件,供测试服务或数据导入步骤加载。"}),"\n",(0,r.jsxs)(e.h3,{id:"1-定义并注册-node",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#1-定义并注册-node",children:"#"}),"1. 定义并注册 Node"]}),"\n",(0,r.jsxs)(e.p,{children:["创建 ",(0,r.jsx)(e.code,{children:"midscene.config.ts"}),":"]}),"\n",(0,r.jsx)(r.Fragment,{children:(0,r.jsx)(e.pre,{className:"shiki css-variables",style:{backgroundColor:"var(--shiki-background)",color:"var(--shiki-foreground)"},tabIndex:"0",lang:"ts",children:(0,r.jsxs)(e.code,{children:[(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"import"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { randomUUID } "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"from"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" 'node:crypto'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"import"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { mkdir"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" writeFile } "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"from"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" 'node:fs/promises'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"import"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { defineNode"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" z } "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"from"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" '@midscene/test'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"import"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { defineTestProject } "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"from"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" '@midscene/test/config'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsx)(e.span,{className:"line"}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" createUser"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" defineNode"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"({"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  name"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" 'user.create'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  description"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" '创建测试用户数据文件。'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  inputSchema"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" z"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".strictObject"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"({"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"    name"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" z"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".string"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"}
1,children:"()"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".min"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"("}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"1"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:")"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".describe"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"("}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:"'测试用户姓名。'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:")"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"    email"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" z"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".string"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"()"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".email"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"()"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".describe"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"("}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:"'测试用户邮箱。'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:")"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  })"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"  async"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" execute"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"({ input }) {"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" user"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { id"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" randomUUID"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"()"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" name"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" input"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".name"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" email"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" input"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".email };"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" filePath"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" `fixtures/users/"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"${"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"user"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".id"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"}"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:".json`"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    await"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" mkdir"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"("}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:"'fixtures/users'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { recursive"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" true"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" });"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    await"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" writeFile"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"(filePath"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" JSON"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".stringify"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"(user"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" null"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" 2"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"));"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  }"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"});"})}),"\n",(0,r.jsx)(e.span,{className:"line"}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"export"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" default"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" defineTestProject"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"({"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  nodes"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" [createUser]"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"});"})})]})})}),"\n",(0,r.jsxs)(e.p,{children:[(0,r.jsx)(e.code,{children:"name"})," 是 YAML 中使用的操作名称。",(0,r.jsx)(e.code,{children:"inputSchema"})," 定义参数,",(0,r.jsx)(e.code,{children:"execute({ input })"})," 接收校验后的参数值。将 Node 加入 ",(0,r.jsx)(e.code,{children:"nodes"})," 数组后,用例就可以调用它。扩展已有项目时,将
1它追加到原有的 ",(0,r.jsx)(e.code,{children:"nodes"})," 数组即可。"]}),"\n",(0,r.jsxs)(e.p,{children:[(0,r.jsx)(e.code,{children:"inputSchema"})," 是可选字段,但定义后,Midscene Test 会在调用 ",(0,r.jsx)(e.code,{children:"execute()"})," 前校验参数。示例中的 ",(0,r.jsx)(e.code,{children:"z.string().min(1)"})," 要求姓名非空,",(0,r.jsx)(e.code,{children:".email()"})," 校验邮箱格式,",(0,r.jsx)(e.code,{children:"z.strictObject()"})," 拒绝未知字段。输入不符合要求时,会抛出 ",(0,r.jsx)(e.code,{children:"NodeInputValidationError"}),"。"]}),"\n",(0,r.jsxs)(e.p,{children:["TypeScript 根据 schema 推导 ",(0,r.jsx)(e.code,{children:"input"})," 的类型。",(0,r.jsx)(e.code,{children:".describe()"})," 中的字段说明会出现在生成的 Node 说明书中。"]}),"\n",(0,r.jsxs)(e.h3,{id:"2-在-yaml-中调用",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#2-在-yaml-中调用",children:"#"}),"2. 在 YAML 中调用"]}),"\n",(0,r.jsxs)(e.p,{children:["创建 ",(0,r.jsx)(e.code,{children:"cases/user.yaml"}),":"]}),"\n",(0,r.jsx)(r.Fragment,{children:(0,r.jsx)(e.pre,{className:"shiki css-variables",style:{backgroundColor:"var(--shiki-background)",color:"var(--shiki-foreground)"},tabIndex:"0",lang:"yaml",children:(0,r.jsxs)(e.code,{children:[(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"cases"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  - "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"name"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" 创建测试用户数据"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    steps"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"      - "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"user.create"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"          name"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" Alice"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"          email"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" [email protected]"})]})]})})}),"\n",(0,r.jsxs)(e.p,{children:["Midscene Test 读取 YAML,找到名为 ",(0,r.jsx)(e.code,{children:"user.create"})," 的 Node,并调用它的 ",(0,r.jsx)(e.code,{children:"execute()"}),"。此时 ",(0,r.jsx)(e.code,{children:"input"})," 为 ",(0,r.jsx)(e.code,{children:'{ name: "Alice", email: "[email protected]" }'}),",Node 无需自行解析 YAML 文件。"]}),"\n",(0,r.jsxs)(e.p,{children:["示例使用 ",(0,r.jsx)(e.code,{children:"async execute({ input })"})," 和 ",(0,r.jsx)(e.code,{children:"await"})," 等待文件写入。写入失败时会抛出错误,使 Node 执行失败。这里创建的是本地测试数据文件;需要在业务系统中创建用户时,将文件写入替换为测试数据接口或数据库调用即可。"]}),"\n",(0,r.jsxs)(e.h3,{id:"3-生成-node-说明书验证注册",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#3-生成-node-说明书验证注册",children:"#"}),"3. 生成 Node 说明书,验证注册"]}),"\n",(0,r.jsx)(e.p,{children:"保存配置后,生成 Node 说明书:"}),"\n",(0,r.jsx)(r.Fragment,{children:(0,r.jsx)(e.pre,{className:"shiki css-variables",style:{backgroundColor:"var(--shiki-background)",color:"var(--shiki-foreground)"},tabIndex:"0",lang:"bash",children:(0,r.jsx)(e.code,{children:(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:"pnpm"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string)"},children:" exec"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string)"},children:" midscene-test"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string)"},children:" nodes"})]})})})}),"\n",(0,r.jsxs)(e.p,{children:["命令加载项目配置,并在当前目录生成 ",(0,r.jsx)(e.code,{children:"midscene-node-spec.md"}
1),"。显式配置 ",(0,r.jsx)(e.code,{children:"projects"})," 时,会为每个执行项目生成 ",(0,r.jsx)(e.code,{children:"midscene-node-spec.<project-name>.md"}),"。检查说明书是否包含:"]}),"\n",(0,r.jsxs)(e.ul,{children:["\n",(0,r.jsxs)(e.li,{children:["可用 Node 列表中的 ",(0,r.jsx)(e.code,{children:"user.create"}),"。"]}),"\n",(0,r.jsx)(e.li,{children:"“创建测试用户数据文件。”这一操作说明。"}),"\n",(0,r.jsxs)(e.li,{children:[(0,r.jsx)(e.code,{children:"name"}),"、",(0,r.jsx)(e.code,{children:"email"})," 两个输入字段及其描述。"]}),"\n"]}),"\n",(0,r.jsxs)(e.p,{children:["这一步验证 Node 是否已注册、输入 schema 是否可以导出,不会执行 Node 或创建用户数据。修改 Node 定义或注册配置后,可以重新生成说明书,供用例作者和 AI Agent 查阅。完整使用方法见",(0,r.jsx)(e.a,{href:"/zh/midscene-test/use.html#%E7%94%9F%E6%88%90%E5%BD%93%E5%89%8D%E9%A1%B9%E7%9B%AE%E7%9A%84-node-%E8%AF%B4%E6%98%8E%E4%B9%A6",children:"生成当前项目的 Node 说明书"}),"。"]}),"\n",(0,r.jsxs)(e.h2,{id:"node-执行参数",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#node-执行参数",children:"#"}),"Node 执行参数"]}),"\n",(0,r.jsxs)(e.p,{children:["Midscene Test 调用 ",(0,r.jsx)(e.code,{children:"execute()"})," 时,会传入包含本次执行参数和运行信息的对象。可以通过 ",(0,r.jsx)(e.code,{children:"execute({ input, $, context })"})," 这样的解构写法,直接取出需要的字段。"]}),"\n",(0,r.jsxs)(e.h3,{id:"业务参数与步骤配置",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#业务参数与步骤配置",children:"#"}),"业务参数与步骤配置"]}),"\n",(0,r.jsxs)(e.p,{children:[(0,r.jsx)(e.code,{children:"input"})," 包含 Node 的 ",(0,r.jsx)(e.code,{children:"inputSchema"})," 定义的业务参数。",(0,r.jsx)(e.code,{children:"$"})," 包含框架处理的步骤配置,例如超时时间和发生错误后是否继续执行。"]}),"\n",(0,r.jsxs)(e.p,{children:["例如,为前面的 ",(0,r.jsx)(e.code,{children:"user.create"})," 调用添加步骤配置:"]}),"\n",(0,r.jsx)(r.Fragment,{children:(0,r.jsx)(e.pre,{className:"shiki css-variables",style:{backgroundColor:"var(--shiki-background)",color:"var(--shiki-foreground)"},tabIndex:"0",lang:"yaml",children:(0,r.jsxs)(e.code,{children:[(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"steps"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  - "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"user.create"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"      name"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" Alice"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"      email"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" [email protected]"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"      $"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"        timeout"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" 30000"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"        continue-on-error"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" true"})]})]})})}),"\n",(0,r.jsxs)(e.p,{children:["框架将 ",(0,r.jsx)(e.code,{children:"$"})," 与业务参数分开,并规范化其中的字段名。在 ",(0,r.jsx)(e.code,{children:"execute({ input, $ })"})," 中,可以读取到:"]}),"\n",(0,r.jsx)(r.Fragment,{children:(0,r.jsx)(e.pre,{className:"shiki css-variables",style:{backgroundColor:"var(--shiki-background)",color:"var(--shiki-foreground)"},tabIndex:"0",lang:"ts",children:(0,r.jsxs)(e.code,{children:[(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"input"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".name; "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-comment)"},children:"// 'Alice'"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"input"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".email; "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-comment)"},children:"// '[email protected]'"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"$"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".timeoutMs; "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-comment)"},children:"// 30000"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"$"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".continueOnError; "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-comment)"},children:"// true"})]})]})})}),"\n",(0,r.jsxs)(e.p,{children:[(0,r.jsx)(e.code,{children:"inputSchema"})," 只需声明 ",(0,r.jsx)(e.code,{children:"name"})," 和 ",(0,r.jsx)(e.code,{children:"email"}),",",(0,r.jsx)(e.code,{children:"input"})," 中不包含 ",(0,r.jsx)(e.code,{children:"$"}),"。超时和出错后是否继续执行由框架控制。",(0,r.jsx)(e.code,{children:"continue-on-error"})," 允许当前阶段的后续步骤在失败后继续执行,但失败的步骤仍会使整个用例失败。"]}),"\n",(0,r.jsxs)(e.h3,{id:"可用的执行字段",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#可用的执行字段",children:"#"}),"可用的执行字段"]}),"\n",(0,r.jsxs)(e.p,{children:[(0,r.jsx)(e.code,{children:"execute()"})," 接收的参数对象包含以下常用字段:"]}),"\n",(0,r.jsxs)(e.ul,{children:["\n",(0,r.jsxs)(e.li,{children:[(0,r.jsx)(e.code,{children:"input"}),":从 YAML 传入并经过 Zod 校验后的业务参数。"]}),"\n",(0,r.jsxs)(e.li,{children:[(0,r.jsx)(e.code,{children:"$"}
1),":由 Midscene Test 控制的通用 Step 属性(如规范化后的 ",(0,r.jsx)(e.code,{children:"timeoutMs"})," 和 ",(0,r.jsx)(e.code,{children:"continueOnError"}),")。"]}),"\n",(0,r.jsxs)(e.li,{children:[(0,r.jsx)(e.code,{children:"signal"}),":超时或运行取消时触发的 ",(0,r.jsx)(e.code,{children:"AbortSignal"}),",在异步请求或长耗时任务中使用它以响应取消。"]}),"\n",(0,r.jsxs)(e.li,{children:[(0,r.jsx)(e.code,{children:"context"}),":在 ",(0,r.jsx)(e.code,{children:"defineProjectSetup()"})," 中返回并共享的项目级运行时资源。"]}),"\n",(0,r.jsxs)(e.li,{children:[(0,r.jsx)(e.code,{children:"onTeardown()"}),":注册当前 Node 所创建资源的清理函数,支持 attempt 级别或 Document 级,按 LIFO(后进先出)顺序执行。"]}),"\n",(0,r.jsxs)(e.li,{children:[(0,r.jsx)(e.code,{children:"scope"}),":标记当前 Node 执行的上下文边界,值为 ",(0,r.jsx)(e.code,{children:"case"})," 或 ",(0,r.jsx)(e.code,{children:"document"}),"。"]}),"\n",(0,r.jsxs)(e.li,{children:[(0,r.jsx)(e.code,{children:"case"})," 或 ",(0,r.jsx)(e.code,{children:"document"}),":当前执行位置的详细运行信息。"]}),"\n"]}),"\n",(0,r.jsxs)(e.p,{children:["其中,",(0,r.jsx)(e.code,{children:"context"})," 用于访问项目共享的资源与状态。下一节介绍如何通过它在多个 Node 之间共享数据。"]}),"\n",(0,r.jsxs)(e.h2,{id:"跨-node-共享上下文",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#跨-node-共享上下文",children:"#"}),"跨 Node 共享上下文"]}),"\n",(0,r.jsx)(e.p,{children:"Node 的每次执行都是独立调用,框架不会自动将上一次执行的结果传入下一次调用。当一个业务流程由多个 Node 完成时,它们可能需要使用同一份测试数据。例如,订单退款用例先创建订单,再打开该订单的退款页面,最后删除测试订单。下面沿着这份数据的使用过程,说明 Node 如何协作。"}),"\n",(0,r.jsxs)(e.h3,{id:"创建共享上下文",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#创建共享上下文",children:"#"}),"创建共享上下文"]}),"\n",(0,r.jsxs)(e.p,{children:["Midscene Test 提供了项目级上下文机制。同一个执行项目中的 Node 可以通过共享的 ",(0,r.jsx)(e.code,{children:"context"})," 对象访问运行资源、传递测试数据。这个对象由项目的 ",(0,r.jsx)(e.code,{children:"setup"}
1)," 创建并返回,框架在执行各 Node 时,将它传入 ",(0,r.jsx)(e.code,{children:"execute()"}),"。"]}),"\n",(0,r.jsxs)(e.p,{children:["对于上面的订单退款流程,setup 可以提供浏览器页面、应用地址和订单服务,订单 ID 则在 Node 创建订单后写入。下面的 ",(0,r.jsx)(e.code,{children:"setup.ts"})," 展示了这个共享对象的创建方式:",(0,r.jsx)(e.code,{children:"ProjectContext"})," 描述它的类型,",(0,r.jsx)(e.code,{children:"setup"})," 负责创建并返回实际的对象。"]}),"\n",(0,r.jsxs)(e.p,{children:["导入的 ",(0,r.jsx)(e.code,{children:"orderService"})," 是你自己的测试数据服务,需要实现 ",(0,r.jsx)(e.code,{children:"create"})," 和 ",(0,r.jsx)(e.code,{children:"remove"})," 方法。应用地址也需替换为测试环境地址。"]}),"\n",(0,r.jsx)(r.Fragment,{children:(0,r.jsx)(e.pre,{className:"shiki css-variables",style:{backgroundColor:"var(--shiki-background)",color:"var(--shiki-foreground)"},tabIndex:"0",lang:"ts",children:(0,r.jsxs)(e.code,{children:[(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"import"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { defineProjectSetup } "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"from"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" '@midscene/test/config'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"import"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { chromium"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" type"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" Page } "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"from"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" 'playwright'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"import"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { orderService } "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"from"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" './order-service'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsx)(e.span,{className:"line"}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"export"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" interface"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" ProjectContext"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" {"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  appBaseUrl"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" string"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  page"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" Page"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  orderId"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"?:"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" string"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"; "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-comment)"},children:"// 用于在 Node 之间共享测试状态"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  orderService"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" {"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:"    create"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"(input"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { status"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" 'paid'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" })"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" Promise"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"<{ id"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" string"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" }>;"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:"    remove"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"(orderId"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" string"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:")"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" Promise"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"<"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"void"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:">;"})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  };"})}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"}"})}),"\n",(0,r.jsx)(e.span,{className:"line"}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"export"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" setup"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" defineProjectSetup"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"<"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:"ProjectContext"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:">({"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  name"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" 'refund'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"  async"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" setup"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"({ onTeardown }) {"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" browser"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" await"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" chromium"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".launch"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"}
1,children:"();"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:"    onTeardown"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"(() "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"=>"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" browser"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".close"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"());"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" page"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" await"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" browser"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".newPage"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"();"})]}),"\n",(0,r.jsx)(e.span,{className:"line"}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" context"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" ProjectContext"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" {"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"      page"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"      appBaseUrl"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" 'https://yoursite.com'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"      orderService"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"    };"})}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    return"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" context;"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  }"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"});"})})]})})}),"\n",(0,r.jsxs)(e.p,{children:["setup 返回的 ",(0,r.jsx)(e.code,{children:"context"})," 是当前执行项目共享的同一个对象。在 ",(0,r.jsx)(e.code,{children:"execute({ input, context })"})," 中,",(0,r.jsx)(e.code,{children:"input"})," 来自当前 YAML 步骤,",(0,r.jsx)(e.code,{children:"context"})," 则是这里创建的对象,此时 ",(0,r.jsx)(e.code,{children:"orderId"})," 尚未赋值。"]}),"\n",(0,r.jsxs)(e.h3,{id:"在-node-中写入数据",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#在-node-中写入数据",children:"#"}),"在 Node 中写入数据"]}),"\n",(0,r.jsxs)(e.p,{children:["接着创建 ",(0,r.jsx)(e.code,{children:"nodes.ts"}),"。",(0,r.jsx)(e.code,{children:"order.prepare"})," 使用 ",(0,r.jsx)(e.code,{children:"context"}
1)," 中的订单服务创建订单,再将 ID 写入 ",(0,r.jsx)(e.code,{children:"context.orderId"}),",供后续 Node 使用:"]}),"\n",(0,r.jsx)(r.Fragment,{children:(0,r.jsx)(e.pre,{className:"shiki css-variables",style:{backgroundColor:"var(--shiki-background)",color:"var(--shiki-foreground)"},tabIndex:"0",lang:"ts",children:(0,r.jsxs)(e.code,{children:[(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"import"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { defineNode"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" z } "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"from"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" '@midscene/test'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"import"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" type"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { ProjectContext } "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"from"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" './setup'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsx)(e.span,{className:"line"}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" emptyInputSchema"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" z"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".strictObject"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"({});"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" prepareOrderInputSchema"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" z"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".strictObject"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"({"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  status"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" z"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".literal"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"("}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:"'paid'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:")"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".describe"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"("}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:"'待创建订单的状态。'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:")"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"});"})}),"\n",(0,r.jsx)(e.span,{className:"line"}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-comment)"},children:"// 1. 准备订单环境"})}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" prepareOrder"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" defineNode"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"<"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"  typeof"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" prepareOrderInputSchema"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  { orderId"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" string }"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  ProjectContext"})}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:">"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"({"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  name"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" 'order.prepare'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  description"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" '调用订单服务创建测试订单。'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  inputSchema"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" prepareOrderInputSchema"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"  async"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" execute"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"({ input"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" context"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" onTeardown }) {"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" order"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" await"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" context"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:"."}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"orderService"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".create"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"(input);"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"    context"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".orderId "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" order"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".id; "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-comment)"},children:"// 将 ID 保存至上下文"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:"    onTeardown"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"("}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"async"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" () "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"=>"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" {"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"      await"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" context"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:"."}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"orderService"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".remove"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"("}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"order"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".id);"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"      delete"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" context"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".orderId;"})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"    });"})}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    return"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" {"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"      summary"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" `已创建测试订单 "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"${"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"order"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".id"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"}"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:"`"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"      data"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { orderId"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" order"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".id }"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"    };"})}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  }"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"});"})})]})})}),"\n",(0,r.jsxs)(e.p,{children:[(0,r.jsx)(e.code,{children:"context.orderId = order.id"})," 修改共享对象,使后续 Node 可以读取这个 ID。返回 ",(0,r.jsx)(e.code,{children:"data"})," 不会自动将它写入 ",(0,r.jsx)(e.code,{children:"context"}),"。"]}),"\n",(0,r.jsxs)(e.p,{children:["创建订单后,",(0,r.jsx)(e.code,{children:"onTeardown()"})," 注册清理函数,在清理时删除本次创建的订单并清除保存的 ID。回调直接使用本次调用的 ",(0,r.jsx)(e.code,{children:"order.id"}),",因此创建与清理封装在同一个 Node 中,YAML 无需额外调用清理步骤。"]}),"\n",(0,r.jsxs)(e.h3,{id:"在后续-node-中读取数据",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#在后续-node-中读取数据",children:"#"}),"在后续 Node 中读取数据"]}),"\n",(0,r.jsxs)(e.p,{children:[(0,r.jsx)(e.code,{children:"browser.openRefundPage"})," 读取保存的 ID,打开退款页面。如果订单 ID 不存在,这个 Node 会报错。"]}
1),"\n",(0,r.jsx)(r.Fragment,{children:(0,r.jsx)(e.pre,{className:"shiki css-variables",style:{backgroundColor:"var(--shiki-background)",color:"var(--shiki-foreground)"},tabIndex:"0",lang:"ts",children:(0,r.jsxs)(e.code,{children:[(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" getOrderId"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" (context"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" ProjectContext"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:") "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"=>"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" {"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"  if"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" ("}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"!"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"context"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".orderId) {"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    throw"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" new"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" Error"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"("}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:"'测试订单尚未创建。'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:");"})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  }"})}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"  return"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" context"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".orderId;"})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"};"})}),"\n",(0,r.jsx)(e.span,{className:"line"}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-comment)"},children:"// 2. 访问已保存的订单状态"})}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" openRefundPage"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" defineNode"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"<"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"  typeof"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" emptyInputSchema"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  unknown"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  ProjectContext"})}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:">"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"({"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  name"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" 'browser.openRefundPage'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  description"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" '打开当前测试订单的退款页面。'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  inputSchema"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" emptyInputSchema"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"  async"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" execute"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"({ context }) {"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" orderId"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" getOrderId"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"(context);"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    await"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" context"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:"."}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"page"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:".goto"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"("}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:"`"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"${"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:"context"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:".appBaseUrl"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"}"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:"/orders/"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"${"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"orderId"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"}"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:"/refund`"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:");"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  }"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"});"})}),"\n",(0,r.jsx)(e.span,{className:"line"}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"export"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" const"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-constant)"},children:" refundNodes"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" ="}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" [prepareOrder"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" openRefundPage];"})]})]})})}),"\n",(0,r.jsx)(e.p,{children:"将 setup 和 Node 一起注册,框架就会把 setup 返回的对象传给各 Node:"}),"\n",(0,r.jsx)(r.Fragment,{children:(0,r.jsx)(e.pre,{className:"shiki css-variables",style:{backgroundColor:"var(--shiki-background)",color:"var(--shiki-foreground)"},tabIndex:"0",lang:"ts",children:(0,r.jsxs)(e.code,{children:[(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"import"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { defineTestProject } "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"from"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" '@midscene/test/config'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"import"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { refundNodes } "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"from"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" './nodes'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"import"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" { setup"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" type"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" ProjectContext } "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"from"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" './setup'"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:";"})]}),"\n",(0,r.jsx)(e.span,{className:"line"}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"export"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:" default"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:" defineTestProject"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"<"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-function)"},children:"ProjectContext"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:">({"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  setup"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  nodes"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" refundNodes"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-punctuation)"},children:","})]}),"\n",(0,r.jsx)(e.span,{className:"line",children:(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"});"})})]})})}),"\n",(0,r.jsxs)(e.p,{children:["在 YAML 中,通过 ",(0,r.jsx)(e.code,{children:"beforeEach"})," 调用 ",(0,r.jsx)(e.code,{children:"order.prepare"}),",然后在用例步骤中使用保存的订单 ID:"]}),"\n",(0,r.jsx)(r.Fragment,{children:(0,r.jsx)(e.pre,{className:"shiki css-variables",style:{backgroundColor:"var(--shiki-background)",color:"var(--shiki-foreground)"},tabIndex:"0",lang:"yaml",children:(0,r.jsxs)(e.code,{children:[(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"beforeEach"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  - "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"order.prepare"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"      status"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" paid"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"cases"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"  - "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"name"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-string-expression)"},children:" 打开退款页面"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"    steps"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"})]}),"\n",(0,r.jsxs)(e.span,{className:"line",children:[(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:"      - "}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:"browser.openRefundPage"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-token-keyword)"},children:":"}),(0,r.jsx)(e.span,{style:{color:"var(--shiki-foreground)"},children:" {}"})]})]})})}
1),"\n",(0,r.jsxs)(e.p,{children:["已注册的清理函数在本次用例的 ",(0,r.jsx)(e.code,{children:"afterEach"})," 阶段之后执行,即使 YAML 没有声明 ",(0,r.jsx)(e.code,{children:"afterEach"})," 步骤也会执行。后续准备步骤或用例步骤失败时,框架仍会执行已注册的清理。每次重试有独立的清理范围;如果创建订单失败、尚未注册回调,则本次调用没有对应的清理函数。"]}),"\n",(0,r.jsxs)(e.p,{children:["Agent 接入和平台资源配置见",(0,r.jsx)(e.a,{href:"/zh/midscene-test/configuration.html",children:"配置测试项目"}),"。"]}),"\n",(0,r.jsxs)(e.h2,{id:"进阶用法",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#进阶用法",children:"#"}),"进阶用法"]}),"\n",(0,r.jsx)(e.p,{children:"以下介绍 Node 内部的执行结果、错误处理、取消和资源清理。"}),"\n",(0,r.jsxs)(e.h3,{id:"记录执行结果",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#记录执行结果",children:"#"}),"记录执行结果"]}),"\n",(0,r.jsxs)(e.p,{children:["上面的订单准备 Node 还返回了 ",(0,r.jsx)(e.code,{children:"summary"})," 和 ",(0,r.jsx)(e.code,{children:"data"}),"。",(0,r.jsx)(e.code,{children:"summary"})," 是供报告展示的执行摘要,",(0,r.jsx)(e.code,{children:"data"})," 保存结构化输出。两者均为可选字段,Node 也可以不返回结果。这些值保存在运行结果中,与共享的 ",(0,r.jsx)(e.code,{children:"context"})," 相互独立。"]}),"\n",(0,r.jsxs)(e.h3,{id:"异步操作错误与取消",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#异步操作错误与取消",children:"#"}),"异步操作、错误与取消"]}),"\n",(0,r.jsxs)(e.p,{children:["异步操作使用 ",(0,r.jsx)(e.code,{children:"async execute()"}),",并通过 ",(0,r.jsx)(e.code,{children:"await"})," 等待完成。操作失败时应抛出错误。"]}),"\n",(0,r.jsxs)(e.p,{children:["Midscene Test 在执行 Node 时通过 ",(0,r.jsx)(e.code,{children:"signal"})," 提供 ",(0,r.jsx)(e.code,{children:"AbortSignal"}),"。调用支持取消的 API 时,将它传入,例如 ",(0,r.jsx)(e.code,{children:"fetch(url, { signal })"}),"。长时间运行的循环可以在每次迭代之间调用 ",(0,r.jsx)(e.code,{children:"signal.throwIfAborted()"}),",以响应步骤超时或运行取消。"]}),"\n",(0,r.jsxs)(e.h3,{id:"资源的生命周期与清理",children:[(0,r.jsx)(e.a,{className:"rp-header-anchor","aria-hidden":"true",href:"#资源的生命周期与清理",children:"#"}),"资源的生命周期与清理"]}),"\n",(0,r.jsxs)(e.p,{children:["Node 创建资源后,可以通过 ",(0,r.jsx)(e.code,{children:"onTeardown()"})," 注册清理函数。清理时机取决于 Node 所在的执行阶段。"]}),"\n",(0,r.jsx)(e.p,{children:"资源清理分为以下范围:"}),"\n",(0,r.jsxs)(e.ul,{children:["\n",(0,r.jsxs)(e.li,{children:["在 ",(0,r.jsx)(e.code,{children:"beforeEach"}),"、用例 ",(0,r.jsx)(e.code,{children:"steps"})," 或 ",(0,r.jsx)(e.code,{children:"afterEach"})," 的 Node 中注册的清理,在本次用例执行的 ",(0,r.jsx)(e.code,{children:"afterEach"})," 之后运行。每次重试有独立的清理范围。"]}),"\n",(0,r.jsxs)(e.li,{children:["在 ",(0,r.jsx)(e.code,{children:"beforeAll"})," 或 ",(0,r.jsx)(e.code,{children:"afterAll"})," 的 Node 中注册的清理,在当前文件的 ",(0,r.jsx)(e.code,{children:"afterAll"})," 之后运行。"]}),"\n"]}),"\n",(0,r.jsx)(e.p,{children:"每个范围内的清理函数按注册顺序的逆序执行,即 LIFO(后进先出)。Node 可以借此释放自己创建的资源,或完成报告生成。注册清理不会自动创建新的 Agent 或重置缓存;这些行为取决于项目实现。"}),"\n",(0,r.jsxs)(e.p,{children:["测试运行被中断时,框架仍会尝试执行 ",(0,r.jsx)(e.code,{children:"afterEach"}),"、",(0,r.jsx)(e.code,{children:"afterAll"})," 和通过 ",(0,r.jsx)(e.code,{children:"onTeardown()"})," 注册的清理函数。取消机制的详细说明见",(0,r.jsx)(e.a,{href:"/zh/midscene-test/configuration.html#%E8%B6%85%E6%97%B6%E4%B8%8E%E5%8F%96%E6%B6%88%E6%9C%BA%E5%88%B6",children:"超时与取消机制"}),"。"]}),"\n",(0,r.jsxs)(e.p,{children:["项目级浏览器和 Agent 的清理见",(0,r.jsx)(e.a,{href:"/zh/midscene-test/configuration.html#%E4%BD%BF%E7%94%A8-setup-%E5%88%9B%E5%BB%BA%E5%92%8C%E6%B8%85%E7%90%86%E5%85%B1%E4%BA%AB%E8%B5%84%E6%BA%90",children:"使用 setup 创建和清理共享资源"}),"。"]})]})}function l(){let s=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},{wrapper:e}={...(0,o.R)(),...s.components};return e?(0,r.jsx)(e,{...s,children:(0,r.jsx)(i,{...s})}):i(s)}l.__RSPRESS_PAGE_META={},l.__RSPRESS_PAGE_META["zh%2Fmidscene-test%2Fextend.mdx"]={toc:[{id:"注册自定义业务-node",text:"注册自定义业务 Node",depth:2},{id:"1-定义并注册-node",text:"1. 定义并注册 Node",depth:3},{id:"2-在-yaml-中调用",text:"2. 在 YAML 中调用",depth:3},{id:"3-生成-node-说明书验证注册",text:"3. 生成 Node 说明书,验证注册",depth:3},{id:"node-执行参数",text:"Node 执行参数",depth:2},{id:"业务参数与步骤配置",text:"业务参数与步骤配置",depth:3},{id:"可用的执行字段",text:"可用的执行字段",depth:3},{id:"跨-node-共享上下文",text:"跨 Node 共享上下文",depth:2},{id:"创建共享上下文",text:"创建共享上下文",depth:3}
1,{id:"在-node-中写入数据",text:"在 Node 中写入数据",depth:3},{id:"在后续-node-中读取数据",text:"在后续 Node 中读取数据",depth:3},{id:"进阶用法",text:"进阶用法",depth:2},{id:"记录执行结果",text:"记录执行结果",depth:3},{id:"异步操作错误与取消",text:"异步操作、错误与取消",depth:3},{id:"资源的生命周期与清理",text:"资源的生命周期与清理",depth:3}],title:"编写自定义 Node",headingTitle:"编写自定义 Node",frontmatter:{}},n.d(e,{default:()=>l})}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.