@frontmcp/testing 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +1358 -0
  3. package/jest-preset.js +61 -0
  4. package/package.json +94 -0
  5. package/src/assertions/index.d.ts +5 -0
  6. package/src/assertions/index.js +18 -0
  7. package/src/assertions/index.js.map +1 -0
  8. package/src/assertions/mcp-assertions.d.ts +81 -0
  9. package/src/assertions/mcp-assertions.js +220 -0
  10. package/src/assertions/mcp-assertions.js.map +1 -0
  11. package/src/auth/auth-headers.d.ts +29 -0
  12. package/src/auth/auth-headers.js +62 -0
  13. package/src/auth/auth-headers.js.map +1 -0
  14. package/src/auth/index.d.ts +9 -0
  15. package/src/auth/index.js +15 -0
  16. package/src/auth/index.js.map +1 -0
  17. package/src/auth/token-factory.d.ts +94 -0
  18. package/src/auth/token-factory.js +181 -0
  19. package/src/auth/token-factory.js.map +1 -0
  20. package/src/auth/user-fixtures.d.ts +26 -0
  21. package/src/auth/user-fixtures.js +92 -0
  22. package/src/auth/user-fixtures.js.map +1 -0
  23. package/src/client/index.d.ts +7 -0
  24. package/src/client/index.js +12 -0
  25. package/src/client/index.js.map +1 -0
  26. package/src/client/mcp-test-client.builder.d.ts +72 -0
  27. package/src/client/mcp-test-client.builder.js +111 -0
  28. package/src/client/mcp-test-client.builder.js.map +1 -0
  29. package/src/client/mcp-test-client.d.ts +360 -0
  30. package/src/client/mcp-test-client.js +929 -0
  31. package/src/client/mcp-test-client.js.map +1 -0
  32. package/src/client/mcp-test-client.types.d.ts +216 -0
  33. package/src/client/mcp-test-client.types.js +7 -0
  34. package/src/client/mcp-test-client.types.js.map +1 -0
  35. package/src/errors/index.d.ts +45 -0
  36. package/src/errors/index.js +85 -0
  37. package/src/errors/index.js.map +1 -0
  38. package/src/expect.d.ts +67 -0
  39. package/src/expect.js +31 -0
  40. package/src/expect.js.map +1 -0
  41. package/src/fixtures/fixture-types.d.ts +166 -0
  42. package/src/fixtures/fixture-types.js +7 -0
  43. package/src/fixtures/fixture-types.js.map +1 -0
  44. package/src/fixtures/index.d.ts +7 -0
  45. package/src/fixtures/index.js +16 -0
  46. package/src/fixtures/index.js.map +1 -0
  47. package/src/fixtures/test-fixture.d.ts +41 -0
  48. package/src/fixtures/test-fixture.js +280 -0
  49. package/src/fixtures/test-fixture.js.map +1 -0
  50. package/src/http-mock/http-mock.d.ts +84 -0
  51. package/src/http-mock/http-mock.js +544 -0
  52. package/src/http-mock/http-mock.js.map +1 -0
  53. package/src/http-mock/http-mock.types.d.ts +124 -0
  54. package/src/http-mock/http-mock.types.js +10 -0
  55. package/src/http-mock/http-mock.types.js.map +1 -0
  56. package/src/http-mock/index.d.ts +6 -0
  57. package/src/http-mock/index.js +11 -0
  58. package/src/http-mock/index.js.map +1 -0
  59. package/src/index.d.ts +65 -0
  60. package/src/index.js +128 -0
  61. package/src/index.js.map +1 -0
  62. package/src/interceptor/index.d.ts +7 -0
  63. package/src/interceptor/index.js +15 -0
  64. package/src/interceptor/index.js.map +1 -0
  65. package/src/interceptor/interceptor-chain.d.ts +77 -0
  66. package/src/interceptor/interceptor-chain.js +207 -0
  67. package/src/interceptor/interceptor-chain.js.map +1 -0
  68. package/src/interceptor/interceptor.types.d.ts +131 -0
  69. package/src/interceptor/interceptor.types.js +7 -0
  70. package/src/interceptor/interceptor.types.js.map +1 -0
  71. package/src/interceptor/mock-registry.d.ts +82 -0
  72. package/src/interceptor/mock-registry.js +189 -0
  73. package/src/interceptor/mock-registry.js.map +1 -0
  74. package/src/matchers/index.d.ts +7 -0
  75. package/src/matchers/index.js +12 -0
  76. package/src/matchers/index.js.map +1 -0
  77. package/src/matchers/matcher-types.d.ts +266 -0
  78. package/src/matchers/matcher-types.js +10 -0
  79. package/src/matchers/matcher-types.js.map +1 -0
  80. package/src/matchers/mcp-matchers.d.ts +47 -0
  81. package/src/matchers/mcp-matchers.js +391 -0
  82. package/src/matchers/mcp-matchers.js.map +1 -0
  83. package/src/playwright/index.d.ts +37 -0
  84. package/src/playwright/index.js +49 -0
  85. package/src/playwright/index.js.map +1 -0
  86. package/src/server/index.d.ts +6 -0
  87. package/src/server/index.js +10 -0
  88. package/src/server/index.js.map +1 -0
  89. package/src/server/test-server.d.ts +99 -0
  90. package/src/server/test-server.js +286 -0
  91. package/src/server/test-server.js.map +1 -0
  92. package/src/setup.d.ts +22 -0
  93. package/src/setup.js +30 -0
  94. package/src/setup.js.map +1 -0
  95. package/src/transport/index.d.ts +6 -0
  96. package/src/transport/index.js +10 -0
  97. package/src/transport/index.js.map +1 -0
  98. package/src/transport/streamable-http.transport.d.ts +65 -0
  99. package/src/transport/streamable-http.transport.js +432 -0
  100. package/src/transport/streamable-http.transport.js.map +1 -0
  101. package/src/transport/transport.interface.d.ts +124 -0
  102. package/src/transport/transport.interface.js +7 -0
  103. package/src/transport/transport.interface.js.map +1 -0
  104. package/src/ui/index.d.ts +17 -0
  105. package/src/ui/index.js +23 -0
  106. package/src/ui/index.js.map +1 -0
  107. package/src/ui/ui-assertions.d.ts +94 -0
  108. package/src/ui/ui-assertions.js +215 -0
  109. package/src/ui/ui-assertions.js.map +1 -0
  110. package/src/ui/ui-matchers.d.ts +39 -0
  111. package/src/ui/ui-matchers.js +275 -0
  112. package/src/ui/ui-matchers.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/ui/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAEH,6CAA2C;AAAlC,yGAAA,UAAU,OAAA;AACnB,iDAA+C;AAAtC,6GAAA,YAAY,OAAA","sourcesContent":["/**\n * @file index.ts\n * @description Barrel exports for UI testing utilities\n *\n * @example\n * ```typescript\n * import { uiMatchers, UIAssertions } from '@frontmcp/testing';\n *\n * // Use matchers with expect.extend\n * expect.extend(uiMatchers);\n *\n * // Or use assertion helpers directly\n * const html = UIAssertions.assertRenderedUI(result);\n * ```\n */\n\nexport { uiMatchers } from './ui-matchers';\nexport { UIAssertions } from './ui-assertions';\n"]}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @file ui-assertions.ts
