Skip to content

Commands

Terminal window
tapstep test <flow.yaml | dir> [--driver web|android|ios|app[:port]] [-v]
tapstep test app.flow.yaml --headed # real browser window
tapstep test app.flow.yaml -c # watch mode: re-run this flow on save
tapstep test flows/ --tags smoke --shards 4 # directory run: tag filter, 4 flows at once
tapstep live <flow.yaml> [--port 7878] # interactive live view in the browser

--driver web|android|ios|app[:port] picks the target (app = a desktop app with the test bridge, default port 9223 — see Desktop apps). Without it tapstep auto-detects: a URL appId means web, otherwise the connected Android device or booted iOS simulator. A flow with a vscode: header runs on the VS Code driver, and a flow with device: none runs without any device.

--browser chrome|chromium|edge|brave|opera|vivaldi|yandex|firefox|managed or a path to a browser executable picks the web browser (managed = pinned Chromium in ~/.tapstep/browsers). --viewport WxH, --locale en-US tune the browser; --video-scale 1–4 records --video at N× the viewport (sharp demo footage). -e KEY=VAL overrides flow variables; --env-file FILE loads extra dotenv files over the auto-discovered .env; --env-set NAME runs against a named environment from the config’s environments: (else its defaultEnvironment), loading .env.NAME with it — see config.yaml.

Other test flags:

  • --tags a,b / --exclude-tags a,b — filter a directory run by flow tags:.
  • --shards N — run up to N flows of a directory concurrently.
  • --config <config.yaml> — workspace defaults: flows, tags, ai, report; flags win — see config.yaml.
  • -c / --continuous, --video, --capture-network — single flow only: re-run on save, record a video next to the flow, capture network traffic.
  • --analyze — AI triage of a failed flow (needs an ai: block in the config or TAPSTEP_AI_ENDPOINT/MODEL/KEY).
  • --strict-selectors — fail the run when a step passed only because one of its or: alternatives matched. Without it such a step is green and the run says ⚠ N step(s) passed on an alternative selector.
  • --report json|junit|html|allure, --output <file|dir> — see Reports.
  • Tuning, in ms: --wait-timeout, --settle-timeout, --settle-stable-for, --poll-interval; retries: --action-retries, --scroll-attempts.

live also takes --driver, --browser, --headed, --viewport, --locale and the tuning flags.

Terminal window
tapstep inspect [target] [--measure] [--driver …] [--browser …] # compact screen snapshot
tapstep list-devices # browsers, Android devices, iOS simulators
tapstep start-device android|ios [--name <avd|sim>] # boot an emulator / simulator
tapstep doctor # check each driver's external tools
tapstep privacy install # on-device PII masking model (~945 MB download)
tapstep privacy status # privacy mode: enabled? model installed?
tapstep validate <flow.yaml | dir> # parse-check flows without a device
tapstep init [path] [--samples] [--agents] # starter flow / samples / agent setup
tapstep skill status # per agent: installed? same version as this binary?
tapstep skill install # agent skill for Claude Code / Codex / Gemini CLI / Cursor / OpenCode
tapstep skill refresh # rewrite installed copies with this binary's version
tapstep mcp [--driver app:9223] [--browser …] # stdio MCP server for an AI client
tapstep appmeta <apk|ipa|app> # parsed build metadata as JSON
tapstep bugreport [path] # zip local diagnostics for an issue
tapstep update [--check] # self-update to the latest release

tapstep mcp defaults to the connected device, else web. tapstep update runs skill refresh afterwards, so installed agent skills follow the binary; skill refresh never installs anew — that is skill install.

tapstep validate reads a flow (or a whole directory, .helper.yaml files included) and reports what test would refuse before a device is booted. It also warns about a ${VAR} an environment leaves open and names which environments miss it — the check that catches a test recorded against one environment before another one runs it. Helpers are not checked that way: their variables come from the caller.

Terminal window
tapstep import playwright <spec.ts|dir> [-o flows] # codegen output -> flows
tapstep export playwright <flow.yaml|dir> [-o playwright] # a flow -> *.spec.ts
tapstep export extester <flow.yaml|dir> [-o extester] # a vscode: flow -> *.test.ts

