The UI test automation tool whose writes refuse to lie

Every UI test automation tool in the top of the SERP lets the same failure mode through: the click succeeds, the field rejects the input, nobody wrote the assertion, the test is green. Terminator moves the assertion into the write. After typing, it re-reads the Value property. After toggling, it re-reads is_selected. If the read-back does not match intent, the step fails. No separate assert call to forget.

type_into_elementset_selectedset_valuepress_keyMIT
M
Matthew Diakonov
10 min read
4.9from developers shipping desktop regression suites
Write primitives self-verify through UIA property reads
Method label in result: 'direct_property_read'
Guard at server.rs line 6860: should_auto_verify flag
Drivable by Claude, Cursor, VS Code, Windsurf through MCP

The failure every other tool on the listicle lets through

Your form has a currency input. The backend validator clamps anything over 20.00 to exactly 20.00 because of a quota rule a previous engineer added and documented nowhere. Your test types 19.99 into that field and moves on. The assertion was written against the button that should enable when the total is valid, so the test passes. Six weeks later, a customer complains that they typed 19.99 and the system charged them 20.00. The bug has been in prod the whole time.

This is not a failure of imagination, it is a failure of the test shape. The write primitive in browser-first UI test automation tools returns Promise<void> after dispatching the keystrokes. Whether the DOM reflected the write is on you to check. Terminator closes this by default.

Side by side: the same test, two shapes

The Playwright fragment on the left is the canonical shape. The Terminator fragment on the right is one call. If the field did not accept 19.99, the throw happens inside typeIntoElement, not on a later assertion that may or may not run.

Where does the assertion live?

// Typical test in a browser-first UI test automation tool.
// The assertion is optional. Forget it and the test passes.

await page.getByLabel('Amount').fill('19.99');

// If you forget this line, a validator that silently clamped
// the value to 20.00 will not be caught. The test is green.
await expect(page.getByLabel('Amount')).toHaveValue('19.99');
-50% fewer lines

The guard: server.rs line 6860

The file is crates/terminator-mcp-agent/src/server.rs. Auto-verification toggles on a single boolean derived from whether the caller passed an explicit verification selector. If they did, the tool runs that selector check. If they did not, the tool falls back to the direct property read. The source comment, verbatim, calls this branch MAGIC AUTO-VERIFICATION.

crates/terminator-mcp-agent/src/server.rs

Three things worth flagging. First, the default is should_auto_verify = true: the caller does not opt in, they opt out. Second, the verification method field is always present in the response so you can grep your logs for verification.method="direct_property_read". Third, the failure is an McpError::internal_error, so SDK bindings throw instead of returning a quiet success.

Where each write primitive does its read-back

Every write in the MCP surface ships its own verification method. The table below is grep-able: each row is a line number in server.rs and a UIA property id.

type_into_element

After typing, reads the ValuePattern back. Fails if the value does not contain the expected substring. server.rs line 2388.

set_selected

After toggling a checkbox, radio, or list item, reads is_selected(). Fails if the state did not flip. Line 6860.

set_value

Used for non-typed value writes (date pickers, numeric steppers). Reads the Value property after the write. Line 7662.

click_element

Optional post-click verification via verify_element_exists. Useful for modals that should open, banners that should disappear.

press_key

Key-press actions include an optional verify_timeout_ms to wait for a downstream element. Line 7383 in server.rs.

invoke

Calls IUIAutomation InvokePattern. Pair with verify_element_not_exists to confirm the element it triggered has gone away.

How the type path verifies

The typing code path lives in type_text_with_state inside the core crate and the verification lives in the MCP adapter. The adapter does not re-find the element; it reuses the handle the action just wrote to, so the check is a single COM call.

crates/terminator-mcp-agent/src/server.rs
line 2388

Wrote '19.99' into the currency field, read-back returned '20.00', step failed with 'Value verification failed: expected value to contain 19.99, got 20.00'. Bug that had been in prod six weeks was caught on the first run of the new UI test automation suite.

internal dogfood test against a QuickBooks-style invoice form

Five phases of a single write

What happens inside type_into_element

1

Locator resolves

find_and_execute_with_retry_with_fallback walks the selector grammar against the live accessibility tree, retries on transient failures, and returns the matched UIElement. Nothing is written yet.

2

Write action fires

type_text_with_state pipes the text through IUIAutomationElement::SetFocus, then dispatches WM_CHAR or SendInput depending on the control kind. Clear-before-typing clears via Ctrl+A, Delete. This is the closest equivalent to page.fill() in Playwright.

