• English
  • 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 selectedSupported fileLoading behavior
    Automatic discovery, when no configuration path is providedmidscene.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.

    FieldPurpose
    setupPrepares browser or device resources shared by a project.
    fileSetupPrepares an environment for each YAML file, in both sequential and concurrent runs.
    nodesRegisters Nodes shared by all execution projects.
    projectsDeclares 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.
    testSets concurrency, the failure threshold, and the default step timeout.
    outputSets 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 the runtime environment

    Complete example: Playwright

    This sequential configuration uses only setup: all files in the execution project share one page. Add 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 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.

    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 and 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 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. For reading and updating shared data in custom Nodes, see 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.

    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.

    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:

    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:

    ArgumentPurpose
    projectContextThe project setup return value, or undefined when no project setup is configured
    project, fileCurrent project configuration, plus the file's sourcePath and execution IDs
    env, signalEnvironment 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 for context scope and cleanup order, and 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 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 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:

    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:

    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:

    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.

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

    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. Its result contains only the reference name and cookie count. Cookie names, values, and scopes are not written to the run result.

    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. References prevent Midscene Test from persisting the cookies, but the environment variable, profile, or storage-state file must still be protected. Do not commit storage-state files containing real cookies.

    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 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():

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

    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. Cookie references and result redaction also behave the same on both integrations. 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:

    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

    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:

    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

    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:

    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: {}

    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

    SettingMeaning
    projects[].filesinclude selects YAML files; exclude removes matches. Patterns are relative to the test directory.
    projects[].tagsIncludes or excludes cases by tag.
    projects[].variablesSupplies values for YAML ${variable} references.
    projects[].retryNumber of retries for a failed case; defaults to 0.
    test.testTimeoutDefault timeout for each step in milliseconds; defaults to 120000. A step's $ timeout overrides it.
    test.bailStops scheduling new work when the failed-case threshold is reached; 0 disables the threshold.
    output.reportDirReport output directory; defaults to ./midscene_run/report.
    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 with images stored by content, and requires HTTP preview. This choice is independent of the Agent source storage format.

    See Run tests for CLI project selection and configuration file options, and 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 for execution order and retry scope, and Concurrency and isolation for concurrency settings.

    Native Test always generates a combined report. The CLI prints its path after the run. See Run tests 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:

    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,
      },
      output: {
        reportDir: './midscene_run/report',
      },
    });

    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:

    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.

    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:

    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:

    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