> ## Documentation Index
> Fetch the complete documentation index at: https://sunpeak.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Visual Regression Testing

> Screenshot comparison testing for MCP Apps across themes, display modes, and hosts.

## Overview

Visual regression tests capture screenshots of your resources and compare them against saved baselines. This catches unintended visual changes across themes, display modes, and hosts.

Screenshot comparisons only run when you pass `--visual`. Without it, `result.screenshot()` calls are silently skipped, so you can include them in your regular e2e tests without affecting normal runs.

## Running Visual Tests

<Tabs>
  <Tab title="pnpm">
    ```bash theme={null}
    pnpm test:visual                           # Compare against baselines
    pnpm test:visual -- --update               # Update baselines
    ```
  </Tab>

  <Tab title="npm">
    ```bash theme={null}
    npm run test:visual                           # Compare against baselines
    npm run test:visual -- --update               # Update baselines
    ```
  </Tab>

  <Tab title="yarn">
    ```bash theme={null}
    yarn test:visual                           # Compare against baselines
    yarn test:visual --update                  # Update baselines
    ```
  </Tab>
</Tabs>

## Writing Visual Tests

Use `result.screenshot()` in any e2e test. It accepts an optional name and Playwright `toHaveScreenshot` options:

```typescript theme={null}
import { test, expect } from 'sunpeak/test';

test('albums renders correctly in light mode', async ({ inspector }) => {
  const result = await inspector.renderTool('show-albums', {}, { theme: 'light' });
  const app = result.app();
  await expect(app.locator('button:has-text("Summer Slice")')).toBeVisible();

  await result.screenshot('albums-light');
});

test('albums renders correctly in dark mode', async ({ inspector }) => {
  const result = await inspector.renderTool('show-albums', {}, { theme: 'dark' });
  const app = result.app();
  await expect(app.locator('button:has-text("Summer Slice")')).toBeVisible();

  await result.screenshot('albums-dark');
});
```

## Screenshot Targets

By default, `inspector.renderTool()` hides the inspector sidebars, and `screenshot()` captures the app content inside the double-iframe. This keeps visual baselines stable when the inspector UI changes. Pass a specific `element` locator to narrow the capture further:

```typescript theme={null}
// Screenshot just the app (default)
await result.screenshot('app-view');

// Screenshot a specific element
await result.screenshot('album-card', {
  element: result.app().locator('button:has-text("Summer Slice")'),
});
```

## Configuring Visual Defaults

Pass a `visual` option to `defineConfig()` to set project-wide defaults for screenshot comparison:

```typescript theme={null}
import { defineConfig } from 'sunpeak/test/config';
export default defineConfig({
  visual: {
    threshold: 0.2,
    maxDiffPixelRatio: 0.05,
    snapshotPathTemplate: '{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}{ext}',
  },
});
```

All Playwright `toHaveScreenshot` options (threshold, maxDiffPixelRatio, maxDiffPixels, animations, etc.) are supported. The `snapshotPathTemplate` controls where baseline images are stored.

## See Also

<Card horizontal title="E2E Testing" icon="flask" href="/testing/e2e">
  Full E2E testing guide with the mcp fixture.
</Card>

<Card horizontal title="Inspector" icon="desktop" href="/testing/inspector">
  The runtime that powers visual tests.
</Card>
