> ## Documentation Index
> Fetch the complete documentation index at: https://momentic.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The canonical docs host is https://momentic.ai/docs. Append .md to any page URL for clean Markdown. Fetch https://momentic.ai/docs/llms.txt for the page index and https://momentic.ai/docs/llms-full.txt for every page in one file.
> For an unattended setup, run `npx --yes @momentic/wizard@latest -y --platform web --editor-tools skills` (use `--platform ios` or `--platform android` for mobile). Never run an interactive command in a non-TTY shell.
> Mo is the AI QA engineer, a separate product from the YAML test framework: its npm package and CLI are `qa` (not `mo` — `npx mo` is a different project). Install with `npm install -g qa`, authenticate the same MOMENTIC_API_KEY way, install repo skills with `qa skills -y`, and start a session with `qa start "<brief>"` or `qa <url>`. Docs: https://momentic.ai/docs/mo.md.
> You cannot complete the browser sign-in yourself. Read the API key from the exact MOMENTIC_API_KEY environment variable (similarly named vars like BILT_MOMENTIC_API_KEY are ignored), or ask the user to create one at https://app.momentic.ai/settings/api-keys and paste it back.
> Momentic's capability summary for agents is at https://momentic.ai/docs/skill.md. Install the skills with `npx momentic skills --yes` (or `npx momentic-mobile skills --yes` for mobile).
> Tests are YAML files in the user's repository. Use only the step names listed at https://momentic.ai/docs/reference/commands.md and the file structure at https://momentic.ai/docs/core-concepts/file-format.md. Do not invent step names, config keys, or CLI flags.
> Web tests run on Chromium, iOS tests on simulators, and Android tests on emulators. Physical devices are not supported.

# From Selenium and Appium

> Port WebDriver and Appium suites to Momentic, including page objects, explicit waits, capabilities, and driver escapes.

Port the user flows from your Selenium or Appium suite into `*.test.yaml` files.
Move reusable page-object behavior into modules, configure browser or device
settings in `momentic.config.yaml`, and retain explicit readiness checks where
the app requires them. This is a manual translation of the test contract, not an
import of WebDriver code or capabilities.

## Prepare the project

* Complete the [web](/docs/quickstart/web), [iOS](/docs/quickstart/ios), or
  [Android](/docs/quickstart/android) setup for the target you are migrating. Keep
  your existing runner and CI job while you verify the new tests.
* Web tests run on Chromium, Google Chrome, or Chrome for Testing. Keep Selenium
  coverage for Firefox, Safari, or other required browser targets.
* Mobile tests need an Android APK or an iOS simulator `.app` build. A
  device-signed `.ipa` is not a simulator build. Momentic supports emulators and
  simulators, not physical devices. Android runs require `adb` and an SDK path
  even when using remote emulators; local iOS runs require macOS, Xcode, and
  idb. See [Android app setup](/docs/platforms/android/app-setup) and
  [iOS app setup](/docs/platforms/ios/app-setup).
* Pick one test and record its starting state, inputs, expected results, and
  cleanup. Include setup hidden in base classes, fixtures, and page-object
  constructors. Use the same app build and test data when comparing runners.

## Before and after

A representative Selenium test (Java/TestNG, but the shape is the same in Python
or JavaScript):

```java CheckoutTest.java theme={null}
@Test
public void guestCanBuyBlanket() {
  WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
  driver.get("https://shop.example.com/products/42");
  wait.until(ExpectedConditions.elementToBeClickable(
      By.cssSelector("[data-testid='add-to-cart']"))).click();
  driver.findElement(By.id("cart-icon")).click();
  wait.until(ExpectedConditions.textToBePresentInElementLocated(
      By.cssSelector(".cart-items"), "Gravity Blanket"));
  driver.findElement(By.xpath("//button[contains(.,'Checkout')]")).click();
  WebElement email = wait.until(ExpectedConditions.visibilityOfElementLocated(
      By.name("email")));
  email.sendKeys("jeff@example.com");
  driver.findElement(By.cssSelector("button[type='submit']")).click();
  wait.until(ExpectedConditions.urlMatches("/orders/\\w+"));
  Assert.assertTrue(
      driver.findElement(By.tagName("body")).getText().contains("Order confirmed"));
}
```

The Momentic version keeps the CSS targets and URL pattern. Browser JavaScript
checks rendered text, and the Checkout button uses a natural-language target:

