Your agent writes the E2E tests.
You review and commit.
unotest is AI-native end-to-end testing. Your AI agent drives your real app over MCP and writes clean, reviewable tests in your repo — for web and for iOS.
Works with Claude Code · Cursor · Codex
unotest web
E2E for web apps. Playwright-vocabulary DSL on a sandboxed engine, driven by your agent over MCP.
unotest mobile
E2E for iOS apps (React Native + native Swift). The agent drives the Simulator through the accessibility tree.
Full control
AI does the work. You keep control.
The whole point: speed from the agent, ownership stays with you.
Plain .js in your repo
Tests are ordinary JavaScript in unotest/e2e/*.js. Git, code review, CI — no proprietary format, no binary blob.
No silent fixes
agent_fix composes context and a suggestion — it never calls an LLM itself and never applies a patch on its own. You read the diff and commit.
Runs locally
Everything runs on your machine. Your app never leaves it. No cloud, no account to start.
Safe to run blindly
Scenarios execute in a sandboxed AST interpreter — no require, no fetch, no filesystem. AI-generated tests can run without surprises.
What's new
unotest 0.49.2
Released 12 October 2026.
Locators that check what they say
getByRole reads disabled, checked, pressed, expanded, selected and level, and locator(sel, options) reads hasText and has, so a check for an enabled button no longer counts a disabled one. Any key a locator does not read is refused by lint and at run time. isChecked, texts(loc) and env(name, fallback) are new, and check waits for a switch that flips after the server answers.
The ternary and clearer answers
Scenarios can write cond ? a : b, and only the chosen branch runs. A lambda passed to evaluate is answered with the string to write instead, and a failed test prints the absolute path of its failure bundle. --notes prints every note of every test, with escape sequences shown as text.
A busy page fails fast
evaluate on a page stuck in a script loop fails after the action timeout instead of hanging, assertions keep their own timeout there, and the failure bundle takes a second instead of half a minute, with the reason for each part it could not read. The viewer paints an @expect-fail test that errored red, not as failed on purpose.