Skip to main content

Setup

Install the testing library:
npm install -D @frontmcp/testing
# or
yarn add -D @frontmcp/testing

Basic Test Structure

import { test, expect } from '@frontmcp/testing';

// Configure test to use your server
test.use({
  server: './src/main.ts',
  port: 3003,
  transport: 'streamable-http',
  publicMode: true,
});

test.describe('Weather Tool UI', () => {
  test('renders weather widget', async ({ mcp }) => {
    const result = await mcp.tools.call('get_weather', { location: 'London' });

    expect(result).toBeSuccessful();
    expect(result).toHaveRenderedHtml();
    expect(result).toBeXssSafe();
  });
});

UI Matchers

toHaveRenderedHtml()

Checks that the tool response has rendered HTML in _meta['ui/html'] and is not the mdx-fallback (which indicates rendering failure):
expect(result).toHaveRenderedHtml();

toContainHtmlElement(tag)

Checks that the rendered HTML contains a specific element:
expect(result).toContainHtmlElement('div');
expect(result).toContainHtmlElement('h1');
expect(result).toContainHtmlElement('table');

toContainBoundValue(value)

Verifies that output values are correctly bound in the rendered HTML:
const output = result.json<WeatherOutput>();

// String values
expect(result).toContainBoundValue(output.location);
expect(result).toContainBoundValue(output.conditions);

// Number values
expect(result).toContainBoundValue(output.temperature);
expect(result).toContainBoundValue(output.humidity);

toBeXssSafe()

Checks that the HTML doesn’t contain potential XSS vulnerabilities:
  • No <script> tags
  • No inline event handlers (onclick=, onerror=, etc.)
  • No javascript: URIs
expect(result).toBeXssSafe();

toHaveWidgetMetadata()

Verifies the response includes widget metadata (ui/html, openai/outputTemplate, or ui/mimeType):
expect(result).toHaveWidgetMetadata();

toHaveCssClass(className)

Checks for specific CSS classes in the rendered HTML:
expect(result).toHaveCssClass('weather-card');
expect(result).toHaveCssClass('btn-primary');

toNotContainRawContent(content)

Verifies that specific raw content is NOT in the HTML. Useful for detecting rendering failures:
// MDX custom components should be rendered, not raw
expect(result).toNotContainRawContent('<Alert');
expect(result).toNotContainRawContent('<WeatherCard');
expect(result).toNotContainRawContent('mdx-fallback');

toHaveProperHtmlStructure()

Checks that HTML has proper structure (not escaped text):
expect(result).toHaveProperHtmlStructure();

UIAssertions Helper

For more control and detailed error messages, use the UIAssertions helper:
import { UIAssertions } from '@frontmcp/testing';

test('comprehensive validation', async ({ mcp }) => {
  const result = await mcp.tools.call('get_weather', { location: 'Tokyo' });

  // Extract HTML and validate
  const html = UIAssertions.assertRenderedUI(result);

  // Validate data binding
  const output = result.json<WeatherOutput>();
  UIAssertions.assertDataBinding(html, output, [
    'location',
    'temperature',
    'conditions',
    'humidity',
  ]);

  // Security validation
  UIAssertions.assertXssSafe(html);

  // Element validation
  UIAssertions.assertContainsElement(html, 'div');
  UIAssertions.assertContainsElement(html, 'h1');

  // Fallback detection
  UIAssertions.assertNotContainsRaw(html, 'mdx-fallback');
});

assertValidUI(result, boundKeys?)

Comprehensive validation that returns the HTML:
const html = UIAssertions.assertValidUI(result, [
  'location',
  'temperature',
  'conditions',
]);

// Continue with additional assertions on html
expect(html).toContain('weather-card');

Testing Different Template Types

React Templates

test('React template renders correctly', async ({ mcp }) => {
  const result = await mcp.tools.call('get_weather', { location: 'Sydney' });

  expect(result).toHaveRenderedHtml();
  expect(result).toHaveProperHtmlStructure();

  // React components should be server-rendered to HTML
  expect(result).toNotContainRawContent('WeatherCard');

  // Data binding
  const output = result.json<WeatherOutput>();
  expect(result).toContainBoundValue(output.location);
});

MDX Templates

test('MDX template renders Markdown and components', async ({ mcp }) => {
  const result = await mcp.tools.call('get_weather_mdx', { location: 'Berlin' });

  expect(result).toHaveRenderedHtml();

  // Markdown should be rendered as HTML
  expect(result).toContainHtmlElement('h1'); // # Header
  expect(result).toContainHtmlElement('h2'); // ## Header
  expect(result).toContainHtmlElement('li'); // - List item

  // Custom MDX components should be rendered
  expect(result).toNotContainRawContent('<Alert');
  expect(result).toNotContainRawContent('<WeatherCard');

  // No fallback
  expect(result).toNotContainRawContent('mdx-fallback');
});

HTML Templates

test('HTML template renders with escaping', async ({ mcp }) => {
  const result = await mcp.tools.call('simple_tool', { data: '<test>' });

  expect(result).toHaveRenderedHtml();
  expect(result).toBeXssSafe();

  // User input should be escaped
  expect(result).toNotContainRawContent('<test>');
});

Testing XSS Prevention