```yaml checkout.test.yaml theme={null}
fileType: momentic/test/v2
id: guest-checkout
url: https://shop.example.com/products/42
steps:
  - checkElementVisible:
      css: "[data-testid='add-to-cart']"
      timeout: 10000
  - checkElementEnabled:
      css: "[data-testid='add-to-cart']"
      timeout: 10000
  - click:
      css: "[data-testid='add-to-cart']"
  - click:
      css: "#cart-icon"
  - javascript:
      environment: browser
      code: |
        const deadline = Date.now() + 10000;
        while (Date.now() < deadline) {
          const cart = document.querySelector(".cart-items");
          if (cart instanceof HTMLElement && cart.innerText.includes("Gravity Blanket")) return;
          await new Promise(resolve => setTimeout(resolve, 200));
        }
        throw new Error("Cart does not show Gravity Blanket");
      timeout: 15000
  - click: Checkout
  - checkElementVisible:
      css: "[name='email']"
      timeout: 10000
  - type:
      text: jeff@example.com
      css: "[name='email']"
      clear: never
  - click:
      css: "button[type='submit']"
  - waitForUrl:
      regex: /orders/\w+
      timeout: 10000
  - javascript:
      environment: browser
      code: |
        if (!document.body.innerText.includes("Order confirmed")) {
          throw new Error("Order confirmation is not visible");
        }
```

