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

# Integrate with Vitest

We recommend [Rstest](/sdk/rstest.md) or [Vitest](/sdk/vitest.md) for writing JavaScript and TypeScript tests on Android, iOS, HarmonyOS, and desktop. This guide covers Vitest. Vitest organizes test cases, runs lifecycle hooks, and aggregates test results, while Midscene Agents connect to devices, interact with the UI, and perform assertions.
## Before you begin

- **Runtime**: Install Node.js `^20.19.0 || ^22.12.0 || >=24.0.0` and pnpm. Use an existing project or run `pnpm init` to create one (skip if `package.json` already exists).
- **Model configuration and verification**: Complete [model configuration](/model-api-setup) and [model verification](/model-config#verify-model-connectivity), then save the configuration in the project's `.env` file or shell environment.
- **Platform environment**: Complete the environment setup for [Android](/platforms/android), [iOS](/platforms/ios), [HarmonyOS](/platforms/harmonyos), or [desktop](/platforms/desktop), including device connections and required permissions.
- **(Optional) Try Playground first**: Run an action and an assertion against your target interface in the [Playground for your platform](/get-started?scene=playground) to verify screenshots, interactions, and model behavior.

## 1. Install dependencies

**Android**

```bash
pnpm add -D vitest dotenv @midscene/android
```

**iOS**

```bash
pnpm add -D vitest dotenv @midscene/ios
```

**HarmonyOS**

```bash
pnpm add -D vitest dotenv @midscene/harmony
```

**Desktop**

```bash
pnpm add -D vitest dotenv @midscene/computer
```

## 2. Configure Vitest
Create `vitest.config.ts`. If your project already uses Vitest for unit tests, you can save this configuration separately as `vitest.device.config.ts` and pass `--config vitest.device.config.ts` when running device tests, leaving your existing test configuration untouched.

```typescript title="vitest.config.ts"
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['e2e/**/*.test.ts'],
    environment: 'node',
    testTimeout: 90_000,
    hookTimeout: 90_000,
    fileParallelism: false,
    sequence: { concurrent: false },
  },
});
```

Because model calls and device connections take longer than standard unit tests, adjust `testTimeout` and `hookTimeout` to fit your test cases. When running against a single shared device, disable file parallelism (`fileParallelism: false`) and run tests sequentially (`sequence.concurrent: false`), and avoid `test.concurrent` or `describe.concurrent`. For details, see the Vitest documentation on [fileParallelism](https://vitest.dev/config/fileparallelism), [testTimeout](https://vitest.dev/config/testtimeout), and [hookTimeout](https://vitest.dev/config/hooktimeout).
## 3. Write a test
Create `e2e/settings.test.ts`, initializing a new Agent for each test in `beforeEach` and destroying it in `afterEach`:

**Android**

```typescript title=e2e/settings.test.ts
import 'dotenv/config';
import { afterEach, beforeEach, test } from 'vitest';
import { agentFromAdbDevice } from '@midscene/android';

let agent: Awaited<ReturnType<typeof agentFromAdbDevice>> | undefined;

beforeEach(async () => {
  agent = await agentFromAdbDevice();
});

afterEach(async () => {
  const currentAgent = agent;
  agent = undefined;
  await currentAgent?.destroy();
});

test('scroll through settings', async () => {
  if (!agent) throw new Error('Agent was not initialized');

  await agent.launch('com.android.settings');
  await agent.aiWaitFor('The Settings app is visible');
  await agent.aiAct('Scroll down one screen');
  await agent.aiAssert('The Settings app is still visible');
});
```

If multiple devices are connected, pass a device ID to `agentFromAdbDevice(deviceId)`. See the [Android SDK guide](/sdk/android.md) for connection options.

**iOS**

```typescript title=e2e/settings.test.ts
import 'dotenv/config';
import { afterEach, beforeEach, test } from 'vitest';
import { agentFromWebDriverAgent } from '@midscene/ios';

let agent: Awaited<ReturnType<typeof agentFromWebDriverAgent>> | undefined;

beforeEach(async () => {
  agent = await agentFromWebDriverAgent({
    wdaHost: 'localhost',
    wdaPort: 8100,
  });
});

afterEach(async () => {
  const currentAgent = agent;
  agent = undefined;
  await currentAgent?.destroy();
});

test('scroll through settings', async () => {
  if (!agent) throw new Error('Agent was not initialized');

  await agent.launch('com.apple.Preferences');
  await agent.aiWaitFor('The Settings app is visible');
  await agent.aiAct('Scroll down one screen');
  await agent.aiAssert('The Settings app is still visible');
});
```

Replace `wdaHost` and `wdaPort` with your verified WebDriverAgent service endpoint. See the [iOS SDK guide](/sdk/ios.md) for connection options.

**HarmonyOS**

```typescript title=e2e/settings.test.ts
import 'dotenv/config';
import { afterEach, beforeEach, test } from 'vitest';
import { agentFromHdcDevice } from '@midscene/harmony';

let agent: Awaited<ReturnType<typeof agentFromHdcDevice>> | undefined;

beforeEach(async () => {
  agent = await agentFromHdcDevice();
});

afterEach(async () => {
  const currentAgent = agent;
  agent = undefined;
  await currentAgent?.destroy();
});

test('scroll through settings', async () => {
  if (!agent) throw new Error('Agent was not initialized');

  await agent.launch('com.huawei.hmos.settings');
  await agent.aiWaitFor('The Settings app is visible');
  await agent.aiAct('Scroll down one screen');
  await agent.aiAssert('The Settings app is still visible');
});
```

If multiple devices are connected, pass a device ID to `agentFromHdcDevice(deviceId)`. See the [HarmonyOS SDK guide](/sdk/harmonyos.md) for connection options.

**Desktop**

```typescript title=e2e/settings.test.ts
import 'dotenv/config';
import { afterEach, beforeEach, test } from 'vitest';
import { agentForComputer } from '@midscene/computer';

let agent: Awaited<ReturnType<typeof agentForComputer>> | undefined;

beforeEach(async () => {
  agent = await agentForComputer();
});

afterEach(async () => {
  const currentAgent = agent;
  agent = undefined;
  await currentAgent?.destroy();
});

test('scroll through settings', async () => {
  if (!agent) throw new Error('Agent was not initialized');

  await agent.aiWaitFor('The Settings app is visible');
  await agent.aiAct('Scroll down one screen');
  await agent.aiAssert('The Settings app is still visible');
});
```

Before running the test, open your system Settings app and bring a scrollable settings page to the foreground. Keep it visible during the test. See the [desktop SDK guide](/sdk/desktop.md) for permissions and connection options.

`dotenv/config` loads model settings from the project's `.env` file (settings in the Playground are not automatically shared with test runs).

When an `aiAssert` check fails, it throws an error, causing the test runner to mark the test as failed before running `afterEach` to clean up the Agent. You can also extract data from the screen with methods like `aiQuery` and pass the result to the test runner's `expect`. For more interaction and extraction methods, see the [Agent API reference](/reference/index.md#interaction-methods).
## 4. Run the test and view the report
```bash
pnpm exec vitest run e2e/settings.test.ts
```

Vitest prints the pass or fail status of each test in the terminal, while the Midscene SDK generates an execution report for each Agent under `midscene_run/report/` by default and prints the report path. Open the generated HTML file in a browser to inspect step-by-step screenshots, AI execution details, and assertion results.

Unlike Playwright Test, Vitest integration uses the SDK's built-in report generation directly and does not require a custom reporter. Vitest test results and Midscene Agent reports are generated separately; to merge or further process report files, see [Process report files](/consume-report-file.md).
## Common methods
| Category           | Representative methods                         | Description                                                                                           |
| ------------------ | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Auto-planning      | `aiAct`                                        | Describe a goal in natural language and let the Agent plan and execute multiple steps.                |
| Direct interaction | `aiTap`, `aiInput`                             | Locate a UI element by description and perform a single specified action without multi-step planning. |
| Data extraction    | `aiQuery`, `aiString`, `aiNumber`, `aiBoolean` | Extract structured or primitive data from the current UI for further processing in code.              |
| Assertions         | `aiAssert`                                     | Verify that the current UI meets an expectation; throws an error if it does not.                      |
| Waiting            | `aiWaitFor`                                    | Wait until the UI meets a specified condition; throws an error on timeout.                            |

In practice, you typically perform actions with `aiAct` or direct interaction methods and then verify the outcome with `aiAssert`—completing an action does not guarantee that the expected UI state was reached. For full parameter and return value details, see the [Agent API reference](/reference/index.md#interaction-methods).
## Lifecycle and exclusive device access
- **Isolate Agents per test**: Create a new Agent for each test case. Each Device instance belongs to a single Agent, and `agent.destroy()` cleans up the associated Device as well—do not reuse a destroyed instance in subsequent tests.
- **Manage application state explicitly**: Destroying an Agent does not reset app data or close the app. Each test should explicitly prepare the required initial screen and data, and clean up state as needed so tests can be retried or run individually.
- **Avoid device contention across processes**: The runner's sequential settings only apply to the current test runner process; other local processes, CI jobs, or manual input can still contend for the same device. To run tests in parallel, assign a dedicated device or desktop session to each worker and explicitly specify its connection target in code.
For complete working projects, see the Vitest examples for [Android](https://github.com/web-infra-dev/midscene-example/tree/main/android/vitest-demo), [iOS](https://github.com/web-infra-dev/midscene-example/tree/main/ios/vitest-demo), [HarmonyOS](https://github.com/web-infra-dev/midscene-example/tree/main/harmony/vitest-demo), and [desktop](https://github.com/web-infra-dev/midscene-example/tree/main/computer/vitest-demo).