Verify that user input is properly escaped:
test('handles XSS in user input', async ({ mcp }) => {
  // Test with script injection
  const result1 = await mcp.tools.call('get_weather', {
    location: '<script>alert("xss")</script>',
  });
  expect(result1).toBeXssSafe();
  expect(result1).toNotContainRawContent('<script>');

  // Test with event handler injection
  const result2 = await mcp.tools.call('get_weather', {
    location: '<img onerror="alert(1)" src="">',
  });
  const html = UIAssertions.assertRenderedUI(result2);
  expect(html).not.toMatch(/<img[^>]*onerror/i);
});

Testing Data Binding

Ensure all output fields are rendered in the UI:
test('all output fields are bound', async ({ mcp }) => {
  const result = await mcp.tools.call('get_weather', { location: 'Paris' });

  const output = result.json<WeatherOutput>();
  const html = UIAssertions.assertRenderedUI(result);

  // Use assertDataBinding for comprehensive check
  UIAssertions.assertDataBinding(html, output, [
    'location',
    'temperature',
    'conditions',
    'humidity',
    'windSpeed',
  ]);
});

Tool Result json() for UI

When a tool has UI enabled, result.json() returns the structuredContent (typed output) instead of parsing text content:
// Tool with UI: json() returns structuredContent
const result = await mcp.tools.call('get_weather', { location: 'London' });
const output = result.json<WeatherOutput>(); // Returns structuredContent

// Check if result has Tool UI
if (result.hasToolUI()) {
  // HTML is in _meta['ui/html']
  expect(result).toHaveRenderedHtml();
}

Complete Test Suite Example

import { test, expect, UIAssertions } from '@frontmcp/testing';

interface WeatherOutput {
  location: string;
  temperature: number;
  units: 'celsius' | 'fahrenheit';
  conditions: string;
  humidity: number;
  windSpeed: number;
}

test.use({
  server: './src/main.ts',
  port: 3003,
  transport: 'streamable-http',
  publicMode: true,
});

test.describe('Weather Tool UI', () => {
  test('renders weather UI with data binding', async ({ mcp }) => {
    const result = await mcp.tools.call('get_weather', { location: 'London' });

    expect(result).toBeSuccessful();
    expect(result).toHaveRenderedHtml();
    expect(result).toBeXssSafe();
    expect(result).toHaveWidgetMetadata();

    const output = result.json<WeatherOutput>();
    expect(result).toContainBoundValue(output.location);
    expect(result).toContainBoundValue(output.temperature);
  });

  test('has proper HTML structure', async ({ mcp }) => {
    const result = await mcp.tools.call('get_weather', { location: 'Tokyo' });

    expect(result).toHaveRenderedHtml();
    expect(result).toHaveProperHtmlStructure();
    expect(result).toContainHtmlElement('div');
  });

  test('handles XSS safely', async ({ mcp }) => {
    const result = await mcp.tools.call('get_weather', {
      location: '<script>alert("xss")</script>',
    });

    expect(result).toBeSuccessful();
    expect(result).toBeXssSafe();
    expect(result).toNotContainRawContent('<script>');
  });

  test('renders known city data correctly', async ({ mcp }) => {
    const result = await mcp.tools.call('get_weather', { location: 'San Francisco' });

    expect(result).toBeSuccessful();
    expect(result).toHaveRenderedHtml();

    const output = result.json<WeatherOutput>();
    expect(output.conditions).toBe('foggy');
    expect(result).toContainBoundValue('San Francisco');
    expect(result).toContainBoundValue(18);
  });

  test('complete validation suite', async ({ mcp }) => {
    const result = await mcp.tools.call('get_weather', { location: 'Paris' });

    const html = UIAssertions.assertValidUI(result, [
      'location',
      'temperature',
      'conditions',
      'humidity',
    ]);

    UIAssertions.assertContainsElement(html, 'div');
    UIAssertions.assertNotContainsRaw(html, 'mdx-fallback');
  });
});

Reference suites in the repo

Use the UI demo under apps/e2e/demo-e2e-ui as golden tests before copying patterns into your own workspace. It exercises every matcher in this guide, including toHaveRenderedHtml, toNotContainRawContent, and the UIAssertions helpers.
  • apps/e2e/demo-e2e-ui/e2e/react-tools.e2e.test.ts verifies the React renderer, CDN-based serving mode, and custom components.
  • apps/e2e/demo-e2e-ui/e2e/mdx-tools.e2e.test.ts ensures the MDX renderer compiles markdown + JSX with custom mdxComponents.
  • apps/e2e/demo-e2e-ui/e2e/html-tools.e2e.test.ts tests HTML template rendering.
  • apps/e2e/demo-e2e-ui/e2e/markdown-tools.e2e.test.ts tests Markdown template rendering.
Run them directly when updating the UI runtime:
pnpm nx test demo-e2e-ui
Keep these suites green before upgrading React, MDX, or @frontmcp/ui—they catch regressions in widget metadata long before production clients do.

Running Tests

# Using frontmcp CLI (recommended)
npx frontmcp test

# Using nx
nx e2e my-app

# With specific test file
npx frontmcp test --testPathPattern weather.e2e.ts

# Run sequentially (useful for debugging)
npx frontmcp test --runInBand

Next Steps