3
+ * @description UI-specific assertion helpers for testing tool UI responses
4
+ *
5
+ * The metadata keys used in these assertions align with the UIMetadata interface
6
+ * from @frontmcp/ui/adapters. Key fields include:
7
+ * - `ui/html`: Inline rendered HTML (universal)
8
+ * - `ui/mimeType`: MIME type for the HTML content
9
+ * - `openai/outputTemplate`: Resource URI for widget template (OpenAI)
10
+ * - `openai/widgetAccessible`: Whether widget can invoke tools (OpenAI)
11
+ *
12
+ * @see {@link https://docs.agentfront.dev/docs/servers/tools#tool-ui | Tool UI Documentation}
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { UIAssertions } from '@frontmcp/testing';
17
+ *
18
+ * const result = await client.tools.call('my-tool', {});
19
+ * const html = UIAssertions.assertRenderedUI(result);
20
+ * UIAssertions.assertXssSafe(html);
21
+ * UIAssertions.assertDataBinding(html, result.json(), ['location', 'temperature']);
22
+ * ```
23
+ */
24
+ import type { ToolResultWrapper } from '../client/mcp-test-client.types';
25
+ /**
26
+ * UI-specific assertion helpers.
27
+ * Use these for imperative-style assertions with detailed error messages.
28
+ */
29
+ export declare const UIAssertions: {
30
+ /**
31
+ * Assert tool result has valid rendered UI HTML.
32
+ * @param result - The tool result wrapper
33
+ * @returns The rendered HTML string
34
+ * @throws Error if no UI HTML found or if mdx-fallback detected
35
+ */
36
+ assertRenderedUI(result: ToolResultWrapper): string;
37
+ /**
38
+ * Assert HTML contains all expected bound values from tool output.
39
+ * @param html - The rendered HTML string
40
+ * @param output - The tool output object
41
+ * @param keys - Array of keys whose values should appear in the HTML
42
+ * @throws Error if any expected value is missing from the HTML
43
+ */
44
+ assertDataBinding(html: string, output: Record<string, unknown>, keys: string[]): void;
45
+ /**
46
+ * Assert HTML is XSS-safe (no scripts, event handlers, or javascript: URIs).
47
+ * @param html - The rendered HTML string
48
+ * @throws Error if potential XSS vulnerabilities are detected
49
+ */
50
+ assertXssSafe(html: string): void;
51
+ /**
52
+ * Assert HTML has proper structure (not escaped raw content).
53
+ * @param html - The rendered HTML string
54
+ * @throws Error if HTML appears to be raw/unrendered content
55
+ */
56
+ assertProperHtmlStructure(html: string): void;
57
+ /**
58
+ * Assert HTML contains a specific element.
59
+ * @param html - The rendered HTML string
60
+ * @param tag - The HTML tag name to look for
61
+ * @throws Error if the element is not found
62
+ */
63
+ assertContainsElement(html: string, tag: string): void;
64
+ /**
65
+ * Assert HTML contains a specific CSS class.
66
+ * @param html - The rendered HTML string
67
+ * @param className - The CSS class name to look for
68
+ * @throws Error if the class is not found
69
+ */
70
+ assertHasCssClass(html: string, className: string): void;
71
+ /**
72
+ * Assert HTML does NOT contain specific content.
73
+ * Useful for verifying custom components were rendered, not left as raw tags.
74
+ * @param html - The rendered HTML string
75
+ * @param content - The content that should NOT appear
76
+ * @throws Error if the content is found
77
+ */
78
+ assertNotContainsRaw(html: string, content: string): void;
79
+ /**
80
+ * Assert that widget metadata is present in the result.
81
+ * Checks for ui/html, openai/outputTemplate, or ui/mimeType.
82
+ * @param result - The tool result wrapper
83
+ * @throws Error if widget metadata is missing
84
+ */
85
+ assertWidgetMetadata(result: ToolResultWrapper): void;
86
+ /**
87
+ * Comprehensive UI validation that runs all checks.
88
+ * @param result - The tool result wrapper
89
+ * @param boundKeys - Optional array of output keys to check for data binding
90
+ * @returns The rendered HTML string
91
+ * @throws Error if any validation fails
92
+ */
93
+ assertValidUI(result: ToolResultWrapper, boundKeys?: string[]): string;
94
+ };
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ /**
3
+ * @file ui-assertions.ts
4
+ * @description UI-specific assertion helpers for testing tool UI responses
5
+ *
6
+ * The metadata keys used in these assertions align with the UIMetadata interface
7
+ * from @frontmcp/ui/adapters. Key fields include:
8
+ * - `ui/html`: Inline rendered HTML (universal)
9
+ * - `ui/mimeType`: MIME type for the HTML content
10
+ * - `openai/outputTemplate`: Resource URI for widget template (OpenAI)
11
+ * - `openai/widgetAccessible`: Whether widget can invoke tools (OpenAI)
12
+ *
13
+ * @see {@link https://docs.agentfront.dev/docs/servers/tools#tool-ui | Tool UI Documentation}
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { UIAssertions } from '@frontmcp/testing';
18
+ *
19
+ * const result = await client.tools.call('my-tool', {});
20
+ * const html = UIAssertions.assertRenderedUI(result);
21
+ * UIAssertions.assertXssSafe(html);
22
+ * UIAssertions.assertDataBinding(html, result.json(), ['location', 'temperature']);
23
+ * ```
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.UIAssertions = void 0;
27
+ // Type-only reference: Metadata keys used below align with UIMetadata from @frontmcp/ui/adapters
28
+ // This is an optional peer dependency, so we don't import it directly
29
+ // ═══════════════════════════════════════════════════════════════════
30
+ // HELPER FUNCTIONS
31
+ // ═══════════════════════════════════════════════════════════════════
32
+ /**
33
+ * Escape special regex metacharacters in a string.
34
+ * This prevents user-provided tag/class names from being interpreted as regex patterns.
35
+ */
36
+ function escapeRegex(str) {
37
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
38
+ }
39
+ // ═══════════════════════════════════════════════════════════════════
40
+ // UI ASSERTIONS
41
+ // ═══════════════════════════════════════════════════════════════════
42
+ /**
43
+ * UI-specific assertion helpers.
44
+ * Use these for imperative-style assertions with detailed error messages.
45
+ */
46
+ exports.UIAssertions = {
47
+ /**
48
+ * Assert tool result has valid rendered UI HTML.
49
+ * @param result - The tool result wrapper
50
+ * @returns The rendered HTML string
51
+ * @throws Error if no UI HTML found or if mdx-fallback detected
52
+ */
53
+ assertRenderedUI(result) {
54
+ const meta = result.raw._meta;
55
+ if (!meta) {
56
+ throw new Error('Expected tool result to have _meta, but _meta is undefined');
57
+ }
58
+ const html = meta['ui/html'];
59
+ if (!html) {
60
+ throw new Error('Expected tool result to have ui/html in _meta, but it is missing');
61
+ }
62
+ if (typeof html !== 'string') {
63
+ throw new Error(`Expected ui/html to be a string, but got ${typeof html}`);
64
+ }
65
+ if (html.includes('mdx-fallback')) {
66
+ throw new Error('Got mdx-fallback instead of rendered HTML - MDX/React rendering failed. ' +
67
+ 'Check that @mdx-js/mdx is installed and the template syntax is valid.');
68
+ }
69
+ return html;
70
+ },
71
+ /**
72
+ * Assert HTML contains all expected bound values from tool output.
73
+ * @param html - The rendered HTML string
74
+ * @param output - The tool output object
75
+ * @param keys - Array of keys whose values should appear in the HTML
76
+ * @throws Error if any expected value is missing from the HTML
77
+ */
78
+ assertDataBinding(html, output, keys) {
79
+ const missingKeys = [];
80
+ for (const key of keys) {
81
+ const value = output[key];
82
+ if (value === undefined || value === null) {
83
+ continue; // Skip undefined/null values
84
+ }
85
+ const stringValue = String(value);
86
+ if (!html.includes(stringValue)) {
87
+ missingKeys.push(`${key}="${stringValue}"`);
88
+ }
89
+ }
90
+ if (missingKeys.length > 0) {
91
+ throw new Error(`Expected HTML to contain bound values for: ${missingKeys.join(', ')}. ` + 'Data binding may have failed.');
92
+ }
93
+ },
94
+ /**
95
+ * Assert HTML is XSS-safe (no scripts, event handlers, or javascript: URIs).
96
+ * @param html - The rendered HTML string
97
+ * @throws Error if potential XSS vulnerabilities are detected
98
+ */
99
+ assertXssSafe(html) {
100
+ const vulnerabilities = [];
101
+ if (/<script[\s>]/i.test(html)) {
102
+ vulnerabilities.push('<script> tag detected');
103
+ }
104
+ if (/\son\w+\s*=/i.test(html)) {
105
+ vulnerabilities.push('inline event handler detected (onclick, onerror, etc.)');
106
+ }
107
+ if (/javascript:/i.test(html)) {
108
+ vulnerabilities.push('javascript: URI detected');
109
+ }
110
+ if (vulnerabilities.length > 0) {
111
+ throw new Error(`Potential XSS vulnerabilities found: ${vulnerabilities.join('; ')}`);
112
+ }
113
+ },
114
+ /**
115
+ * Assert HTML has proper structure (not escaped raw content).
116
+ * @param html - The rendered HTML string
117
+ * @throws Error if HTML appears to be raw/unrendered content
118
+ */
119
+ assertProperHtmlStructure(html) {
120
+ // Check for escaped HTML entities that suggest content wasn't rendered
121
+ if (html.includes('&lt;') && html.includes('&gt;')) {
122
+ throw new Error('HTML contains escaped HTML entities (&lt;, &gt;) - content was likely not rendered. ' +
123
+ 'Check that the template is being processed correctly.');
124
+ }
125
+ // Check that there's at least one HTML tag
126
+ if (!/<[a-z]/i.test(html)) {
127
+ throw new Error('HTML contains no HTML tags - content may be plain text or rendering failed.');
128
+ }
129
+ },
130
+ /**
131
+ * Assert HTML contains a specific element.
132
+ * @param html - The rendered HTML string
133
+ * @param tag - The HTML tag name to look for
134
+ * @throws Error if the element is not found
135
+ */
136
+ assertContainsElement(html, tag) {
137
+ // Escape regex metacharacters to prevent user input from breaking the regex
138
+ const regex = new RegExp(`<${escapeRegex(tag)}[\\s>]`, 'i');
139
+ if (!regex.test(html)) {
140
+ throw new Error(`Expected HTML to contain <${tag}> element`);
141
+ }
142
+ },
143
+ /**
144
+ * Assert HTML contains a specific CSS class.
145
+ * @param html - The rendered HTML string
146
+ * @param className - The CSS class name to look for
147
+ * @throws Error if the class is not found
148
+ */
149
+ assertHasCssClass(html, className) {
150
+ // Escape regex metacharacters to prevent user input from breaking the regex
151
+ const classRegex = new RegExp(`class(?:Name)?\\s*=\\s*["'][^"']*\\b${escapeRegex(className)}\\b[^"']*["']`, 'i');
152
+ if (!classRegex.test(html)) {
153
+ throw new Error(`Expected HTML to have CSS class "${className}"`);
154
+ }
155
+ },
156
+ /**
157
+ * Assert HTML does NOT contain specific content.
158
+ * Useful for verifying custom components were rendered, not left as raw tags.
159
+ * @param html - The rendered HTML string
160
+ * @param content - The content that should NOT appear
161
+ * @throws Error if the content is found
162
+ */
163
+ assertNotContainsRaw(html, content) {
164
+ if (html.includes(content)) {
165
+ throw new Error(`HTML contains raw content "${content}" - this component may not have been rendered. ` +
166
+ 'Check that all custom components are properly passed to the renderer.');
167
+ }
168
+ },
169
+ /**
170
+ * Assert that widget metadata is present in the result.
171
+ * Checks for ui/html, openai/outputTemplate, or ui/mimeType.
172
+ * @param result - The tool result wrapper
173
+ * @throws Error if widget metadata is missing
174
+ */
175
+ assertWidgetMetadata(result) {
176
+ const meta = result.raw._meta;
177
+ if (!meta) {
178
+ throw new Error('Expected tool result to have _meta with widget metadata');
179
+ }
180
+ // Check for any widget-related metadata fields (aligned with toHaveWidgetMetadata matcher)
181
+ const hasUiHtml = Boolean(meta['ui/html']);
182
+ const hasOutputTemplate = Boolean(meta['openai/outputTemplate']);
183
+ const hasMimeType = Boolean(meta['ui/mimeType']);
184
+ if (!hasUiHtml && !hasOutputTemplate && !hasMimeType) {
185
+ throw new Error('Expected _meta to have widget metadata (ui/html, openai/outputTemplate, or ui/mimeType)');
186
+ }
187
+ },
188
+ /**
189
+ * Comprehensive UI validation that runs all checks.
190
+ * @param result - The tool result wrapper
191
+ * @param boundKeys - Optional array of output keys to check for data binding
192
+ * @returns The rendered HTML string
193
+ * @throws Error if any validation fails
194
+ */
195
+ assertValidUI(result, boundKeys) {
196
+ // 1. Get and validate HTML exists
197
+ const html = exports.UIAssertions.assertRenderedUI(result);
198
+ // 2. Check HTML structure
199
+ exports.UIAssertions.assertProperHtmlStructure(html);
200
+ // 3. Check XSS safety
201
+ exports.UIAssertions.assertXssSafe(html);
202
+ // 4. Check data binding if keys provided
203
+ if (boundKeys && boundKeys.length > 0) {
204
+ try {
205
+ const output = JSON.parse(result.text() || '{}');
206
+ exports.UIAssertions.assertDataBinding(html, output, boundKeys);
207
+ }
208
+ catch {
209
+ // If we can't parse output, skip data binding check
210
+ }
211
+ }
212
+ return html;
213
+ },
214
+ };
215
+ //# sourceMappingURL=ui-assertions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-assertions.js","sourceRoot":"","sources":["../../../src/ui/ui-assertions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file ui-assertions.ts\n * @description UI-specific assertion helpers for testing tool UI responses\n *\n * The metadata keys used in these assertions align with the UIMetadata interface\n * from @frontmcp/ui/adapters. Key fields include:\n * - `ui/html`: Inline rendered HTML (universal)\n * - `ui/mimeType`: MIME type for the HTML content\n * - `openai/outputTemplate`: Resource URI for widget template (OpenAI)\n * - `openai/widgetAccessible`: Whether widget can invoke tools (OpenAI)\n *\n * @see {@link https://docs.agentfront.dev/docs/servers/tools#tool-ui | Tool UI Documentation}\n *\n * @example\n * ```typescript\n * import { UIAssertions } from '@frontmcp/testing';\n *\n * const result = await client.tools.call('my-tool', {});\n * const html = UIAssertions.assertRenderedUI(result);\n * UIAssertions.assertXssSafe(html);\n * UIAssertions.assertDataBinding(html, result.json(), ['location', 'temperature']);\n * ```\n */\n\nimport type { ToolResultWrapper } from '../client/mcp-test-client.types';\n\n// Type-only reference: Metadata keys used below align with UIMetadata from @frontmcp/ui/adapters\n// This is an optional peer dependency, so we don't import it directly\n\n// ═══════════════════════════════════════════════════════════════════\n// HELPER FUNCTIONS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Escape special regex metacharacters in a string.\n * This prevents user-provided tag/class names from being interpreted as regex patterns.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// UI ASSERTIONS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * UI-specific assertion helpers.\n * Use these for imperative-style assertions with detailed error messages.\n */\nexport const UIAssertions = {\n /**\n * Assert tool result has valid rendered UI HTML.\n * @param result - The tool result wrapper\n * @returns The rendered HTML string\n * @throws Error if no UI HTML found or if mdx-fallback detected\n */\n assertRenderedUI(result: ToolResultWrapper): string {\n const meta = result.raw._meta as Record<string, unknown> | undefined;\n\n if (!meta) {\n throw new Error('Expected tool result to have _meta, but _meta is undefined');\n }\n\n const html = meta['ui/html'];\n\n if (!html) {\n throw new Error('Expected tool result to have ui/html in _meta, but it is missing');\n }\n\n if (typeof html !== 'string') {\n throw new Error(`Expected ui/html to be a string, but got ${typeof html}`);\n }\n\n if (html.includes('mdx-fallback')) {\n throw new Error(\n 'Got mdx-fallback instead of rendered HTML - MDX/React rendering failed. ' +\n 'Check that @mdx-js/mdx is installed and the template syntax is valid.',\n );\n }\n\n return html;\n },\n\n /**\n * Assert HTML contains all expected bound values from tool output.\n * @param html - The rendered HTML string\n * @param output - The tool output object\n * @param keys - Array of keys whose values should appear in the HTML\n * @throws Error if any expected value is missing from the HTML\n */\n assertDataBinding(html: string, output: Record<string, unknown>, keys: string[]): void {\n const missingKeys: string[] = [];\n\n for (const key of keys) {\n const value = output[key];\n if (value === undefined || value === null) {\n continue; // Skip undefined/null values\n }\n\n const stringValue = String(value);\n if (!html.includes(stringValue)) {\n missingKeys.push(`${key}=\"${stringValue}\"`);\n }\n }\n\n if (missingKeys.length > 0) {\n throw new Error(\n `Expected HTML to contain bound values for: ${missingKeys.join(', ')}. ` + 'Data binding may have failed.',\n );\n }\n },\n\n /**\n * Assert HTML is XSS-safe (no scripts, event handlers, or javascript: URIs).\n * @param html - The rendered HTML string\n * @throws Error if potential XSS vulnerabilities are detected\n */\n assertXssSafe(html: string): void {\n const vulnerabilities: string[] = [];\n\n if (/<script[\\s>]/i.test(html)) {\n vulnerabilities.push('<script> tag detected');\n }\n\n if (/\\son\\w+\\s*=/i.test(html)) {\n vulnerabilities.push('inline event handler detected (onclick, onerror, etc.)');\n }\n\n if (/javascript:/i.test(html)) {\n vulnerabilities.push('javascript: URI detected');\n }\n\n if (vulnerabilities.length > 0) {\n throw new Error(`Potential XSS vulnerabilities found: ${vulnerabilities.join('; ')}`);\n }\n },\n\n /**\n * Assert HTML has proper structure (not escaped raw content).\n * @param html - The rendered HTML string\n * @throws Error if HTML appears to be raw/unrendered content\n */\n assertProperHtmlStructure(html: string): void {\n // Check for escaped HTML entities that suggest content wasn't rendered\n if (html.includes('&lt;') && html.includes('&gt;')) {\n throw new Error(\n 'HTML contains escaped HTML entities (&lt;, &gt;) - content was likely not rendered. ' +\n 'Check that the template is being processed correctly.',\n );\n }\n\n // Check that there's at least one HTML tag\n if (!/<[a-z]/i.test(html)) {\n throw new Error('HTML contains no HTML tags - content may be plain text or rendering failed.');\n }\n },\n\n /**\n * Assert HTML contains a specific element.\n * @param html - The rendered HTML string\n * @param tag - The HTML tag name to look for\n * @throws Error if the element is not found\n */\n assertContainsElement(html: string, tag: string): void {\n // Escape regex metacharacters to prevent user input from breaking the regex\n const regex = new RegExp(`<${escapeRegex(tag)}[\\\\s>]`, 'i');\n if (!regex.test(html)) {\n throw new Error(`Expected HTML to contain <${tag}> element`);\n }\n },\n\n /**\n * Assert HTML contains a specific CSS class.\n * @param html - The rendered HTML string\n * @param className - The CSS class name to look for\n * @throws Error if the class is not found\n */\n assertHasCssClass(html: string, className: string): void {\n // Escape regex metacharacters to prevent user input from breaking the regex\n const classRegex = new RegExp(`class(?:Name)?\\\\s*=\\\\s*[\"'][^\"']*\\\\b${escapeRegex(className)}\\\\b[^\"']*[\"']`, 'i');\n if (!classRegex.test(html)) {\n throw new Error(`Expected HTML to have CSS class \"${className}\"`);\n }\n },\n\n /**\n * Assert HTML does NOT contain specific content.\n * Useful for verifying custom components were rendered, not left as raw tags.\n * @param html - The rendered HTML string\n * @param content - The content that should NOT appear\n * @throws Error if the content is found\n */\n assertNotContainsRaw(html: string, content: string): void {\n if (html.includes(content)) {\n throw new Error(\n `HTML contains raw content \"${content}\" - this component may not have been rendered. ` +\n 'Check that all custom components are properly passed to the renderer.',\n );\n }\n },\n\n /**\n * Assert that widget metadata is present in the result.\n * Checks for ui/html, openai/outputTemplate, or ui/mimeType.\n * @param result - The tool result wrapper\n * @throws Error if widget metadata is missing\n */\n assertWidgetMetadata(result: ToolResultWrapper): void {\n const meta = result.raw._meta as Record<string, unknown> | undefined;\n\n if (!meta) {\n throw new Error('Expected tool result to have _meta with widget metadata');\n }\n\n // Check for any widget-related metadata fields (aligned with toHaveWidgetMetadata matcher)\n const hasUiHtml = Boolean(meta['ui/html']);\n const hasOutputTemplate = Boolean(meta['openai/outputTemplate']);\n const hasMimeType = Boolean(meta['ui/mimeType']);\n\n if (!hasUiHtml && !hasOutputTemplate && !hasMimeType) {\n throw new Error('Expected _meta to have widget metadata (ui/html, openai/outputTemplate, or ui/mimeType)');\n }\n },\n\n /**\n * Comprehensive UI validation that runs all checks.\n * @param result - The tool result wrapper\n * @param boundKeys - Optional array of output keys to check for data binding\n * @returns The rendered HTML string\n * @throws Error if any validation fails\n */\n assertValidUI(result: ToolResultWrapper, boundKeys?: string[]): string {\n // 1. Get and validate HTML exists\n const html = UIAssertions.assertRenderedUI(result);\n\n // 2. Check HTML structure\n UIAssertions.assertProperHtmlStructure(html);\n\n // 3. Check XSS safety\n UIAssertions.assertXssSafe(html);\n\n // 4. Check data binding if keys provided\n if (boundKeys && boundKeys.length > 0) {\n try {\n const output = JSON.parse(result.text() || '{}');\n UIAssertions.assertDataBinding(html, output, boundKeys);\n } catch {\n // If we can't parse output, skip data binding check\n }\n }\n\n return html;\n },\n};\n"]}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file ui-matchers.ts
3
+ * @description UI-specific Jest matchers for validating tool UI responses
4
+ *
5
+ * The metadata keys used in these matchers align with the UIMetadata interface
6
+ * from @frontmcp/ui/adapters. Key fields include:
7
+ * - `ui/html`: Inline rendered HTML (universal)
8
+ * - `ui/mimeType`: MIME type for the HTML content
9
+ * - `openai/outputTemplate`: Resource URI for widget template (OpenAI)
10
+ * - `openai/widgetAccessible`: Whether widget can invoke tools (OpenAI)
11
+ *
12
+ * @see {@link https://docs.agentfront.dev/docs/servers/tools#tool-ui | Tool UI Documentation}
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { test, expect } from '@frontmcp/testing';
17
+ *
18
+ * test('tool has rendered UI', async ({ mcp }) => {
19
+ * const result = await mcp.tools.call('my-tool', {});
20
+ * expect(result).toHaveRenderedHtml();
21
+ * expect(result).toBeXssSafe();
22
+ * expect(result).toContainBoundValue('expected-value');
23
+ * });
24
+ * ```
25
+ */
26
+ import type { MatcherFunction } from 'expect';
27
+ /**
28
+ * All UI matchers as an object for expect.extend()
29
+ */
30
+ export declare const uiMatchers: {
31
+ toHaveRenderedHtml: MatcherFunction<[]>;
32
+ toContainHtmlElement: MatcherFunction<[tag: string]>;
33
+ toContainBoundValue: MatcherFunction<[value: string | number]>;
34
+ toBeXssSafe: MatcherFunction<[]>;
35
+ toHaveWidgetMetadata: MatcherFunction<[]>;
36
+ toHaveCssClass: MatcherFunction<[className: string]>;
37
+ toNotContainRawContent: MatcherFunction<[content: string]>;
38
+ toHaveProperHtmlStructure: MatcherFunction<[]>;
39
+ };
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+ /**
3
+ * @file ui-matchers.ts
4
+ * @description UI-specific Jest matchers for validating tool UI responses
5
+ *
6
+ * The metadata keys used in these matchers align with the UIMetadata interface
7
+ * from @frontmcp/ui/adapters. Key fields include:
8
+ * - `ui/html`: Inline rendered HTML (universal)
9
+ * - `ui/mimeType`: MIME type for the HTML content
10
+ * - `openai/outputTemplate`: Resource URI for widget template (OpenAI)
11
+ * - `openai/widgetAccessible`: Whether widget can invoke tools (OpenAI)
12
+ *
13
+ * @see {@link https://docs.agentfront.dev/docs/servers/tools#tool-ui | Tool UI Documentation}
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { test, expect } from '@frontmcp/testing';
18
+ *
19
+ * test('tool has rendered UI', async ({ mcp }) => {
20
+ * const result = await mcp.tools.call('my-tool', {});
21
+ * expect(result).toHaveRenderedHtml();
22
+ * expect(result).toBeXssSafe();
23
+ * expect(result).toContainBoundValue('expected-value');
24
+ * });
25
+ * ```
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.uiMatchers = void 0;
29
+ // Type-only reference: Metadata keys used below align with UIMetadata from @frontmcp/ui/adapters
30
+ // This is an optional peer dependency, so we don't import it directly
31
+ // ═══════════════════════════════════════════════════════════════════
32
+ // HELPER FUNCTIONS
33
+ // ═══════════════════════════════════════════════════════════════════
34
+ /**
35
+ * Escape special regex metacharacters in a string.
36
+ * This prevents user-provided tag/class names from being interpreted as regex patterns.
37
+ */
38
+ function escapeRegex(str) {
39
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
40
+ }
41
+ /**
42
+ * Extract UI HTML from a tool result wrapper or raw result.
43
+ */
44
+ function extractUiHtml(received) {
45
+ if (typeof received === 'string') {
46
+ return received;
47
+ }
48
+ // ToolResultWrapper has raw._meta
49
+ const wrapper = received;
50
+ const meta = wrapper?.raw?._meta || wrapper?._meta;
51
+ if (meta && typeof meta === 'object') {
52
+ const uiHtml = meta['ui/html'];
53
+ if (typeof uiHtml === 'string') {
54
+ return uiHtml;
55
+ }
56
+ }
57
+ return undefined;
58
+ }
59
+ /**
60
+ * Extract _meta object from a tool result wrapper.
61
+ */
62
+ function extractMeta(received) {
63
+ const wrapper = received;
64
+ const meta = wrapper?.raw?._meta || wrapper?._meta;
65
+ if (meta && typeof meta === 'object') {
66
+ return meta;
67
+ }
68
+ return undefined;
69
+ }
70
+ // ═══════════════════════════════════════════════════════════════════
71
+ // UI MATCHERS
72
+ // ═══════════════════════════════════════════════════════════════════
73
+ /**
74
+ * Check if tool result has rendered HTML in _meta['ui/html'].
75
+ * Fails if the HTML is the mdx-fallback (escaped raw content).
76
+ */
77
+ const toHaveRenderedHtml = function (received) {
78
+ const html = extractUiHtml(received);
79
+ const hasHtml = html !== undefined && html.length > 0;
80
+ const isFallback = hasHtml && html.includes('mdx-fallback');
81
+ const pass = hasHtml && !isFallback;
82
+ return {
83
+ pass,
84
+ message: () => {
85
+ if (isFallback) {
86
+ return 'Expected rendered HTML but got mdx-fallback (raw escaped content). MDX rendering may have failed.';
87
+ }
88
+ if (!hasHtml) {
89
+ return 'Expected _meta to have ui/html property with rendered HTML';
90
+ }
91
+ return 'Expected result not to have rendered HTML';
92
+ },
93
+ };
94
+ };
95
+ /**
96
+ * Check if HTML contains a specific HTML element tag.
97
+ * @param tag - The HTML tag name to look for (e.g., 'div', 'h1', 'span')
98
+ */
99
+ const toContainHtmlElement = function (received, tag) {
100
+ const html = extractUiHtml(received);
101
+ if (!html) {
102
+ return {
103
+ pass: false,
104
+ message: () => `Expected to find <${tag}> element, but no HTML content found`,
105
+ };
106
+ }
107
+ // Match opening tags: <tag> or <tag attributes>
108
+ // Escape regex metacharacters to prevent user input from breaking the regex
109
+ const regex = new RegExp(`<${escapeRegex(tag)}[\\s>]`, 'i');
110
+ const pass = regex.test(html);
111
+ return {
112
+ pass,
113
+ message: () => pass ? `Expected HTML not to contain <${tag}> element` : `Expected HTML to contain <${tag}> element`,
114
+ };
115
+ };
116
+ /**
117
+ * Check if a bound value from tool output appears in the rendered HTML.
118
+ * @param value - The value to look for (string or number)
119
+ */
120
+ const toContainBoundValue = function (received, value) {
121
+ const html = extractUiHtml(received);
122
+ if (!html) {
123
+ return {
124
+ pass: false,
125
+ message: () => `Expected HTML to contain bound value "${value}", but no HTML content found`,
126
+ };
127
+ }
128
+ const stringValue = String(value);
129
+ const pass = html.includes(stringValue);
130
+ return {
131
+ pass,
132
+ message: () => pass
133
+ ? `Expected HTML not to contain bound value "${stringValue}"`
134
+ : `Expected HTML to contain bound value "${stringValue}"`,
135
+ };
136
+ };
137
+ /**
138
+ * Check if HTML is XSS-safe (no script tags, event handlers, or javascript: URIs).
139
+ */
140
+ const toBeXssSafe = function (received) {
141
+ const html = extractUiHtml(received);
142
+ if (!html) {
143
+ // No HTML means nothing to exploit
144
+ return {
145
+ pass: true,
146
+ message: () => 'Expected HTML to be XSS unsafe (no HTML found)',
147
+ };
148
+ }
149
+ const hasScript = /<script[\s>]/i.test(html);
150
+ const hasOnHandler = /\son\w+\s*=/i.test(html);
151
+ const hasJavascriptUri = /javascript:/i.test(html);
152
+ const issues = [];
153
+ if (hasScript)
154
+ issues.push('<script> tag');
155
+ if (hasOnHandler)
156
+ issues.push('inline event handler (onclick, etc.)');
157
+ if (hasJavascriptUri)
158
+ issues.push('javascript: URI');
159
+ const pass = !hasScript && !hasOnHandler && !hasJavascriptUri;
160
+ return {
161
+ pass,
162
+ message: () => pass ? 'Expected HTML not to be XSS safe' : `Expected HTML to be XSS safe, but found: ${issues.join(', ')}`,
163
+ };
164
+ };
165
+ /**
166
+ * Check if tool result has widget metadata.
167
+ * Checks for ui/html (universal), openai/outputTemplate, or ui/mimeType.
168
+ */
169
+ const toHaveWidgetMetadata = function (received) {
170
+ const meta = extractMeta(received);
171
+ if (!meta) {
172
+ return {
173
+ pass: false,
174
+ message: () => 'Expected _meta to have widget metadata, but no _meta found',
175
+ };
176
+ }
177
+ // Check for any widget-related metadata fields
178
+ const hasUiHtml = Boolean(meta['ui/html']);
179
+ const hasOutputTemplate = Boolean(meta['openai/outputTemplate']);
180
+ const hasMimeType = Boolean(meta['ui/mimeType']);
181
+ const pass = hasUiHtml || hasOutputTemplate || hasMimeType;
182
+ return {
183
+ pass,
184
+ message: () => pass
185
+ ? 'Expected result not to have widget metadata'
186
+ : 'Expected _meta to have widget metadata (ui/html, openai/outputTemplate, or ui/mimeType)',
187
+ };
188
+ };
189
+ /**
190
+ * Check if HTML has CSS classes (for styling validation).
191
+ * @param className - The CSS class name to look for
192
+ */
193
+ const toHaveCssClass = function (received, className) {
194
+ const html = extractUiHtml(received);
195
+ if (!html) {
196
+ return {
197
+ pass: false,
198
+ message: () => `Expected HTML to have CSS class "${className}", but no HTML content found`,
199
+ };
200
+ }
201
+ // Match class="... className ..." or className="... className ..."
202
+ // Escape regex metacharacters to prevent user input from breaking the regex
203
+ const classRegex = new RegExp(`class(?:Name)?\\s*=\\s*["'][^"']*\\b${escapeRegex(className)}\\b[^"']*["']`, 'i');
204
+ const pass = classRegex.test(html);
205
+ return {
206
+ pass,
207
+ message: () => pass ? `Expected HTML not to have CSS class "${className}"` : `Expected HTML to have CSS class "${className}"`,
208
+ };
209
+ };
210
+ /**
211
+ * Check that HTML does NOT contain specific content (useful for fallback checks).
212
+ * @param content - The content that should NOT be in the HTML
213
+ */
214
+ const toNotContainRawContent = function (received, content) {
215
+ const html = extractUiHtml(received);
216
+ if (!html) {
217
+ return {
218
+ pass: true,
219
+ message: () => `Expected HTML to contain raw content "${content}", but no HTML found`,
220
+ };
221
+ }
222
+ const pass = !html.includes(content);
223
+ return {
224
+ pass,
225
+ message: () => pass
226
+ ? `Expected HTML to contain raw content "${content}"`
227
+ : `Expected HTML not to contain raw content "${content}" (may indicate rendering failure)`,
228
+ };
229
+ };
230
+ /**
231
+ * Check if HTML has proper structure (not just escaped text).
232
+ */
233
+ const toHaveProperHtmlStructure = function (received) {
234
+ const html = extractUiHtml(received);
235
+ if (!html) {
236
+ return {
237
+ pass: false,
238
+ message: () => 'Expected proper HTML structure, but no HTML content found',
239
+ };
240
+ }
241
+ // Check for escaped HTML entities that suggest content wasn't rendered
242
+ const hasEscapedTags = html.includes('&lt;') && html.includes('&gt;');
243
+ // Check that there's at least one HTML tag
244
+ const hasHtmlTags = /<[a-z]/i.test(html);
245
+ const pass = hasHtmlTags && !hasEscapedTags;
246
+ return {
247
+ pass,
248
+ message: () => {
249
+ if (hasEscapedTags) {
250
+ return 'Expected proper HTML structure, but found escaped HTML entities - content may not have been rendered';
251
+ }
252
+ if (!hasHtmlTags) {
253
+ return 'Expected proper HTML structure, but found no HTML tags';
254
+ }
255
+ return 'Expected result not to have proper HTML structure';
256
+ },
257
+ };
258
+ };
259
+ // ═══════════════════════════════════════════════════════════════════
260
+ // EXPORTS
261
+ // ═══════════════════════════════════════════════════════════════════
262
+ /**
263
+ * All UI matchers as an object for expect.extend()
264
+ */
265
+ exports.uiMatchers = {
266
+ toHaveRenderedHtml,
267
+ toContainHtmlElement,
268
+ toContainBoundValue,
269
+ toBeXssSafe,
270
+ toHaveWidgetMetadata,
271
+ toHaveCssClass,
272
+ toNotContainRawContent,
273
+ toHaveProperHtmlStructure,
274
+ };
275
+ //# sourceMappingURL=ui-matchers.js.map