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.48.0
Released 8 October 2026.
A CI log that says why
bundle push --run --follow now prints what each run printed while it goes: every scenario's start and end, each failed step with where it is and its error, and the run's log and note lines. The box masks the environment's secrets before a line leaves it, and gives the lines only to the token that ordered the run.
Values merge, bundles come back
env push merges into the box's values instead of replacing them, so a value set on the box survives a push; --prune removes the rest. bundle rm takes back a bundle pushed with the wrong token, and a box operator adds an environment without editing the config or restarting the box.
Scenarios closer to JavaScript
!x negates and arr.push(x) appends in place. setViewport(null) runs a test on the real browser window, assertCount and the other asserts take their options object, and wait(ms) is answered with what to use instead. The viewer's bar labels the suite's and the box's versions apart.