• 简体中文
  • 配置测试项目

    midscene.config.ts 是 Midscene Test 的项目配置文件。你可以在其中配置浏览器或设备的初始化方式、注册用例可调用的 Node,并设置并发数、超时等运行参数。本文介绍配置文件结构、Agent 与平台接入、多执行项目管理,以及编程式调用。

    配置文件结构

    使用 defineTestProject() 定义并导出项目配置。执行项目负责选择用例并提供运行环境。

    Midscene Test 支持以下配置文件:

    配置文件的选择方式支持的文件加载方式
    未指定配置路径,由 Midscene Test 自动查找midscene.config.ts 或 midscene.config.mjs.ts 会先经过 TypeScript 转换;.mjs 由 Node.js 作为原生 ESM 直接加载
    通过 CLI 的 --config <路径> 或 loadTestProject(路径) 显式指定.ts 或 .mjs.ts 会先经过 TypeScript 转换;.mjs 由 Node.js 作为原生 ESM 直接加载,不经过 TypeScript 转换

    Midscene Test 默认会加载 midscene.config.ts 或 midscene.config.mjs。如果两个文件同时存在,Midscene Test 会报告冲突,而不是静默选择其中一个;此时可以通过 --config <路径> 明确指定要使用的文件。

    在 setup 中准备项目共享资源,在 fileSetup 中准备每份文件独立使用的资源,并通过 nodes 注册对应 Agent 的 Node。

    字段用途
    setup准备项目共享的浏览器或设备资源。
    fileSetup为每份 YAML 准备运行环境,串行和并发执行时都可使用。
    nodes注册所有执行项目共享的 Node。
    projects声明具名执行项目,分别配置资源和用例选择规则。声明后,setup 和 fileSetup 都放在各项目中;省略时,顶层配置用于默认项目。
    test设置并发数、失败阈值和默认步骤超时。
    output设置报告输出目录。

    下面先以单个执行项目为例介绍配置方法。如果需要管理多个执行项目,或为不同项目注册各自的 Node,请参阅配置多个执行项目。

    配置运行环境

    完整示例:Playwright

    下面是只使用 setup 的串行配置:同一执行项目中的所有文件共用一个页面。需要每份文件使用独立环境时,添加 fileSetup,不必先开启并发。

    如果要在已有工程中手动接入,请先安装 @midscene/test、@midscene/web 和 playwright(pnpm add -D @midscene/test @midscene/web playwright),再执行 pnpm exec playwright install chromium 安装浏览器。运行测试前,还需按模型配置设置 API Key 等环境变量。

    import {
      defineProjectSetup,
      defineTestProject,
    } from '@midscene/test/config';
    import { createMidsceneNodes } from '@midscene/test/midscene';
    import { PlaywrightAgent } from '@midscene/web/playwright/agent';
    import { chromium, type Browser, type Page } from 'playwright';
    
    interface ProjectContext {
      browser: Browser;
      page: Page;
      agent?: PlaywrightAgent;
    }
    
    const midsceneNodes = createMidsceneNodes<ProjectContext>({
      agentClass: PlaywrightAgent,
      getAgent: ({ context }) => {
        context.agent ??= new PlaywrightAgent(context.page);
        return context.agent;
      },
    });
    
    // 声明浏览器环境的启动与清理
    const playwrightSetup = defineProjectSetup<ProjectContext>({
      name: 'playwright',
      async setup({ onTeardown }) {
        const browser = await chromium.launch({ headless: true });
        onTeardown(() => browser.close());
        const browserContext = await browser.newContext();
        const page = await browserContext.newPage();
        const context: ProjectContext = { browser, page };
        onTeardown(async () => { await context.agent?.destroy(); });
        return context;
      },
    });
    
    // 导出项目配置
    export default defineTestProject<ProjectContext>({
      projects: [
        {
          name: 'chromium',
          setup: playwrightSetup,
          files: { include: ['cases/**/*.{yaml,yml}'] },
        },
      ],
      nodes: midsceneNodes,
    });

    这个配置中,各部分的关系如下:

    • setup 创建浏览器和页面,并通过 onTeardown() 注册清理函数。
    • context 保存运行时资源。Node 通过 getAgent 获取 Agent;首次调用时创建实例并保存在 context.agent 中,供后续调用复用。Playwright Node 使用该 Agent 的页面。
    • nodes 注册通用 AI 操作和 Playwright 操作,供 YAML 用例调用。
    • projects 声明名为 chromium 的执行项目,将 setup 与 cases/ 下的 YAML 文件关联起来。

    接下来可以编写 YAML 测试用例并运行测试。下面进一步说明这些资源如何共享和清理。

    使用 setup 创建和清理共享资源

    defineProjectSetup() 定义项目的准备工作,通过 setup 字段配置。测试运行时,每个执行项目调用一次其中的 setup()。上面的示例返回浏览器和页面,供该项目的 Node 共用。

    需要文件隔离时,用 defineFileSetup() 定义每份文件的准备工作,通过 fileSetup 字段配置。它接收项目 setup 的返回值,再为当前文件创建资源。两者的关系和示例见用 fileSetup 隔离资源。

    共享资源不会在每个用例开始时自动重建。需要重置页面、Cookie 或业务状态时,可以通过 YAML 的生命周期钩子调用相应 Node。自定义 Node 如何读写共享数据,见跨 Node 共享上下文。

    创建浏览器、Agent 等资源后,应立即通过 onTeardown() 注册对应的清理函数。即使 setup 或测试执行失败,框架也会在项目结束时尝试执行已注册的清理函数。清理顺序与注册顺序相反:在上面的 Playwright 示例中,先销毁 Agent,再关闭浏览器。

    这里的 onTeardown() 在项目结束时执行。在 Node 内部注册的清理函数有独立的执行范围。

    项目生命周期

    setup、fileSetup 与 beforeAll

    三者按 setup → fileSetup → beforeAll → 用例 的顺序执行,未配置的部分会跳过。

    配置位置执行次数用途
    setup项目配置每个项目一次准备共享资源,例如 Browser
    fileSetup项目配置每份文件一次创建独立页面、Agent,返回 Node 使用的 context
    beforeAllYAML每份文件的用例开始前一次使用 context 登录、准备测试数据等

    正常执行两份 YAML 时,setup 执行一次,fileSetup 和 beforeAll 各执行两次;并发数不改变执行次数。

    fileSetup 配置一次即可用于所有文件;beforeAll 由各文件声明。自定义 Node 也能在 beforeAll 中创建资源,但框架不会因此创建独立的文件 context。

    项目、文件与用例的生命周期

    通过 CLI 或 runTestProject() 运行测试时,每个执行项目都有独立的生命周期。下面展示一个包含两个 YAML 文件的项目在顺序执行时的正常流程;未声明的 YAML 钩子会跳过:

    执行项目开始
    ├─ setup → 返回项目共享的 context
    ├─ 文件 A
    │  ├─ fileSetup(配置时执行)→ 返回文件 context
    │  ├─ beforeAll
    │  ├─ 用例 1:beforeEach → steps → afterEach → 本次执行的 Node 清理
    │  ├─ 用例 2:beforeEach → steps → afterEach → 本次执行的 Node 清理
    │  ├─ afterAll
    │  └─ 文件级 Node 清理 → fileSetup 中登记的清理
    ├─ 文件 B
    │  └─ 同样执行该文件的钩子、用例和清理
    └─ 项目清理:执行 setup 中注册的 onTeardown()

    beforeAll 和 afterAll 每份 YAML 各执行一次;beforeEach 和 afterEach 包围每个用例的每次尝试。配置了文件并发后,不同文件可以同时推进,文件内部仍按图中顺序执行。

    用例重试沿用原来的文件环境,只重复 beforeEach → steps → afterEach → 本次执行的 Node 清理。项目 setup、fileSetup 和 beforeAll 都不会重跑。重试条件和失败后的跳过规则见失败时如何执行。

    onTeardown() 的清理范围取决于注册位置:

    注册位置清理时机
    项目 setup项目结束时,所有已执行文件的清理之后
    fileSetup当前文件的 afterAll 与文件范围 Node 清理之后
    beforeAll、afterAll 中的 Node当前文件的 afterAll 之后
    beforeEach、steps、afterEach 中的 Node本次用例执行的 afterEach 之后,每次重试独立清理

    每个范围内的清理函数都按注册顺序的逆序执行。一个函数抛错后仍会尝试后续函数;函数尚未结束时,后续清理等待它结束。Node 清理的实现方式见资源的生命周期与清理。

    context 的共享范围

    Node 收到的 context 由配置决定:

    配置方式context 的来源共享范围
    只有项目 setupsetup 的返回值当前项目中的所有文件
    配置了 fileSetup当前文件调用 fileSetup 的返回值当前文件的钩子和用例,包括用例重试

    同一共享范围内,Node 收到的是同一个对象。写入 context.orderId 后,后续用例也可能读到它。框架不会在用例切换或重试时重置 context、页面或登录状态;用例需要的初始状态应在 beforeEach 中恢复。

    在 setup() 函数内部创建资源和返回对象。多个项目或文件即使引用同一个定义,也会分别调用函数;如果返回模块顶层的同一个可变对象,仍会共享状态。数据传递示例见跨 Node 共享上下文。

    注册官方 Node

    通用 AI 操作与 Agent 接入

    Midscene Test 内核不隐式注册 Node。midscene-test create 生成的配置会调用 @midscene/test/midscene 导出的 createMidsceneNodes(),注册所选平台的官方 Node。手写配置时,需要将该函数返回的 Node 定义显式放入顶层 nodes 或 projects[].nodes。

    createMidsceneNodes() 返回两部分能力:agentClass.getTestRunnerNodeDefinitions() 声明的通用 Agent Node 和平台 Node,以及 Test 提供的 wait Node。通用能力包含 AI 操作、断言与信息提取、界面定位、报告记录等;平台 Agent 再追加浏览器或设备操作。完整名称和输入字段不在网站中维护重复清单,请以当前项目生成的 Node 说明书 为准。

    调用 createMidsceneNodes() 时,通过 agentClass 指定 Agent 类,通过 getAgent 回调提供运行时使用的 Agent 实例:

    import { createMidsceneNodes } from '@midscene/test/midscene';
    import { PlaywrightAgent } from '@midscene/web/playwright/agent';
    
    const midsceneNodes = createMidsceneNodes<ProjectContext>({
      agentClass: PlaywrightAgent,
      getAgent: ({ context }) => {
        context.agent ??= new PlaywrightAgent(context.page);
        return context.agent;
      },
    });

    createMidsceneNodes() 不会直接修改全局注册表;只有把返回数组加入项目配置后,这些 Node 才会生效。agentClass 是 Agent Node 定义的唯一来源,getAgent 则说明执行期间如何从当前 context 取得 Agent 实例。如果该类没有提供 getTestRunnerNodeDefinitions(),工厂会在注册阶段抛出异常。传入 PlaywrightAgent、PlaywrightPageAgent、PlaywrightBrowserAgent、PuppeteerAgent、PuppeteerPageAgent、PuppeteerBrowserAgent、AndroidAgent、IOSAgent 或 HarmonyAgent 时,会同时获得通用 Node 定义和对应的平台 Node 定义。基础 Agent 和 ComputerAgent 仅提供通用 Agent Node。

    Android/iOS 的注册示例见配置多个执行项目。

    在 Android、iOS 等设备平台接入 Agent 时,需注意资源的所有权。每个 Device 实例只归属于一个 Agent,Agent.destroy() 也会销毁对应 Device。每个执行项目应创建独立的资源,不要在 Agent 销毁后复用它的 Device。

    各平台都通过 createMidsceneNodes() 注册,使用 agentClass 声明平台,通过 getAgent 获取实例。项目 context 的字段名由你自行定义。

    注册后,可在 YAML 中调用这些 Node。参数写法见其他 Node 的调用模式,具体字段以项目生成的 Node 说明书为准。

    Playwright

    通过 createMidsceneNodes({ agentClass: PlaywrightAgent, getAgent }) 会自动注册 gotoUrl、setCookies、clearCookies 和 setViewportSize。使用这些 Node 前,需要在项目中安装 playwright。它是 @midscene/web 的同级依赖(peer dependency):

    pnpm add -D playwright

    Cookie 默认从 process.env 读取。只有需要自定义 Cookie 来源时,才在创建 Agent 时传入 testRunner 选项,例如 getEnv、getCookieProfile 或 resolveStorageStatePath。这些回调中的 context 是当前 Agent。

    使用 setCookies 时,不能在 YAML 中直接填写 Cookie 值。Midscene Test 会把 Node 输入保存到 运行结果。如果直接填写 Cookie,这些敏感信息也会被保存。

    请使用 cookiesEnv、profile 或 storageStatePath 引用 Cookie,三者必须选择一个。 Node 只在执行时读取实际的 Cookie,并将它直接传给 Playwright BrowserContext。Node 结果只记录引用名称和 Cookie 数量,不会记录 Cookie 的名称、值和作用域。因此, Cookie 不会进入 Midscene Test 的运行结果。

    环境变量可以包含 Cookie header、Cookie JSON 数组或 Playwright storage-state JSON。 相对的 storage-state 路径默认从当前工作目录解析。如果项目需要使用其他根目录,请配置 resolveStorageStatePath。引用方式只能避免 Cookie 进入 Midscene Test 的持久化数据; 环境变量、profile 和 storage-state 文件本身仍需妥善保管。请勿将包含真实 Cookie 的 storage-state 文件提交到代码仓库。

    beforeEach:
      - clearCookies: {}
      - setCookies:
          cookiesEnv: E2E_COOKIES
          url: https://example.com
      - setViewportSize:
          width: 1440
          height: 900
      - gotoUrl:
          url: https://example.com/chat
          waitUntil: domcontentloaded

    导航参数以当前项目的 Node 说明书为准。

    Puppeteer

    Puppeteer 提供同名的 gotoUrl、setCookies、clearCookies 和 setViewportSize Node。安装 puppeteer 同级依赖,并把 PuppeteerAgent(或 PuppeteerBrowserAgent)传给 createMidsceneNodes():

    pnpm add -D puppeteer
    import { PuppeteerAgent } from '@midscene/web/puppeteer';
    import { createMidsceneNodes } from '@midscene/test/midscene';
    
    const midsceneNodes = createMidsceneNodes<ProjectContext>({
      agentClass: PuppeteerAgent,
      getAgent: ({ context }) => {
        context.agent ??= new PuppeteerAgent(context.page, {
          testRunner: { baseURL: 'https://example.com' },
        });
        return context.agent;
      },
    });

    Playwright 和 Puppeteer 使用同一套 Web Node Schema 和执行实现,浏览器相关代码只保留 Driver 适配层。为了让 YAML 可以跨浏览器运行,gotoUrl.waitUntil 应使用 domcontentloaded、load 或 networkidle;Puppeteer 适配器会把 networkidle 映射为 networkidle0。现有的浏览器专用值继续兼容:Playwright 支持 commit,Puppeteer 支持 networkidle0 和 networkidle2;把这些值用于另一种 Driver 时会返回明确错误。两种接入的 Cookie 引用和结果脱敏规则也保持一致。相对 URL 会先基于 testRunner.baseURL 解析;没有配置时,再基于当前 HTTP(S) 页面地址解析。

    Android

    使用下面的设备平台示例时,请在 ProjectContext 中声明对应平台类型的 agent,并在 setup 返回的对象中提供该实例。各平台示例独立使用,按需选择即可。

    使用 createMidsceneNodes({ agentClass: AndroidAgent, getAgent }) 可以注册 launch、terminate、runAdbShell、back、home 和 recentApps。传入的 Agent 需要提供这些 Node 对应的方法:

    import { AndroidAgent } from '@midscene/android';
    import { createMidsceneNodes } from '@midscene/test/midscene';
    
    const androidNodes = createMidsceneNodes<ProjectContext>({
      agentClass: AndroidAgent,
      getAgent: ({ context }) => context.agent,
    });
    beforeEach:
      - runAdbShell:
          command: pm clear com.example.app
          options:
            timeout: 5000
      - launch:
          uri: com.example.app

    runAdbShell 的完整响应会保存在 Node 结果中。

    iOS

    使用 createMidsceneNodes({ agentClass: IOSAgent, getAgent }) 可以注册 launch、terminate、runWdaRequest、home 和 appSwitcher:

    import { IOSAgent } from '@midscene/ios';
    import { createMidsceneNodes } from '@midscene/test/midscene';
    
    const iosNodes = createMidsceneNodes<ProjectContext>({
      agentClass: IOSAgent,
      getAgent: ({ context }) => context.agent,
    });
    steps:
      - launch:
          uri: com.example.app
      - runWdaRequest:
          request:
            method: GET
            endpoint: /status
      - terminate:
          uri: com.example.app

    runWdaRequest 的完整响应会保存在 Node 结果中。

    HarmonyOS

    使用 createMidsceneNodes({ agentClass: HarmonyAgent, getAgent }) 可以注册 launch、terminate、runHdcShell、back、home 和 recentApps:

    import { HarmonyAgent } from '@midscene/harmony';
    import { createMidsceneNodes } from '@midscene/test/midscene';
    
    const harmonyNodes = createMidsceneNodes<ProjectContext>({
      agentClass: HarmonyAgent,
      getAgent: ({ context }) => context.agent,
    });
    steps:
      - runHdcShell:
          command: bm dump -a
      - home: {}

    runHdcShell 的完整响应会保存在 Node 结果中。

    配置执行项目

    执行项目将运行环境与用例关联起来。单个执行项目也可以设置用例筛选、变量和重试;需要在多个浏览器、设备或环境中运行时,再声明多个执行项目。

    选择用例与控制执行

    配置项含义
    projects[].filesinclude 选择 YAML 文件,exclude 排除匹配文件。匹配规则相对于测试目录。
    projects[].tags根据标签包含或排除用例。
    projects[].variables为 YAML 中的 ${variable} 引用提供值。
    projects[].retry用例失败后的重试次数,默认为 0。
    test.testTimeout每个步骤的默认超时时间,单位为毫秒,默认为 120000。步骤中的 $ 超时配置可覆盖此值。
    test.bail达到失败用例数阈值后停止调度新任务,0 表示不启用阈值。
    output.reportDir报告输出目录,默认为 ./midscene_run/report。

    要通过 CLI 选择项目或指定配置文件,请参阅运行测试。要为单个步骤设置超时和错误处理方式,请参阅设置超时和错误处理。

    脚手架生成的配置默认选择 cases/**/*.{yaml,yml}。未配置 files 时,Midscene Test 会在测试目录下按 **/*.{yaml,yml} 递归查找用例文件。

    选中的文件会去重并按路径排序。执行顺序和重试范围见项目生命周期,并发设置见并发与资源隔离。

    原生 Test 始终生成统一总报告。报告路径会在运行结束后由 CLI 输出,查看方式见查看测试结果。

    配置多个执行项目

    如果需要在不同浏览器或设备上运行测试,可以通过 defineTestProject() 配置多个执行项目。

    顶层 nodes 注册的 Node 对所有项目生效,省略时默认为 []。在单个项目中,也可以通过与 setup、files 平级的 nodes 字段注册局部 Node。局部 Node 仅对当前项目生效,并整体覆盖同名的全局定义;其余全局 Node 仍可使用。同一注册层内出现重复名称时,框架会报错。

    同时配置 Android 和 iOS 项目时,应在各项目中注册对应平台 Agent 的官方 Node。下面的示例从 ./setup 导入两个独立的 setup,各自管理资源并返回 { agent },从 ./nodes 导入共享业务 Node:

    import { AndroidAgent } from '@midscene/android';
    import { IOSAgent } from '@midscene/ios';
    import { defineTestProject } from '@midscene/test/config';
    import { createMidsceneNodes, type MidsceneUIAgent } from '@midscene/test/midscene';
    import { sharedNodes } from './nodes';
    import { androidSetup, iosSetup } from './setup';
    
    interface ProjectContext {
      agent: MidsceneUIAgent;
    }
    
    export default defineTestProject<ProjectContext>({
      nodes: sharedNodes,
      projects: [
        {
          name: 'android-smoke',
          setup: androidSetup,
          nodes: createMidsceneNodes<ProjectContext>({
            agentClass: AndroidAgent,
            getAgent: ({ context }) => context.agent,
          }),
          files: {
            include: ['cases/**/*.{yaml,yml}'],
            exclude: ['cases/**/*.draft.yaml'],
          },
          tags: { include: ['smoke'], exclude: ['manual'] },
          retry: 1,
          variables: { appUri: 'com.example.app' },
        },
        {
          name: 'ios-smoke',
          setup: iosSetup,
          nodes: createMidsceneNodes<ProjectContext>({
            agentClass: IOSAgent,
            getAgent: ({ context }) => context.agent,
          }),
          files: { include: ['cases/**/*.{yaml,yml}'] },
          variables: { appUri: 'com.example.ios' },
        },
      ],
      test: {
        maxConcurrentProjects: 1, // 同时运行的执行项目数量上限
        bail: 0,           // 大于 0 时,失败用例数达到此值后停止调度新任务
        testTimeout: 120_000,
      },
      output: {
        reportDir: './midscene_run/report',
      },
    });

    两个项目可以使用同一份 YAML 文件。收集用例、校验输入和执行测试时,launch 等平台 Node 都使用当前项目中生效的定义。一个项目的局部 Node 不会影响其他项目。

    为各项目生成 Node 说明书

    运行 midscene-test nodes 可以生成 Node 说明书,查看项目中可用的 Node。使用 --project 指定执行项目后,说明书会列出该项目合并全局和局部注册后实际生效的 Node:

    pnpm exec midscene-test nodes --project android-smoke

    声明 projects 后,未指定 --project 时会为每个执行项目分别生成说明书,即使各项目的 Node 定义相同。

    文件命名、输出规则、说明书内容与命令参数见生成当前项目的 Node 说明书。

    并发与资源隔离

    两个上限同时生效:maxConcurrentFiles 控制整次运行同时推进的文件数,maxConcurrentProjects 控制已开始准备、尚未清理完的项目数。两者省略时各自默认为 1,不会根据另一个字段推导。null 表示不限制该项;数值必须是正整数。

    test 配置执行方式
    省略逐个项目完成,每个项目内逐个文件执行
    { maxConcurrentFiles: 4 }一次一个项目,项目内最多同时推进 4 份文件
    { maxConcurrentProjects: 2 }全局仍逐个文件执行,最多保留 2 个项目环境
    { maxConcurrentFiles: null, maxConcurrentProjects: 1 }逐个项目完成,不设全局文件上限
    { maxConcurrentFiles: null, maxConcurrentProjects: null }不设这两个全局上限,仍受项目文件上限和实际资源约束

    执行使用同一个 Node.js 进程内的异步并发。只提高项目上限,不会提高文件并发数。不限制并发也不一定更快,还要考虑浏览器容量、设备数量和模型服务限额。

    使用 maxConcurrentFiles 开启文件并发

    配置含义
    test.maxConcurrentFiles所有项目合计的活动文件上限,默认 1,null 表示不限制
    test.maxConcurrentProjects从开始准备到清理完成的项目数上限,默认 1,null 表示不限制
    projects[].maxConcurrentFiles当前项目的文件上限,只接受正整数;省略时沿用全局文件上限,不能突破全局上限
    projects[].fileSetup为每份文件准备独立环境;项目有效文件上限大于 1(包括不限制)时必填,即使只选中一份文件

    例如,设置 test: { maxConcurrentFiles: 3, maxConcurrentProjects: 2 },Web 和 Android 的文件上限各为 2,可以同时运行两份 Web 加一份 Android,也可以一份 Web 加两份 Android。项目数不会与全局文件上限相乘。

    文件从开始准备到清理完成都占一个文件名额,包括等待设备和用例重试。启动新项目时,先同时取得项目名额和首份文件的名额,再执行 setup;同项目的其他文件等待初始化,不额外占文件名额。具备执行条件的项目轮流领取文件。

    项目在文件之间暂时空闲时,仍保留项目名额。最后一份文件保留文件名额完成项目清理,项目名额也等到实际清理结束才释放。因此,maxConcurrentProjects: 1 会等前一个项目全部清理完再启动下一个;只设置 maxConcurrentFiles: 1,则在项目上限允许时,不同项目的文件仍可交错执行。

    取消或 bail 后,已初始化但暂无活动文件的项目,也在全局文件上限内安排清理。准备和清理的时限见超时与取消机制。

    用 fileSetup 隔离资源

    setup 准备项目共享资源,fileSetup 准备每份文件独享的资源。Web 测试可以共享一个 Browser,每份文件分别创建 BrowserContext、Page 和 Agent,隔离 Cookie 等浏览器状态。

    import {
      defineFileSetup,
      defineProjectSetup,
      defineTestProject,
    } from '@midscene/test/config';
    import { createMidsceneNodes } from '@midscene/test/midscene';
    import { PlaywrightAgent } from '@midscene/web/playwright/agent';
    import { chromium, type Browser } from 'playwright';
    
    type SharedContext = { browser: Browser };
    type FileContext = { agent: PlaywrightAgent };
    
    const setup = defineProjectSetup<SharedContext>({
      name: 'browser',
      async setup({ onTeardown }) {
        const browser = await chromium.launch();
        onTeardown(() => browser.close());
        return { browser };
      },
    });
    
    const fileSetup = defineFileSetup<FileContext, SharedContext>({
      name: 'page',
      async setup({ projectContext, signal, onTeardown }) {
        if (!projectContext) throw new Error('Browser setup did not complete');
        const session = await projectContext.browser.newContext();
        onTeardown(() => session.close());
        signal.throwIfAborted();
        const page = await session.newPage();
        const agent = new PlaywrightAgent(page);
        onTeardown(() => agent.destroy());
        return { agent };
      },
    });
    
    export default defineTestProject<FileContext, SharedContext>({
      test: { maxConcurrentFiles: 3 },
      projects: [
        {
          name: 'web',
          setup,
          fileSetup,
          maxConcurrentFiles: 2,
          files: { include: ['cases/web/**/*.yaml'] },
        },
      ],
      nodes: createMidsceneNodes<FileContext>({
        agentClass: PlaywrightAgent,
        getAgent: ({ context }) => context.agent,
      }),
    });

    YAML 使用已准备好的 Agent,beforeAll 设置视口,beforeEach 为每个用例打开首页:

    beforeAll:
      - setViewportSize:
          width: 1280
          height: 720
    
    beforeEach:
      - gotoUrl: https://your-shop.example
    
    cases:
      - name: 搜索商品
        steps:
          - aiAct: 搜索马克杯
          - aiAssert: 搜索结果中包含马克杯

    本例在 fileSetup 中创建 Agent。文件结束后依次销毁 Agent、关闭 BrowserContext;所有文件清理完成后关闭共享 Browser。

    defineFileSetup<FileContext, SharedContext> 的两个类型分别描述文件返回值和项目返回值。fileSetup 返回完整的文件 context,不会与 projectContext 自动合并;Node 需要的共享字段应显式返回。

    准备函数可以使用以下参数:

    参数用途
    projectContext项目 setup 的返回值;未配置项目 setup 时为 undefined
    project、file当前项目配置,以及文件路径 sourcePath 和执行 ID
    env、signal环境变量和取消通知
    onTeardown()登记文件结束时的清理函数

    只有一组配置时,可以省略 projects,把 setup 和 fileSetup 放在顶层。文件能自行准备全部资源时,也可以只配置 fileSetup。

    Android、iOS 等平台需要为每份并发文件独占分配设备,并创建独立的 Device/Agent。每个 Device 实例只归属于一个 Agent,Agent.destroy() 会销毁对应 Device;设备归还应安排在 Agent 销毁之后。设备池和业务状态重置由项目实现。只有一台手机或一个桌面时,将该项目上限设为 1;多个 Agent 操作同一设备仍会互相干扰。

    文件环境的共享范围和清理顺序见项目生命周期,准备失败后的处理见失败时如何执行。

    共享资源故障需要由 Node 或 fileSetup 显式标记,不能根据 Browser 关闭的报错文案推断。报告方式见异步操作、错误与取消。

    现有配置

    test.maxConcurrency 已移除,加载旧配置会报错并给出迁移示例。旧值控制项目并发,不能只改名为 maxConcurrentFiles。例如,保留旧值 2 的行为,需要设置 test: { maxConcurrentProjects: 2, maxConcurrentFiles: 2 },并把各项目的 maxConcurrentFiles 设为 1。未声明 projects 时,全局文件上限保持 1。

    fileSetup、项目文件上限和准备/清理超时都可以单独配置,不要求显式填写全局并发字段。项目有效文件上限为 1 时,fileSetup 可省略,文件可以共用 setup 返回的环境。

    项目不会自动均分用例。同一 YAML 被两个项目选中,就会分别执行一次;要分批运行,使用互不重叠的 files 或 tags 规则。

    旧 tasks/flow YAML 继续通过 midscene 执行。它的 concurrent / --concurrent、前置 setup 文件和浏览器复用行为保持不变;这些配置与原生 Test 的 setup、fileSetup 分别处理。迁移方式见迁移到 Midscene Test。

    超时与取消机制

    超时配置

    配置控制范围默认值
    test.testTimeout单个步骤,可由步骤中的 $ 覆盖120000 ms
    test.setupTimeout每次项目 setup 或 fileSetup,包括等待设备120000 ms
    test.teardownTimeout每个清理范围,包括清理钩子、等待操作退出和清理函数30000 ms

    省略并发配置时,准备和清理时限仍然生效。取消或 bail 后,各清理范围共用一个基于 teardownTimeout 的收尾时限。

    取消与清理

    步骤超时会取消该步骤的 signal。CLI 收到 SIGINT(如 Ctrl+C)或 SIGTERM 时会取消运行。Node 需要把 signal 传给支持取消的 API,或主动检查状态;异步操作不会自动终止。用法见异步操作、错误与取消。

    运行中断后,框架仍会尝试执行清理。afterEach 和 afterAll 中的 Node 使用尚未取消的清理 signal,仍受步骤超时限制,也不能与同一 Agent 上尚未退出的旧操作重叠执行。清理 Node 应能处理准备步骤只完成一部分的情况。

    onTeardown() 注册的清理函数不会收到新的 signal。避免在其中复用原步骤的 signal,它可能已经取消。

    超时只限制等待结果的时间。操作或清理尚未结束时,框架记录 cleanup pending,继续跟踪资源,等操作实际结束后再释放。此时停止启动新文件。完整失败规则见失败时如何执行。

    编程式 API

    除了通过 CLI 运行测试,你也可以通过 API 将 Midscene Test 集成到其他工具中,例如带图形界面的本地测试面板。以下 API 分别支持加载项目配置、运行整个项目或执行单个用例:

    import { loadTestProject, runTestProject } from '@midscene/test/config';
    import {
      CaseRunner,
      createCaseRunner,
      runWorkflowDocument,
    } from '@midscene/test';
    • loadTestProject(path):异步加载显式指定的 TypeScript(.ts)或 JavaScript ESM(.mjs)项目配置。CLI 未收到 --config 时,会自动查找 midscene.config.ts 或 midscene.config.mjs;如果两者同时存在,则要求明确指定一个。Midscene Test 不支持同步加载。
    • runTestProject():异步发现、运行并汇总整个项目。
    • CaseRunner / createCaseRunner():直接执行纯对象形式的单个用例(不含文件解析和生命周期控制)。
    • runWorkflowDocument():执行单个文档的完整生命周期及内部全部 Case。

    使用注意事项

    • 按需配置。 没有共享资源可只用 fileSetup;有效文件上限为 1 时可省略它,文件共用 setup 的返回值。
    • 并发需要隔离。 有效文件上限大于 1(含不限制)时必须配置 fileSetup,即使只选中一份文件。各文件应使用独立页面、Agent 和设备,后端账号与数据也需自行隔离。
    • 重试不重做准备。 setup、fileSetup、beforeAll 不重跑;每次尝试需要的状态在 beforeEach 中重置。
    • 及时登记清理。 创建资源后立即调用 onTeardown()。先清理文件资源,再清理项目共享资源;清理逻辑应能处理准备只完成一部分的情况。