> For AI agents: the complete documentation index is available at /zh/llms.txt, the full documentation bundle is available at /zh/llms-full.txt.

# 配置测试工程

`midscene.config.ts` 是 Midscene Test 的工程配置文件。你可以在其中定义浏览器或设备的初始化逻辑、注册供 YAML 调用的 Node，以及设置并发数、超时时间等运行参数。本文介绍配置文件的基本结构、Agent 与各平台接入方式、多执行项目（Project）管理，以及文件级资源隔离。

## 配置文件结构

测试工程通过 `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 直接加载 |

默认情况下，Midscene Test 会在测试根目录下查找 `midscene.config.ts` 或 `midscene.config.mjs`。如果两个文件同时存在，框架会直接报错提示冲突，而不会静默选择其中一个；此时可通过 `--config <路径>` 明确指定要使用的配置文件。

核心配置项如下：

| 字段          | 用途                                                                                                                    |
| ----------- | --------------------------------------------------------------------------------------------------------------------- |
| `setup`     | 准备执行项目（Project）内共享的浏览器或设备资源。                                                                                          |
| `fileSetup` | 为每份 YAML 文件准备独立的运行环境（串行与并发执行时均可使用）。                                                                                   |
| `nodes`     | 注册所有执行项目共享的全局 Node。                                                                                                   |
| `projects`  | 声明一个或多个执行项目（Project），分别配置运行环境与用例匹配规则。声明 `projects` 后，`setup` 和 `fileSetup` 需配置在各项目内部；省略 `projects` 时，顶层配置将作为默认执行项目运行。 |
| `test`      | 设置并发数、失败中止阈值（`bail`）和默认步骤超时时间。                                                                                        |
| `output`    | 设置 HTML 测试报告的输出目录。                                                                                                    |

下文先以单个执行项目为例说明如何配置运行环境。如果需要管理多个执行项目，或为不同项目注册各自的 Node，请参阅[配置多个执行项目](#配置多个执行项目)。

## 配置运行环境

### 完整示例：Playwright

下面是一个仅使用 `setup` 的串行配置示例：同一个执行项目中的所有 YAML 文件共用同一个浏览器页面。如果希望每份 YAML 文件使用相互隔离的浏览器上下文，可以配合使用 [`fileSetup`](#用-filesetup-隔离资源)（无需开启并发即可使用）。

如果要在已有工程中手动接入 Web 测试，请先安装依赖：

```bash
pnpm add -D @midscene/test @midscene/web playwright
pnpm exec playwright install chromium
```

运行测试前，还需按照[使用模型 API Key](/zh/model-api-setup.md)配置模型环境变量。如果你将模型配置保存在 `.env` 文件中，手写 `midscene.config.ts` 时需要自行加载（例如安装 `dotenv` 并在配置文件顶部引入 `import 'dotenv/config'`），或者直接通过 Shell / CI 注入环境变量，详见[加载环境变量](/zh/midscene-test/running.md#加载环境变量)。

```ts filename=midscene.config.ts
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 实例——首次调用时创建 `PlaywrightAgent` 并缓存到 `context.agent` 中，后续步骤直接复用该实例及其关联的 `page`。
- `nodes`：注册通用 AI 操作和 Playwright 平台操作，供 YAML 用例调用。
- `projects`：声明名为 `chromium` 的执行项目，将 `playwrightSetup` 与 `cases/` 目录下的 YAML 用例关联起来。

完成上述配置后，即可开始[编写 YAML 用例](/zh/midscene-test/writing.md)并[运行测试](/zh/midscene-test/running.md)。下面进一步说明如何管理资源的共享与隔离。

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

`defineProjectSetup()` 用于定义执行项目（Project）级别的初始化逻辑，并挂载到 `setup` 字段上。测试运行时，每个执行项目只会调用一次自身的 `setup()`，返回的对象将作为共享上下文供该项目内的步骤使用。

