MotusRelease

Motus 1.0.15: attach to a browser you did not start

Point Motus at a browser that is already running, treat a cross-origin frame like any other frame, split a suite across agents, and tell a flaky test from a broken one.

Motus 1.0.15 is on NuGet. Most of what went into it came out of the same complaint: your test needs a browser session it does not fully own. Somebody else started the browser. The browser put an iframe in a process of its own. The suite is spread across four agents that never talk to each other. Or the run is just flaky, and saying so is more useful than a red X.

What changed

Area What shipped
Attach MotusLauncher.ConnectAsync, over a WebSocket URL or an HTTP debugging endpoint
Ownership OwnsProcess, DisconnectAsync, a CloseAsync that spares a browser it did not start
Frames A frame in its own renderer process is an ordinary IFrame, at any depth
Execution worlds EvaluateOptions { World = ExecutionWorld.Isolated } on IFrame.EvaluateAsync
Frame events FrameAttached, FrameNavigated, FrameDetached, IsDetached
Sharding motus run --shard <i>/<n> and motus shard merge --expect <n>
Flaky and quarantine --retries, --retry-policy, --fail-on-flaky, --quarantine, --flaky-history
Crash recovery A dead browser is replaced and its cached pages invalidated
Pinning motus install --revision, launch.executablePath, MOTUS_EXECUTABLE_PATH
Cursor ContextOptions.ShowCursor and NaturalMouseMotion
MCP server --connect, browser_attach, browser_status, frame_list, frame_select
Documentation XML docs in every package, four guides, a CLI reference

A browser you already have running

Sometimes the browser you want to drive is already open. You signed into a profile by hand, or CI keeps a warm browser between runs, or the thing you are testing is an application built on an embedded Chromium runtime that exposes a debugging port. Until now Motus had to start its own.

ConnectAsync takes the CDP WebSocket URL or the HTTP debugging endpoint, and comes back once it has adopted the contexts and pages that were already open.

await using IBrowser browser = await MotusLauncher.ConnectAsync("http://127.0.0.1:9222");

foreach (IBrowserContext context in browser.Contexts)
foreach (IPage page in context.Pages)
    Console.WriteLine($"{await page.TitleAsync()} - {page.Url}");

Anything that opens or closes after that gets tracked too, so a long session keeps up instead of describing the moment you connected. ConnectOptions carries AdoptExistingTargets, SlowMo and a Timeout that bounds the connect itself.

Your .NET process ConnectAsync WebSocket Debugging port 127.0.0.1:9222 Running browser Contexts already open
Attach mode. The debugging port is the only seam, and the contexts outlive the connection.

One limit worth knowing up front: ConnectAsync speaks the Chrome DevTools Protocol, so the endpoint has to be a Chromium one.

Ownership is something you can check. On a browser you attached to, CloseAsync closes the contexts Motus created and disconnects, and leaves the process running. OwnsProcess tells you which case you are in. The attach guide is blunt about the security side: attaching is the same as sitting down at the machine, and there is no narrower permission to hand out.

Frames the browser puts in their own process

If you have ever fought a cross-origin iframe, you know the browser renders it in a separate process, which gives it its own protocol target, session and execution contexts. Motus now handles that for you. A frame like that is an ordinary IFrame in page.Frames, at any depth, and locators, clicks, evaluation and navigation all go to the session that owns it.

IFrame.EvaluateAsync also picked up an overload that chooses the frame's main world or an isolated one. The main world is still the default, and the trap here runs the other way from what you would expect: an isolated world shares the document and nothing else, so reading a framework handle off window there comes back undefined, which looks exactly like an application that never loaded.

Splitting a suite and putting it back together

If a suite takes too long on one agent, you want it on four. Splitting it is the easy half. Trusting the merged result is the hard half.

motus run bin/Release/net8.0/MyTests.dll --shard 1/4 --reporter junit:results.shard-1.xml
motus shard merge results.shard-*.xml --output junit:results.xml --expect 4

Each shard works out its own slice from the sorted test set and its shard coordinates, so no agent has to coordinate with any other. Two things to watch. Shards balance by count, not by how long a test takes, so a suite dominated by one slow test is not rescued by adding agents. And a shard that never ran leaves you a smaller report that is entirely green, which is the failure --expect exists to catch: every shard stamps its coordinates into its result file, and the merge fails when an index is missing or shows up twice.

A flaky test is not a broken one

Retries have a bad reputation because they usually hide something. These try not to.

--retry-policy transient is the default, and it only re-runs a failure caused by the browser disconnecting. flake re-runs any failure, and a test that passes on the second try is reported as flaky rather than green. A flaky test still passes the run with a warning unless you say --fail-on-flaky. For the ones you already know about, [Quarantine] keeps a test running and reported without gating the build, --quarantine <file> does the same from a list of names, and --flaky-history <file> turns one bad morning into a trend you can look at.

Behavior changes

Seven of them, and each is here because it would otherwise read as a regression.

Change What to do
CloseAsync spares a browser Motus did not start End the process yourself. DisposeAsync will not, and never did
A locator from IFrame resolves inside that frame Use page.Locator(...) where you meant the main frame
BoundingBoxAsync answers in page coordinates Stop correcting for the frame's offset by hand
ScreenshotAsync honors ScreenshotOptions.Clip A clip that was dropped now captures the region
A lost browser is reported, not a verdict Catch MotusTargetClosedException as well
MCP evaluate returns its value under result Read result. Hand-wrapped expressions can stop
MCP nullable parameters are no longer required Nothing. Explicit nulls still work

Getting it

dotnet add package Motus
dotnet tool install --global Motus.Cli
motus install

All ten packages are at 1.0.15 on nuget.org. There are new guides for attaching, frames, sharding and flaky tests, plus a CLI reference. If you drive Motus from an agent, motus mcp --connect starts the server already attached, and the MCP walk-through covers the rest. Try attach mode against something you already have running, and tell me what surprises you.