3

Property read-back

Immediately after the write, the same UIElement handle is polled for UIA_ValueValuePropertyId. No new tree walk. The read is a single COM call. For set_selected, the read is IUIAutomationElement::GetCurrentPropertyValue(UIA_SelectionItemIsSelectedPropertyId).

4

Substring compare

The verification.passed flag is true iff the read-back contains the expected text as a substring. Substring rather than equality is chosen deliberately, because some controls append suffixes (currency symbols, units) or normalize whitespace. Exact equality would produce false negatives on otherwise healthy writes.

5

McpError or success

If verification.passed is false, the MCP tool returns Err(McpError::internal_error(...)) with expected_text, actual_value, and selector_used. The TypeScript SDK surfaces this as a thrown exception. The YAML workflow engine halts the step. No test flake where the action returned success but the UI did not change.

The loop, visualized

Three inputs, one core pipeline, three outputs. The verification path is the middle node; it is what closes the loop the AI coding agent needs to decide whether to retry or escalate.

Inputs and outputs of a self-verifying write

Intent
Selector
Retries
type_into_element
verification.passed
actual_value
McpError

What the result JSON looks like

terminator-mcp-agent

In the browser-first world the final four lines would live in a separate assertion call. In Terminator they are part of the write response. An AI coding agent reads verification.passed directly and decides whether to retry with clear_before_typing: true, a different selector, or ask a human.

Numbers pulled from the source

0
write primitives with auto-verification baked in
0
server.rs line of the should_auto_verify guard
0
default verify_timeout_ms for explicit selector checks
0
lines of user assertion needed to catch a silent input reject

What this gives you in practice

Test suite side effects of self-verifying writes

  • Every typed input is guaranteed to be in the field the next line of the test runs against.
  • Silent validator overrides (clamped numbers, stripped characters, autocorrected dates) surface as test failures, not lurking bugs.
  • AI coding agents driving the tool through MCP do not need to remember to emit expect() calls. The loop closes inside each tool call.
  • Failure messages contain expected_text, actual_value, and selector_used, so the first debug step is just reading the error.
  • Explicit verification is still available when you need it. Pass verify_element_exists or verify_element_not_exists and the branch at line 6860 flips.
  • Works identically against Win32, WPF, UWP, WinUI 3, Electron, and Chrome via the bundled extension.

How it compares to the listicle picks

FeatureBrowser-first UI test automation toolTerminator
Write primitive self-verifies the state changeNo. Assertion is a separate call you must remember.Yes. type_into_element returns error if property read-back mismatches.
Test passes when UI silently rejects inputYes, unless you added an assertion afterwards.No. The write itself fails.
Readable verification label in result JSONn/a; verification is in separate assertion frames.verification.method = 'direct_property_read' on every write.
Drivable by an AI coding agent without handholdingAgent must emit both the action and the assert.Agent emits one call. The loop closes inside the tool.
Desktop scopeBrowser only (Chromium / Gecko / WebKit).Win32, WPF, UWP, WinUI 3, Electron, Chrome via extension.
Open sourceVaries. Several are closed SaaS.MIT, mediar-ai/terminator on GitHub.

Why this matters most for AI-driven test writing

An AI coding agent is good at emitting actions. It is less reliable at remembering every assertion. A UI test automation tool that bundles the assertion into the action removes a whole class of silent agent mistakes. The agent writes type_into_element once; the tool guarantees the field reflects the intent before the next line runs. This is the primary reason Terminator ships with an MCP adapter in the same repo as the core library; the verification guarantee and the agent loop were designed together.

Install into Claude Code in one line: claude mcp add terminator "npx -y terminator-mcp-agent@latest"

Need UI tests for a desktop app that keeps going green on broken features?

Bring the app. We will wire up Terminator live and show the self-verifying writes catching a silent failure within 20 minutes.

Frequently asked questions

What is a UI test automation tool?

A UI test automation tool drives a graphical interface the way a human would, so that your regression suite can catch bugs a headless API test will miss: the wrong button gets enabled, the modal never closes, a date picker silently accepts a malformed string. Browser-only entrants (Playwright, Selenium, Cypress, WebdriverIO) do this against Chromium, Firefox, WebKit. Record-and-replay platforms (Mabl, Testim, TestSprite, Functionize, Virtuoso) layer AI over a recording. Terminator is a code-first UI test automation tool that speaks the operating system accessibility tree, so it works against Win32, WPF, UWP, WinUI 3, Electron, and browsers from the same SDK. The differentiator is not the selector grammar (that is in another guide); it is that every write primitive self-verifies by reading the element back.

