SmarTTest

The platform

Tests

A test is one document that both a person and a machine can execute. The written steps and the automation plan live side by side, so they cannot drift apart.

Anatomy of a test#

Every test has a written half and, optionally, an executable half. The written half is what a person reads during a manual pass and what a reviewer checks. The executable half is an ordered list of browser commands. Both describe the same behaviour.

The test editor
The test editor. Written steps on one side, the automation plan attached to the same record.

Fields#

These are the fields the test editor gives you, in the order they appear.

ParameterTypeDescription
TitlereqtextWhat the test verifies, in one line.
PriorityreqLow | Medium | High | CriticalUsed for filtering, and for deciding what belongs in a smoke run.
Tagsmulti-selectCross-cutting labels. See Tags.
ReferencestextTicket ids or links. What explains why this test exists.
Automated?No | Yes | Impossible | Discardeddefault NoWhether this test has automation. See below.
Automation statusCompleted | In Progress | Needs Attention | BrokenHow the automation is holding up. Only appears when Automated? is Yes.
Why can't it be automated? / Discard reasontextOnly appears for Impossible and Discarded. Maximum 500 characters.
DescriptiontextThe context a reader needs before the steps make sense.
PreconditionstextThe state the system must be in before step 1. Setup that has to actually run belongs in a reusable action instead.
Stepsreqordered listEach step has an action, its own expected result, and optionally the automation commands that perform it.

Note

The editor has two tabs over the steps: Manual Test Design for the written version, and SmarTT Playwright Automation for the executable one. They describe the same test, so changing one is a prompt to check the other.

The four automation states#

"Automated: yes or no" loses the most useful information, which is why a test that could be automated still isn't. SmarTTest keeps four states instead:

  • Yes: it has an automation plan. Pair it with an automationStatus so you can tell finished automation from work in progress and from automation that is currently broken.
  • No: not automated yet. The default, and the honest state for a backlog.
  • Impossible: cannot be automated at all: a physical device, a third-party flow you do not control, a visual judgement. Requires a reason.
  • Discarded: could be automated, but the team decided it isn't worth it. Requires a reason.

Why the reason is mandatory

Impossible and Discarded are the two states that get questioned in a review a year later. Forcing a reason at the moment of the decision is the only time anyone actually remembers it. It also makes the Health Dashboard honest: unautomated tests with a justification are not the same problem as unautomated tests without one.

Heads up

automatedRejectReason is not a description of the automation. Putting a summary of the script there when the test is automated is rejected. The field only exists for the two negative states.

The automation plan#

Automation is stored as an ordered list of actions. Each action has a command, an input object, and optionally a saveAs that captures its result into a runtime variable.

an action
{
  "command": "playwright_browser_click",
  "input": { "role": "button", "name": "Sign in" }
}

Available commands

Grouped by how often you will reach for them.

ParameterTypeDescription
playwright_browser_navigatecommonOpen a URL. Usually the first action of a test.
playwright_browser_clickcommonClick an element.
playwright_browser_typecommonType into a field.
playwright_browser_fill_formcommonFill several fields in one action, instead of a click-and-type pair for each.
playwright_browser_select_optioncommonChoose from a dropdown.
playwright_browser_press_keycommonPress a key. Only real key names are accepted, so a typo is rejected on save.
playwright_browser_hovercommonHover, to reveal menus or tooltips.
playwright_browser_wait_for_elementcommonWait until an element is there. The correct alternative to a fixed sleep.
playwright_browser_wait_forcommonWait for text to appear or disappear, or for a fixed time.
playwright_browser_take_screenshotcommonCapture the page, or a single element. The image is attached to the run result.
smartt_read_elementcaptureRead a value off the page into a runtime variable. Requires saveAs.
smartt_assertassertionCompare a captured value against an expectation. See below.
smartt_run_reusable_actioncommonRun a shared block of steps here. Editing the block updates every test that uses it.
playwright_browser_navigate_backless commonBrowser back.
playwright_browser_dragless commonDrag an element.
playwright_browser_dropless commonDrop onto a target.
playwright_browser_file_uploadless commonAttach a file to an input.
playwright_browser_handle_dialogless commonAccept or dismiss a native alert / confirm / prompt.
playwright_browser_resizeless commonResize the window mid-test.
playwright_browser_tabsless commonOpen, switch, or close tabs.
playwright_browser_get_urlcaptureCurrent URL into a variable. Requires saveAs.
playwright_browser_get_titlecapturePage title into a variable. Requires saveAs.
playwright_browser_console_messagescaptureConsole output into a variable, useful to assert no errors were logged.
playwright_browser_network_requestscaptureAll network activity into a variable.
playwright_browser_network_requestcaptureOne matched request into a variable, to assert on its status or body.
playwright_browser_closeadvancedClose the browser explicitly.

Runtime variables

Six commands return a value, and all six must capture it. That is what saveAs is for. A command that reads something and throws the result away is almost always a mistake, so it is rejected rather than silently accepted.

Once captured, refer to the value anywhere as {{runtime.NAME}}.

capture, then assert
[
  {
    "command": "smartt_read_element",
    "input": { "role": "heading", "name": "Order confirmed" },
    "saveAs": "confirmation"
  },
  {
    "command": "smartt_assert",
    "input": {
      "actual": "{{runtime.confirmation}}",
      "operator": "contains",
      "expected": "Order confirmed"
    }
  }
]

The most common automation mistake

