# Motus

**Extensible browser automation and testing framework for .NET, built on a fully extensible architecture.**

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
[![.NET 8 | 10](https://img.shields.io/badge/.NET-8.0%20%7C%2010.0-purple.svg)](https://dotnet.microsoft.com/download/dotnet/10.0)
[![NuGet](https://img.shields.io/nuget/v/Motus?label=NuGet)](https://www.nuget.org/packages/Motus)

<video src="https://github.com/user-attachments/assets/7ad5ed29-1f65-45b1-a666-3b9537835b0a" autoplay loop muted playsinline></video>

## The Story

Every .NET browser automation framework either wraps a JavaScript tool behind a process boundary or bolts extensibility on as an afterthought. Playwright for .NET proxies commands through a Node.js sidecar. Selenium's extension model grew organically over fifteen years and it shows.

Motus started from a premise proven by the architecture of [Verso](https://github.com/DataficationSDK/Verso): if the framework's own features are built on the same public plugin interfaces available to third-party authors, the architecture stays honest. Every built-in selector strategy, lifecycle hook, wait condition, and reporter is registered through the same `IPluginContext` that any consumer can use. There are no internal shortcuts.

The result is a framework that talks directly to Chromium and Firefox over WebSocket (CDP and WebDriver BiDi), ships source-generated protocol bindings for NativeAOT, and gives you compile-time diagnostics for common automation mistakes before your tests ever run.

## Getting Started

### Prerequisites

- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- Chrome, Edge, or Firefox installed (or use `motus install` to download a browser binary)

### Install

```bash
dotnet add package Motus
dotnet add package Motus.Testing.MSTest # or Motus.Testing.xUnit / Motus.Testing.NUnit
dotnet tool install --global Motus.Cli
```

### Write Your First Test

```csharp
using Motus;
using Motus.Abstractions;
using Motus.Testing.MSTest;
using static Motus.Assertions.Expect;

[TestClass]
public class SearchTests : MotusTestBase
{
    [TestMethod]
    public async Task SearchReturnsResults()
    {
        var page = await Context.NewPageAsync();
        await page.GotoAsync("https://example.com");

        await page.Locator("input[name='q']").FillAsync("motus automation");
        await page.Locator("button[type='submit']").ClickAsync();

        await That(page).ToHaveUrlAsync("**/search**");
        await That(page.Locator(".results")).ToBeVisibleAsync();
    }
}
```

### Generate Page Objects from Live Pages

<video src="https://github.com/user-attachments/assets/cb74ef25-23f6-4c43-9946-8219a634381f" autoplay loop muted playsinline></video>

```bash
motus codegen https://example.com/login --output ./Pages --namespace MyApp.Pages

# Or open a browser, navigate yourself, then press Enter to analyze
motus codegen --headed --output ./Pages
```

This navigates to the URL, crawls the DOM for interactive elements, infers the best selector for each using pluggable strategies, and emits a typed `.g.cs` Page Object Model class. Use `--detect-listeners` to also discover elements with directly-attached JS event handlers (vanilla JS, jQuery, etc.).

### Record a Test Session

```bash
motus record --output ./Tests/LoginTest.cs --framework mstest --selector-priority testid,role,text,css
```

### Run Tests

```bash
motus run ./bin/Debug/net8.0/MyTests.dll --workers auto --reporter console
motus run ./bin/Debug/net8.0/MyTests.dll --visual
motus run ./bin/Debug/net8.0/MyTests.dll --reporter html:./reports/result.html
motus run ./bin/Debug/net8.0/MyTests.dll --reporter junit:./reports/junit.xml
```

### Accessibility Testing

Motus audits pages against WCAG 2.1 Level A/AA rules using the browser's native accessibility tree (no JavaScript injection). Enable the built-in audit hook to run checks automatically after every navigation, or use assertions for targeted validation.

```csharp
// Assert the page passes all accessibility rules
await Expect.That(page).ToPassAccessibilityAuditAsync();

// Skip specific rules or exclude warnings
await Expect.That(page).ToPassAccessibilityAuditAsync(opts =>
{
    opts.SkipRules("a11y-color-contrast");
    opts.IncludeWarnings = false;
});

// Assert accessible name and role on individual elements
await Expect.That(page.Locator("button#submit")).ToHaveAccessibleNameAsync("Submit order");
await Expect.That(page.Locator("nav")).ToHaveRoleAsync("navigation");
```

Enable the audit lifecycle hook via config or CLI to run audits automatically:

```bash
# Report violations without failing tests
motus run ./bin/Debug/net8.0/MyTests.dll --a11y warn

# Fail tests on error-severity violations
motus run ./bin/Debug/net8.0/MyTests.dll --a11y enforce
```

Or in `motus.config.json`:

```json
{
  "accessibility": {
    "enable": true,
    "mode": "enforce",
    "skipRules": ["a11y-color-contrast"]
  }
}
```

### Performance Testing

Motus collects Core Web Vitals (LCP, FCP, TTFB, CLS, INP) and supplementary metrics (JS heap size, DOM node count) directly from the browser during test execution. Set budget thresholds via attributes or config, and performance regressions fail your tests automatically.

```csharp
// Assert all metrics are within the class-level budget
[PerformanceBudget(Lcp = 2500, Fcp = 1800, Cls = 0.1)]
[TestClass]
public class DashboardTests : MotusTestBase
{
    [TestMethod]
    public async Task DashboardLoadsWithinBudget()
    {
        await Page.GotoAsync("https://app.example.com/dashboard");
        await Expect.That(Page).ToMeetPerformanceBudgetAsync();
    }
}

// Or assert individual metrics
await Expect.That(page).ToHaveLcpBelowAsync(2500);
await Expect.That(page).ToHaveClsBelowAsync(0.1);
```

Enable budget enforcement from the CLI:

```bash
motus run ./bin/Debug/net8.0/MyTests.dll --perf-budget
```

Or in `motus.config.json`:

```json
{
  "performance": {
    "enable": true,
    "lcp": 2500,
    "cls": 0.1,
    "inp": 200
  }
}
```

### Code Coverage

Motus collects JavaScript coverage via the CDP `Profiler`/`Debugger` domains and CSS rule-usage coverage via the CDP `CSS` domain on every page driven by your tests. Coverage is collected per-test and aggregated across the run, with optional thresholds that fail the run when not met.

Enable coverage from the CLI with one or more output formats:

```bash
# Console summary table
motus run ./bin/Debug/net8.0/MyTests.dll --coverage console

# Static HTML report (index + per-file source views with line highlighting)
motus run ./bin/Debug/net8.0/MyTests.dll --coverage html:./coverage

# Cobertura XML for CI ingestion
motus run ./bin/Debug/net8.0/MyTests.dll --coverage cobertura:./coverage.xml

# Repeat the flag for multiple formats at once
motus run ./bin/Debug/net8.0/MyTests.dll --coverage console --coverage html:./coverage
```

Source-mapped JS bundles are remapped back to original sources automatically when sourcemaps are reachable. Configure thresholds and defaults in `motus.config.json`:

```json
{
  "coverage": {
    "enable": true,
    "includeJavaScript": true,
    "includeCss": true,
    "js": { "lines": 70, "functions": 60 },
    "css": { "rules": 50 }
  }
}
```

When a threshold is set and the aggregated run coverage falls below it, the run exits non-zero so CI fails the build.

### Retrying Flaky Runs

CDP WebSocket connections are occasionally dropped by the browser under heavy tracing or when sibling targets close mid-command. Use `--retries N` to re-run a failing test up to `N` additional times, but only when the failure is a transient CDP disconnect. Non-transient failures (assertion errors, timeouts) are not retried, so real bugs aren't masked.

```bash
motus run ./bin/Debug/net8.0/MyTests.dll --retries 2
```

Each retry runs the entire test fresh: new browser context, new WebSocket, new test instance. A `[RETRY]` line is logged to stderr for every attempt so flake patterns are visible.

### MCP Server for AI Agents

Motus exposes its browser engine to AI agents through a [Model Context Protocol](https://modelcontextprotocol.io) server, shipped as the `motus mcp` verb on the CLI tool. Agents navigate, snapshot the accessibility tree, click and type against referenced elements, act at raw coordinates on canvas surfaces (including drag and drop), intercept network traffic, run accessibility and performance audits, record traces, HARs, and videos, and generate Page Object Model code, all over stdio or Streamable HTTP.

Register it with Claude Code against the installed tool:

```bash
dotnet tool install --global Motus.Cli
motus install
claude mcp add motus -- motus mcp
```

See [MCP Server](docs/guides/mcp-server.md) for the full registration story, the tool catalog, and the HTTP transport.

## How It Works

Motus communicates directly with the browser over WebSocket. For Chromium-based browsers, it speaks the Chrome DevTools Protocol (CDP). For Firefox, it uses WebDriver BiDi. There is no Node.js sidecar, no driver binary, and no process boundary between your test code and the protocol layer.

All CDP types are source-generated at build time from the protocol JSON schema. Serialization uses `System.Text.Json` source generators for zero-reflection, NativeAOT-compatible marshalling.

### Architecture

| Project | Role |
|---------|------|
| `Motus.Abstractions` | Public interfaces and types (zero dependencies) |
| `Motus` | Core engine: transport, browser management, page controller, locators, selectors, assertions |
| `Motus.Codegen` | Roslyn source generator for CDP protocol types and plugin discovery |
| `Motus.Analyzers` | Roslyn diagnostic analyzers and code fixes |
| `Motus.Recorder` | Action capture, selector inference, POM generation |
| `Motus.Runner` | Blazor visual test runner with live screencast and timeline |
| `Motus.Cli` | `motus` CLI tool |
| `Motus.Testing.*` | Test framework integrations (MSTest, xUnit, NUnit) |

## The Extension Model

Five interfaces define every point of extensibility, all registered through `IPluginContext`:

| Interface | What It Does |
|-----------|-------------|
| `ISelectorStrategy` | Resolve elements and generate selectors for a custom prefix (e.g. `data-testid=`) |
| `ILifecycleHook` | Intercept navigation, actions, page create/close, console messages, and errors |
| `IWaitCondition` | Define named wait conditions for use with `WaitForAsync` |
| `IReporter` | Receive test run events for custom reporting (multiple reporters run simultaneously) |
| `IAccessibilityRule` | Define custom WCAG accessibility rules evaluated against the browser's accessibility tree |
| `IAccessibilityReporter` | Opt-in interface for reporters to receive per-violation accessibility events |
| `IPerformanceReporter` | Opt-in interface for reporters to receive performance metrics and budget results |
| `ICoverageReporter` | Opt-in interface for reporters to receive per-test and aggregated JS/CSS coverage data |
| `IMotusLogger` | Structured logging for plugin diagnostics |

### Plugin Discovery

Plugins are discovered two ways:

1. **Compile-time auto-discovery**: Mark a class with `[MotusPlugin]` and the Roslyn source generator emits a `[ModuleInitializer]` that registers it at startup. No reflection.
2. **Manual registration**: Pass instances via `LaunchOptions.Plugins` for full control.

```csharp
[MotusPlugin]
public class MyPlugin : IPlugin
{
    public string PluginId => "my-plugin";
    public string Name => "My Plugin";
    public string Version => "1.0.0";

    public Task OnLoadedAsync(IPluginContext context)
    {
        context.RegisterSelectorStrategy(new MyCustomStrategy());
        context.RegisterLifecycleHook(new MyHook());
        return Task.CompletedTask;
    }

    public Task OnUnloadedAsync() => Task.CompletedTask;
}
```

### Dogfooding All the Way Down

The five built-in selector strategies (CSS, XPath, Text, Role, TestId) are registered through `IPluginContext`. The console, HTML, JUnit, and TRX reporters implement `IReporter`. The visual runner's timeline is powered by an `ILifecycleHook`. The nine built-in WCAG rules and the accessibility audit hook are registered as plugins through the same `IPluginContext`. None of them have special access to engine internals.

## Roslyn Analyzers

The `Motus.Analyzers` package ships seven diagnostics that catch common automation mistakes at compile time:

| Rule | Severity | What It Catches |
|------|----------|----------------|
| MOT001 | Warning | Non-awaited automation call |
| MOT002 | Info | Hardcoded `Task.Delay` or `Thread.Sleep` in test code |
| MOT003 | Info | Fragile selector (deep nesting, `nth-child` chains, auto-generated class names) |
| MOT004 | Warning | `IBrowser` or `IBrowserContext` not disposed |
| MOT005 | Warning | Locator created but never used |
| MOT006 | Info | Deprecated selector prefix |
| MOT007 | Warning | Assertion after navigation without intervening wait |

Code fixes are provided for MOT001, MOT002, and MOT004.

## Visual Runner

Launch the Blazor-based visual runner with `motus run --visual`:

- **Live browser view** with real-time screencast via CDP `Page.startScreencast`
- **Action timeline** with clickable steps, before/after screenshots, network requests, and console messages
- **Step-through debugging** with pause, inspect, and resume controls
- **Visual regression** with pixel-level diff, side-by-side comparison, and baseline management

## Key Features

| Category | Details |
|----------|---------|
| **Browser Support** | Chromium (CDP), Firefox (WebDriver BiDi) |
| **Selectors** | CSS, XPath, Text, ARIA Role, Test ID, plus custom strategies via `ISelectorStrategy` |
| **Auto-Wait** | Actionability checks (visible, enabled, stable, receives events) with configurable timeout |
| **Shadow DOM** | Automatic piercing of open shadow roots (configurable per-locator) |
| **Assertions** | Auto-retry polling assertions for locators, pages, and responses with `.Not` negation |
| **Network** | Request interception, response mocking, route matching with glob patterns |
| **Recording** | Capture browser interactions and emit idiomatic C# test code (MSTest, xUnit, NUnit) |
| **Codegen** | Generate Page Object Model classes from live pages with selector inference |
| **Accessibility** | Built-in WCAG 2.1 Level A/AA audits via CDP accessibility tree, lifecycle hook, page and locator assertions, custom rules via `IAccessibilityRule` |
| **Performance** | Core Web Vitals collection (LCP, FCP, TTFB, CLS, INP), configurable budgets via `[PerformanceBudget]` attribute or config, auto-retry assertions, reporter integration via `IPerformanceReporter` |
| **Coverage** | Per-test JS coverage via CDP `Profiler`/`Debugger`, CSS rule-usage coverage via CDP `CSS`, source-map remapping to original sources, console / HTML / Cobertura reporters, configurable thresholds that fail the run, custom reporters via `ICoverageReporter` |
| **Reporters** | Console, HTML, JUnit XML, TRX, plus custom reporters via `IReporter` (with opt-in `IAccessibilityReporter`, `IPerformanceReporter`, `ICoverageReporter` for domain events) |
| **Tracing** | Screenshots, DOM snapshots, network logs, HAR export, and WebM video recording |
| **Parallel** | Context-level, browser-level, and worker-level parallel execution |
| **Configuration** | Layered: `motus.config.json`, environment variables, code (code always wins) |
| **NativeAOT** | Source-generated serialization and plugin discovery with zero reflection |

## CLI Reference

```
motus run <assemblies>  Run tests with optional --visual, --filter, --workers, --reporter, --a11y, --perf-budget, --coverage, --retries
motus record           Record a browser session and emit test code
motus codegen          Generate POM classes from live pages (--headed, --connect, --detect-listeners)
motus screenshot       Capture a screenshot (--full-page, --delay, --hide-banners, --width, --height)
motus pdf              Generate a PDF from a URL (--delay, --hide-banners, --width, --timeout)
motus trace show       Open a trace file in the visual runner with timeline, screenshots, and network
motus install          Download and install browser binaries
motus update-protocol  Fetch and update CDP protocol schema files
motus mcp              Run the MCP server for AI agents (stdio by default, or --http)
```

## Build from Source

```bash
git clone https://github.com/DataficationSDK/Motus
cd Motus
dotnet build Motus.sln
dotnet test Motus.sln
```

## Contributing

Contributions are welcome. Open an issue to discuss what you'd like to work on.

## Sponsors

Thanks to [@fluentfoundation](https://github.com/fluentfoundation) and [@fiVISION](https://github.com/fiVISION) for supporting this project.

## License

[MIT](LICENSE.md)

Motus is a Datafication project.
