Today we're shipping Motus 1.0. If you've written browser tests in .NET, you've probably lived with a Node process or a driver binary sitting between your code and the browser, and you've probably lost an afternoon to one of them in CI. Motus doesn't have one. Your test talks to Chrome, Edge or Firefox directly, over the same protocols the browser's own dev tools use: the Chrome DevTools Protocol for Chromium, WebDriver BiDi for Firefox.
It's MIT licensed and on NuGet today. The first published version is 1.0.1, and ten packages went out with it.
Why there's nothing in the middle
A .NET automation framework usually shows up one of two ways. It wraps a tool from another ecosystem behind a process boundary, or it grows an extension model after the fact. Playwright for .NET ships a bundled Node.js process that acts as the automation server, reached over a named-pipe IPC channel. Selenium WebDriver sends every command as an HTTP round trip to a driver binary such as chromedriver or geckodriver, which you install, pin, and keep in step with the browser.
Both work. Both also put something between your code and the thing it is trying to control, and that something has to be installed on every build agent. We said in our four rules that a tool built for .NET should install with dotnet add package and run inside your process. Motus is that rule pointed at a browser.
What's between your code and the browser
Under the hood there's one WebSocket, opened from your process and kept for the whole run. Every page and worker the browser reports comes back over that same connection, so there's one thing to reason about and nothing extra to install.
The protocol types are written at build time instead of discovered at run time, which keeps reflection out of the engine and out of the abstractions layer. If you publish ahead-of-time, that's the part you care about: the library publishes AOT with no trimming warnings of its own.
Getting it installed
dotnet add package Motus
dotnet add package Motus.Testing.MSTest # or Motus.Testing.xUnit / Motus.Testing.NUnit
dotnet add package Motus.Analyzers # optional
dotnet tool install --global Motus.Cli
motus install
motus install downloads a Chromium build into ~/.motus/browsers. From there, a first MSTest suite is an assembly initializer and a test class:
// AssemblySetup.cs
using Motus.Abstractions;
using Motus.Testing.MSTest;
[TestClass]
public class AssemblySetup
{
[AssemblyInitialize]
public static async Task Initialize(TestContext _) =>
await MotusTestBase.LaunchBrowserAsync(new LaunchOptions { Headless = true });
[AssemblyCleanup]
public static async Task Cleanup() =>
await MotusTestBase.CloseBrowserAsync();
}
// SearchTests.cs
using Motus.Testing.MSTest;
using static Motus.Assertions.Expect;
[TestClass]
public class SearchTests : MotusTestBase
{
[TestMethod]
public async Task PageHasTitle()
{
await Page.GotoAsync("https://example.com");
await That(Page).ToHaveTitleAsync("Example Domain");
}
[TestMethod]
public async Task ClickLink()
{
await Page.GotoAsync("https://example.com");
await Page.GetByRole("link", "More information...").ClickAsync();
await That(Page).ToHaveUrlAsync("*iana.org*");
}
}
dotnet test runs it. So does motus run ./bin/Debug/net8.0/MyTests.dll --workers auto --reporter console, which is the CLI's own discovery and parallel runner.
The ten packages
| Package | Role |
|---|---|
Motus |
Engine: transport, browsers, pages, locators, assertions |
Motus.Abstractions |
Public interfaces and types, zero dependencies |
Motus.Codegen |
Source generator for protocol types and plugin discovery |
Motus.Analyzers |
Roslyn diagnostics and code fixes |
Motus.Recorder |
Action capture, selector inference, page object generation |
Motus.Testing |
Shared base types for the framework integrations |
Motus.Testing.MSTest |
MSTest integration |
Motus.Testing.xUnit |
xUnit integration |
Motus.Testing.NUnit |
NUnit integration |
Motus.Cli |
The motus global tool |
All ten are at 1.0.1, MIT licensed, targeting .NET 8 and .NET 10.
A name collision on NuGet
An unrelated motion-planning library also publishes under the Motus name. Use the package page at nuget.org/packages/Motus rather than searching.
How it compares
Here's how it stacks up. I've kept this to things you can check yourself.
| Aspect | Motus | Playwright for .NET | Selenium WebDriver |
|---|---|---|---|
| Between your code and the browser | a WebSocket your process owns | a bundled Node.js automation server over a named-pipe IPC channel | a driver binary reached over W3C WebDriver HTTP |
| Driver binaries to install and pin | none | not compared | chromedriver, geckodriver |
| Element references | locators re-resolve on each action | not compared | IWebElement handles go stale and throw StaleElementReferenceException |
| Waiting | actionability checks inside each action | not compared | WebDriverWait, implicit waits, or ExpectedConditions |
| API shape | async throughout | not compared | synchronous |
| Extension model | .NET plugins through IPluginContext |
JavaScript-side selector engines and browser context options | not compared |
I haven't benchmarked Motus against either one, so there's no speed claim in this post.
What else shipped
Flaky browser tests usually trace back to a short list of mistakes. Motus.Analyzers adds seven compile-time diagnostics, MOT001 to MOT007, for the ones we keep running into: an un-awaited async call, a hardcoded delay, a fragile selector, a browser or context not disposed with await using, an unused locator result, an old selector prefix, and a navigation with no wait after it. Three carry a code fix, and all three support Fix All.
The rest of the tooling lives in the CLI. motus record opens a headed browser, watches what you do, and writes a compilable test class. motus codegen <url> crawls a live page and emits a typed page object. motus run --visual starts a runner in your browser with a timeline instead of printing to the terminal. motus screenshot and motus pdf capture a page in one command.
What to watch for
Chromium and Firefox are the whole browser list, and the two transports are not equal. Target multiplexing, network interception, emulation overrides, and tracing are CDP features, so asking for one on a Firefox session throws a NotSupportedException that names the feature and the transport. Locators, actions, assertions, script evaluation, and BiDi's own network interception work on both.
The source and the notes for the 1.0.1 release are on GitHub. Give it a try, and if the plugin model is missing something you need, that's the issue I most want to see.