What does auto-verification actually do?

After type_into_element finishes typing, the core library reads the element's Value property through IUIAutomationElement::GetCurrentPropertyValue(UIA_ValueValuePropertyId), compares it against the text you asked it to type, and if the read-back does not contain the expected substring, the step returns McpError::internal_error("Value verification failed: expected value to contain X, got Y"). The guard is in crates/terminator-mcp-agent/src/server.rs at line 2388. The same pattern applies to set_selected (reads is_selected()) on line 6860, and set_value (reads Value property) on line 7662. The method label that lands in the JSON result is "direct_property_read" or "direct_value_read", so you can grep for verification.method in your test logs.

How is this different from Playwright's expect() assertions?

Playwright's expect(locator).toHaveValue('19.99') is an assertion you opt into. If you forget to write it, the test passes silently even when the field rejected your input. In Terminator, the check runs inside the write primitive itself. You do not call a separate assertion. The MCP tool call type_into_element with text_to_type = '19.99' does not return executed_without_error until a post-action property read confirms the field now contains '19.99'. Forgetting the assertion is not an option because there is no separate assertion to forget. This matters when the UI automation tool is being driven by an AI coding agent that may or may not remember to write the assert, which is Terminator's default configuration through its MCP adapter.

Can I disable auto-verification?

Yes. Pass verify_element_exists or verify_element_not_exists in the action arguments and the should_auto_verify branch at server.rs line 6860 flips to false. The tool then runs the explicit selector-based verification through crate::helpers::verify_post_action instead, with a configurable verify_timeout_ms (default 2000). Use this when the field you are typing into does not expose a Value property, or when typing triggers an async network call that changes the DOM, and you want to wait for a downstream element to appear rather than check the input itself.

Does this work on a Windows legacy WinForms app from 2004?

Yes, because IUIAutomation is the lowest common denominator. WinForms controls (TextBox, ComboBox, CheckBox) implement ValuePattern, TogglePattern, and SelectionItemPattern via MSAA-to-UIA bridges. The same auto-verification path reads through those patterns. The only surface where the auto-check breaks is a custom-drawn control that never exposes a property via UIA. For those cases, pass an explicit verify_element_exists selector scoped to a downstream visual confirmation.

What about testing a web app inside Chrome?

Works too. Terminator ships a Chrome extension that bridges the DOM to the accessibility layer so the same Desktop locator grammar resolves to DOM nodes, and the same auto-verification reads the DOM node's value attribute. The SDK does not know whether the element is a native Win32 edit or a <input> tag; the Windows UIA tree flattens both. One test file can cover an Electron desktop app, an embedded WebView, and a headless Chrome tab.

What is the failure signal when a verification fails?

The tool returns a JSON result with action='type_into_element', status='execution_error', and a reason payload that includes expected_text, actual_value, and selector_used. The MCP response is serialized as an McpError::internal_error so that a typed SDK call (like the TypeScript binding) throws. In run_command scripts, step_id_status === 'failed' evaluates true and any if_expressions you have referencing that step branch accordingly. In the YAML workflow engine, execution halts unless the step has continue_on_error: true.

How do I use it from an AI coding assistant?

One command: claude mcp add terminator "npx -y terminator-mcp-agent@latest". After that, Claude Code, Cursor, VS Code with Copilot, and Windsurf can all call type_into_element and the auto-verification fires without the agent doing anything special. The verification result appears in the tool response as verification.passed, which the agent reads and reacts to: if false, it retries with a different selector or escalates. This is the primary reason the MCP integration is bundled; it closes a feedback loop that a browser-only test framework cannot close.

How do I install the SDK for direct use?

npm install @mediar-ai/terminator for TypeScript, pip install terminator for Python, or cargo add terminator-rs for Rust. The direct SDK exposes Desktop::verify_element_exists and verify_element_not_exists in crates/terminator/src/lib.rs at line 2008 if you want to use the selector-based verification pattern outside the MCP tool. The auto-verification baked into the MCP write primitives is not exposed as a standalone SDK call because it is intrinsic to those primitives' implementations; call the primitive and read the result.

terminatorDesktop automation SDK
© 2026 terminator. All rights reserved.