Web interaction steps wait for their target using the browser's smart-waiting
setting. Condition steps accept their own `timeout`. Preserve explicit waits
when a transition can outlast the interaction's default: the checks above wait
for visibility and enabled state before clicking. Each check has its own
deadline. A Selenium `Duration.ofSeconds(10)` becomes `timeout: 10000`, not
`timeout: 10`. See [auto-waiting](/docs/core-concepts/finding-elements#auto-waiting).

Keep readiness checks for conditions that finding a target does not establish,
such as a completed background save. The JavaScript loop above preserves the
cart's rendered-text wait; its step timeout allows the polling loop to finish.

An AI `assert` checks meaning; it does not preserve a literal text equality or
substring assertion. Web
[element content checks](/docs/reference/commands/check-element-content) compare DOM
`textContent`, which can include hidden text. Selenium `getText()` reads
rendered text. Use a browser JavaScript check, as above, when that distinction
affects coverage; review whitespace and visibility behavior for your app.

### Appium example

An Appium Java test might enter an email, continue, and compare the welcome
message exactly:

```java theme={null}
driver.activateApp("com.example.shop");
driver.findElement(AppiumBy.accessibilityId("Email")).sendKeys("jeff@example.com");
driver.findElement(AppiumBy.accessibilityId("Continue")).click();
Assert.assertEquals(
    driver.findElement(AppiumBy.accessibilityId("welcome-message")).getText(),
    "Welcome back");
```

In Momentic, describe the intended controls and retain the exact text check.
Install the same APK through the run command's build options before this test
runs:

```yaml mobile-tests/welcome.test.yaml theme={null}
fileType: momentic/mobile-test/v2
id: migrated-welcome
description: Enter an email and verify the welcome message
platform: android
steps:
  - openApp: com.example.shop
  - type:
      text: jeff@example.com
      into: the Email field
      clearContent: false
  - tap: Continue
  - checkElementContentEquals:
      on: the welcome message
      value: Welcome back
```

The accessibility ID is no longer a selector object. Confirm that the
description identifies the same element, especially when multiple controls have
the same text. Keep the original wait and reset behavior if the test's fixtures
add it outside this excerpt.

## API mapping

| Selenium / WebDriver | Momentic |
| - | - |
| `driver.get(url)` | `navigate: <url>` or the test's top-level `url` |
| `findElement(By.*)` + `click()` | `click: <natural language>` or `click: { css: "..." }` |
| `sendKeys(value)` | `type: { text: ..., into: <field> }` |
| `WebDriverWait` + `ExpectedConditions.*` | the step's own waiting, or `assert`/`checkElement*` with `timeout` |
| `Thread.sleep(ms)` | `wait: <ms>` (prefer a condition) |
| `Assert.assertEquals` etc. | `checkElementContentEquals` compares DOM text; browser JavaScript checks rendered text; Node JavaScript has `assert` for values |
| `Actions` chains (hover, drag) | `hover`, `dragAndDrop`, `mouseDrag` |
| `driver.navigate().back()` / `forward()` | `goBack` / `goForward` |
| Open or close a tab | `newTab` opens a URL and switches to it; `closeTab` closes an active or matching tab. Rewrite window-handle logic separately. |
| `switchTo().alert().accept()`/dismiss | `dialog: ACCEPT` or `DISMISS`, placed before the step that raises it |
| `driver.manage().cookies()` | `cookie` step; `localStorage` step for storage |
| `JavascriptExecutor` | `javascript` step with `environment: browser` |
| Screenshots on failure | automatic per-step screenshots; video needs `recordVideo` |
| `DesiredCapabilities` / `ChromeOptions` | Map supported settings to `browser` and `environments` in `momentic.config.yaml`; capability objects are not accepted. |
| Selenium Grid / BrowserStack / Sauce | Hosted browsers use `browser.remoteBrowser: true`; provider-specific capabilities and device matrices do not carry over. |
| JUnit/TestNG/pytest reports | `--reporter junit` |

| Appium | Momentic (`momentic-mobile`) |
| - | - |
| `driver.activateApp(bundleId)` | `openApp: <package or bundle id>` |
| `driver.terminateApp` | `killApp` |
| `findElement` + `click()` | `tap: <natural language>` |
| `sendKeys` | `type: { text: ..., into: <field> }` |
| `TouchAction` / W3C gestures | `swipe`, `dragAndDrop`, `scrollTo` |
| `driver.pressKey` | `press` / `pressKey` |
| `driver.execute("mobile: ...")` | `appium` with `script` and JSON-string `args`; verify the command on the target platform |
| `adb` shell commands | `adb` step (Android only) |
| Desired/app capabilities (`platformName`, ...) | `platform:` on the test plus `momentic.config.yaml` |
| Local emulator/device farm | remote emulators and simulators hosted by Momentic |

## Page objects and helpers

Extract reusable page-object actions into [modules](/docs/core-concepts/modules). You
do not need a module for every page class. Keep assertions that define a test's
result in the calling test. This web module accepts credentials:

```yaml modules/log-in.module.yaml theme={null}
fileType: momentic/module/v2
id: log-in
name: Log in
parameters:
  - name: USERNAME
  - name: PASSWORD
steps:
  - type:
      text: "{{ env.USERNAME }}"
      into: the Username field
  - type:
      text: "{{ env.PASSWORD }}"
      into: the Password field
  - click: the Submit button
```

Call it from `before:` or `steps:` with `inputs`. Callers pass concrete values
or `env` references. For example, a web test can call the module during setup:

```yaml account.test.yaml theme={null}
fileType: momentic/test/v2
id: migrated-account
url: https://shop.example.com/login
defaultEnv: staging
before:
  - module:
      path: ./modules/log-in.module.yaml
      inputs:
        USERNAME:
          string: test@example.com
        PASSWORD: env.TEST_PASSWORD
steps:
  - checkElementVisible: Account menu
```

Declare `TEST_PASSWORD` in the selected environment's
[secret configuration](/docs/configuration/environments#secrets). A module input such
as `env.TEST_PASSWORD` is a JavaScript expression; `{ string: ... }` is a
literal. Mobile modules use `fileType: momentic/mobile-module/v2`, a `platform`,
and a different parameter declaration. See
[module parameters](/docs/core-concepts/modules#parameters).

Modules cannot call other modules. Flatten page-object methods that call other
helpers into a single module, or call the modules in sequence from the test.

## Setup, teardown, and state

Move per-test fixtures such as `@BeforeMethod`, `@AfterMethod`, or pytest
fixtures into `before` and `after`. A failed setup skips the main steps.
Teardown runs after setup or main-step failures; make cleanup tolerate data that
setup never created. Run the whole test when validating hooks: running an
individual editor step skips them. See
[before and after sections](/docs/core-concepts/file-format#before-and-after-sections).

Suite-wide build commands and external service startup belong in your CI job.
Keep test data independent: allocate unique accounts or records per test, and
clean up only those records. A new browser or device session does not reset
shared backend data. Configure credentials through
[environments](/docs/configuration/environments), rather than copying secrets from
driver setup into YAML.

For Appium, check the old reset, permissions, app installation, and launch
behavior separately. `openApp` launches the installed app; it does not express
all the behavior of an Appium capabilities object. `killApp` stops the current
foreground app. Use it before `openApp` only when the intended app is already in
the foreground. For an Android restart that targets a package regardless of
foreground state, use the
[named-package restart example](/docs/guides/migrate/from-maestro#before-and-after).
Preserve any required data reset explicitly; stopping an app does not clear its
data.

<span id="what-keeps-working-unchanged" />

## Reuse setup and custom code

* `javascript` steps provide `axios`, `pg`, `faker`, and `child_process` in a
  Node sandbox. Reuse seed endpoints and database operations, but adapt helpers
  that depend on your test framework or imported packages. The sandbox does not
  expose your Selenium or Appium driver object. See
  [JavaScript integration](/docs/integrations/javascript).
* Rewrite `JavascriptExecutor` page scripts as web `javascript` steps with
  `environment: browser`. Replace driver argument references with DOM queries
  and return or throw explicitly. Mobile JavaScript has no browser execution
  context.
* On mobile, [`appium`](/docs/reference/mobile-commands/appium) executes a named
  script with JSON-string arguments. Command availability and arguments depend
  on the device platform. Verify each script on the selected emulator or
  simulator before retiring its legacy coverage.
* Replace the grid and driver setup with
  `npx momentic install-browsers chromium` (web) or a hosted browser/emulator.
  See [Hosted test environments](/docs/running-tests/hosted-test-environments).

## What does not map

* There is no `WebDriver` object to hold or pass. You cannot run two driver
  sessions in one test; parallel testing is per-test parallelism, not
  multi-session choreography.
* Selectors still work as targets, but the intended use is natural language. Web
  steps accept CSS selectors, not WebDriver `By` objects. Rewrite XPath or
  mobile selector objects as a description of the intended target. Preserve
  index, ancestor, and state constraints when they distinguish the target.
* A custom `ExpectedCondition` does not translate by name. Use a supported
  condition step or rewrite a DOM check as browser JavaScript. Preserve its
  timeout and failure behavior; a single JavaScript evaluation is not a polling
  wait.
* Remote execution covers Android emulators and iOS simulators, not physical
  device farms. Keep a device-farm runner for the tests that need hardware.
* Selenium suites that drive WebDriver against non-browser targets do not map;
  Momentic tests browsers and mobile apps.

## Selenium, Playwright, or Momentic in 2026

Choose per flow based on required browsers and devices, framework-dependent
helpers, and the maintenance work you can measure:

* Keep Selenium or Appium coverage for unsupported browsers, physical devices,
  or driver-specific behavior you still need. A stable suite may not need a
  migration.
* Compare a Playwright port when you want to retain browser tests as code.
  Inventory selector, wait, fixture, and CI changes before estimating the work.
* Port a bounded flow to Momentic when you want repository YAML with preset
  steps, AI actions, and modules. Adapt its setup, assertions, and cleanup using
  this guide, then compare the run evidence with the original test.

Estimate migration effort from that pilot, including authoring, review, helper
adaptation, and CI changes. A coding agent can draft YAML, but you still need to
verify the test's behavior. Replace coverage only after the migrated flow passes
the checks below. Do not port a suite you plan to retire.

## Incremental strategy

1. Pick one bounded flow with recurring locator or wait failures. Port its
   setup, steps, assertions, and cleanup to a single `*.test.yaml` before
   converting the rest of the suite.
2. Lint and run that file using the commands below. Compare the original and
   migrated run evidence on the same build. Check that the assertion fails when
   the expected outcome is absent; a passing happy path alone is not evidence of
   equivalent coverage.
3. Extract modules once another test needs the same sequence. For mobile, verify
   the build, OS version, app state, and permissions on your intended local or
   remote target.
4. Add a separate Momentic CI job. Keep the Selenium or Appium job and its
   reports while you compare failures. Use separate test data when both jobs
   mutate the same backend.
5. Replace gates flow by flow after reviewing equivalent coverage and repeated
   results. Retain legacy tests for unsupported browsers, hardware, and custom
   behavior. Retire their grid or device-farm jobs only when that coverage is no
   longer required.

For the web example, lint and run from the configured project root:

```bash theme={null}
npx momentic lint checkout.test.yaml
npx momentic run checkout.test.yaml --upload-results --reporter junit
```

For the Android example on a local AVD, provide both the AVD and APK:

```bash theme={null}
npx momentic-mobile lint mobile-tests/welcome.test.yaml
npx momentic-mobile run mobile-tests/welcome.test.yaml \
  --local-avd-id Pixel_9_API_35 --local-apk-path ./build/app.apk \
  --parallel 1 --upload-results --reporter junit
```

For iOS, use `--local-ios-device-type` and `--local-app-path` instead, or upload
the build and select a channel and tag for remote execution. See
[mobile run options](/docs/cli-reference/momentic-mobile/commands/run). The AVD name
and build path above must match your machine.

In CI, use the same committed test files and lockfile, set `MOMENTIC_API_KEY`
from your CI secret store, and install the browser or device prerequisites. Keep
the CLI's exit status as the job result and publish the JUnit report from
`./reports`. See [GitHub Actions](/docs/running-tests/ci/github-actions) or the
[other CI guides](/docs/guides).

## Porting at scale with a coding agent

Hand the port to your coding agent with a prompt like:

```text theme={null}
Port the Selenium tests in src/test/java/checkout/ to Momentic tests.
Read the momentic-test skill first. Write one *.test.yaml per @Test method,
convert each Page class to a *.module.yaml, remove only waits replaced by equivalent condition checks, and port
any JavascriptExecutor blocks to javascript steps with environment: browser. Run each ported test with
npx momentic run <file>.
```

## Related

* [Test portability](/docs/get-started/test-portability)
* [Web steps reference](/docs/reference/commands/index)
* [Mobile steps reference](/docs/reference/mobile-commands/index)
* [Hosted test environments](/docs/running-tests/hosted-test-environments)
* [Modules](/docs/core-concepts/modules)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.