• 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 programmatic execution.

    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.

    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.

    Project lifecycle

    setup, fileSetup, and beforeAll

    They run in the order setup → fileSetup → beforeAll → cases. Unconfigured parts are skipped.

    SettingLocationFrequencyPurpose
    setupProject configurationOnce per projectPrepare shared resources, such as a Browser
    fileSetupProject configurationOnce per fileCreate an independent page and Agent; return the Node context
    beforeAllYAMLOnce before the file's casesUse context to log in or prepare test data

    In a normal run of two YAML files, setup runs once, while fileSetup and beforeAll each run twice. Concurrency limits do not change these counts.

    Configure fileSetup once for all files; declare beforeAll in each file. Custom Nodes in beforeAll can also create resources, but this does not make the framework create an independent file context.

    Project, file, and case lifecycle

    When running tests through the CLI or runTestProject(), each execution project has its own lifecycle. The following shows normal sequential execution for a project with two YAML files. YAML hooks that are not declared are skipped:

    Execution project starts
    ├─ setup → returns the shared project context
    ├─ File A
    │  ├─ fileSetup (when configured) → returns the file context
    │  ├─ beforeAll
    │  ├─ Case 1: beforeEach → steps → afterEach → Node cleanup for this attempt
    │  ├─ Case 2: beforeEach → steps → afterEach → Node cleanup for this attempt
    │  ├─ afterAll
    │  └─ File-scoped Node cleanup → cleanup registered in fileSetup
    ├─ File B
    │  └─ Runs its own hooks, cases, and cleanup in the same order
    └─ Project cleanup: onTeardown() callbacks registered in setup

    beforeAll and afterAll each run once per YAML file; beforeEach and afterEach surround every case attempt. With file concurrency enabled, files can advance together, while each file follows the order shown above.

    Case retries reuse the file environment and repeat only beforeEach → steps → afterEach → Node cleanup for this attempt. Project setup, fileSetup, and beforeAll do not rerun. See Handling failures for retry conditions and skip rules.

    The scope of onTeardown() depends on where it is registered:

    Registration locationCleanup timing
    Project setupAt project completion, after cleanup of all files that ran
    fileSetupAfter the current file's afterAll and file-scoped Node cleanup
    Nodes in beforeAll or afterAllAfter the current file's afterAll
    Nodes in beforeEach, steps, or afterEachAfter the current attempt's afterEach, with separate cleanup for each retry

    Within each scope, callbacks run in reverse registration order. If one throws, remaining callbacks are still attempted; if it has not finished, later cleanup waits for it. See Resource lifecycle and cleanup for implementing Node cleanup.

    Context sharing scope

    The configuration determines which context Nodes receive:

    ConfigurationSource of contextShared by
    Project setup onlyThe return value of setupAll files in the current project
    With fileSetupThe return value of that file's fileSetup invocationHooks and cases in the current file, including case retries

    Nodes within the same sharing scope receive the same object. A value written to context.orderId can remain visible to later cases. The framework does not reset the context, page, or login state between cases or retries; restore the required initial state in beforeEach.

    Create resources and the returned object inside the setup() function. Projects or files invoke the function separately even when they reference the same definition; returning a mutable object from module scope would still share state. See Share context across Nodes for a data-passing example.

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

    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.

    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 View test results for how to open it.

    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.

    Concurrency and isolation

    Two limits apply together: maxConcurrentFiles limits files across the whole run, and maxConcurrentProjects limits projects whose preparation has started but whose cleanup has not finished. Each defaults to 1 independently. Set either to null to remove that limit; numeric values must be positive integers.

    test configurationBehavior
    OmittedFinish one project at a time, running its files sequentially
    { maxConcurrentFiles: 4 }One project at a time, with up to 4 files running together
    { maxConcurrentProjects: 2 }Files remain globally sequential; up to 2 project environments may stay open
    { maxConcurrentFiles: null, maxConcurrentProjects: 1 }Finish one project at a time, without a global file limit
    { maxConcurrentFiles: null, maxConcurrentProjects: null }Remove both global limits; local file limits and available resources still apply

    Execution is asynchronous within one Node.js process. Raising only the project limit does not raise file concurrency. Removing limits does not guarantee a faster run: browser capacity, devices, and model service quotas still matter.

    File concurrency with maxConcurrentFiles

    ConfigurationMeaning
    test.maxConcurrentFilesTotal active files across all projects; default 1, null for unlimited
    test.maxConcurrentProjectsProjects from the start of preparation through completed cleanup; default 1, null for unlimited
    projects[].maxConcurrentFilesPositive integer file limit for this project; inherits the global file limit when omitted and cannot raise it
    projects[].fileSetupPrepares an independent environment for each file; required when the effective project file limit exceeds 1, including unlimited, even if only one file is selected

    For example, with test: { maxConcurrentFiles: 3, maxConcurrentProjects: 2 } and Web and Android file limits of 2 each, two Web files and one Android file can run together, or one Web file and two Android files. The project count does not multiply the global file limit.

    A file occupies a slot from preparation through completed cleanup, including device waits and case retries. Starting a new project claims both a project slot and its first file slot before setup; other files in that project wait for initialization without taking additional slots. Eligible projects take turns admitting files.

    A project keeps its slot between files, even when none is currently running. Its last file retains a file slot through project cleanup; the project slot is released only when cleanup actually finishes. With maxConcurrentProjects: 1, the next project starts after the previous one is fully cleaned up. Setting only maxConcurrentFiles: 1 allows files from different projects to interleave if the project limit permits it.

    After cancellation or bail, cleanup for initialized projects with no active files is also scheduled within the global file limit. See Timeouts and cancellation.

    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.

    Existing configurations

    test.maxConcurrency has been removed; loading an old configuration reports an error with a migration example. It controlled project concurrency, so simply renaming it to maxConcurrentFiles changes behavior. To preserve an old limit of 2, set test: { maxConcurrentProjects: 2, maxConcurrentFiles: 2 } and each project's maxConcurrentFiles to 1. Without explicit projects, keep the global file limit at 1.

    fileSetup, project file limits, and preparation/cleanup timeouts can be configured without explicitly setting either global limit. When a project's effective file limit is 1, fileSetup is optional and its files can share the setup return value.

    Cases are not automatically divided between projects. If two projects select the same YAML file, each runs it once. Use non-overlapping files or tags rules to split the work.

    Legacy tasks/flow YAML still runs through midscene. Its concurrent / --concurrent, setup-file behavior, and browser reuse remain unchanged; those settings are handled separately from native Test's setup and fileSetup. See Migrate to Midscene Test.

    Timeouts and cancellation

    Timeout settings

    ConfigurationScopeDefault
    test.testTimeoutOne step; overridden by the step's $ settings120000 ms
    test.setupTimeoutEach project setup or fileSetup, including device waits120000 ms
    test.teardownTimeoutEach cleanup scope, including hooks, waiting for operations, and cleanup callbacks30000 ms

    Preparation and cleanup limits also apply when concurrency settings are omitted. After cancellation or bail, cleanup scopes share a shutdown deadline based on teardownTimeout.

    Cancellation and cleanup

    A step timeout aborts that step's signal. The CLI cancels the run on SIGINT (such as Ctrl+C) or SIGTERM. Nodes must pass signal to APIs that support cancellation or check its state; asynchronous operations do not stop automatically. See Asynchronous operations, errors, and cancellation.

    After interruption, the framework still attempts cleanup. Nodes in afterEach and afterAll use a cleanup signal that has not been aborted. Step timeouts still apply, and cleanup cannot overlap an unfinished operation on the same Agent. Cleanup Nodes should handle partially completed preparation.

    Callbacks registered with onTeardown() do not receive a new signal. Avoid reusing the original step's signal, which may already be aborted.

    Timeouts bound the wait for results. If operations or cleanup remain unfinished, the framework records cleanup pending, keeps tracking resources, and releases them after operations actually finish. No new files start in this state. See Handling failures for the full rules.

    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.

    Usage notes

    • Configure what you need. Use only fileSetup when there are no shared resources. With an effective file limit of 1, omit it to share the setup result across files.
    • Concurrent files need isolation. An effective file limit above 1, including unlimited, requires fileSetup, even if only one file is selected. Use separate pages, Agents, and devices; isolate backend accounts and data separately.
    • Retries do not repeat preparation. setup, fileSetup, and beforeAll do not rerun. Reset state needed for each attempt in beforeEach.
    • Register cleanup immediately. Call onTeardown() after creating each resource. File cleanup precedes shared project cleanup; cleanup code must tolerate partial preparation.