2026.10.11 · 5 min · testing · ai · typescript · news
e2e: the agent clicks once, then replays.
TesterArmy's open-source e2e framework lets you write a test step in plain English. An agent drives it once, the step is recorded when a later check passes, and every run after that is a replay with no model calls. What it does, as of 2026.10, and where the catch is.
Most end-to-end tests I have seen fail for a boring reason. Nobody broke the feature; a redesign renamed a label, and getByLabel('Card number') stopped matching. The test describes the clicks, so when the clicks change, the test breaks.
e2e, an open-source TypeScript framework from TesterArmy, takes a different angle: you describe the goal, an AI agent works out the clicks once, and every run after that replays what it did without calling the model. This is where it stands as of 2026.10.
Watch the 60-second version on Instagram.
What it is
e2e is Apache-2.0, written in TypeScript, and tests live in *.e2e.ts files. npx e2e init sets a project up and asks two things: the engine and the model provider. The web engine drives Chromium, Firefox and WebKit through Playwright; the mobile engine drives iOS simulators and Android emulators. The repository went up on 2026.07.22 and had about 9,300 GitHub stars by 2026.10.11.
A test mixes agent steps with ordinary locators and assertions. This is the example from the README:
import { test, expect } from 'e2e';
test('a member upgrades to Pro', async ({ app, agent, screen }) => {
await app.open('/settings/billing');
await agent.act('upgrade the workspace to the Pro plan');
await agent.assert('the invoice preview shows a prorated amount');
await expect(screen.getByRole('status')).toContainText('Pro');
});app.open and the final expect are deterministic, as in Playwright. The two highlighted lines are written in English. The agent reads the screen, decides what to click and type, and carries on.
Record once, replay after
The part that makes this more than "an LLM clicks around on every run" is the cache. An agent.act() step is recorded only when a later verification passes: a locator matcher such as toHaveText, an engine assertion such as toHaveURL, or agent.assert / agent.waitFor. A plain value assertion or agent.extract() doesn't count. If nothing checks the step, nothing gets saved.
The recording is a JSON file under .e2e/cache/ with the actions, target descriptions, typed text and the end state the step reached. No prompts, no screenshots, no secret values. On the next run, the step replays from that file and the model isn't called at all. You pay for the agent on the first pass, not on every run.
A few details from the docs that matter in practice:
- The cache key includes the test name, the instruction, its params, the agent config, the engine and the project. Changing the model does not cause a miss. Renaming the test or rewording the instruction does.
.e2e/cache/is gitignored by default. Every machine records its own copy unless you commit it. If you want CI and your teammates to start warm, commit the folder.- Modes:
read-writeis the local default,read-onlythe CI default whencacheis unset, and--no-cacheturns it off.
When the replay misses
A replay can fail. The docs name the reasons: target-not-found, target-ambiguous, end-mismatch, wrong-context, viewport-changed and a few more. When one does, the agent takes over from the current screen, with a record of what already ran and why the replay stopped. It finishes the step and the entry is recorded again. A failing attempt keeps the steps it verified and deletes the unverified ones.
The marketing word for this would be "self-healing". The docs don't use it, and I think they are right not to: nothing heals. A replay misses, a model runs, and you get a new recording. That costs tokens, and in CI it can happen quietly. For that, there is --strict-cache: a stale replay fails with REPLAY_STALE (exit code 2, not retried) instead of falling back to the model.
The catch
| Area | What to know |
|---|---|
| Model | Bring your own. There is no default. It needs tool calling and image input; OpenAI, Anthropic, Google, OpenRouter, Ollama and OpenAI-compatible endpoints all work. |
| Cost | First runs and every cache miss call the model, so they are slower and cost tokens. |
| Determinism | Replays are deterministic. Misses are not: the agent may take a different path the second time. |
| Stability | Pre-1.0. The README says APIs and config can still change between minor releases. 0.19.0 (2026.10.09) made a failed expect.soft before test.skip() fail the test, as Playwright does. |
The most useful line in the documentation is in its Playwright migration guide, and it argues against using the agent everywhere: where a run of scripted interactions breaks often, replace it with an agent.act goal; keep navigation and final assertions deterministic with app.open() and locators. That matches how I would use it. Locators for the parts of the UI that don't move, an agent step for the form that gets redesigned every quarter, and a real assertion after every agent step, because that assertion is what makes the recording exist at all.
I wouldn't put it in a client project's CI yet. At 0.19, I'd run it on my own projects first and see how the cache holds up over a few weeks of real UI changes.
What's new in 0.19
The latest release, e2e@0.19.0 on 2026.10.09, adds toHaveScreenshot for visual comparison on every engine that captures pixels, replaces the Playwright trace zip with a markdown trace page per test, and adds a list command that reports which tests a config and filters would select without starting anything.
The short version: write the goal, not the clicks, for the steps that keep breaking. Pay the model once, replay the rest, and make CI fail loudly when the replay goes stale.
Sources: tester-army/e2e on GitHub · e2e docs, Cache · e2e docs, Models · e2e docs, Migrate from Playwright · e2e releases (0.19.0)