如果你希望每个 YAML 文件拥有独立的运行环境，可以使用 `defineFileSetup()` 定义文件级初始化逻辑，并挂载到 `fileSetup` 字段上。`fileSetup` 会接收项目级 `setup` 的返回值，在此基础上为当前文件创建独立资源，详见下一节[用 fileSetup 隔离资源](#用-filesetup-隔离资源)。

注意，`setup` 创建的共享资源不会在每条用例开始前自动重置。如需在用例间重置页面、Cookie 或业务状态，请在 YAML 的[生命周期钩子](/zh/midscene-test/execution.md#yaml-中的生命周期钩子)中调用相应的 Node；自定义 Node 之间读写共享数据的方式请参阅[跨 Node 共享上下文](/zh/midscene-test/extend.md#跨-node-共享上下文)。

创建 Browser、Agent 等资源后，应立即通过 `onTeardown()` 注册清理函数。即便 `setup` 过程或用例执行失败，框架也会在执行项目结束时逆序（后进先出）调用已注册的清理函数——例如在上面的 Playwright 示例中，会先销毁 `agent`，再关闭 `browser`。注意，在 `setup` 中注册的 `onTeardown()` 仅在整个执行项目结束时触发；而在自定义 Node 内部注册的 `onTeardown()` 则具有独立的[清理时机](/zh/midscene-test/extend.md#资源的生命周期与清理)。

### 用 fileSetup 隔离资源

当多个 YAML 文件需要隔离运行环境（或开启文件并发）时，可以用 `setup` 准备项目级共享资源，再用 `fileSetup` 为每个文件创建独占资源。例如在 Web 测试中，可以让整个项目共享同一个 Browser 进程，而为每个 YAML 文件单独创建 `BrowserContext`、`Page` 和 `Agent`，从而隔离各文件之间的 Cookie 与本地存储：

```ts filename=midscene.config.ts
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 文件直接使用 `fileSetup` 准备好的 Agent，在 `beforeAll` 中设置视口大小，在 `beforeEach` 中为每条用例打开首页：

```yaml filename=cases/web/search.yaml
beforeAll:
  - setViewportSize:
      width: 1280
      height: 720

beforeEach:
  - gotoUrl: https://your-shop.example

cases:
  - name: 搜索商品
    steps:
      - aiAct: 搜索马克杯
      - aiAssert: 搜索结果中包含马克杯
```

在这个示例中，每个 YAML 文件执行完毕后会依次销毁自身的 `Agent` 并关闭 `BrowserContext`；当该项目的所有文件均清理完毕后，才会关闭项目级共享的 `Browser`。

`defineFileSetup<FileContext, SharedContext>` 的两个泛型参数分别代表**文件级 `context` 类型**和**项目级 `projectContext` 类型**。注意：`fileSetup` 的返回值会直接作为该文件的完整 `context` 传给 Node，框架不会将它与 `projectContext` 自动合并；如果 Node 需要访问 `projectContext` 中的字段，请在 `fileSetup` 的返回对象中显式透传。

`fileSetup` 的 `setup` 函数接收以下参数：

| 参数               | 用途                                              |
| ---------------- | ----------------------------------------------- |
| `projectContext` | 项目级 `setup` 的返回值；未配置项目级 `setup` 时为 `undefined`。 |
| `project`、`file` | 当前执行项目的配置对象，以及当前文件的路径（`sourcePath`）与执行 ID。      |
| `env`、`signal`   | 环境变量映射与取消信号（`AbortSignal`）。                     |
| `onTeardown()`   | 注册当前文件执行结束时的清理函数。                               |

如果工程只有一组环境配置，可以省略 `projects` 数组，直接将 `setup` 和 `fileSetup` 写在顶层配置中；如果每个文件能独立完成所有资源初始化，也可以只配置 `fileSetup` 而不配置 `setup`。

对于 Android、iOS 等移动端或桌面端平台，若要开启文件并发，必须为每个并发文件分配独占的物理设备或模拟器，并分别创建独立的 `Device` 和 `Agent`。每个 `Device` 实例只能归属于一个 `Agent`，调用 `Agent.destroy()` 时会一并销毁其关联的 `Device`，因此设备归还逻辑应当排在 `Agent` 销毁之后。如果你手头只有一台测试手机或一个桌面环境，请将该项目的并发上限设为 `1`，否则多个 Agent 同时操作同一台设备会互相干扰。

关于文件环境的共享范围与清理顺序，请参阅[生命周期与并发](/zh/midscene-test/execution.md#项目生命周期)；关于初始化失败后的调度行为，请参阅[失败时如何执行](/zh/midscene-test/execution.md#失败时如何执行)。此外，如果项目级共享资源（如共享 Browser）在运行期间意外不可用，应在 `fileSetup` 或自定义 Node 中显式标记共享资源错误，框架不会仅凭浏览器关闭的报错文本自动推断，详见[异步操作、错误与取消](/zh/midscene-test/extend.md#异步操作错误与取消)。

## 注册官方 Node

### 通用 AI 操作与 Agent 接入

Midscene Test 内核不会隐式注册任何 Node。使用 `midscene-test create` 创建项目时，生成的配置文件会调用 `@midscene/test/midscene` 导出的 `createMidsceneNodes()` 来注册所选平台的官方 Node。如果你手写配置文件，需要将 `createMidsceneNodes()` 返回的数组显式传入顶层 `nodes` 或 `projects[].nodes` 中才会生效：

```ts
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()` 时需传入两个核心参数：

- `agentClass`：指定平台对应的 Midscene GUI Agent 类（必须实现 `getTestRunnerNodeDefinitions()`，否则会在注册阶段报错）。传入 `PlaywrightAgent`、`PuppeteerAgent`、`AndroidAgent`、`IOSAgent` 或 `HarmonyAgent`（及其 Page/Browser 变体）时，会同时导出通用 AI Node（如 `aiAct`、`aiAssert`、数据提取、视觉定位等）、内置 `wait` Node 以及该平台专属的操作 Node；而基础 `Agent` 和 `ComputerAgent` 则提供通用 AI Node 与 `wait`。
- `getAgent`：声明在步骤执行期间如何从当前 `context` 中获取 Agent 实例（`context` 的具体字段结构由你自行定义）。

在 Android、iOS 等移动端平台接入 Agent 时，请注意 `Device` 的生命周期约束：每个 `Device` 实例只能绑定给一个 `Agent`，调用 `Agent.destroy()` 时会自动销毁该 `Device`。因此，请为每个 Agent 创建独立的 `Device` 实例，切勿在 Agent 销毁后复用同一个 `Device`。

注册完成后，即可在 YAML 用例中直接调用这些官方 Node。完整的可用 Node 名称与参数字段，请以当前项目生成的 [Node 说明书](/zh/midscene-test/writing.md#生成当前项目的-node-说明书)为准。

#### 为多个 Midscene Agent 的 Node 命名

同一个工作流使用多个 Midscene Agent 时，可以通过 `namespace` 为各组 Node 注册不同的名称。让 setup 在 context 中返回独立的 Midscene Agent 实例，再将两组 Node 注册到同一个 Project：

```ts
import { Agent } from '@midscene/core/agent';
import { createMidsceneNodes } from '@midscene/test/midscene';

interface MultiAgentContext {
  mobile: Agent;
  web: Agent;
}

const nodes = [
  ...createMidsceneNodes<MultiAgentContext>({
    namespace: 'mobile',
    agentClass: Agent,
    getAgent: ({ context }) => context.mobile,
  }),
  ...createMidsceneNodes<MultiAgentContext>({
    namespace: 'web',
    agentClass: Agent,
    getAgent: ({ context }) => context.web,
  }),
];
```

将 `nodes` 加入项目配置后，YAML 使用注册后的名称调用：

```yaml
cases:
  - name: 跨设备完成一个 Todo
    steps:
      - mobile.aiAct: 创建一个标题为“Buy milk”的 Todo。
      - web.aiWaitFor: 列表中显示标题为“Buy milk”的 Todo。
      - web.aiTap: “Buy milk”旁边的复选框
```

namespace 会给返回的每个 Node 加上前缀，包括通用 AI 操作、平台操作和 `wait`。例如，上述注册提供 `mobile.aiAct` 和 `mobile.wait`，不会同时注册不带前缀的名称。不传 `namespace` 时保留原有名称。

namespace 必须是非空字符串，不能包含空白字符、首尾的点或空的分段；支持 `checkout.mobile` 这样的嵌套名称。无效值会在注册阶段报错，此时尚未获取 Agent。

namespace 只改变 Node 名称。运行时仍由 `getAgent` 或 `agentProvider.getAgent` 选择 Agent，执行记录仍关联到对应 Step。namespace 不创建资源，也不提供并发隔离。不同 Project 可以分别注册相同的 Node 名称，无需添加 namespace。

### Playwright

通过 `createMidsceneNodes({ agentClass: PlaywrightAgent, getAgent })` 会自动注册 Web 专属的 `gotoUrl`、`setCookies`、`clearCookies` 和 `setViewportSize` 四个 Node。使用前需在项目中安装同级依赖 `playwright`：

```bash
pnpm add -D playwright
```

**安全设置 Cookie**：调用 `setCookies` 时，**不支持在 YAML 中直接明文填写 Cookie 值**。这是因为 Midscene Test 会将 Node 的输入参数记录到运行结果文件中，明文填写会导致敏感凭证泄露。

你必须在 `cookiesEnv`（环境变量名）、`profile`（预置配置名）或 `storageStatePath`（Playwright 状态文件路径）三者中任选其一，以**引用方式**传入 Cookie。`setCookies` 仅在执行瞬间读取真实 Cookie 并注入 `BrowserContext`，而在运行结果中只记录引用名称与 Cookie 条数，从而避免敏感信息进入测试报告或结果文件：

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

关于 Cookie 引用的补充说明：

- 环境变量默认从 `process.env` 读取，内容可以是 `Cookie` 请求头字符串、Cookie JSON 数组或 Playwright `storageState` JSON。
- 相对路径的 `storageStatePath` 默认相对于当前工作目录解析。如需自定义环境变量读取、Profile 映射或路径解析逻辑，可在实例化 Agent 时通过 `testRunner` 选项传入 `getEnv`、`getCookieProfile` 或 `resolveStorageStatePath` 回调（回调中的 `context` 为当前 Agent 实例）。
- 引用机制仅用于防止 Cookie 被写入 Midscene Test 的持久化结果中；环境变量、Profile 和 `storageState` 文件本身仍需妥善保管，切勿将包含真实 Cookie 的文件提交到代码仓库。

### Puppeteer

Puppeteer 同样提供同名的 `gotoUrl`、`setCookies`、`clearCookies` 和 `setViewportSize` Node，参数结构与 Cookie 脱敏规则与 Playwright 完全一致。使用时需安装 `puppeteer` 并将 `PuppeteerAgent`（或 `PuppeteerBrowserAgent`）传给 `createMidsceneNodes()`：

```bash
pnpm add -D puppeteer
```

```ts
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;
  },
});
```

为了方便同一份 YAML 用例在 Playwright 和 Puppeteer 之间平滑切换，`gotoUrl.waitUntil` 推荐使用跨浏览器通用的 `domcontentloaded`、`load` 或 `networkidle`（Puppeteer 适配层会自动将 `networkidle` 映射为 `networkidle0`）。两者的专有枚举值（Playwright 的 `commit`，Puppeteer 的 `networkidle0` 与 `networkidle2`）仍可在各自驱动下使用，但跨驱动混用时会直接报错。此外，调用 `gotoUrl` 传入相对路径时，会优先基于 `testRunner.baseURL` 解析；若未配置 `baseURL`，则基于当前页面的 HTTP(S) 地址解析。

### Android

接入以下移动端平台时，请在 `ProjectContext` 中声明对应平台类型的 `agent`，并在 `setup` 返回的对象中提供该实例。

使用 `createMidsceneNodes({ agentClass: AndroidAgent, getAgent })` 会注册 Android 专属的 `launch`、`terminate`、`runAdbShell`、`back`、`home` 和 `recentApps`：

```ts
import { AndroidAgent } from '@midscene/android';
import { createMidsceneNodes } from '@midscene/test/midscene';

