Desktop
The desktop app is the no-code way in — and everything it makes is a plain
.flow.yaml file an engineer can open in any editor. This page is a tour of
the app; the flow language itself lives under Reference.
Sign in & first run
Section titled “Sign in & first run”The first launch shows the license, terms and privacy policy in one scroll — the three documents in full, one under the other, and Agree and continue at the bottom. One checkbox opts in to anonymous usage data; it is off by default, and it says what it covers: which features are used, the app version, OS and CPU type, a random id for this install, and automatic crash reports (the error message and stack) — never your tests, flows or their data. It stays changeable under Settings → General, and the documents stay readable under Settings → Legal.
Then a sign-in wall, which now says why it is there: a free account so your tests stay yours across machines, and after this the app works offline. Sign in / Create account does one browser round-trip through tapstep’s SSO. If the browser never comes back the screen explains it rather than spinning — no network, or the sign-in server not answering — and offers to open the page again. The identity is cached in the OS keychain (macOS Keychain, Windows Credential Manager, Secret Service on Linux). Account at the foot of the rail opens a popover with who is signed in, Manage account / Change password (both open in the browser) and Sign out.
Layout
Section titled “Layout”Five zones, and they stay in place whatever the app is doing.
- Rail (left, with labels): Tests, Runs, Devices — Extensions instead, in a VS Code project — Settings at the foot, and Account under it.
- Sidebar: one column, and the section picks what stands in it — the tests,
the runs, the devices of this machine. Devices and Extensions carry a
Manage… link that opens their full screen as a tab. Changing section moves
nothing else: the tab you were working in stays open and stays active.
Ctrl+B/⌘Bhides the column. - Tabs: a flat strip. Home is pinned at the left end under the project’s
name; every other tab carries an icon of what it is doing, a
×, and a ⋯ menu at the right end lists them all. - Bottom panel (
Ctrl+J/⌘J): Run output · Network · Console · Terminal · AI chat. Collapsed it is a strip of those names with their counts. The terminal is a real shell opened at the project root — for the CLI, git, whatever you’d type anyway;Ctrl+`still goes straight to it. - Status bar: the panel toggle; what is running —
▶ Running Login · step 4/7,● Recording · 7 steps → flows/pay.flow.yaml, and a click on it goes to that tab; the target chip (Web (Chrome) · 1280×800, click to pick another device or, in a VS Code project, another testspace); whether the project is saved; its name and test count; the version and the updater chip. Too narrow a window puts the segments out from the least important up — what is running never goes out. - Title bar: the project menu, the environment switcher (see Environments), the branch and Publish controls once a remote is connected, and Report a bug.
Settings is a tab, not a screen that replaces your work: General / Appearance / AI / Project / Storage / Legal down its side. Account is a popover from the rail: who is signed in, History, and Share with the team… until a remote is connected.
Command palette (Ctrl+K / ⌘K): record, new blank test, Save version /
Publish, toggle the panel, the terminal or the chat, History, go to any section,
open or run any test by name.
The app speaks English and Russian, and follows the system language until
you pick one under Settings → General → Language; the switch takes effect
without a restart. The View menu makes the whole interface bigger
(Ctrl+= / ⌘=), smaller (Ctrl+- / ⌘-) or its own size again (Ctrl+0 /
⌘0) — see Settings.
Tests panel
Section titled “Tests panel”Tests in the sidebar is one list with one model: the project’s tests and its
shared steps, in the folders they live in. A sub-folder is a group, and a folder
can be made in any folder the panel shows — not only under flows/.
A row is a name, one dot and words for what it is. The dot after the name is how the last run went — passed, failed, stopped, not run — and a test running right now pulses whether or not that dot is switched on. A shared-steps file carries the words Shared steps after its name and Used by N tests in its tooltip and its ⋯ menu; the export kinds a test can leave as collapse into one icon whose tooltip lists them. The ⋯ menu holds Run on…, Rename…, Duplicate, Convert to shared steps / Convert to test, Export and Delete….
Names may be written in any alphabet — a Russian name becomes a Russian file
name — and only what a path cannot carry is taken out. A name that leaves
nothing, one already taken, or one of the names Windows keeps for its devices
(CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9) says so rather than
quietly doing nothing.
Delete moves the file to the system Trash and the toast offers Undo for ten seconds; a folder goes with everything inside it. A machine with no trash falls back to asking first, the way it always did.
The Project group
Section titled “The Project group”At the foot of the panel, below Suites, a collapsible Project group gives every file the project’s README names an address instead of leaving it unlisted. The rows carry real file names, and a click opens the screen that already edits that file:
config.yaml→ Settings → Project. Deleted, the row reads missing and the click offers to write it again the way the wizard does..env, and each.env.<name>the folder holds → the project’s Variables, with that environment selected.reports/→ the newest run’s result. Before the first run the row reads no runs yet and opens nothing.playwright/,extester/→ the Exports block, listed once the folder exists orconfig.yamlnames it.fixtures/→ the Extensions rail, in a VS Code project.README.mdand the other markdown at the project root → a tab.
Each row’s ⋯ menu is Reveal in Finder, Copy path, and Open in VS Code where this machine has it. Nothing in this group is dragged and nothing takes a drop.
What the panel shows
Section titled “What the panel shows”Settings → Appearance → Tests panel holds four preferences over that one list. Each is a re-render and never a rescan, and none of them makes a second view:

- Names — Friendly (
Pay broken) or File (pay-broken.flow.yaml, in the mono face). It is the row’s text only: the order, the filter and the tooltip still read the readable name and the path. The Suites list follows it too. - Shared steps — Inline, in the folders they live in, or Grouped: one collapsible Shared steps group at the end of the list, with the folder as a suffix after the name. A folder that held nothing but shared steps goes with them.
- Sort — Name, Last run (failed first, then not run, then passed by recency) or Changed (the most recently written first). Folders keep their name order.
- Show — three switches: Run status (the dot), Git changes (the mark on a test the working tree has changed) and Project files (the Project group).
Keyboard and selection
Section titled “Keyboard and selection”The list is a tree with one tab stop. ↑ / ↓ walk the rows as they are drawn,
→ / ← open and close a folder or step into and out of it, Home / End go
to the ends, and letters typed within half a second of each other jump to the
next row that starts with them. Enter opens a test, or opens and closes a
folder; ⌘Enter runs it, Space picks it, F2 renames it, Delete /
Backspace sends it to the Trash, and Esc clears the selection, then the
filter.
A click picks one row, Shift-click a range, ⌘-click toggles one. Over a
selection the panel offers Export what is picked… (one batch, one folder)
and Delete (one undo toast for the lot). Dragging a row that is picked
carries the whole selection onto a folder — dragging one that is not picked
carries only it — and a drag of more than one row shows a ghost saying how
many. The filter leaves the tree standing and opens the folders whose rows
matched; the filter, the selection and the focused row survive leaving the
section and coming back.
Projects
Section titled “Projects”Welcome leads with one card — New project — and keeps the other two doors as quiet links under it: Open a folder you already have and Join a team workspace by URL (any git URL; an access token is optional — for private repos and Publish). Recent projects are listed underneath and in the title-bar project menu.
New project asks for a name, the platforms it tests (web / android / ios
checkboxes — a default hint, not a limit; change it later in Settings) and
whether this is a VS Code extension testing project (experimental — tests a
.vsix in a real workspace, needs VS Code installed on the machine). It does
not ask for a URL: that is asked at the first recording, and saved from there.
Until the project has its first run, Home is a single Record a test button with three lines saying what will happen — You click — we write the steps. You press Run — the machine repeats them. Something breaks — you see which step, with the screenshot. — and a quiet Or write a test by hand underneath. Once runs exist Home becomes the place you come back to: Failing now, the Tests you touched last with a Run button on each, Recent runs with All runs →, and Record in the corner.
A project is a folder: config.yaml, tests as *.flow.yaml under flows/
(sub-folders become groups), shared steps as *.helper.yaml, and reports/ for the
latest run report. Under the hood every project is a git repository —
initialised by the app, no setup step.
Settings → Project holds what the whole team shares: Staging address
(env.STAGING_URL in config.yaml — where Record and openLink start),
Target platforms, the Git remote, the project’s Variables
inventory (every ${VAR} the flows and config mention, its committed default
and the local .env value — set missing ones right there), the project’s
Hooks (the top-level before: / after: of config.yaml — what every
test in the project runs around its own steps, marked from project in the
Editor), the Environments matrix and the Exports folders.
Environments
Section titled “Environments”A test is written once and run against staging today and integration tomorrow, so the address and the credentials are not part of it. The environment switcher in the title bar says which one every run, every recording and every panel in the window works against; Environments… in it opens the matrix.
The matrix is one table: a column per environment, a row per variable, and a
chip on every value saying where it lives — shared (config.yaml env:,
committed, every environment), the environment’s own name
(environments.<name>, committed, this environment) or local secret
(.env / .env.<name>, never committed). A cell an environment does not
override reads inherits. Rename, duplicate and delete an environment, set one
as the project’s default, add a variable where it belongs, Promote to
shared or Reset to shared — every edit writes config.yaml or the dotenv
file, so the result is a text file git can diff. A secret is local to each
environment: promoting one copies it into the shared .env instead of
committing it.
The switcher reaches the rest of the app:
- Record says which environment it is recording against, and writes a value
that environment already names as
${VAR}instead of the literal you typed — the card offers keep literal when you meant the text. - The Variables panel over the steps shows each value and its source, so
${BASE_URL}reads differently in staging and in prod without the file changing. - The Run dialog carries an environment chip: it picks which environment the run resolves in — it does not copy that environment’s values into the rows below, which are still overrides on top of it. Shared only runs against the shared variables and nothing else.
- The report of the run names the environment it ran against, and so do the JUnit, Allure and HTML reports the CLI writes.
- The AI chat says so when its own runs go to the project’s default environment rather than the one the title bar is on.
The file side of all this — environments:, defaultEnvironment,
.env.<name>, ${TAPSTEP_ENV} and the layer order — is on
config.yaml.
Record
Section titled “Record”Record a test opens What are we testing? — one row per target: A website (opens in a browser on this machine), An Android app, An iPhone app, and Desktop app (:port) when one is already on the tapstep bridge. The device the status bar points at is already picked. A target that is not up says so in place, with the fix and a Check devices link that looks again inside the dialog rather than walking you to another screen: No browser found — install Chrome, Connect a phone or start an emulator, Start a simulator in Xcode. A website asks for the address, and the first one you give becomes the project’s staging address — next time it is already filled in. Android and iOS ask for the app id. A VS Code project records against the testspace you have running, so launch one under Extensions first.
What are we testing? — the staging address is remembered after the first recording.
Recording then gets a window of its own, next to whatever you are recording, and the tab it belongs to shows Recording · Web (Chrome), the step count so far, and buttons to Show the recording window or Discard.
The recording window’s bar: a running REC m:ss beside Recording · Web (Chrome), then Do / Check, Re-read (re-read the window, or refresh the hotspots on a mirror), Card on / off, and Discard. At the bottom: a name field, the file the test will land in, and Save / Save & run — Save & run saves and plays the test straight back in the same tab.
Discard asks once, and the question names what comes back out: the steps go
with the window, and anything the recording wrote into the project — the staging
address in config.yaml, a password filed into .env — is taken back. Whatever
those files already held stays as it is.
Web and VS Code — your own window
Section titled “Web and VS Code — your own window”A web recording opens a real browser window and stands the recording window beside it, matching its height, without moving or waking it. A VS Code recording drives the VS Code window you already have running. There is no mirror in between — you click in your own window and every click arrives in the recording window as a step, and what you type lands as one when you leave the field.
The browser it opens is a fresh window on a throwaway profile, not the one you have open: no bookmarks, no logins, no extensions, so expect the cookie banner every time. It is sized 1280×800. Any Chromium browser works — Chrome, Edge, Brave, Chromium. Firefox is refused up front, because it cannot report what you do in its own window; flows still run on Firefox, they just aren’t recorded there.
The card beside what you click. When a click could be written down more than
one way, a small card comes up next to the element itself: Step recorded: click
on button “Pay” → How should the test find this button? → the ways, numbered,
one of them marked — chosen and any that several elements answer to carrying
the count (6×) → then Add a check: “Paid” appeared, Skip this step,
Close, and last Nothing to do — the step is already recorded. Every row is a
button and a key: ⌥1…⌥4 name the element, ⌥5…⌥8 add a check after
the step, ⌥0 takes the step back, ⌥Esc closes the card (⌃⌥ when the window
being recorded is VS Code; Ctrl+Shift+ on Windows and Linux, with Ctrl+Alt+Esc
to close). Clicking the card never takes the keyboard away from the page you are
recording, and until you have pressed three of them the keys are spelled out in
words. The card is only shown where there is something to win: a click on an
element with a unique id or test id is recorded silently. Card off in the
bar stops it appearing at all; a mirror recording has no card, because it has no
window of its own to stand beside.
Type into a field and the card asks what should become of the text — Keep it as
text, Make it a variable, It’s a secret — to .env, not into the test file,
and, when the value ends in digits, Name just the … — or Skip what you
typed. A masked field never waits to be asked: the value goes to the project’s
.env, the step becomes inputText: ${NAME}, and the card says so. The name
comes from the field’s label (Your password → YOUR_PASSWORD, falling back to
PASSWORD), and a name already in use gets a suffix rather than being
overwritten. The value itself never reaches the step list, the log or the flow
file.
In Check mode a click checks instead of acting: Alt-click (Option on
macOS) an element in your own window — the page never sees the click, and the
card asks what to name the check.
A Check URL button (web only) records a check that the page is on the
address it is on now. When that address sits under the project’s staging
address it is written through the placeholder — ${STAGING_URL}/cart — so it
survives repointing staging; an address outside staging is written literally.
Clicks inside an extension’s own panel (vscode-webview://) are recorded too,
including panels opened during the recording and ones that rewrite their own
document.
Mobile — the device mirror
Section titled “Mobile — the device mirror”Android and iOS record through a mirror of the device: click → tapOn, drag →
swipe, hold → long-press, type on your keyboard → inputText, wheel →
scroll. Back records a pressKey: back step and is offered on Android only,
where the key exists. A Type into the focused field… disclosure holds
Type and Clear (records clearText) for values too long to type. Where
no element is recognised you can still click anywhere — the tap is recorded by
position.
The Do / Check toggle switches clicks from tapping to checking: in Check
mode a click records assertVisible and runs it against the live screen at
once, so a check that would fail on replay fails while you watch.
A Fresh email chip fills the focused field with a qa+…@example.com address
and keeps it in an EMAIL variable, so a confirm field can reuse the same one —
click it again and you get the same address, not a second one. What lands in the
flow’s env: is the expression qa+${Date.now()}@example.com, which the engine
evaluates once per run: fresh every run,
identical in every step of it.
Steps as they are recorded
Section titled “Steps as they are recorded”Every recorded step shows what it is addressed by, as a row of chips: the
visible text, a test id, a role and name, a label, a placeholder. A chip that
several elements answer to carries the count (· 3 matches); the engine’s own
pick is marked Chosen automatically, and one click moves the step onto
another way of naming the same element.
The recording window beside a real browser: each tap carries its alternatives, the engine’s pick is marked, nothing else is pre-selected.
Scrolling. A scroll you make yourself is noticed however you make it — the
wheel, the scrollbar, a finger, the arrow keys — and a scroll the page does on
its own is not. What becomes of it depends on what you do next. Scroll to reach
something and then click it, and no scroll is written at all: on replay every
action scrolls its target into view first, so the flow does not depend on where
the page was standing. The window says so rather than leaving you looking for a
step that is not there — Scrolled down to reach it — the run will scroll there
on its own. Scroll with nothing after it — you were reading — and the window
offers it: Add step writes scroll,
half a window at a time. A scroll inside a box that scrolls on its own — a
results list, a side panel — names that box: the step carries within:, so the
run moves the same box rather than the middle of the window. Sideways is
swipe.
A click nothing on screen answers to is recorded as a position and says so, so you know that step will not survive a redesign. That is the one case where the scrolling in front of it is written out — a coordinate does not scroll with the page, so the step needs the page put back where it was.
Not every step is a click. Enter, and a chord the card has no key of its
own for — Ctrl+Shift+P — are written as
pressKey, behind whatever you had just
typed. A rest that opened a menu is written as
hover, ahead of the click inside what
opened; a rest that opened nothing is not a step. Choosing in a native list is
selectOption — the click that put
the list up was the browser’s own window, and there is nothing there for a run
to reach. A file handed to an uploader is
uploadFile, named by css when the page
hides the input the files go to, which is the usual case for a styled one.
A drag is dragAndDrop and carries the
way the pointer actually went, so a list that counts the moves between the ends
gets them; both ends have to be elements, and a drag whose ends nothing names is
said in the window instead of written.
Said, not recorded. A link that opens a new tab is noted in the recording
window and never becomes a step: the second tab is the browser’s own, outside the page this
flow drives. Add openLink by hand, or record that tab as a flow of its own.
Switches and checkboxes are written as check / uncheck rather than
tapOn, and the verb is the state the press found, not the one the click
left: replayed blind, a tap flips whatever it lands on, while a check reads
the state first and does nothing when the box is already on. A radio is always
check. A control that reports no state at all stays tapOn.
A control nothing names — the bare switch a designer left captionless — is
addressed by the caption beside it, as a relational selector: { role: switch, leftOf: "Published" }. The side is the control’s, not the caption’s, and the
caption of the control’s own row comes first, then the one over its column,
and only then the nearest text above it. Where one caption reaches several
controls the step is pinned with index:, counted out from the caption, and
the card says so — more than one switch stands left of “Published” — the step
takes the 1st of 2, counting out from the caption — with the others offered
as alternatives.
A button with no word on it — the trash icon at the end of a row — is
recorded as a button all the same: the recorder reads what answered the click
(the element’s own onclick, a React or Vue handler, a listener the browser
knows about, or the pointer cursor under it) and then names it by its label, by
the caption in its row, or by the picture it draws — { css: "[data-icon=\"trash\"]" },
with index: where two rows carry the same icon. The step’s note says which of
those named it, and a position is left only for a click nothing answered.
The list is the full step editor while you record: reorder, delete, edit a
selector, add a step by hand. A Type step whose text is a plain literal
offers Move this text into a flow variable, which puts the value in the flow’s
env: under a name it suggests and rewrites the step to ${NAME}.
Name the test and save; it lands in the Tests rail as a .flow.yaml, and its
tab stops being a recording and becomes the test.
The first run of a test opens the Run dialog; after that Run repeats those choices on whatever the status bar’s target is, without asking. Run on… always opens the dialog. If the target the test last ran on is not up, the dialog opens with nothing selected and Run disabled until you pick — nothing is quietly substituted.
The dialog lists devices on this machine — browsers, Android devices/emulators,
iOS simulators, and Desktop app (:port) when an app with the tapstep bridge
is up (see Testing desktop apps). The list is narrowed to the
project’s target platforms. VS Code projects pick a testspace instead, with
Launch to open the VS Code window first — runs and recordings then attach
to that window rather than opening a second one, which is what keeps a
vscodeCommand by id answering through the helper extension it was launched
with. A testspace can carry Start
clean — the setup that closes the editors, panels and notifications a
previous run left open, so a test never starts wherever the window happened to
be; it is the before: clean preset, and turning it off is how you write your
own setup for that testspace. The hooks on Extensions are the testspace’s
own; what the whole project runs around every test lives in Settings →
Project → Hooks and is marked from project in the Editor.
Options: Show the browser window, Browser size presets (1280×800 default, Laptop, Full HD, Tablet, Phone), Record video and — for browsers — Video quality: Actual size or Sharp · 2× (for demos). Choices are remembered.
A recorded run video (Record video, Actual size) — what lands next to the flow as <flow>.mp4.
Variables at the bottom: the environment this run resolves its variables in
(or Shared only) and free NAME / value rows that override it; remembered per
test. API flows (device: none) skip the device list and show only this
section — and never ask for a device at all.
The run happens in the test’s own tab: the screen, the steps going green and
a 0:03 · Cancel in the corner. Run output, Network (scoped to the
clicked step, or All) and Console stream into the bottom panel, and stay
there after the run so you can read them from any tab. A second Run while one is
going is refused rather than silently cancelling the first — a toast says Login
is running — stop it first with a Show button that takes you to it. Closing
the tab asks Stop the run? first.
After every run the report is written into the project as
reports/<flow>.html — report: in config.yaml overrides format
(html / junit / json) and directory.
A run that passed — the same tab it was started from, with the panel holding the output and the traffic.
A green run ends in a verdict and the video, in that tab. A red one offers See what broke, and the tab turns into the triage of that failure.
Runs, triage, history
Section titled “Runs, triage, history”- Runs — the sidebar feed, grouped Failing now, Suite runs and All runs (a suite = every test with a tag, run from the Tests rail). A suite row opens its HTML report as a tab and offers Export Allure; a single run row offers Export JUnit. Clicking a run opens it in that test’s own tab — an older run, or one whose test is busy recording or running, opens beside it, so two results stand side by side.
- Triage — the failing screenshot or the recorded video, the step list with shared-step bodies and repeat iterations nested under their step, Network and Console in the panel marked at the failing step, and Open test, Fix with AI, Re-run, plus Show it again from here when the failing step can be re-recorded in place — see below.
- History — from the Account popover; it opens as a tab: versions → changed files → diff.
Open test on a run whose file is gone no longer does nothing: it rescans first, then says Test not found with the path that was renamed, moved or deleted since that run, and offers Show the folder — the nearest folder of that old path the project still has.
Triage of a failed run — Show it again from here is the suggested action, Re-run steps aside.
Repair from the failing step
Section titled “Repair from the failing step”When a test breaks on step N, triage offers Show it again from here instead
of making you re-record the whole thing. It is the suggested action on that
screen; Re-run steps aside next to it.
A sheet comes first, because the steps before the failure are about to run for real: Show step N again names how many of them will replay, on which device and with which variables (both changeable there), and warns in as many words that steps 1–N−1 run for real on the target — if one of them places an order or sends an email, it happens again. When the failure is on step 1 it says instead that nothing runs before it, the target just opens. The device is the one the failed run used — repeating the steps somewhere else would not be the same test.
The sheet before a repair: what replays, where, and the warning that those steps run for real.
Confirm, and the repair happens in the same tab: tapstep drives the target up
to step N, then hands it to you on the device mirror under a header reading
Fixing flows/pay.flow.yaml · step 7, with the Do / Check toggle and the
hotspots of the screen. The file’s own steps frame the one being replaced — the
ones before it above, the rest below — so you only demonstrate the one step.
Save writes just that step back into the same file, keeping every other
step, key and comment; Delete this step from the test removes it instead.
Saving takes a version of the whole project first, so the edit is always one
git step away from being undone. When it lands, a toast says Step 7
replaced with a Re-run button — the repair itself is not a run. Cancel
or Esc goes back to the result, and the tab’s × asks Cancel the repair?
first.
Showing step 7 again — the file’s own steps frame the one being replaced; the new step goes in its place.
A VS Code run can be shown a step again too: the sheet picks the testspace (the one the run used, by default), the replay opens that window with its extensions and folder, and you demonstrate the step in the real editor rather than a mirror. Web repairs still go through the mirror — recording in your own browser window is not wired to the repair yet.
The flow’s before: hooks replay ahead of the steps, so step 1 is repaired in
the state a run gives it.
It refuses rather than half-works. When the target the run used is not up, it
asks you to bring it back rather than replaying onto something else. And when
the step is not one it can replace on its own, no button appears — a line says
why, because a missing button with nothing beside it reads as a broken one. The
failure was in the setup rather than in a step, so a setup step is what there is
to fix first. Or the run’s target cannot be driven again at all — a platform the
app does not reopen, or none. Or the step itself is blocked: it comes from
shared steps (open that file instead), it sits inside a repeat / retry /
runFlow block or a conditional one, it belongs to before / after, the flow
drives no device (device: none), a variable or a parameter was missing, the
run kept no report of its steps, the run never reached a step, or the file
changed since that run and step N is a different step now.
If the file changes underneath while the repair is open, the save is refused outright — nothing is written — rather than replacing the wrong step.
Editor
Section titled “Editor”Steps ⇄ YAML: two views of one file; YAML is the source of truth,
comments and formatting survive Steps edits. Steps view is three zones —
Before (runs before step 1 — a failure here skips the steps), Steps
(the numbered ones) and After (always runs after the last step, whatever
happened before) — and the hooks are before: / after: in the file
(onFlowStart / onFlowComplete are the older names and still work). Rows the
project or the testspace contributes are marked from project / from
testspace inheritHooks: false when you turn it off. A zone standing on a
preset (before: clean) says so and offers Expand into steps instead of a
row to add. Last run’s per-step screenshots appear inline.
The Steps view of a recorded login — the same file the YAML tab shows.
What a step looks for is one control at the top of its form: text, #id,
regex, ⌖ point or role — role draws the engine’s own list of roles beside
a field for the element’s name, which is what the recorder writes for a button.
+ Condition adds any other selector key the engine publishes — testId,
label, placeholder, css, index:, enabled:, above / below /
leftOf / rightOf and the rest — ranked the way the engine ranks locators.
The keys that only narrow a match — index:, of:, enabled:, checked:,
focused:, selected:, tolerance: — wait until the selector says what it
matches, since on their own they write a selector the engine refuses. The crosshair beside
them picks the element off the screen instead; it has a screen to read only
while a recording is live, so without one it says Start a recording to pick
from the screen and offers Record… next to it.
${ autocompletes the flow’s variables in the YAML editor and in step inputs;
undefined ${NAME} references are flagged. The Variables panel above the
steps lists every name the flow can see, its value and source — default
(params:), flow, config, .env, runtime (with the producing step) — and lets
you set an undefined one into .env inline.
Shared steps: + → Blank shared steps; a test’s row menu has Convert to
shared steps / Convert to test; the editor header of such a file reads
Shared steps — used by N tests. The Include flow (runFlow) step has a
picker (shared steps first, + New shared steps…) and a form for the target’s
declared params:. Repeat / retry blocks nest their own steps.
Exports
Section titled “Exports”A test can leave as somebody else’s test. A test’s row menu — and the chat —
offer Export: Playwright (*.spec.ts) for anything, and ExTester
(*.test.ts) only where the test has a vscode: header. A bulk export takes
the flows the chosen kind can carry and says how many it walked past.
Where the files land is the project’s setting, not a question per export:
Settings → Project → Exports holds a folder per kind. A relative path
travels with the project, so the whole team exports to the same place; an
absolute one is yours alone, which is how a QA suite in its own repository
works. Empty means playwright/ and extester/ inside the project. The first
export of a kind asks once — Playwright tests land in playwright/ — Export?
— with Change… beside it.
Home has one Exports section, a block per folder: how many specs it holds, when it was last exported, Re-export all, Reveal, Open in VS Code, and a status chip on every file — exported (the spec is what the test says), stale (the test changed since), edited (the spec was changed by hand, and exporting again loses that) or not from a test (no tapstep header). A folder inside the project also carries a Track in git switch; a folder in another repository says so and leaves that decision there.
Overwriting is asked about, and the question separates the two cases: specs this app exported before, and specs someone edited by hand.
AI chat
Section titled “AI chat”The ✨ AI chat tab of the bottom panel is an agent that sees the device and the workspace: it records steps, writes and edits flows, runs them and heals selectors. Providers, in the chat’s ⋯ menu: Anthropic API key, OpenAI-compatible (Ollama, LM Studio, OpenAI…), Claude Code, Codex, Gemini CLI, OpenCode. Toggles: Fast (capped thinking) and Draft / Verified (Verified must reach a green run before the turn ends). You can steer a running turn by typing; attachments and chat history are kept per project.
The chat is one stream: the agent’s steps stand inline in the conversation where they happened, not in a separate view, and a single Details toggle opens or closes every step’s input and result at once. The status line under it names the phase — Thinking, Working (with what it is working on) or Verifying — and every turn ends with a done, whatever it ended on. When the title bar is on an environment other than the project’s default, the chat says so: its own runs go to the default.
When the agent needs a real credential it opens a dialog instead of asking in
the conversation — the value goes to the project’s .env, the flow references
${NAME}, the model never sees it. Settings → AI turns on Privacy
masking (an on-device model masks names, emails, phones and secrets before a
request leaves the machine), exposes the MCP server for Claude Code, Cursor
& co., and installs the Agent skill. Details: AI agents.
Save version & Publish
Section titled “Save version & Publish”Save version = a local checkpoint with a comment; works offline. Once a git
remote is connected the same button becomes Publish to team, and it stands in
the title bar; before that, the way in is Account → Share with the team… or
Settings → Project → Git remote. Connect any remote — https://… or git@…:
SSH uses your keys / ssh-agent, HTTPS uses this computer’s git credential helper;
a token is only for a machine whose git isn’t set up (it stays in the OS
keychain). Local operations (init, commit, history, clone) are embedded in the
app; fetch and push run through the system git, so Publish needs git
installed.
Branches: a title-bar switcher with New branch… (a personal user/<you>
branch keeps edits off main until published). Before a publish the app scans
committed files (config.yaml, flow env:) for credential-looking values and
asks you to move them to .env and keep placeholders — Publish anyway is
there. Conflicts are resolved whole-file, no merge markers: Keep mine /
Take {Name}’s / Compare.
Devices
Section titled “Devices”The Devices sidebar is the short version: the devices this machine has, a
click to make one the target, Boot for an emulator or simulator that is not
up yet, and a refresh. Manage… opens the full screen as a tab — a graphical
tapstep doctor: detect → fix. Locate existing… / Install SDK… for
Android, Install · ~170 MB for a managed Chromium (a pinned build, same as
CI), Boot for emulators and simulators. Stop on an emulator row runs
adb emu kill — it also clears a window-less zombie instance. The list rescans
whenever the window regains focus.
Settings
Section titled “Settings”Settings opens as a tab, with its own sections down the side:
- General — Language & updates: Language (System / English / Русский; System follows this computer, and switching needs no restart), updates (Check for updates), Report a problem (zips app logs and version info; inspect it locally or send it — you get a report id and the row shows its status), Anonymous statistics.
- Appearance — this machine’s look, in four groups (below).
- AI — chat provider, privacy masking, MCP server, agent skill.
- Project — Staging address, target platforms, git remote, variables,
Hooks (the project’s own
before:/after:), Environments, Exports. - Storage — what tapstep keeps on this machine (run history, testspace zones, temp leftovers) with Clear buttons.
- Legal — license, terms, privacy policy, third-party notices.
Appearance
Section titled “Appearance”Everything here is per machine, not per project, and every change is live — nothing waits for a restart.
- Interface — Theme (System / Light / Dark; System follows this
computer), Interface scale (80–150 %, in steps of 10;
⌘=/⌘−/⌘0and the View menu move it), Density (Comfortable / Compact — how tight the rows in the rails and lists stand) and Reduce motion (Off / On / System, which followsprefers-reduced-motion). - Editor font — used by the editors, the terminal and the panels: Family (JetBrains Mono, SF Mono, Menlo, Fira Code, Cascadia Code, or a name you type; a font this computer does not have falls back to the default), Size (11–20 px), Line height (1.2–2) and Ligatures.
- Tests panel — how the list of tests reads: Names, Shared steps, Sort and what the rows Show — see Tests panel.
- Editor — Word wrap, Line numbers, Highlight the current line and the Autosave delay (200–2000 ms: how long an edit waits before the file is written).
Reset appearance puts the theme, the scale, the font, the editor settings, the density, the motion setting and the tests panel’s own settings back to a fresh install. Nothing else about the window changes — open tabs and panel sizes are Storage’s Reset the interface, which is a different button.
Report a bug
Section titled “Report a bug”Report a bug in the title bar opens a form, once you are signed in — an issue carries your name so we can come back to you about it, and nothing goes anonymously from there. A title, a severity (low / medium / high), and three optional boxes: What I did, What I expected, What happened — at least one of them must be filled in. You can attach a screenshot (Retake re-takes it) and the app’s logs; the logs are masked when the on-device privacy model is installed, and the checkbox says plainly when they are not.
Your own issues are listed under the form with their status — new, seen, fixed (with the version that fixed it) or closed — and the button carries a badge while any of them is still open. Offline, the last list you saw is shown and marked as such. This is the signed, tracked route; Settings → General → Report a problem is the other one, which sends logs anonymously and does not open an issue.
Updates
Section titled “Updates”Updates download in the background. The status bar shows
Downloading v{X} · N%, then a v{X} ready · Relaunch chip — one click
installs it.
Testing desktop apps
Section titled “Testing desktop apps”A Tauri/webview app with the tapstep bridge shows up as Desktop app (:port)
in the Run dialog, with Launch app when config.yaml declares how to start
it — see Testing desktop apps.