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.0
Released 9 October 2026.
Several tests in one file
A scenario file can hold several test_* functions, and each one is a test of its own: its own run, browser context, retry and verdict. e2e --test and run_test {test} play one of them, a collection counts tests, and the viewer shows a file as a node with a row per test. A test that skips itself now shows as skipped, not as passed.
One verdict per test on the box
Run again on one test in a box's viewer runs that test only. Slack, Telegram, webhook and GitHub checks count tests and name a failed one as file › test, and bundle push --follow prints each test of a file with its own steps, logs and notes.
Values from your source, checks that say more
A generate command writes unotest/generated/ from your application's source before a local run, and scenarios read it with readJson(). A collection sets its own browser window. A failed step prints its notes and the whole diff, and filter() refuses a key it does not read instead of checking less than it says.