const androidNodes = createMidsceneNodes<ProjectContext>({
  agentClass: AndroidAgent,
  getAgent: ({ context }) => context.agent,
});
```

```yaml
beforeEach:
  - runAdbShell:
      command: pm clear com.example.app
      options:
        timeout: 5000
  - launch:
      uri: com.example.app
```

`runAdbShell` 的完整命令输出会保存在该步骤的执行结果中。

### iOS

使用 `createMidsceneNodes({ agentClass: IOSAgent, getAgent })` 会注册 iOS 专属的 `launch`、`terminate`、`runWdaRequest`、`home` 和 `appSwitcher`：

```ts
import { IOSAgent } from '@midscene/ios';
import { createMidsceneNodes } from '@midscene/test/midscene';

const iosNodes = createMidsceneNodes<ProjectContext>({
  agentClass: IOSAgent,
  getAgent: ({ context }) => context.agent,
});
```

```yaml
steps:
  - launch:
      uri: com.example.app
  - runWdaRequest:
      request:
        method: GET
        endpoint: /status
  - terminate:
      uri: com.example.app
```

`runWdaRequest` 的完整响应数据会保存在该步骤的执行结果中。

### HarmonyOS

使用 `createMidsceneNodes({ agentClass: HarmonyAgent, getAgent })` 会注册 HarmonyOS 专属的 `launch`、`terminate`、`runHdcShell`、`back`、`home` 和 `recentApps`：

```ts
import { HarmonyAgent } from '@midscene/harmony';
import { createMidsceneNodes } from '@midscene/test/midscene';

