v0.8.0
Released November 2, 2026
v0.8.0 is the biggest release of E2E for .NET so far. More than 150 pull requests bring the port much closer to upstream e2e. Highlights:
- Run only the tests a change can affect.
- Sign in once and reuse the session.
- Run web tests on Firefox and WebKit.
- Set up a project for coding agents with one command.
- Gestures, scrolling, and recording spans for web and mobile.
- A new documentation site with Get started guides, samples, and examples.
dotnet add package E2E --version 0.8.0dotnet add package E2E.NUnit --version 0.8.0 # or E2E.XUnit.V3dotnet tool update --global E2E.Cli⚠️ This release has a few breaking changes. See Breaking changes before you upgrade.
Highlights
Section titled “Highlights”Run only the tests a change can affect (test filters)
Section titled “Run only the tests a change can affect (test filters)”A test filter leaves tests out before the run starts. It sees every test and the files changed since a base ref (ChangedSince or E2E_CHANGED_SINCE). Skipped tests show as ignored in NUnit.
// Outside any namespace, so it covers the whole assembly.[SetUpFixture]public sealed class AppCommands : E2ERunFixture{ protected override IReadOnlyList<ITestFilter> Filters => [new ChangedFilesFilter()];}
public sealed class ChangedFilesFilter : ITestFilter{ public string Name => "changed-files";
public Task<TestFilterResult> FilterAsync(IReadOnlyList<TestFilterCandidate> tests, TestFilterContext context) => Task.FromResult(new TestFilterResult { Exclude = tests .Where(test => !context.Changes.Files.Contains(test.File)) .ToDictionary(test => test.Id, test => $"{test.File} did not change"), });}E2E_CHANGED_SINCE=origin/main dotnet testSign in once, reuse the session (NUnit)
Section titled “Sign in once, reuse the session (NUnit)”A setup test signs in and saves a named session. Other tests start from it with [E2ESession].
public sealed class AccountTests : E2ETest{ [E2ESetup("sign in", Saves = ["member"])] public async Task Sign_in() { await App.OpenAsync("/login"); await Screen.GetByLabel("Email").FillAsync("member@example.com"); await Screen.GetByLabel("Password").FillAsync(Secrets.Get("member-password")); await Screen.GetByRole("button", "Sign in").ClickAsync(); await SaveSessionAsync("member"); }
[Test, E2ESession("member")] public async Task Member_sees_the_dashboard() { await App.OpenAsync("/dashboard"); await Expect.That(Screen.GetByRole("heading", "Dashboard")).ToBeVisibleAsync(); }}Firefox and WebKit
Section titled “Firefox and WebKit”WebEngine was Chromium-only. Now WebEngineOptions.BrowserName picks the browser it launches and installs.
protected override IEngine CreateEngine() => new WebEngine(new WebEngineOptions{ BrowserName = BrowserName.Firefox, // or BrowserName.Webkit; Chromium is the default});e2e install firefox webkit --with-depse2e init for coding agents
Section titled “e2e init for coding agents”One command sets a project up for Claude Code, Cursor, and other coding agents:
- Scaffolds
e2e.config.json. - Installs the e2e skill into
.agents/skillsand.claude/skills. - Registers the
e2e mcpserver in.mcp.jsonand.cursor/mcp.json. - Updates
.gitignore.
It asks which engine, model gateway, and locations to use, or --yes takes the defaults. It never overwrites an existing config.
dotnet tool install --global E2E.Clie2e initThe MCP server now answers each action with the test line that repeats it, so the agent writes the test as it walks the flow. The migrate-to-e2e skill moves Playwright .NET, Selenium, Appium, Cypress, Detox, Maestro, or TesterArmy suites one slice at a time.
Scroll until visible, and gestures on locators
Section titled “Scroll until visible, and gestures on locators”ScrollUntilVisibleAsync swipes the screen, or a scroll container, until the target shows up. Locators also gained SecondaryTapAsync, LongPressAsync, HoverAsync, DragToAsync, SwipeAsync, and click modifiers.
var feed = Screen.GetByRole("list", "Feed");await feed.ScrollUntilVisibleAsync(Screen.GetByText("Accept"));
await Screen.GetByText("Invoice #1042").LongPressAsync();await Screen.GetByText("Draft").DragToAsync(Screen.GetByRole("region", "Archive"));Filter locators by what they don’t contain
Section titled “Filter locators by what they don’t contain”Filter now takes hasNot and hasNotText.
var items = Screen.GetByRole("listitem");var editable = items.Filter(has: Screen.GetByRole("button", "Remove"), hasNot: Screen.GetByText("Read only"));await Expect.That(editable).ToHaveCountAsync(1);Record one span of the screen
Section titled “Record one span of the screen”Record just the part of a test that matters, such as one transition, on the web or on devices. (Turn the test’s video off; the engine has one recorder.)
await App.StartRecordingAsync(new RecordingOptions { Name = "checkout-dialog" });await Screen.GetByRole("button", "Pay").ClickAsync();await Expect.That(Screen.GetByRole("dialog")).ToBeVisibleAsync();var videos = await App.StopRecordingAsync(); // recordings/001-checkout-dialog.webmEvidence screenshots, soft assertions, and parameterized judgments
Section titled “Evidence screenshots, soft assertions, and parameterized judgments”App.ScreenshotAsync(label)returns the absolute path of the redacted screenshot.Expect.Soft(...)keeps a failure and lets the test continue.Agent.AssertAsyncandAgent.WaitForAsynctakeParams.
var path = await App.ScreenshotAsync("cart");await Expect.Soft(Screen.GetByRole("status")).ToContainTextAsync("3 items");await Agent.AssertAsync("the cart total is {total}", new AssertOptions{ Params = new Dictionary<string, object?> { ["total"] = "$42.00" },});Network and browser control
Section titled “Network and browser control”route.FetchAsync()fetches the real upstream response inside a route handler, so you can change it and fulfill it.Browser.WaitForDownloadsAsync(trigger, count)collects several downloads from one click.Browser.OnDialogAsynchandles dialogs.Browser.ClearCookiesAsyncclears cookies.- Upload buttons that open a file chooser are supported.
app.permissionsgrants browser permissions.
await Browser.RouteAsync("**/api/prices", async route =>{ var upstream = await route.FetchAsync(); var prices = await upstream.JsonAsync<Prices>(); await route.FulfillAsync(new RouteFulfillResponse { Json = prices! with { Discount = 0.1m } });});
var files = await Browser.WaitForDownloadsAsync( () => Screen.GetByRole("button", "Export all").ClickAsync(), count: 2);Host sessions in your own runner
Section titled “Host sessions in your own runner”E2EHost opens isolated sessions for a runner that is not NUnit or xUnit.
await using var host = await E2EHost.OpenAsync(new OpenHostOptions { Cwd = projectDir });await using var session = await host.OpenSessionAsync(new OpenSessionOptions { Name = "home screen" });await session.Fixtures.App.OpenAsync("/");All changes
Section titled “All changes”Features
Section titled “Features”- Test filters with change detection, wired into NUnit (
E2ERunFixture.Filters,ChangedSince) (#662, #926) - Setup runner for NUnit:
E2ESetup,E2ESession,SaveSessionAsync; session store (#728, #707, #853) - Firefox and WebKit through
WebEngineOptions.BrowserName(#725) e2e initwith interactive prompts: config, skill, MCP registration (#761, #779)screen/locator.ScrollUntilVisibleAsync(#664)- Locator
SecondaryTapAsync,LongPressAsync,HoverAsync,DragToAsync,SwipeAsync, click modifiers (#860) - Locator filters
hasNotandhasNotText(#790) App.StartRecordingAsync/StopRecordingAsync(#862)App.ScreenshotAsyncreturns the absolute path (#797)Expect.Soft(screen)and wheel-gesture scrolling on the web (#897)ParamsonAgent.AssertAsyncandAgent.WaitForAsync(#891)route.FetchAsyncin route handlers (#840)- Multiple downloads from one trigger (#834);
OnDialog,WaitForDownload,sanitizeFilename,artifactPath(#686) - Upload buttons that open file choosers (#841); the agent upload tool and its path policy (#864)
Browser.ClearCookiesAsync(#789);browser.Locator,browser.FrameLocator,Expect(browser).ToHaveClassAsync(#710)- Browser permissions from
app.permissions;app.identity,app.environment,launchArguments(#795, #759) - Config:
app.command,readyUrl, port 0; a shared app command stopped at the end of the run (#699, #780) - Config:
maxObservationBytesandmaxInputTokenswith observation cutting (#711) - Public host:
E2EHost.OpenAsyncandhost.OpenSessionAsync, with host traces (#910, #758, #919) - Markdown report renderer and the reporter seam (
Reporter,FinishedRun,onRunFinished) (#880, #883) - Run summary cache tally, and the app’s first errors quoted in failures (#820, #844, #886)
- Run one NUnit test in an
e2e mcpsession; MCP writes each test line as you walk the flow (#899, #848) - MCP screenshot saves to a path;
open_sessionnames the app command, URL, and port (#889) exploreruns in a given working directory (#896)- Retries for model requests; reasoning replay on Google and Bedrock; Bedrock uses the AWS credential chain (#678, #704, #719, #726)
- Mobile: provider recording,
device.Locator, in-flight commands,Device.UnstableRaw, Compose Multiplatform text fields on iOS, skips devices another session holds (#697, #813, #849, #787) - Run-wide
ModelUsageaccounting (#781)
Fixes and reliability
Section titled “Fixes and reliability”- Self-healing replay parity: misses before the first action, scrolled lists re-found by their place, multi-line
unique()values, the recorded end wait (#712, #769, #828, #829) - Replay cache: placeholder grammar, uri/slug encodings, navigations recorded without the origin (#838, #852, #863, #888, #921)
- Mobile inputs, reads, observe, and screenshots bounded by the operation’s remaining budget (#660, #893, #909)
- Mobile: stale refs rejected after transitions; input bindings revalidated before fill/clear; fill/clear refused on nodes that can’t take text (#866, #878, #925)
- Web locate reads under the wait’s remaining budget; a ref stays strict only when its locator matched alone (#703, #772)
- Closed shadow roots stay tracked on pages that replace
WeakMap(#816) - The screenshot caret is hidden with a stylesheet (#784)
- Cleanup failures: the exit code reflects them, and failed session teardown marks cleanup as failed (#786, #822)
- Interrupted retries keep the earlier verdict and exit 130 (#825, #836, #859)
- Text downloads are rewritten through the secret ledger (#722)
- Flaky test fixes across Linux CI and the timing tests (#683, #723, #730, #771, #930)
- New Astro Starlight docs site at hardkoded.com/e2e-dotnet, with a homepage (#667, #713, #716, #720)
- Get started and Samples sections (#663)
- Examples: i18n, file uploads, location, biometrics, deep links (#912)
- Self-healing tests section; coding-agents and Limrun integration docs;
migrate-to-e2eskill (#792, #672)
Breaking changes
Section titled “Breaking changes”- Error codes renamed:
NOT_FOUND→LOCATOR_NOT_FOUND,STRICT_MODE→LOCATOR_AMBIGUOUS. These are plain strings, so the compiler won’t warn aboutex.Code == "STRICT_MODE"(#907) App.BaseUrlhas no trailing slash:$"{App.BaseUrl}/api/users"now joins with one slash (#879)ModelUsage→ModelCallUsagefor customIAgentModelauthors.ModelUsageis now the run-wide accumulator (#781)- Model screenshots are kept after a secret fill. The
PIXEL_TAINTEDguarantee andStepExecutorContext.PixelsTaintedare gone. Secure fields are still masked, but use test credentials (#874)