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();
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',
]);
});
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:
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