const harmonyNodes = createMidsceneNodes<ProjectContext>({
  agentClass: HarmonyAgent,
  getAgent: ({ context }) => context.agent,
});
```

```yaml
steps:
  - runHdcShell:
      command: bm dump -a
  - home: {}
```

`runHdcShell` 的完整命令输出会保存在该步骤的执行结果中。

## 配置执行项目

执行项目（Project）用于将特定的运行环境与一组测试用例绑定。即便工程里只有一个执行项目，你也可以在其中配置用例筛选规则、项目变量和失败重试次数；当需要在不同浏览器、设备或环境上运行测试时，则可以在 `projects` 数组中声明多个执行项目。

### 选择用例与控制执行

| 配置项                    | 含义                                                                                               |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| `projects[].files`     | `include` 指定要包含的 YAML 文件，`exclude` 排除匹配的文件（路径相对于测试根目录）。                                          |
| `projects[].tags`      | 按 `include` / `exclude` 标签规则筛选用例。                                                                |
| `projects[].variables` | 定义项目变量，供 YAML 中的 `${variable}` 占位符读取。                                                            |
| `projects[].retry`     | 用例执行失败后的最大重试次数，默认为 `0`。                                                                          |
| `test.testTimeout`     | 单个步骤的默认超时时间（毫秒），默认为 `120000`。步骤内的 `$.timeout` 可覆盖该默认值。                                           |
| `test.bail`            | 失败用例数达到该阈值后停止调度新任务；设为 `0` 表示不限制。                                                                 |
| `output.reportDir`     | 可选的报告目录覆盖值，相对测试项目解析；默认位于产物根下的 `report/`，未设置 `MIDSCENE_RUN_DIR` 时为 `midscene_run/report/`。        |
| `output.reportFormat`  | `'single-html'`（默认）将执行数据和图片打包在一个 HTML 中，可直接离线打开；`'html-and-external-assets'` 生成目录报告，需通过 HTTP 预览。 |

脚手架生成的配置默认匹配 `cases/**/*.{yaml,yml}`；如果未配置 `files`，Midscene Test 会按 `**/*.{yaml,yml}` 在测试根目录下递归查找用例文件，自动去重并按路径排序。

关于如何通过命令行筛选运行目标，请参阅[运行测试](/zh/midscene-test/running.md)；单个步骤的超时与容错配置请参阅[设置超时和错误处理](/zh/midscene-test/writing.md#设置超时和错误处理)；执行顺序、重试与并发规则请参阅[生命周期与并发](/zh/midscene-test/execution.md)。所有执行项目的运行结果最终都会汇总生成一份统一的 HTML 报告，查看与归档方式见[运行测试](/zh/midscene-test/running.md)。

### 配置多个执行项目

当需要在多个平台或设备上运行测试时，可以在 `defineTestProject()` 的 `projects` 数组中配置多个执行项目。

在顶层 `nodes` 中注册的 Node 对所有执行项目全局生效（省略时默认为 `[]`）。同时，每个执行项目也可以通过自身的 `nodes` 字段注册项目级 Node——项目级 Node 仅对当前项目生效，若与全局 Node 同名则会覆盖全局定义。注意，在同一层级内重复注册同名 Node 会直接报错。

例如，同时配置 Android 和 iOS 两个执行项目时，可以将跨端通用的业务 Node 放在顶层 `nodes`，而在各自项目中分别注册对应平台的官方 Node：

```ts filename=midscene.config.ts
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,
  },
});
```

通过这种方式，两个执行项目可以复用同一份 YAML 用例文件：在收集用例、校验参数和实际执行时，`launch` 等同名平台 Node 会自动绑定到当前项目注册的实现上，互不干扰。

### 为各项目生成 Node 说明书

运行 `midscene-test nodes` 可以根据项目的注册配置生成 Node 说明书。传入 `--project` 参数时，生成的说明书会列出该执行项目合并全局与项目级 Node 后最终生效的完整列表：

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

如果配置了 `projects` 且未传入 `--project` 参数，命令默认会为每个执行项目分别生成一份独立的说明书。关于文件命名规则与说明书结构，请参阅[生成当前项目的 Node 说明书](/zh/midscene-test/writing.md#生成当前项目的-node-说明书)。

## 编程式 API

除了使用 CLI 运行测试外，你也可以通过编程式 API 将 Midscene Test 集成到自定义工具链（如内部测试平台或本地 GUI 面板）中：

```ts
import { loadTestProject, runTestProject } from '@midscene/test/config';
import {
  CaseRunner,
  createCaseRunner,
  runWorkflowDocument,
} from '@midscene/test';
```

- **`loadTestProject(path)`**：异步加载指定路径的 `.ts` 或 `.mjs` 配置文件（不支持同步加载）。
- **`runTestProject()`**：异步发现、执行并汇总整个测试工程的运行结果。
- **`runWorkflowDocument()`**：执行单个工作流文档（包含其完整生命周期钩子与内部所有 Case）。
- **`CaseRunner` / `createCaseRunner()`**：直接执行纯对象形式的单条用例（不包含文件解析与文档级生命周期）。

以下示例展示如何在代码中运行整个测试工程并传递退出码：

```ts
import { runTestProject } from '@midscene/test/config';

try {
  const result = await runTestProject({
    projectRoot: process.cwd(),
    configPath: './midscene.config.ts',
    resultDir: './test-results',
  });
  console.log(result.summaryPath, result.reportPath);
  process.exitCode = result.exitCode;
} catch (error) {
  console.error(error);
  process.exitCode = 1;
}
```

## 下一步

- [生命周期与并发](/zh/midscene-test/execution.md)：了解资源共享范围、重试机制、并发调度、超时与取消规则。
- [运行测试](/zh/midscene-test/running.md)：了解命令行参数、路径解析规则与加载环境变量方式。
