# Getting Started with Plugins

Motus is designed to be extended. The plugin system lets you ship custom selector strategies, lifecycle hooks, wait conditions, reporters, and accessibility rules as ordinary .NET class libraries. This tutorial walks through creating your first plugin from scratch: a lifecycle hook that logs navigation events.

---

## Prerequisites

- .NET 8 SDK or later
- A working Motus installation (the `Motus` and `Motus.Abstractions` NuGet packages)
- Familiarity with async/await in C#

---

## Project Setup

Create a new class library targeting `net8.0` or `net10.0`:

```bash
dotnet new classlib -n MyCompany.Motus.NavigationLogger
cd MyCompany.Motus.NavigationLogger
```

Add a reference to `Motus.Abstractions`. This is the only package you need to build a plugin. The full `Motus` engine package is not required as a compile-time dependency.

```xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <LangVersion>latest</LangVersion>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Motus.Abstractions" Version="1.0.0" />
  </ItemGroup>

</Project>
```

`Motus.Abstractions` bundles the Motus source generator (`Motus.Codegen`) as an analyzer, so the compile-time discovery described below works automatically without any extra `<PackageReference>` entries.

---

## Creating a Plugin Class

Every plugin implements `IPlugin`. This interface provides identity metadata and two lifecycle methods that Motus calls when the plugin is loaded and unloaded.

```csharp
using Motus.Abstractions;

namespace MyCompany.Motus.NavigationLogger;

public sealed class NavigationLoggerPlugin : IPlugin
{
    public string PluginId  => "mycompany.navigation-logger";
    public string Name      => "Navigation Logger";
    public string Version   => "1.0.0";
    public string? Author   => "My Company";
    public string? Description => "Logs every navigation event to the Motus logger.";

    public Task OnLoadedAsync(IPluginContext context)
    {
        // Register extensions here; see the next section.
        return Task.CompletedTask;
    }

    public Task OnUnloadedAsync()
    {
        // Release any resources acquired in OnLoadedAsync.
        return Task.CompletedTask;
    }
}
```

Keep `PluginId` globally unique. A reverse-domain prefix (e.g. `mycompany.plugin-name`) works well.

---

## Compile-Time Discovery

Motus includes a Roslyn incremental source generator that automatically discovers plugins at compile time. To opt in, apply `[MotusPlugin]` to your plugin class:

```csharp
using Motus.Abstractions;

namespace MyCompany.Motus.NavigationLogger;

[MotusPlugin]
public sealed class NavigationLoggerPlugin : IPlugin
{
    // ...
}
```

At build time the generator scans your assembly (and all referenced assemblies) for classes carrying `[MotusPlugin]` and emits a file called `MotusPluginRegistry.g.cs` into the `Motus.Generated` namespace. The generated file looks like this:

```csharp
// <auto-generated/>
#nullable enable
using System.Runtime.CompilerServices;
using Motus.Abstractions;

namespace Motus.Generated;

internal static class MotusPluginRegistry
{
    [ModuleInitializer]
    internal static void Register()
    {
        PluginDiscovery.Factory = static () =>
            new IPlugin[]
            {
                new global::MyCompany.Motus.NavigationLogger.NavigationLoggerPlugin(),
            };
    }
}
```

The `[ModuleInitializer]` attribute causes `Register()` to run automatically when the assembly is first loaded. Motus reads `PluginDiscovery.Factory` in `PluginHost` and merges the returned instances with any manually registered plugins before loading begins.

### Generator Diagnostics

The generator validates each `[MotusPlugin]` class and emits build warnings for common mistakes:

| Code | Cause |
|------|-------|
| `MOTUS001` | The class is `abstract` and cannot be instantiated |
| `MOTUS002` | The class does not implement `IPlugin` |
| `MOTUS003` | The class has no public parameterless constructor |
| `MOTUS004` | The class is generic |

Treat these as errors in CI to catch problems early.

---

## Manual Registration

If you need full control over plugin instantiation (for example, to inject constructor arguments or to conditionally include a plugin), use `LaunchOptions.Plugins` instead of relying on the source generator.

```csharp
using Motus;
using Motus.Abstractions;
using MyCompany.Motus.NavigationLogger;

var options = new LaunchOptions
{
    Headless = true,
    Plugins =
    [
        new NavigationLoggerPlugin(),
    ],
};

await using var browser = await Motus.LaunchAsync(options);
```

Manually registered plugins take precedence over auto-discovered plugins. If the same `PluginId` appears in both lists, the manual instance wins. Built-in Motus plugins (selector engine, accessibility rules) are always loaded first and cannot be overridden by user plugins.

---

## Registering Components

`OnLoadedAsync` receives an `IPluginContext` that exposes five registration methods and a logger factory. Call these during load to wire your implementations into the engine.

