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

# Configure test projects

`midscene.config.ts` is the project configuration file for Midscene Test. Use it to configure browser or device initialization, register Nodes for test cases to call, and set execution options such as concurrency and timeouts. This guide covers configuration structure, Agent and platform integration, multiple execution projects, and file resource isolation.

## Configuration structure

Use `defineTestProject()` to define and export your project configuration. An Execution Project selects cases and supplies their runtime environment.

Midscene Test supports the following configuration files:

| How the configuration is selected                                                             | Supported file                                | Loading behavior                                                                                                  |
| --------------------------------------------------------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Automatic discovery, when no configuration path is provided                                   | `midscene.config.ts` or `midscene.config.mjs` | `.ts` is transformed from TypeScript; `.mjs` is loaded by Node.js as native ESM                                   |
| An explicit path passed through the CLI's `--config <path>` option or `loadTestProject(path)` | `.ts` or `.mjs`                               | `.ts` is transformed from TypeScript; `.mjs` is loaded by Node.js as native ESM without TypeScript transformation |

Midscene Test loads `midscene.config.ts` or `midscene.config.mjs` by default. If both files exist, it reports a conflict instead of choosing one silently; use `--config <path>` to select the intended file explicitly.

Prepare shared project resources in `setup`, prepare resources for each file in `fileSetup`, and register the corresponding Agent Nodes through `nodes`.

| Field       | Purpose                                                                                                                                                                                                     |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setup`     | Prepares browser or device resources shared by a project.                                                                                                                                                   |
| `fileSetup` | Prepares an environment for each YAML file, in both sequential and concurrent runs.                                                                                                                         |
| `nodes`     | Registers Nodes shared by all execution projects.                                                                                                                                                           |
| `projects`  | Declares named execution projects with their own resources and case selection. Place `setup` and `fileSetup` inside each project when declared; otherwise, top-level settings apply to the default project. |
| `test`      | Sets concurrency, the failure threshold, and the default step timeout.                                                                                                                                      |
| `output`    | Sets the report output directory.                                                                                                                                                                           |

The example below starts with a single execution project. To manage multiple execution projects or register different Nodes for each project, see [Configure multiple execution projects](#configure-multiple-execution-projects).

## Configure the runtime environment

### Complete example: Playwright

This sequential configuration uses only `setup`: all files in the execution project share one page. Add [`fileSetup`](#isolate-resources-with-filesetup) when each file needs its own environment, even without concurrency.

To add Midscene Test to an existing project manually, first install `@midscene/test`, `@midscene/web`, and `playwright` (`pnpm add -D @midscene/test @midscene/web playwright`), then run `pnpm exec playwright install chromium` to install the browser. Before running tests, follow [Model configuration](/model-api-setup.md) to set the required environment variables, including your API Key.

For a handwritten configuration, load `.env` explicitly (for example, install `dotenv`, import `dotenv/config` at the top, and run from the project root), or supply shell/CI variables. See [Environment loading](/midscene-test/running.md#environment-loading).

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

// Define browser environment setup and cleanup
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 the project configuration
export default defineTestProject<ProjectContext>({
  projects: [
    {
      name: 'chromium',
      setup: playwrightSetup,
      files: { include: ['cases/**/*.{yaml,yml}'] },
    },
  ],
  nodes: midsceneNodes,
});
```

The configuration connects these parts:

- `setup` creates the browser and page and registers cleanup functions through `onTeardown()`.
- `context` holds runtime resources. Nodes obtain the Agent through `getAgent`, which creates an instance on first use and stores it in `context.agent` for subsequent calls. Playwright Nodes use that Agent’s page.
- `nodes` registers common AI operations and Playwright operations for YAML cases to call.
- `projects` declares an execution project named `chromium`, connecting its `setup` to the YAML files under `cases/`.

