> ## Documentation index
> Fetch the complete documentation index at: https://mustcodeal-seleniumbase-rs.docs7.io/llms.txt
> Ask it with GET https://mustcodeal-seleniumbase-rs.docs7.io/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.
> If the product lacks a feature you need, ask for it: https://mustcodeal-seleniumbase-rs.docs7.io/feature-requests.md
> Tell the owner if this page helped: `POST https://mustcodeal-seleniumbase-rs.docs7.io/_docs7/analytics/feedback` with `Content-Type: application/json` and `{ "path": "/tutorials/test_plugins", "helpful": true, "comment": "…" }`. `helpful` is true or false. `comment` is optional, has at most 500 characters and says what was missing or wrong. Do not send code, secrets or personal data.

# Test Plugins

A test plugin runs code around a test: when it starts, when it fails (with the
browser still open, so it can take evidence), and when it finishes. The ones
that ship with the crate save a screenshot and the page source on failure and
write a report. They are the counterpart of Python SeleniumBase's pytest
plugins.

```mermaid
flowchart LR
  S["started"] --> T["the test"]
  T -- "error" --> F["failed: the browser is still open"]
  F --> E["finished"]
  T -- "no error" --> E
```

*The hooks of a test plugin*

<Info>

**What you will learn**

- Attach the bundled plugins to a test run
- Use the same plugins with the Pure CDP engine
- Write your own plugin
- What happens when a plugin itself fails

</Info>

Summary for AI exports: build a `Plugins` set, for example `Plugins::new().with(ScreenshotOnFailurePlugin::new("latest_logs"))`, and run each test through `plugins.run_browser_test(name, BrowserConfig::default(), async |sb| { .. }).await?`, or `plugins.run_cdp_test(name, LaunchOptions::default(), async |page| { .. }).await?` for the pure CDP engine. The bundled plugins are `ScreenshotOnFailurePlugin`, `PageSourceOnFailurePlugin`, `ReportPlugin::json` and `::html`, and `ResultStorePlugin` (feature `turso`). A custom plugin implements `TestPlugin`; every hook is optional. A hook that returns an error is logged as a warning and never changes how the test turns out.

## Attaching plugins to a test

Build a `Plugins` set and run the test through it. It works like
`run_browser_test`, and returns the same result:

```rust,no_run
use seleniumbase_rs::plugins::observer::Plugins;
use seleniumbase_rs::plugins::page_source::PageSourceOnFailurePlugin;
use seleniumbase_rs::plugins::reports::ReportPlugin;
use seleniumbase_rs::plugins::screen_shots::ScreenshotOnFailurePlugin;
use seleniumbase_rs::BrowserConfig;

# async fn demo() -> Result<(), seleniumbase_rs::SeleniumBaseError> {
let mut plugins = Plugins::new()
    .with(ScreenshotOnFailurePlugin::new("latest_logs"))
    .with(PageSourceOnFailurePlugin::new("latest_logs"))
    .with(ReportPlugin::json("latest_logs/report.json"));

plugins
    .run_browser_test("login_works", BrowserConfig::default(), async |sb| {
        sb.open("https://example.com").await?;
        sb.assert_title("Example Domain").await
    })
    .await?;
# Ok(())
# }
```

Reuse one `Plugins` value for every test in a run. A `ReportPlugin` keeps the
results it has seen and rewrites its file after each test, so the report is
valid even if the run stops part-way.

## The bundled plugins

| Plugin | When | What it does |
|---|---|---|
| `ScreenshotOnFailurePlugin` | a test fails | Saves a PNG named after the test and the time. |
| `PageSourceOnFailurePlugin` | a test fails | Saves the page's HTML the same way. |
| `ReportPlugin::json` / `::html` | a test finishes | Rewrites a JSON or HTML report of all tests so far. |
| `ResultStorePlugin` (`turso` feature) | a test finishes | Records the test in a [`ResultStore`](/tutorials/results_and_profiles), for trends and `sbrs report`. |

Details worth knowing:

- Files are never overwritten: a second failure of the same test in the same
  second gets a numbered name.
- A test name is turned into letters, digits, `-` and `_` before it is used in
  a file name, so a test called `../../etc/passwd` cannot write outside the
  directory you chose.
- If the browser cannot take the screenshot (it has crashed, say), **no file is
  written**. An earlier version wrote a text note into a `.png` file; a file that
  only pretends to be a screenshot is worse than none.