smartt_assert requires actual to hold a {{runtime.NAME}} reference, not a literal. Writing "actual": "Order confirmed" compares a constant to a constant, which proves nothing about the page, so the Quality Engine flags it. Capture the real value first, then assert on the capture.

Assertion operators

smartt_assert takes actual, an operator, and usually an expected:

  • Comparison: equals, notEquals, contains, notContains, matches (regular expression).
  • Numeric: greaterThan, lessThan, greaterOrEqual, lessOrEqual.
  • No expected needed: isTrue, isFalse, isEmpty, isNotEmpty.

How to point at an element

Most commands need to identify an element. The order below is not stylistic. It is how resistant each one is to a redesign that does not change behaviour:

  • Role and name: { "role": "button", "name": "Sign in" }. Matches what a user sees, and survives markup changes.
  • Label: for form fields, the visible label.
  • Test id: stable, but only if your app actually maintains them.
  • CSS selector: the last resort. It breaks on refactors that changed nothing a user would notice, and that is exactly what makes a suite feel unreliable.

Names match by substring, so a link named Free also matches "Start for Free". When more than one element matches, the step fails instead of picking one at random, which is the right call: a test that silently chose would pass or fail depending on render order.

  • exact: true requires the whole name to match. This is the fix you want most of the time.
  • nth: 0 picks one when several elements really are identical, like a row in a list.

Tip

validate_automation tells you how many elements a locator matches before you run anything, and shows the text of each one. See the tool reference.

The Quality Engine#

Automation can be syntactically valid and still be worthless: no assertions, fixed sleeps, a password typed in as a literal. The Quality Engine reads a test's automation and returns a score out of 100 with specific findings. It is entirely deterministic: no AI, no cost, same input gives the same output.

The same screen, two tests

The scorecard sits above the automation plan. These two tests were written against the same application on the same afternoon.

A test scoring 49 out of 100, marked Fragile, with ten findings
49/100, Fragile. The line that matters is 8 actions, 0 verifications: this test clicks its way through checkout and never checks that anything happened, so it would pass on a completely broken payment flow. The ten findings are listed per step and per action, not as a vague overall complaint.
A test scoring 100 out of 100 with no issues found
100/100 on a test doing the same kind of work. It verifies a result, finds its elements by role and name, and reads its credentials from variables instead of having them typed into the steps.

Nothing about the first test is unusual. It was written quickly to move a coverage number, it runs, and it goes green. That is exactly why a deterministic score is worth having: nobody would have caught this by reading it.

It checks eight things:

  • Verifications: does the test actually assert anything, or does it just click around?
  • Locators: how brittle the element references are.
  • Hard waits: fixed sleeps, which are the main source of flakiness.
  • Text waits: waiting on text that will not reliably appear.
  • Step actions: whether the automation matches the written steps.
  • Hardcoded data: credentials, URLs, and other values that belong in a variable.
  • Runtime variables: values captured but never used, or asserted without being captured.
  • Empty steps: written steps with no automation behind them.

It catches flakiness before you have any

A flaky test is usually not born flaky. It is written with a pattern that will eventually flap, and the flapping only shows up weeks later on a slower day. Much of what the engine flags is exactly that pattern, caught while the automation is still on screen:

  • A fixed sleep instead of a wait. Waiting three seconds passes on your machine and fails on a loaded CI box. The fix is waiting for the element, and the finding says so.
  • A CSS selector where a role and name would do. It survives until somebody reorders a div, and then it fails for a reason that has nothing to do with the product.
  • Waiting on text that is not guaranteed to appear. A wait that sometimes has nothing to wait for is a timeout waiting to happen.
  • A value read and never asserted. The test looks like it checks something and does not, so it passes through real breakages and fails on unrelated noise.

Note

This is the preventive half. The Health Dashboard handles the other half: tests already producing inconsistent results across runs, ranked by how unstable they are. One stops flakiness being written, the other finds what slipped through.

Every finding comes with a prompt

Each finding has a copy fix prompt action. It generates an instruction written for that specific finding, on that specific step, ready to paste into a terminal agent such as Claude Code or Cursor. The prompts are built per rule rather than generically, because "fix this test" makes an agent guess, and a guess is how a brittle locator becomes a differently brittle locator.

Tip

An agent connected through the MCP skips the copying entirely: it calls analyze_test_quality on its own work and fixes what comes back. That loop of write, score, fix is what makes the difference between automation that runs and automation you can trust.

A side effect worth having: accessibility

The engine pushes you towards locating elements by their role and accessible name, because that is what survives a redesign. That happens to be the definition of an accessible interface too: a button a screen reader can announce is a button your test can find by name.

Plenty of legacy applications have neither. When a test cannot be written against roles and names because the page exposes none, that is not a testing problem to work around with a CSS selector. It is an accessibility gap the automation just found. Teams automating older applications with SmarTTest tend to end up adding labels and roles to the product itself, which makes the tests stable and the application usable by more people at the same time.

Flakiness tracking#

As a test accumulates results, SmarTTest tracks whether it produces consistent outcomes. A test is STABLE, FLAKY, or a REGRESSION, with a score behind it. Flaky tests surface on the Health Dashboard, because a suite where failures are routinely ignored is worse than no suite at all.

Archiving#

Tests are never hard-deleted. Archiving is a soft delete: the test disappears from the tree but keeps its history and its past run results. See Test Archive.

Through the MCP#

list_tests, get_test, create_test, update_test, archive_test, restore_test, and analyze_test_quality. Full parameters in the tool reference.