Where an export lands is the project’s setting, not a per-command habit: without -o both write into the folder config.yaml’s exports: block names, and only without that into playwright/ and extester/ at the project root. The desktop app and the MCP tools read the same setting; an exports: block that does not parse stops the export rather than quietly falling back to the default folder.

Every exported file opens with a // tapstep: line naming the flow it came from and the hashes of both sides, so a later export can tell whether the file is still what the flow says, whether the flow has moved on since, and whether someone edited the file by hand.

Import reads what playwright codegen writes: one flow per test(...), locators mapped to selectors (getByTestIdtestId, getByRolerole + name, .nth()index, .filter({ hasText })containsDescendants). Whatever it cannot carry over — loops, helper functions, xpath locators, arguments computed at run time — is reported with its line and written into the flow as a # TODO comment where it happened. Nothing is dropped in silence, so the report is the list of what still needs a person.

export playwright writes the browser half of a flow. Steps with no Playwright equivalent (the mobile catalog, AI assertions) become // TODO comments and are listed in the summary. ${VAR} becomes process.env.VAR, so the exported spec reads its values the same way the flow did. A tapOn followed by typing becomes one fill(), and an assertion that narrows on text keeps both halves (expect(locator).toHaveText(…)). The flow’s before: / after: — the whole resolved chain, project and testspace hooks included — become the spec’s test.beforeEach / test.afterEach; a flow with no hooks gets no blocks. A flow with a vscode: header exports as a standalone Electron spec instead: it installs the flow’s vsix into a throwaway profile and launches the installed VS Code through Playwright’s Electron launcher, the way VS Code’s own smoke tests do. A vscodeCommand becomes the palette title it answers to, and an id with no known title is left as a // TODO rather than exported as a command doing something else.

export extester is the other way to leave with a VS Code flow: a vscode-extension-tester test — Mocha and chai over Selenium, run by extest. It refuses a flow without a vscode: header rather than writing something that cannot run, and a directory export walks past the flows that have none, saying how many. The first export into a folder also writes a README.md with the extest commands for it (get-vscode, get-chromedriver, install-vsix, run-tests, or setup-and-run in one go), the --code_version this machine’s VS Code reports, and the workspace the runner must open with -r. In the tests themselves: one TIMEOUT constant is the budget for every wait in a file (the flow’s own per-step timeouts are deliberately not carried over), and elements are found through a withWebview(...) helper that tries the workbench document and then each iframe.webview#active-frame in turn, because a flow records that a step has an element, not which document holds it.

--report json|junit|html|allure prints a json/junit/html report to stdout, or writes it to --output <file>; allure writes an allure-results/ directory in the current dir (or --output <dir>). A directory run with --report produces one combined report.

With --config config.yaml carrying a report: block (format, output — defaults htmlreports/, relative to the config’s folder) every run writes <flow>.<ext> there without any flag; a directory run writes one combined file. --report/--output still win.

A block — a runFlow into a .helper.yaml, a repeat or a retry — is one step named after what it ran, with its own steps nested under it, so a failing helper shows which of its steps failed. The top-level list still counts one step per command, which is what CI reports index by.

A step that had to act on a page that never settled says so in every format: the JSON step carries "settled": false and an "unsettledReason" (Loading, Network(2), Mutating, Navigating or Timeout), JUnit adds a <property name="unsettled"> to that test case, Allure an unsettled parameter, and the HTML an acted while loading chip on the row. A step on a settled screen carries neither field — see how a run knows the page settled.

A step that passed only because one of its or: alternatives matched says so the same way: the JSON step carries a drifted object (used, primary, alternative), JUnit a <property name="drifted">, the HTML a drifted chip on the row, and the run summary counts them. Such a run still exits 0--strict-selectors is what makes it fail.

0 — every flow passed. 1 — flows ran and some failed. 2 — a config or infra error (bad flag, no device, unparseable config), so CI can tell a red suite from a broken runner.

On a failed run tapstep prints the screen at failure and writes <flow-stem>-failure.png to the current directory — plus <flow-stem>-failure.network.json when network traffic was recorded.

Terminal window
tapstep login [--token tapstep_pat_…] # browser SSO; --token for CI
tapstep logout

tapstep login signs in via browser SSO by default; --token saves a token instead (CI), and TAPSTEP_TOKEN in the environment counts as signed in. Every command except login, logout, doctor, bugreport, update, privacy, and skill refresh requires a prior sign-in; once signed in, everything works offline.