## With the Pure CDP engine

Evidence comes from anything that implements `PageEvidence`. It is implemented
for `BaseCase` and for `sb_cdp::Page`, so the same plugins work with either.

<Tabs>

<Tab title="run_cdp_test" icon="play">

`Plugins::run_cdp_test` is the CDP twin of `run_browser_test`: it launches the
browser, runs the test on its first tab (an async closure over `&Page`), shows
the plugins the tab on failure and always closes the browser:

```rust,no_run
use seleniumbase_rs::plugins::observer::Plugins;
use seleniumbase_rs::plugins::screen_shots::ScreenshotOnFailurePlugin;
use seleniumbase_rs::sb_cdp::LaunchOptions;

# async fn demo() -> Result<(), seleniumbase_rs::SeleniumBaseError> {
let mut plugins = Plugins::new().with(ScreenshotOnFailurePlugin::new("latest_logs"));
plugins
    .run_cdp_test("checkout", LaunchOptions::default(), async |page| {
        page.goto("https://example.com").await?;
        page.locator("#buy").click().await
    })
    .await?;
# Ok(())
# }
```

</Tab>

<Tab title="Call the hooks yourself" icon="wrench">

Outside those runners, call the hooks yourself:

```rust,no_run
use seleniumbase_rs::plugins::observer::Plugins;
use seleniumbase_rs::plugins::screen_shots::ScreenshotOnFailurePlugin;
use seleniumbase_rs::sb_cdp::{Browser, LaunchOptions};

# async fn demo() -> Result<(), seleniumbase_rs::SeleniumBaseError> {
let browser = Browser::launch(LaunchOptions::default()).await?;
let page = browser.default_page().await?;
let mut plugins = Plugins::new().with(ScreenshotOnFailurePlugin::new("latest_logs"));

plugins.started("checkout").await;
let outcome = async {
    page.goto("https://example.com").await?;
    page.locator("#buy").click().await
}
.await;
if let Err(error) = &outcome {
    plugins.failed("checkout", error, &page).await; // takes the screenshot now
}
plugins.finished("checkout", std::time::Duration::ZERO, &outcome).await;
# Ok(())
# }
```

</Tab>

</Tabs>

## Writing your own

Implement `TestPlugin`. Every hook is optional and returns a `Result`:

```rust
use async_trait::async_trait;
use seleniumbase_rs::plugins::observer::{PageEvidence, TestPlugin};
use seleniumbase_rs::SeleniumBaseError;

struct SlackOnFailure;

#[async_trait]
impl TestPlugin for SlackOnFailure {
    async fn test_failed(
        &mut self,
        test: &str,
        error: &SeleniumBaseError,
        page: &dyn PageEvidence,
    ) -> Result<(), SeleniumBaseError> {
        let png = page.screenshot_png().await?;
        // ... post `test`, `error` and `png` somewhere ...
        # let _ = (test, error, png);
        Ok(())
    }
}
```

## When a plugin fails

<Info>

A plugin can never change how a test turns out. If a hook returns an error, it
is logged as a warning (with the plugin, hook and test name as fields) and the
remaining plugins still run. The test's own result, combined with any error
from closing the browser, is what you get back.

</Info>

## Uploading what a plugin saved

`S3LoggingPlugin`, `AzureLoggingPlugin` and `GcpLoggingPlugin` are uploaders,
not hooks: call their `upload_file` after a test on the files the failure
plugins wrote. See [Cloud Integrations](/tutorials/cloud_integrations).

## Migrating from the old plugin API

<Warning>

Earlier releases had a `SeleniumBasePlugin` trait with `before_command` and
`after_command`, a `PluginManager`, and a `DbReportingPlugin`. Nothing ever
called the manager, so none of their hooks ran. They are removed. Use
`TestPlugin` and `Plugins`; `ReportPlugin` replaces `DbReportingPlugin` (the
same JSON row format, one row per test instead of one per command), and
`ResultStorePlugin` covers the database case.
There is no per-command hook any more: the per-test hooks are the ones with a
natural place to run.

</Warning>

## What has and has not been verified

<Note>

The plugins, the dispatch order, failure isolation, the file naming and the
reports are covered by tests that need no browser, and the screenshot and
page-source plugins are tested through the Pure CDP mock. `Plugins::run_browser_test`
(the `BaseCase` path) has not been run against a real WebDriver on the machine
this was written on.

</Note>
