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

# Integrate with Rstest

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 Rstest. Rstest 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 @rstest/core dotenv @midscene/android
```

**iOS**

```bash
pnpm add -D @rstest/core dotenv @midscene/ios
```

**HarmonyOS**

```bash
pnpm add -D @rstest/core dotenv @midscene/harmony
```

**Desktop**

```bash
pnpm add -D @rstest/core dotenv @midscene/computer
```

## 2. Configure Rstest
Create `rstest.config.ts`. If your project already uses Rstest for unit tests, you can save this configuration separately as `rstest.device.config.ts` and pass `--config rstest.device.config.ts` when running device tests, leaving your existing test configuration untouched.
```typescript title="rstest.config.ts"
import { defineConfig } from '@rstest/core';

export default defineConfig({
  include: ['e2e/**/*.test.ts'],
  testEnvironment: 'node',
  testTimeout: 90_000,
  hookTimeout: 90_000,
  pool: { maxWorkers: 1 },
  maxConcurrency: 1,
  output: {
    externals: {
      '@silvia-odwyer/photon': 'commonjs @silvia-odwyer/photon',
    },
  },
});
```
Model calls and device connections can take longer than unit tests, so adjust `testTimeout` and `hookTimeout` for your cases. For a shared device, use one worker (`pool.maxWorkers: 1`) and `maxConcurrency: 1`, and avoid `test.concurrent` and `describe.concurrent`. The external declaration keeps the Photon native addon out of the test bundle. See the [Rstest configuration reference](https://rstest.rs/config/).
## 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 '@rstest/core';
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 '@rstest/core';
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 '@rstest/core';
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 '@rstest/core';
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 rstest run e2e/settings.test.ts
```
Rstest 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, Rstest integration uses the SDK's built-in report generation directly and does not require a custom reporter. Rstest 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.