You can now [write YAML test cases](/midscene-test/writing.md) and [run tests](/midscene-test/running.md#run-tests). The next section explains how these resources are shared and cleaned up.

### Create and clean up shared resources with setup

`defineProjectSetup()` defines project preparation, assigned through the `setup` field. During a test run, each execution project calls its `setup()` function once. The example above returns a browser and page for the project's Nodes to share.

For file isolation, define per-file preparation with `defineFileSetup()` and assign it through `fileSetup`. It receives the project setup's return value and creates resources for the current file. See [Isolate resources with fileSetup](#isolate-resources-with-filesetup) for the relationship and an example.

Shared resources are not automatically recreated before each case. To reset pages, cookies, or business state, call the appropriate Nodes through YAML [lifecycle hooks](/midscene-test/execution.md#yaml-lifecycle-hooks). For reading and updating shared data in custom Nodes, see [Share context across Nodes](/midscene-test/extend.md#share-context-across-nodes).

After creating a browser, Agent, or other resource, immediately register its cleanup function with `onTeardown()`. The framework attempts the registered cleanup functions when the project finishes, even if setup or test execution fails. Cleanup runs in reverse registration order: in the Playwright example above, the Agent is destroyed before the browser closes.

The `onTeardown()` callbacks registered here run when the project finishes. Cleanup functions registered inside Nodes have their own [execution scope](/midscene-test/extend.md#resource-lifecycle-and-cleanup).

### Isolate resources with fileSetup

Use `setup` for shared project resources and `fileSetup` for resources owned by each file. Web tests can share one Browser while creating a separate BrowserContext, Page, and Agent per file to isolate cookies and other browser state.

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

The YAML uses the prepared Agent: `beforeAll` sets the viewport, and `beforeEach` opens the home page for every case:

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

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

cases:
  - name: Search for mugs
    steps:
      - aiAct: Search for mugs
      - aiAssert: The search results include mugs
```

This example creates the Agent in `fileSetup`. File cleanup destroys the Agent and then closes the BrowserContext. The shared Browser closes after all file cleanup.

The type parameters in `defineFileSetup<FileContext, SharedContext>` describe the file return value and the project return value, respectively. `fileSetup` returns the complete file context; it is not automatically merged with `projectContext`. Explicitly return shared fields that Nodes need.

The preparation function receives these arguments:

| Argument          | Purpose                                                                              |
| ----------------- | ------------------------------------------------------------------------------------ |
| `projectContext`  | The project `setup` return value, or `undefined` when no project setup is configured |
| `project`, `file` | Current project configuration, plus the file's `sourcePath` and execution IDs        |
| `env`, `signal`   | Environment variables and cancellation notifications                                 |
| `onTeardown()`    | Registers cleanup to run when the file finishes                                      |

For a single configuration, omit `projects` and put `setup` and `fileSetup` at the top level. If a file can prepare all its resources itself, configure only `fileSetup`.

On Android, iOS, and other device platforms, allocate an exclusive device to each concurrent file and create a separate Device/Agent. Each Device instance belongs to one Agent, and `Agent.destroy()` destroys that Device; return the device allocation after destroying the Agent. Project code supplies the device pool and business-state reset. With one phone or one desktop, set that project's limit to `1`; multiple Agents operating the same device still interfere with each other.

See [Project lifecycle](/midscene-test/execution.md#project-lifecycle) for context scope and cleanup order, and [Handling failures](/midscene-test/execution.md#handling-failures) for preparation failures.

Nodes or `fileSetup` must explicitly report shared-resource failures; Browser closure messages alone do not establish ownership. See [Asynchronous operations, errors, and cancellation](/midscene-test/extend.md#asynchronous-operations-errors-and-cancellation) for how to report them.

## Register official Nodes

### Common AI operations and Agent integration

The Midscene Test kernel does not register Nodes implicitly. Configuration generated by `midscene-test create` calls `createMidsceneNodes()` from `@midscene/test/midscene` to register official Nodes for the selected platform. In handwritten configuration, explicitly add the returned definitions to top-level `nodes` or `projects[].nodes`.

`createMidsceneNodes()` returns the common and platform Node definitions declared by `agentClass.getTestRunnerNodeDefinitions()`, plus the Test-provided `wait` Node. Common capabilities cover AI operations, assertions and extraction, interface location, and report recording. Platform Agents add browser or device operations. The website does not maintain a duplicate exhaustive list; use the current Project's generated [Node Spec](/midscene-test/writing.md#generate-the-current-projects-node-spec) for exact names and inputs.

When calling `createMidsceneNodes()`, use `agentClass` to specify the platform-specific Midscene GUI Agent class and a `getAgent` callback to supply the Agent instance at runtime:

```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()` does not mutate a global registry; its Nodes become available only after the returned array is included in project configuration. `agentClass` is the sole source of Agent-backed Node definitions, while `getAgent` describes how execution obtains an Agent instance from the current context. The factory throws during registration if the class does not expose `getTestRunnerNodeDefinitions()`. PlaywrightAgent, PlaywrightPageAgent, PlaywrightBrowserAgent, PuppeteerAgent, PuppeteerPageAgent, PuppeteerBrowserAgent, AndroidAgent, IOSAgent, and HarmonyAgent provide common and platform Node definitions together. The base Agent and ComputerAgent only provide common Agent Nodes.

#### Name Nodes for multiple Midscene Agents

When one workflow uses multiple Midscene Agents, set `namespace` to register their Nodes under different names. Have your setup return separate Midscene Agent instances in the context, then register both sets in the same 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,
  }),
];
```

Add `nodes` to your Project configuration. YAML calls the registered names:

```yaml
cases:
  - name: Complete a Todo across devices
    steps:
      - mobile.aiAct: Create a Todo titled "Buy milk".
      - web.aiWaitFor: The Todo "Buy milk" is visible.
      - web.aiTap: the checkbox beside "Buy milk"
```

The namespace prefixes every returned Node, including common AI operations, platform operations, and `wait`. For example, this registration provides `mobile.aiAct` and `mobile.wait`; it does not also register the unprefixed names. Omit `namespace` to keep the original names.

A namespace must be a non-empty string without whitespace, leading or trailing dots, or empty dot-separated segments. Nested namespaces such as `checkout.mobile` are supported. Invalid values fail during registration, before an Agent is acquired.

Namespaces only change Node names. `getAgent` or `agentProvider.getAgent` still selects the runtime Agent, and execution traces remain associated with the corresponding Steps. Namespaces do not create resources or provide concurrency isolation. Separate Projects can register the same Node names without a namespace.

For an Android/iOS registration example, see [Configure multiple execution projects](#configure-multiple-execution-projects).

When integrating an Agent on device platforms such as Android or iOS, observe resource ownership. Each device instance belongs to one Agent. `Agent.destroy()` also destroys its device. Create separate resources for each execution project, and do not reuse a device after its Agent is destroyed.

Register each platform through `createMidsceneNodes()`: specify its Agent class with `agentClass` and supply the instance through `getAgent`. You can choose your own field names for the project `context`.

Once registered, these Nodes can be called from YAML. See [Calling other Nodes](/midscene-test/writing.md#calling-other-nodes) for parameter syntax, and consult the project-generated Node Spec for specific fields.

### Playwright

Use `createMidsceneNodes({ agentClass: PlaywrightAgent, getAgent })` to automatically register `gotoUrl`, `setCookies`, `clearCookies`, and `setViewportSize`. Before using these Nodes, install `playwright` in your project. It is a peer dependency of `@midscene/web`:

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

Cookies are read from `process.env` by default. Only configure Agent `testRunner` options such as `getEnv`, `getCookieProfile`, or `resolveStorageStatePath` when you need custom cookie sources. The `context` in these callbacks is the current Agent.

`setCookies` does not accept cookie values in YAML. Midscene Test persists every
Node input in the run result. An inline cookie would be copied into that
record.

Use exactly one of `cookiesEnv`, `profile`, or `storageStatePath` as a cookie
reference. The Node resolves the actual cookies only at execution time and
passes them directly to the Playwright BrowserContext. On success, the result records the reference name and cookie count. On failure,
the underlying error may contain cookie values.

An environment variable may contain a Cookie header, a JSON cookie array, or
Playwright storage-state JSON. Relative storage-state paths resolve from the
current working directory by default; use `resolveStorageStatePath` when a
project needs a different root. Protect the environment variable, profile, or storage-state file. Do not commit storage-state files containing real cookies.

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

See the [current project's Node Spec](/midscene-test/writing.md#generate-the-current-projects-node-spec) for navigation inputs.

### Puppeteer

Puppeteer exposes the same `gotoUrl`, `setCookies`, `clearCookies`, and `setViewportSize` Node names. Install the `puppeteer` peer dependency and pass `PuppeteerAgent` (or `PuppeteerBrowserAgent`) to `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;
  },
});
```

The Playwright and Puppeteer integrations use the same Web Node schemas and execution implementation. Browser-specific code is limited to a driver adapter. For portable YAML, use `domcontentloaded`, `load`, or `networkidle` for `gotoUrl.waitUntil`; the Puppeteer adapter maps `networkidle` to `networkidle0`. Existing engine-specific values remain supported: Playwright accepts `commit`, while Puppeteer accepts `networkidle0` and `networkidle2`. Using one of those values with the other driver produces an explicit error. Both integrations accept the same cookie references. Relative URLs resolve against `testRunner.baseURL`, then against the current HTTP(S) page URL.

### Android

To use the device presets below, declare an `agent` of the corresponding platform type in `ProjectContext` and include that instance in the object returned by `setup`. Each platform example is independent; choose the one you need.

Use `createMidsceneNodes({ agentClass: AndroidAgent, getAgent })` to register `launch`, `terminate`, `runAdbShell`, `back`, `home`, and `recentApps`. The supplied Agent must provide the methods these Nodes call:

```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
```

The complete response from `runAdbShell` is saved in the Node result.

### iOS

Use `createMidsceneNodes({ agentClass: IOSAgent, getAgent })` to register `launch`, `terminate`,
`runWdaRequest`, `home`, and `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
```

The complete response from `runWdaRequest` is saved in the Node result.

### HarmonyOS

Use `createMidsceneNodes({ agentClass: HarmonyAgent, getAgent })` to register `launch`, `terminate`,
`runHdcShell`, `back`, `home`, and `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: {}
```

The complete response from `runHdcShell` is saved in the Node result.

## Configure execution projects

An execution project connects a runtime environment to its cases. Even a single execution project can configure case selection, variables, and retries. Declare multiple execution projects to run across browsers, devices, or environments.

### Select cases and control execution

| Setting                | Meaning                                                                                                                                                                                      |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `projects[].files`     | `include` selects YAML files; `exclude` removes matches. Patterns are relative to the test directory.                                                                                        |
| `projects[].tags`      | Includes or excludes cases by tag.                                                                                                                                                           |
| `projects[].variables` | Supplies values for YAML `${variable}` references.                                                                                                                                           |
| `projects[].retry`     | Number of retries for a failed case; defaults to `0`.                                                                                                                                        |
| `test.testTimeout`     | Default timeout for each step in milliseconds; defaults to `120000`. A step's `$` timeout overrides it.                                                                                      |
| `test.bail`            | Stops scheduling new work when the failed-case threshold is reached; `0` disables the threshold.                                                                                             |
| `output.reportDir`     | Optional report directory override, relative to the Test project. Defaults to `report/` under the artifact root (`midscene_run/report/` by default).                                         |
| `output.reportFormat`  | `'single-html'` (default) includes execution data and images in one HTML file for direct offline viewing. `'html-and-external-assets'` writes a directory report that requires HTTP preview. |

See [Run tests](/midscene-test/running.md#run-tests) for CLI project selection and configuration file options, and [Configure timeouts and error handling](/midscene-test/writing.md#configure-timeouts-and-error-handling) for step overrides.

The scaffold generates a configuration that selects `cases/**/*.{yaml,yml}` by default. Without a `files` configuration, Midscene Test recursively searches for `**/*.{yaml,yml}` under the test directory.

Selected files are deduplicated and sorted by path. See [Project lifecycle](/midscene-test/execution.md#project-lifecycle) for execution order and retry scope, and [Concurrency and isolation](/midscene-test/execution.md#concurrency-and-isolation) for concurrency settings.

Native Test always generates a combined report. The CLI prints its path after the run. See [Run tests](/midscene-test/running.md) for report outputs and archiving.

### Configure multiple execution projects

To run tests across different browsers or devices, configure multiple Execution Projects with `defineTestProject()`.

Nodes registered in the top-level `nodes` field apply to every Project; the field defaults to `[]` when omitted. Within an individual Project, you can also register local Nodes using the `nodes` field alongside `setup` and `files`. A local Node applies only to that Project and replaces the entire global definition with the same name. Other global Nodes remain available. Duplicate names within the same registration layer cause an error.

When configuring both Android and iOS projects, register each platform Agent's official Nodes within its Project. The example below imports two independent setups from `./setup`, each managing its own resources and returning `{ agent }`. It imports shared business Nodes from `./nodes`:

```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, // Maximum number of active Execution Projects
    bail: 0, // Stop scheduling new tasks after this many failed cases when > 0
    testTimeout: 120_000,
  },
});
```

Both Projects can use the same YAML file. During case collection, input validation, and test execution, `launch` and other platform Nodes use the definitions that apply to the current Project. A Project's local Nodes do not affect other Projects.

### Generate a Node Spec for each Project

Run `midscene-test nodes` to generate a Node Spec of the available Nodes. Use `--project` to select an execution project and list the Nodes available after combining its global and local registrations:

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

When `projects` is declared, omitting `--project` generates a separate Spec for each Execution Project, even when their Node definitions are identical.

For filenames, output rules, Spec contents, and command options, see [Generate the current Project's Node Spec](/midscene-test/writing.md#generate-the-current-projects-node-spec).

## Programmatic APIs

You can also use APIs to integrate Midscene Test into other tools, such as a local test panel with a graphical interface. The following APIs let you load project configuration, run an entire project, or execute an individual case:

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

- **`loadTestProject(path)`**: asynchronously loads an explicitly provided TypeScript (`.ts`) or JavaScript ESM (`.mjs`) project configuration. Without `--config`, the CLI discovers `midscene.config.ts` or `midscene.config.mjs`; if both exist, it asks you to select one explicitly. Midscene Test does not support synchronous loading.
- **`runTestProject()`**: asynchronously discovers, runs, and summarizes the entire project.
- **`CaseRunner` / `createCaseRunner()`**: directly runs one test case represented as a plain object, without file parsing or lifecycle management.
- **`runWorkflowDocument()`**: runs the complete lifecycle and every Case in one document.

Run a complete project and preserve its failure exit status:

```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;
}
```

## Next steps

- [Execution lifecycle and concurrency](/midscene-test/execution.md): resource sharing, retries, concurrency, timeouts, and cancellation.
- [Run tests and CLI reference](/midscene-test/running.md): targets, paths, and environment variables.