```csharp
public Task OnLoadedAsync(IPluginContext context)
{
    // Custom selector strategy (e.g. data-test=login-btn)
    context.RegisterSelectorStrategy(new DataTestSelectorStrategy());

    // Lifecycle hook (before/after navigation and actions)
    context.RegisterLifecycleHook(new NavigationAuditHook());

    // Custom wait condition
    context.RegisterWaitCondition(new NetworkQuietCondition());

    // Test result reporter
    context.RegisterReporter(new SlackReporter());

    // Accessibility rule applied during audits
    context.RegisterAccessibilityRule(new ColorContrastRule());

    return Task.CompletedTask;
}
```

You can register multiple implementations of the same extension type. The engine invokes all registered lifecycle hooks in registration order, and all reporters receive the same events simultaneously.

For logging within your plugin, call `context.CreateLogger(categoryName)` to get a scoped `IMotusLogger`:

```csharp
private IMotusLogger? _logger;

public Task OnLoadedAsync(IPluginContext context)
{
    _logger = context.CreateLogger(nameof(NavigationLoggerPlugin));
    context.RegisterLifecycleHook(new NavigationLifecycleHook(_logger));
    return Task.CompletedTask;
}
```

---

## Complete Example: Logging Navigation Events

The following shows a complete, runnable plugin that registers a lifecycle hook to log every navigation.

### The Lifecycle Hook

```csharp
using Motus.Abstractions;

namespace MyCompany.Motus.NavigationLogger;

internal sealed class NavigationLifecycleHook : ILifecycleHook
{
    private readonly IMotusLogger _logger;

    public NavigationLifecycleHook(IMotusLogger logger)
    {
        _logger = logger;
    }

    public Task BeforeNavigationAsync(IPage page, string url)
    {
        _logger.LogInformation("Navigating to {Url}", url);
        return Task.CompletedTask;
    }

    public Task AfterNavigationAsync(IPage page, IResponse? response)
    {
        var status = response?.Status ?? 0;
        _logger.LogInformation("Navigation complete. Status: {Status}", status);
        return Task.CompletedTask;
    }

    // Unused hooks -- return completed tasks to satisfy the interface.
    public Task BeforeActionAsync(IPage page, string action) => Task.CompletedTask;
    public Task AfterActionAsync(IPage page, string action, ActionResult result) => Task.CompletedTask;
    public Task OnPageCreatedAsync(IPage page) => Task.CompletedTask;
    public Task OnPageClosedAsync(IPage page) => Task.CompletedTask;
    public Task OnConsoleMessageAsync(IPage page, ConsoleMessageEventArgs message) => Task.CompletedTask;
    public Task OnPageErrorAsync(IPage page, PageErrorEventArgs error) => Task.CompletedTask;
}
```

### The Plugin Entry Point

```csharp
using Motus.Abstractions;

namespace MyCompany.Motus.NavigationLogger;

[MotusPlugin]
public sealed class NavigationLoggerPlugin : IPlugin
{
    public string PluginId    => "mycompany.navigation-logger";
    public string Name        => "Navigation Logger";
    public string Version     => "1.0.0";
    public string? Author     => "My Company";
    public string? Description => "Logs every navigation event to the Motus logger.";

    private IMotusLogger? _logger;

    public Task OnLoadedAsync(IPluginContext context)
    {
        _logger = context.CreateLogger(nameof(NavigationLoggerPlugin));
        context.RegisterLifecycleHook(new NavigationLifecycleHook(_logger));
        return Task.CompletedTask;
    }

    public Task OnUnloadedAsync()
    {
        _logger = null;
        return Task.CompletedTask;
    }
}
```

Build the project, reference the assembly from your test project, and Motus will discover and load `NavigationLoggerPlugin` automatically when the test assembly is loaded.

---

## Loading and Debugging Tips

**Verify discovery.** Set a breakpoint in `OnLoadedAsync` or add a console write. If it is never hit, check that `[MotusPlugin]` is applied and that the generator ran. Inspect `obj/Debug/net8.0/generated/Motus.Codegen/` for the `MotusPluginRegistry.g.cs` file.

**Check for generator warnings.** Run `dotnet build` and look for `MOTUS001`-`MOTUS004` diagnostics. Any flagged class is silently skipped by the engine.

**PluginId collisions.** If two plugins share the same `PluginId`, only the first one encountered is loaded. Manual registrations in `LaunchOptions.Plugins` are processed before auto-discovered plugins, so a manual instance always wins.

**Load failures are swallowed.** If `OnLoadedAsync` throws, the plugin is skipped and the engine continues. During development, wrap your registration logic in a try/catch and log the exception so failures are visible.

**Unload order.** Plugins are unloaded in reverse registration order. `OnUnloadedAsync` exceptions are swallowed to ensure all plugins get a chance to clean up.

---

## See Also

- [Plugin Interfaces Reference](plugin-interfaces.md) - detailed documentation for `ISelectorStrategy`, `ILifecycleHook`, `IWaitCondition`, `IReporter`, and `IAccessibilityRule`
- [Plugin Best Practices](best-practices.md) - thread safety, error handling, and packaging guidelines
