@frontmcp/testing 0.5.1 → 0.6.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 (57) hide show
  1. package/package.json +3 -3
  2. package/src/auth/mock-api-server.d.ts +99 -0
  3. package/src/auth/mock-api-server.js +200 -0
  4. package/src/auth/mock-api-server.js.map +1 -0
  5. package/src/auth/mock-oauth-server.d.ts +85 -0
  6. package/src/auth/mock-oauth-server.js +253 -0
  7. package/src/auth/mock-oauth-server.js.map +1 -0
  8. package/src/client/mcp-test-client.builder.d.ts +43 -1
  9. package/src/client/mcp-test-client.builder.js +52 -0
  10. package/src/client/mcp-test-client.builder.js.map +1 -1
  11. package/src/client/mcp-test-client.js +22 -14
  12. package/src/client/mcp-test-client.js.map +1 -1
  13. package/src/client/mcp-test-client.types.d.ts +67 -6
  14. package/src/client/mcp-test-client.types.js +9 -0
  15. package/src/client/mcp-test-client.types.js.map +1 -1
  16. package/src/example-tools/index.d.ts +19 -0
  17. package/src/example-tools/index.js +40 -0
  18. package/src/example-tools/index.js.map +1 -0
  19. package/src/example-tools/tool-configs.d.ts +170 -0
  20. package/src/example-tools/tool-configs.js +222 -0
  21. package/src/example-tools/tool-configs.js.map +1 -0
  22. package/src/expect.d.ts +6 -5
  23. package/src/expect.js.map +1 -1
  24. package/src/fixtures/fixture-types.d.ts +19 -0
  25. package/src/fixtures/fixture-types.js.map +1 -1
  26. package/src/fixtures/test-fixture.d.ts +3 -1
  27. package/src/fixtures/test-fixture.js +35 -4
  28. package/src/fixtures/test-fixture.js.map +1 -1
  29. package/src/index.d.ts +7 -0
  30. package/src/index.js +40 -1
  31. package/src/index.js.map +1 -1
  32. package/src/matchers/matcher-types.js.map +1 -1
  33. package/src/matchers/mcp-matchers.d.ts +7 -0
  34. package/src/matchers/mcp-matchers.js +8 -4
  35. package/src/matchers/mcp-matchers.js.map +1 -1
  36. package/src/platform/index.d.ts +28 -0
  37. package/src/platform/index.js +47 -0
  38. package/src/platform/index.js.map +1 -0
  39. package/src/platform/platform-client-info.d.ts +97 -0
  40. package/src/platform/platform-client-info.js +155 -0
  41. package/src/platform/platform-client-info.js.map +1 -0
  42. package/src/platform/platform-types.d.ts +72 -0
  43. package/src/platform/platform-types.js +110 -0
  44. package/src/platform/platform-types.js.map +1 -0
  45. package/src/server/test-server.d.ts +4 -0
  46. package/src/server/test-server.js +58 -3
  47. package/src/server/test-server.js.map +1 -1
  48. package/src/transport/streamable-http.transport.js +6 -0
  49. package/src/transport/streamable-http.transport.js.map +1 -1
  50. package/src/transport/transport.interface.d.ts +3 -0
  51. package/src/transport/transport.interface.js.map +1 -1
  52. package/src/ui/ui-assertions.d.ts +59 -0
  53. package/src/ui/ui-assertions.js +152 -0
  54. package/src/ui/ui-assertions.js.map +1 -1
  55. package/src/ui/ui-matchers.d.ts +8 -0
  56. package/src/ui/ui-matchers.js +218 -0
  57. package/src/ui/ui-matchers.js.map +1 -1
@@ -24,6 +24,7 @@
24
24
  */
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.UIAssertions = void 0;
27
+ const platform_types_1 = require("../platform/platform-types");
27
28
  // Type-only reference: Metadata keys used below align with UIMetadata from @frontmcp/ui/adapters
28
29
  // This is an optional peer dependency, so we don't import it directly
29
30
  // ═══════════════════════════════════════════════════════════════════
@@ -211,5 +212,156 @@ exports.UIAssertions = {
211
212
  }
212
213
  return html;
213
214
  },
215
+ // ═══════════════════════════════════════════════════════════════════
216
+ // PLATFORM META ASSERTIONS
217
+ // ═══════════════════════════════════════════════════════════════════
218
+ /**
219
+ * Assert tool result has correct meta keys for OpenAI platform.
220
+ * Verifies openai/* keys are present and ui/*, frontmcp/* keys are absent.
221
+ * @param result - The tool result wrapper
222
+ * @throws Error if meta keys don't match OpenAI expectations
223
+ */
224
+ assertOpenAIMeta(result) {
225
+ exports.UIAssertions.assertPlatformMeta(result, 'openai');
226
+ },
227
+ /**
228
+ * Assert tool result has correct meta keys for ext-apps platform (SEP-1865).
229
+ * Verifies ui/* keys are present and openai/*, frontmcp/* keys are absent.
230
+ * @param result - The tool result wrapper
231
+ * @throws Error if meta keys don't match ext-apps expectations
232
+ */
233
+ assertExtAppsMeta(result) {
234
+ exports.UIAssertions.assertPlatformMeta(result, 'ext-apps');
235
+ },
236
+ /**
237
+ * Assert tool result has correct meta keys for FrontMCP platforms (Claude, Cursor, etc.).
238
+ * Verifies frontmcp/* + ui/* keys are present and openai/* keys are absent.
239
+ * @param result - The tool result wrapper
240
+ * @throws Error if meta keys don't match FrontMCP expectations
241
+ */
242
+ assertFrontmcpMeta(result) {
243
+ exports.UIAssertions.assertPlatformMeta(result, 'claude');
244
+ },
245
+ /**
246
+ * Assert tool result has correct meta keys for a specific platform.
247
+ * @param result - The tool result wrapper
248
+ * @param platform - The platform type to check for
249
+ * @throws Error if meta keys don't match platform expectations
250
+ */
251
+ assertPlatformMeta(result, platform) {
252
+ const meta = result.raw._meta;
253
+ if (!meta) {
254
+ throw new Error(`Expected tool result to have _meta with platform meta for "${platform}"`);
255
+ }
256
+ const expectedPrefixes = (0, platform_types_1.getToolCallMetaPrefixes)(platform);
257
+ const forbiddenPrefixes = (0, platform_types_1.getForbiddenMetaPrefixes)(platform);
258
+ const metaKeys = Object.keys(meta);
259
+ // Check for expected prefixes
260
+ const hasExpectedPrefix = metaKeys.some((key) => expectedPrefixes.some((prefix) => key.startsWith(prefix)));
261
+ if (!hasExpectedPrefix) {
262
+ throw new Error(`Expected _meta to have keys with prefixes [${expectedPrefixes.join(', ')}] for platform "${platform}", ` +
263
+ `but found: [${metaKeys.join(', ')}]`);
264
+ }
265
+ // Check for forbidden prefixes
266
+ const forbiddenKeys = metaKeys.filter((key) => forbiddenPrefixes.some((prefix) => key.startsWith(prefix)));
267
+ if (forbiddenKeys.length > 0) {
268
+ throw new Error(`Expected _meta NOT to have keys [${forbiddenKeys.join(', ')}] for platform "${platform}" ` +
269
+ `(forbidden prefixes: [${forbiddenPrefixes.join(', ')}])`);
270
+ }
271
+ },
272
+ /**
273
+ * Assert that no cross-namespace pollution exists in meta.
274
+ * @param result - The tool result wrapper
275
+ * @param expectedNamespace - The namespace that SHOULD be present
276
+ * @throws Error if other namespaces are found
277
+ */
278
+ assertNoMixedNamespaces(result, expectedNamespace) {
279
+ const meta = result.raw._meta;
280
+ if (!meta) {
281
+ throw new Error(`Expected tool result to have _meta with namespace "${expectedNamespace}"`);
282
+ }
283
+ const metaKeys = Object.keys(meta);
284
+ const wrongKeys = metaKeys.filter((key) => !key.startsWith(expectedNamespace));
285
+ if (wrongKeys.length > 0) {
286
+ throw new Error(`Expected _meta to ONLY have keys with namespace "${expectedNamespace}", ` +
287
+ `but found: [${wrongKeys.join(', ')}]`);
288
+ }
289
+ },
290
+ /**
291
+ * Assert that _meta has the correct MIME type for a platform.
292
+ * @param result - The tool result wrapper
293
+ * @param platform - The platform type to check for
294
+ * @throws Error if MIME type doesn't match platform expectations
295
+ */
296
+ assertPlatformMimeType(result, platform) {
297
+ const meta = result.raw._meta;
298
+ const expectedMimeType = (0, platform_types_1.getPlatformMimeType)(platform);
299
+ if (!meta) {
300
+ throw new Error(`Expected tool result to have _meta with MIME type for platform "${platform}"`);
301
+ }
302
+ // Determine which key to check based on platform
303
+ let mimeTypeKey;
304
+ switch (platform) {
305
+ case 'openai':
306
+ mimeTypeKey = 'openai/mimeType';
307
+ break;
308
+ case 'ext-apps':
309
+ mimeTypeKey = 'ui/mimeType';
310
+ break;
311
+ default:
312
+ mimeTypeKey = 'frontmcp/mimeType';
313
+ }
314
+ const actualMimeType = meta[mimeTypeKey];
315
+ if (actualMimeType !== expectedMimeType) {
316
+ throw new Error(`Expected _meta["${mimeTypeKey}"] to be "${expectedMimeType}" for platform "${platform}", ` +
317
+ `but got "${actualMimeType}"`);
318
+ }
319
+ },
320
+ /**
321
+ * Assert that _meta has HTML in the correct platform-specific key.
322
+ * @param result - The tool result wrapper
323
+ * @param platform - The platform type to check for
324
+ * @returns The HTML string
325
+ * @throws Error if HTML is missing or in wrong key
326
+ */
327
+ assertPlatformHtml(result, platform) {
328
+ const meta = result.raw._meta;
329
+ if (!meta) {
330
+ throw new Error(`Expected tool result to have _meta with platform HTML for "${platform}"`);
331
+ }
332
+ // Determine which key to check based on platform
333
+ let htmlKey;
334
+ switch (platform) {
335
+ case 'openai':
336
+ htmlKey = 'openai/html';
337
+ break;
338
+ case 'ext-apps':
339
+ htmlKey = 'ui/html';
340
+ break;
341
+ default:
342
+ htmlKey = 'frontmcp/html';
343
+ }
344
+ const html = meta[htmlKey];
345
+ if (typeof html !== 'string' || html.length === 0) {
346
+ throw new Error(`Expected _meta["${htmlKey}"] to contain HTML for platform "${platform}", ` +
347
+ `but ${html === undefined ? 'key not found' : `got ${typeof html}`}`);
348
+ }
349
+ return html;
350
+ },
351
+ /**
352
+ * Comprehensive platform meta validation.
353
+ * @param result - The tool result wrapper
354
+ * @param platform - The platform type to validate for
355
+ * @returns The platform-specific HTML string
356
+ * @throws Error if any platform-specific validation fails
357
+ */
358
+ assertValidPlatformMeta(result, platform) {
359
+ // 1. Check correct namespace keys
360
+ exports.UIAssertions.assertPlatformMeta(result, platform);
361
+ // 2. Check MIME type
362
+ exports.UIAssertions.assertPlatformMimeType(result, platform);
363
+ // 3. Get and return HTML
364
+ return exports.UIAssertions.assertPlatformHtml(result, platform);
365
+ },
214
366
  };
215
367
  //# sourceMappingURL=ui-assertions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ui-assertions.js","sourceRoot":"","sources":["../../../src/ui/ui-assertions.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;;;AAIH,iGAAiG;AACjG,sEAAsE;AAEtE,sEAAsE;AACtE,mBAAmB;AACnB,sEAAsE;AAEtE;;;GAGG;AACH,SAAS,WAAW,CAAC,GAAW;IAC9B,OAAO,GAAG,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACpD,CAAC;AAED,sEAAsE;AACtE,gBAAgB;AAChB,sEAAsE;AAEtE;;;GAGG;AACU,QAAA,YAAY,GAAG;IAC1B;;;;;OAKG;IACH,gBAAgB,CAAC,MAAyB;QACxC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,KAA4C,CAAC;QAErE,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;QAChF,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;QAE7B,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;QACtF,CAAC;QAED,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,4CAA4C,OAAO,IAAI,EAAE,CAAC,CAAC;QAC7E,CAAC;QAED,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CACb,0EAA0E;gBACxE,uEAAuE,CAC1E,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;OAMG;IACH,iBAAiB,CAAC,IAAY,EAAE,MAA+B,EAAE,IAAc;QAC7E,MAAM,WAAW,GAAa,EAAE,CAAC;QAEjC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YAC1B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBAC1C,SAAS,CAAC,6BAA6B;YACzC,CAAC;YAED,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;YAClC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;gBAChC,WAAW,CAAC,IAAI,CAAC,GAAG,GAAG,KAAK,WAAW,GAAG,CAAC,CAAC;YAC9C,CAAC;QACH,CAAC;QAED,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CACb,8CAA8C,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAC3G,CAAC;QACJ,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,aAAa,CAAC,IAAY;QACxB,MAAM,eAAe,GAAa,EAAE,CAAC;QAErC,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/B,eAAe,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;QAChD,CAAC;QAED,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,eAAe,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAC;QACjF,CAAC;QAED,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,eAAe,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QACnD,CAAC;QAED,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,wCAAwC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,yBAAyB,CAAC,IAAY;QACpC,uEAAuE;QACvE,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CACb,sFAAsF;gBACpF,uDAAuD,CAC1D,CAAC;QACJ,CAAC;QAED,2CAA2C;QAC3C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAC;QACjG,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,qBAAqB,CAAC,IAAY,EAAE,GAAW;QAC7C,4EAA4E;QAC5E,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAC5D,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,6BAA6B,GAAG,WAAW,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,iBAAiB,CAAC,IAAY,EAAE,SAAiB;QAC/C,4EAA4E;QAC5E,MAAM,UAAU,GAAG,IAAI,MAAM,CAAC,uCAAuC,WAAW,CAAC,SAAS,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;QACjH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,oCAAoC,SAAS,GAAG,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACH,oBAAoB,CAAC,IAAY,EAAE,OAAe;QAChD,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CACb,8BAA8B,OAAO,iDAAiD;gBACpF,uEAAuE,CAC1E,CAAC;QACJ,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,oBAAoB,CAAC,MAAyB;QAC5C,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,KAA4C,CAAC;QAErE,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC7E,CAAC;QAED,2FAA2F;QAC3F,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;QAC3C,MAAM,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC;QACjE,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC;QAEjD,IAAI,CAAC,SAAS,IAAI,CAAC,iBAAiB,IAAI,CAAC,WAAW,EAAE,CAAC;YACrD,MAAM,IAAI,KAAK,CAAC,yFAAyF,CAAC,CAAC;QAC7G,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACH,aAAa,CAAC,MAAyB,EAAE,SAAoB;QAC3D,kCAAkC;QAClC,MAAM,IAAI,GAAG,oBAAY,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAEnD,0BAA0B;QAC1B,oBAAY,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC;QAE7C,sBAAsB;QACtB,oBAAY,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QAEjC,yCAAyC;QACzC,IAAI,SAAS,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC,CAAC;gBACjD,oBAAY,CAAC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;YAC1D,CAAC;YAAC,MAAM,CAAC;gBACP,oDAAoD;YACtD,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAC","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"]}
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';\nimport type { TestPlatformType } from '../platform/platform-types';\nimport { getForbiddenMetaPrefixes, getToolCallMetaPrefixes, getPlatformMimeType } from '../platform/platform-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 // ═══════════════════════════════════════════════════════════════════\n // PLATFORM META ASSERTIONS\n // ═══════════════════════════════════════════════════════════════════\n\n /**\n * Assert tool result has correct meta keys for OpenAI platform.\n * Verifies openai/* keys are present and ui/*, frontmcp/* keys are absent.\n * @param result - The tool result wrapper\n * @throws Error if meta keys don't match OpenAI expectations\n */\n assertOpenAIMeta(result: ToolResultWrapper): void {\n UIAssertions.assertPlatformMeta(result, 'openai');\n },\n\n /**\n * Assert tool result has correct meta keys for ext-apps platform (SEP-1865).\n * Verifies ui/* keys are present and openai/*, frontmcp/* keys are absent.\n * @param result - The tool result wrapper\n * @throws Error if meta keys don't match ext-apps expectations\n */\n assertExtAppsMeta(result: ToolResultWrapper): void {\n UIAssertions.assertPlatformMeta(result, 'ext-apps');\n },\n\n /**\n * Assert tool result has correct meta keys for FrontMCP platforms (Claude, Cursor, etc.).\n * Verifies frontmcp/* + ui/* keys are present and openai/* keys are absent.\n * @param result - The tool result wrapper\n * @throws Error if meta keys don't match FrontMCP expectations\n */\n assertFrontmcpMeta(result: ToolResultWrapper): void {\n UIAssertions.assertPlatformMeta(result, 'claude');\n },\n\n /**\n * Assert tool result has correct meta keys for a specific platform.\n * @param result - The tool result wrapper\n * @param platform - The platform type to check for\n * @throws Error if meta keys don't match platform expectations\n */\n assertPlatformMeta(result: ToolResultWrapper, platform: TestPlatformType): 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 platform meta for \"${platform}\"`);\n }\n\n const expectedPrefixes = getToolCallMetaPrefixes(platform);\n const forbiddenPrefixes = getForbiddenMetaPrefixes(platform);\n const metaKeys = Object.keys(meta);\n\n // Check for expected prefixes\n const hasExpectedPrefix = metaKeys.some((key) => expectedPrefixes.some((prefix) => key.startsWith(prefix)));\n\n if (!hasExpectedPrefix) {\n throw new Error(\n `Expected _meta to have keys with prefixes [${expectedPrefixes.join(', ')}] for platform \"${platform}\", ` +\n `but found: [${metaKeys.join(', ')}]`,\n );\n }\n\n // Check for forbidden prefixes\n const forbiddenKeys = metaKeys.filter((key) => forbiddenPrefixes.some((prefix) => key.startsWith(prefix)));\n\n if (forbiddenKeys.length > 0) {\n throw new Error(\n `Expected _meta NOT to have keys [${forbiddenKeys.join(', ')}] for platform \"${platform}\" ` +\n `(forbidden prefixes: [${forbiddenPrefixes.join(', ')}])`,\n );\n }\n },\n\n /**\n * Assert that no cross-namespace pollution exists in meta.\n * @param result - The tool result wrapper\n * @param expectedNamespace - The namespace that SHOULD be present\n * @throws Error if other namespaces are found\n */\n assertNoMixedNamespaces(result: ToolResultWrapper, expectedNamespace: string): 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 namespace \"${expectedNamespace}\"`);\n }\n\n const metaKeys = Object.keys(meta);\n const wrongKeys = metaKeys.filter((key) => !key.startsWith(expectedNamespace));\n\n if (wrongKeys.length > 0) {\n throw new Error(\n `Expected _meta to ONLY have keys with namespace \"${expectedNamespace}\", ` +\n `but found: [${wrongKeys.join(', ')}]`,\n );\n }\n },\n\n /**\n * Assert that _meta has the correct MIME type for a platform.\n * @param result - The tool result wrapper\n * @param platform - The platform type to check for\n * @throws Error if MIME type doesn't match platform expectations\n */\n assertPlatformMimeType(result: ToolResultWrapper, platform: TestPlatformType): void {\n const meta = result.raw._meta as Record<string, unknown> | undefined;\n const expectedMimeType = getPlatformMimeType(platform);\n\n if (!meta) {\n throw new Error(`Expected tool result to have _meta with MIME type for platform \"${platform}\"`);\n }\n\n // Determine which key to check based on platform\n let mimeTypeKey: string;\n switch (platform) {\n case 'openai':\n mimeTypeKey = 'openai/mimeType';\n break;\n case 'ext-apps':\n mimeTypeKey = 'ui/mimeType';\n break;\n default:\n mimeTypeKey = 'frontmcp/mimeType';\n }\n\n const actualMimeType = meta[mimeTypeKey];\n\n if (actualMimeType !== expectedMimeType) {\n throw new Error(\n `Expected _meta[\"${mimeTypeKey}\"] to be \"${expectedMimeType}\" for platform \"${platform}\", ` +\n `but got \"${actualMimeType}\"`,\n );\n }\n },\n\n /**\n * Assert that _meta has HTML in the correct platform-specific key.\n * @param result - The tool result wrapper\n * @param platform - The platform type to check for\n * @returns The HTML string\n * @throws Error if HTML is missing or in wrong key\n */\n assertPlatformHtml(result: ToolResultWrapper, platform: TestPlatformType): 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 with platform HTML for \"${platform}\"`);\n }\n\n // Determine which key to check based on platform\n let htmlKey: string;\n switch (platform) {\n case 'openai':\n htmlKey = 'openai/html';\n break;\n case 'ext-apps':\n htmlKey = 'ui/html';\n break;\n default:\n htmlKey = 'frontmcp/html';\n }\n\n const html = meta[htmlKey];\n\n if (typeof html !== 'string' || html.length === 0) {\n throw new Error(\n `Expected _meta[\"${htmlKey}\"] to contain HTML for platform \"${platform}\", ` +\n `but ${html === undefined ? 'key not found' : `got ${typeof html}`}`,\n );\n }\n\n return html;\n },\n\n /**\n * Comprehensive platform meta validation.\n * @param result - The tool result wrapper\n * @param platform - The platform type to validate for\n * @returns The platform-specific HTML string\n * @throws Error if any platform-specific validation fails\n */\n assertValidPlatformMeta(result: ToolResultWrapper, platform: TestPlatformType): string {\n // 1. Check correct namespace keys\n UIAssertions.assertPlatformMeta(result, platform);\n\n // 2. Check MIME type\n UIAssertions.assertPlatformMimeType(result, platform);\n\n // 3. Get and return HTML\n return UIAssertions.assertPlatformHtml(result, platform);\n },\n};\n"]}
@@ -24,6 +24,7 @@
24
24
  * ```
25
25
  */
26
26
  import type { MatcherFunction } from 'expect';
27
+ import type { TestPlatformType } from '../platform/platform-types';
27
28
  /**
28
29
  * All UI matchers as an object for expect.extend()
29
30
  */
@@ -36,4 +37,11 @@ export declare const uiMatchers: {
36
37
  toHaveCssClass: MatcherFunction<[className: string]>;
37
38
  toNotContainRawContent: MatcherFunction<[content: string]>;
38
39
  toHaveProperHtmlStructure: MatcherFunction<[]>;
40
+ toHavePlatformMeta: MatcherFunction<[platform: TestPlatformType]>;
41
+ toHaveMetaKey: MatcherFunction<[key: string]>;
42
+ toHaveMetaValue: MatcherFunction<[key: string, value: unknown]>;
43
+ toNotHaveMetaKey: MatcherFunction<[key: string]>;
44
+ toHaveOnlyNamespacedMeta: MatcherFunction<[namespace: string]>;
45
+ toHavePlatformMimeType: MatcherFunction<[platform: TestPlatformType]>;
46
+ toHavePlatformHtml: MatcherFunction<[platform: TestPlatformType]>;
39
47
  };
@@ -26,6 +26,7 @@
26
26
  */
27
27
  Object.defineProperty(exports, "__esModule", { value: true });
28
28
  exports.uiMatchers = void 0;
29
+ const platform_types_1 = require("../platform/platform-types");
29
30
  // Type-only reference: Metadata keys used below align with UIMetadata from @frontmcp/ui/adapters
30
31
  // This is an optional peer dependency, so we don't import it directly
31
32
  // ═══════════════════════════════════════════════════════════════════
@@ -257,12 +258,221 @@ const toHaveProperHtmlStructure = function (received) {
257
258
  };
258
259
  };
259
260
  // ═══════════════════════════════════════════════════════════════════
261
+ // PLATFORM META MATCHERS
262
+ // ═══════════════════════════════════════════════════════════════════
263
+ /**
264
+ * Check if tool result has correct meta keys for a specific platform.
265
+ * @param platform - The platform type to check for
266
+ *
267
+ * This matcher verifies:
268
+ * - OpenAI: Has openai/* keys, no ui/* or frontmcp/* keys
269
+ * - ext-apps: Has ui/* keys, no openai/* or frontmcp/* keys
270
+ * - Others: Has frontmcp/* + ui/* keys, no openai/* keys
271
+ */
272
+ const toHavePlatformMeta = function (received, platform) {
273
+ const meta = extractMeta(received);
274
+ if (!meta) {
275
+ return {
276
+ pass: false,
277
+ message: () => `Expected _meta to have platform meta for "${platform}", but no _meta found`,
278
+ };
279
+ }
280
+ const expectedPrefixes = (0, platform_types_1.getToolCallMetaPrefixes)(platform);
281
+ const forbiddenPrefixes = (0, platform_types_1.getForbiddenMetaPrefixes)(platform);
282
+ const metaKeys = Object.keys(meta);
283
+ // Check for expected prefixes
284
+ const hasExpectedPrefix = metaKeys.some((key) => expectedPrefixes.some((prefix) => key.startsWith(prefix)));
285
+ // Check for forbidden prefixes
286
+ const forbiddenKeys = metaKeys.filter((key) => forbiddenPrefixes.some((prefix) => key.startsWith(prefix)));
287
+ const pass = hasExpectedPrefix && forbiddenKeys.length === 0;
288
+ return {
289
+ pass,
290
+ message: () => {
291
+ if (!hasExpectedPrefix) {
292
+ return `Expected _meta to have keys with prefixes [${expectedPrefixes.join(', ')}] for platform "${platform}", but found: [${metaKeys.join(', ')}]`;
293
+ }
294
+ if (forbiddenKeys.length > 0) {
295
+ return `Expected _meta NOT to have keys [${forbiddenKeys.join(', ')}] for platform "${platform}" (forbidden prefixes: [${forbiddenPrefixes.join(', ')}])`;
296
+ }
297
+ return `Expected result not to have platform meta for "${platform}"`;
298
+ },
299
+ };
300
+ };
301
+ /**
302
+ * Check if _meta has a specific key.
303
+ * @param key - The meta key to look for (e.g., 'ui/html', 'openai/mimeType')
304
+ */
305
+ const toHaveMetaKey = function (received, key) {
306
+ const meta = extractMeta(received);
307
+ if (!meta) {
308
+ return {
309
+ pass: false,
310
+ message: () => `Expected _meta to have key "${key}", but no _meta found`,
311
+ };
312
+ }
313
+ const pass = key in meta;
314
+ return {
315
+ pass,
316
+ message: () => (pass ? `Expected _meta not to have key "${key}"` : `Expected _meta to have key "${key}"`),
317
+ };
318
+ };
319
+ /**
320
+ * Check if _meta has a specific key with a specific value.
321
+ * @param key - The meta key to look for
322
+ * @param value - The expected value
323
+ */
324
+ const toHaveMetaValue = function (received, key, value) {
325
+ const meta = extractMeta(received);
326
+ if (!meta) {
327
+ return {
328
+ pass: false,
329
+ message: () => `Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but no _meta found`,
330
+ };
331
+ }
332
+ const actualValue = meta[key];
333
+ const pass = JSON.stringify(actualValue) === JSON.stringify(value);
334
+ return {
335
+ pass,
336
+ message: () => pass
337
+ ? `Expected _meta["${key}"] not to equal ${JSON.stringify(value)}`
338
+ : `Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but got ${JSON.stringify(actualValue)}`,
339
+ };
340
+ };
341
+ /**
342
+ * Check if _meta does NOT have a specific key.
343
+ * @param key - The meta key that should be absent
344
+ */
345
+ const toNotHaveMetaKey = function (received, key) {
346
+ const meta = extractMeta(received);
347
+ if (!meta) {
348
+ // No meta means key is definitely absent
349
+ return {
350
+ pass: true,
351
+ message: () => `Expected _meta to have key "${key}", but no _meta found`,
352
+ };
353
+ }
354
+ const pass = !(key in meta);
355
+ return {
356
+ pass,
357
+ message: () => (pass ? `Expected _meta to have key "${key}"` : `Expected _meta not to have key "${key}"`),
358
+ };
359
+ };
360
+ /**
361
+ * Check if _meta ONLY has keys with a specific namespace prefix.
362
+ * @param namespace - The namespace prefix (e.g., 'openai/', 'ui/', 'frontmcp/')
363
+ *
364
+ * Note: For platforms like Claude that use both frontmcp/* and ui/*,
365
+ * use toHavePlatformMeta() instead.
366
+ */
367
+ const toHaveOnlyNamespacedMeta = function (received, namespace) {
368
+ const meta = extractMeta(received);
369
+ if (!meta) {
370
+ return {
371
+ pass: false,
372
+ message: () => `Expected _meta to have keys with namespace "${namespace}", but no _meta found`,
373
+ };
374
+ }
375
+ const metaKeys = Object.keys(meta);
376
+ const wrongKeys = metaKeys.filter((key) => !key.startsWith(namespace));
377
+ const pass = wrongKeys.length === 0 && metaKeys.length > 0;
378
+ return {
379
+ pass,
380
+ message: () => {
381
+ if (metaKeys.length === 0) {
382
+ return `Expected _meta to have keys with namespace "${namespace}", but _meta is empty`;
383
+ }
384
+ if (wrongKeys.length > 0) {
385
+ return `Expected _meta to ONLY have keys with namespace "${namespace}", but found: [${wrongKeys.join(', ')}]`;
386
+ }
387
+ return `Expected _meta not to have only keys with namespace "${namespace}"`;
388
+ },
389
+ };
390
+ };
391
+ /**
392
+ * Check if _meta has the correct MIME type for a platform.
393
+ * @param platform - The platform type to check for
394
+ *
395
+ * MIME types:
396
+ * - OpenAI: text/html+skybridge
397
+ * - Others: text/html+mcp
398
+ */
399
+ const toHavePlatformMimeType = function (received, platform) {
400
+ const meta = extractMeta(received);
401
+ const expectedMimeType = (0, platform_types_1.getPlatformMimeType)(platform);
402
+ if (!meta) {
403
+ return {
404
+ pass: false,
405
+ message: () => `Expected _meta to have MIME type "${expectedMimeType}" for platform "${platform}", but no _meta found`,
406
+ };
407
+ }
408
+ // Determine which key to check based on platform
409
+ let mimeTypeKey;
410
+ switch (platform) {
411
+ case 'openai':
412
+ mimeTypeKey = 'openai/mimeType';
413
+ break;
414
+ case 'ext-apps':
415
+ mimeTypeKey = 'ui/mimeType';
416
+ break;
417
+ default:
418
+ // For other platforms, check frontmcp/mimeType (primary) or ui/mimeType (compatibility)
419
+ mimeTypeKey = 'frontmcp/mimeType';
420
+ }
421
+ const actualMimeType = meta[mimeTypeKey];
422
+ const pass = actualMimeType === expectedMimeType;
423
+ return {
424
+ pass,
425
+ message: () => pass
426
+ ? `Expected _meta["${mimeTypeKey}"] not to be "${expectedMimeType}"`
427
+ : `Expected _meta["${mimeTypeKey}"] to be "${expectedMimeType}" for platform "${platform}", but got "${actualMimeType}"`,
428
+ };
429
+ };
430
+ /**
431
+ * Check if _meta has HTML content in the correct platform-specific key.
432
+ * @param platform - The platform type to check for
433
+ *
434
+ * HTML keys:
435
+ * - OpenAI: openai/html
436
+ * - ext-apps: ui/html
437
+ * - Others: frontmcp/html (or ui/html for compatibility)
438
+ */
439
+ const toHavePlatformHtml = function (received, platform) {
440
+ const meta = extractMeta(received);
441
+ if (!meta) {
442
+ return {
443
+ pass: false,
444
+ message: () => `Expected _meta to have platform HTML for "${platform}", but no _meta found`,
445
+ };
446
+ }
447
+ // Determine which key to check based on platform
448
+ let htmlKey;
449
+ switch (platform) {
450
+ case 'openai':
451
+ htmlKey = 'openai/html';
452
+ break;
453
+ case 'ext-apps':
454
+ htmlKey = 'ui/html';
455
+ break;
456
+ default:
457
+ htmlKey = 'frontmcp/html';
458
+ }
459
+ const html = meta[htmlKey];
460
+ const pass = typeof html === 'string' && html.length > 0;
461
+ return {
462
+ pass,
463
+ message: () => pass
464
+ ? `Expected _meta not to have platform HTML in "${htmlKey}"`
465
+ : `Expected _meta["${htmlKey}"] to contain HTML for platform "${platform}", but ${html === undefined ? 'key not found' : `got ${typeof html}`}`,
466
+ };
467
+ };
468
+ // ═══════════════════════════════════════════════════════════════════
260
469
  // EXPORTS
261
470
  // ═══════════════════════════════════════════════════════════════════
262
471
  /**
263
472
  * All UI matchers as an object for expect.extend()
264
473
  */
265
474
  exports.uiMatchers = {
475
+ // Existing HTML matchers
266
476
  toHaveRenderedHtml,
267
477
  toContainHtmlElement,
268
478
  toContainBoundValue,
@@ -271,5 +481,13 @@ exports.uiMatchers = {
271
481
  toHaveCssClass,
272
482
  toNotContainRawContent,
273
483
  toHaveProperHtmlStructure,
484
+ // Platform meta matchers
485
+ toHavePlatformMeta,
486
+ toHaveMetaKey,
487
+ toHaveMetaValue,
488
+ toNotHaveMetaKey,
489
+ toHaveOnlyNamespacedMeta,
490
+ toHavePlatformMimeType,
491
+ toHavePlatformHtml,
274
492
  };
275
493
  //# sourceMappingURL=ui-matchers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ui-matchers.js","sourceRoot":"","sources":["../../../src/ui/ui-matchers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file ui-matchers.ts\n * @description UI-specific Jest matchers for validating tool UI responses\n *\n * The metadata keys used in these matchers 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 { test, expect } from '@frontmcp/testing';\n *\n * test('tool has rendered UI', async ({ mcp }) => {\n * const result = await mcp.tools.call('my-tool', {});\n * expect(result).toHaveRenderedHtml();\n * expect(result).toBeXssSafe();\n * expect(result).toContainBoundValue('expected-value');\n * });\n * ```\n */\n\nimport type { MatcherFunction } from 'expect';\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 * Extract UI HTML from a tool result wrapper or raw result.\n */\nfunction extractUiHtml(received: unknown): string | undefined {\n if (typeof received === 'string') {\n return received;\n }\n\n // ToolResultWrapper has raw._meta\n const wrapper = received as ToolResultWrapper & { _meta?: Record<string, unknown> };\n const meta = wrapper?.raw?._meta || wrapper?._meta;\n\n if (meta && typeof meta === 'object') {\n const uiHtml = (meta as Record<string, unknown>)['ui/html'];\n if (typeof uiHtml === 'string') {\n return uiHtml;\n }\n }\n\n return undefined;\n}\n\n/**\n * Extract _meta object from a tool result wrapper.\n */\nfunction extractMeta(received: unknown): Record<string, unknown> | undefined {\n const wrapper = received as ToolResultWrapper & { _meta?: Record<string, unknown> };\n const meta = wrapper?.raw?._meta || wrapper?._meta;\n\n if (meta && typeof meta === 'object') {\n return meta as Record<string, unknown>;\n }\n\n return undefined;\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// UI MATCHERS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Check if tool result has rendered HTML in _meta['ui/html'].\n * Fails if the HTML is the mdx-fallback (escaped raw content).\n */\nconst toHaveRenderedHtml: MatcherFunction<[]> = function (received) {\n const html = extractUiHtml(received);\n const hasHtml = html !== undefined && html.length > 0;\n const isFallback = hasHtml && html.includes('mdx-fallback');\n\n const pass = hasHtml && !isFallback;\n\n return {\n pass,\n message: () => {\n if (isFallback) {\n return 'Expected rendered HTML but got mdx-fallback (raw escaped content). MDX rendering may have failed.';\n }\n if (!hasHtml) {\n return 'Expected _meta to have ui/html property with rendered HTML';\n }\n return 'Expected result not to have rendered HTML';\n },\n };\n};\n\n/**\n * Check if HTML contains a specific HTML element tag.\n * @param tag - The HTML tag name to look for (e.g., 'div', 'h1', 'span')\n */\nconst toContainHtmlElement: MatcherFunction<[tag: string]> = function (received, tag) {\n const html = extractUiHtml(received);\n\n if (!html) {\n return {\n pass: false,\n message: () => `Expected to find <${tag}> element, but no HTML content found`,\n };\n }\n\n // Match opening tags: <tag> or <tag attributes>\n // Escape regex metacharacters to prevent user input from breaking the regex\n const regex = new RegExp(`<${escapeRegex(tag)}[\\\\s>]`, 'i');\n const pass = regex.test(html);\n\n return {\n pass,\n message: () =>\n pass ? `Expected HTML not to contain <${tag}> element` : `Expected HTML to contain <${tag}> element`,\n };\n};\n\n/**\n * Check if a bound value from tool output appears in the rendered HTML.\n * @param value - The value to look for (string or number)\n */\nconst toContainBoundValue: MatcherFunction<[value: string | number]> = function (received, value) {\n const html = extractUiHtml(received);\n\n if (!html) {\n return {\n pass: false,\n message: () => `Expected HTML to contain bound value \"${value}\", but no HTML content found`,\n };\n }\n\n const stringValue = String(value);\n const pass = html.includes(stringValue);\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected HTML not to contain bound value \"${stringValue}\"`\n : `Expected HTML to contain bound value \"${stringValue}\"`,\n };\n};\n\n/**\n * Check if HTML is XSS-safe (no script tags, event handlers, or javascript: URIs).\n */\nconst toBeXssSafe: MatcherFunction<[]> = function (received) {\n const html = extractUiHtml(received);\n\n if (!html) {\n // No HTML means nothing to exploit\n return {\n pass: true,\n message: () => 'Expected HTML to be XSS unsafe (no HTML found)',\n };\n }\n\n const hasScript = /<script[\\s>]/i.test(html);\n const hasOnHandler = /\\son\\w+\\s*=/i.test(html);\n const hasJavascriptUri = /javascript:/i.test(html);\n\n const issues: string[] = [];\n if (hasScript) issues.push('<script> tag');\n if (hasOnHandler) issues.push('inline event handler (onclick, etc.)');\n if (hasJavascriptUri) issues.push('javascript: URI');\n\n const pass = !hasScript && !hasOnHandler && !hasJavascriptUri;\n\n return {\n pass,\n message: () =>\n pass ? 'Expected HTML not to be XSS safe' : `Expected HTML to be XSS safe, but found: ${issues.join(', ')}`,\n };\n};\n\n/**\n * Check if tool result has widget metadata.\n * Checks for ui/html (universal), openai/outputTemplate, or ui/mimeType.\n */\nconst toHaveWidgetMetadata: MatcherFunction<[]> = function (received) {\n const meta = extractMeta(received);\n\n if (!meta) {\n return {\n pass: false,\n message: () => 'Expected _meta to have widget metadata, but no _meta found',\n };\n }\n\n // Check for any widget-related metadata fields\n const hasUiHtml = Boolean(meta['ui/html']);\n const hasOutputTemplate = Boolean(meta['openai/outputTemplate']);\n const hasMimeType = Boolean(meta['ui/mimeType']);\n\n const pass = hasUiHtml || hasOutputTemplate || hasMimeType;\n\n return {\n pass,\n message: () =>\n pass\n ? 'Expected result not to have widget metadata'\n : 'Expected _meta to have widget metadata (ui/html, openai/outputTemplate, or ui/mimeType)',\n };\n};\n\n/**\n * Check if HTML has CSS classes (for styling validation).\n * @param className - The CSS class name to look for\n */\nconst toHaveCssClass: MatcherFunction<[className: string]> = function (received, className) {\n const html = extractUiHtml(received);\n\n if (!html) {\n return {\n pass: false,\n message: () => `Expected HTML to have CSS class \"${className}\", but no HTML content found`,\n };\n }\n\n // Match class=\"... className ...\" or className=\"... className ...\"\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 const pass = classRegex.test(html);\n\n return {\n pass,\n message: () =>\n pass ? `Expected HTML not to have CSS class \"${className}\"` : `Expected HTML to have CSS class \"${className}\"`,\n };\n};\n\n/**\n * Check that HTML does NOT contain specific content (useful for fallback checks).\n * @param content - The content that should NOT be in the HTML\n */\nconst toNotContainRawContent: MatcherFunction<[content: string]> = function (received, content) {\n const html = extractUiHtml(received);\n\n if (!html) {\n return {\n pass: true,\n message: () => `Expected HTML to contain raw content \"${content}\", but no HTML found`,\n };\n }\n\n const pass = !html.includes(content);\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected HTML to contain raw content \"${content}\"`\n : `Expected HTML not to contain raw content \"${content}\" (may indicate rendering failure)`,\n };\n};\n\n/**\n * Check if HTML has proper structure (not just escaped text).\n */\nconst toHaveProperHtmlStructure: MatcherFunction<[]> = function (received) {\n const html = extractUiHtml(received);\n\n if (!html) {\n return {\n pass: false,\n message: () => 'Expected proper HTML structure, but no HTML content found',\n };\n }\n\n // Check for escaped HTML entities that suggest content wasn't rendered\n const hasEscapedTags = html.includes('&lt;') && html.includes('&gt;');\n\n // Check that there's at least one HTML tag\n const hasHtmlTags = /<[a-z]/i.test(html);\n\n const pass = hasHtmlTags && !hasEscapedTags;\n\n return {\n pass,\n message: () => {\n if (hasEscapedTags) {\n return 'Expected proper HTML structure, but found escaped HTML entities - content may not have been rendered';\n }\n if (!hasHtmlTags) {\n return 'Expected proper HTML structure, but found no HTML tags';\n }\n return 'Expected result not to have proper HTML structure';\n },\n };\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// EXPORTS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * All UI matchers as an object for expect.extend()\n */\nexport const uiMatchers = {\n toHaveRenderedHtml,\n toContainHtmlElement,\n toContainBoundValue,\n toBeXssSafe,\n toHaveWidgetMetadata,\n toHaveCssClass,\n toNotContainRawContent,\n toHaveProperHtmlStructure,\n};\n"]}
1
+ {"version":3,"file":"ui-matchers.js","sourceRoot":"","sources":["../../../src/ui/ui-matchers.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;;AAKH,+DAAoH;AAEpH,iGAAiG;AACjG,sEAAsE;AAEtE,sEAAsE;AACtE,mBAAmB;AACnB,sEAAsE;AAEtE;;;GAGG;AACH,SAAS,WAAW,CAAC,GAAW;IAC9B,OAAO,GAAG,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACpD,CAAC;AAED;;GAEG;AACH,SAAS,aAAa,CAAC,QAAiB;IACtC,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,kCAAkC;IAClC,MAAM,OAAO,GAAG,QAAmE,CAAC;IACpF,MAAM,IAAI,GAAG,OAAO,EAAE,GAAG,EAAE,KAAK,IAAI,OAAO,EAAE,KAAK,CAAC;IAEnD,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrC,MAAM,MAAM,GAAI,IAAgC,CAAC,SAAS,CAAC,CAAC;QAC5D,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC/B,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;GAEG;AACH,SAAS,WAAW,CAAC,QAAiB;IACpC,MAAM,OAAO,GAAG,QAAmE,CAAC;IACpF,MAAM,IAAI,GAAG,OAAO,EAAE,GAAG,EAAE,KAAK,IAAI,OAAO,EAAE,KAAK,CAAC;IAEnD,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrC,OAAO,IAA+B,CAAC;IACzC,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,sEAAsE;AACtE,cAAc;AACd,sEAAsE;AAEtE;;;GAGG;AACH,MAAM,kBAAkB,GAAwB,UAAU,QAAQ;IAChE,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACrC,MAAM,OAAO,GAAG,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IACtD,MAAM,UAAU,GAAG,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IAE5D,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,UAAU,CAAC;IAEpC,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE;YACZ,IAAI,UAAU,EAAE,CAAC;gBACf,OAAO,mGAAmG,CAAC;YAC7G,CAAC;YACD,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,4DAA4D,CAAC;YACtE,CAAC;YACD,OAAO,2CAA2C,CAAC;QACrD,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,oBAAoB,GAAmC,UAAU,QAAQ,EAAE,GAAG;IAClF,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAErC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,GAAG,EAAE,CAAC,qBAAqB,GAAG,sCAAsC;SAC9E,CAAC;IACJ,CAAC;IAED,gDAAgD;IAChD,4EAA4E;IAC5E,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAE9B,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE,CACZ,IAAI,CAAC,CAAC,CAAC,iCAAiC,GAAG,WAAW,CAAC,CAAC,CAAC,6BAA6B,GAAG,WAAW;KACvG,CAAC;AACJ,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,mBAAmB,GAA8C,UAAU,QAAQ,EAAE,KAAK;IAC9F,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAErC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,GAAG,EAAE,CAAC,yCAAyC,KAAK,8BAA8B;SAC5F,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAExC,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE,CACZ,IAAI;YACF,CAAC,CAAC,6CAA6C,WAAW,GAAG;YAC7D,CAAC,CAAC,yCAAyC,WAAW,GAAG;KAC9D,CAAC;AACJ,CAAC,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,GAAwB,UAAU,QAAQ;IACzD,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAErC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,mCAAmC;QACnC,OAAO;YACL,IAAI,EAAE,IAAI;YACV,OAAO,EAAE,GAAG,EAAE,CAAC,gDAAgD;SAChE,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,YAAY,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/C,MAAM,gBAAgB,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEnD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,SAAS;QAAE,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC3C,IAAI,YAAY;QAAE,MAAM,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IACtE,IAAI,gBAAgB;QAAE,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAErD,MAAM,IAAI,GAAG,CAAC,SAAS,IAAI,CAAC,YAAY,IAAI,CAAC,gBAAgB,CAAC;IAE9D,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE,CACZ,IAAI,CAAC,CAAC,CAAC,kCAAkC,CAAC,CAAC,CAAC,4CAA4C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;KAC9G,CAAC;AACJ,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,oBAAoB,GAAwB,UAAU,QAAQ;IAClE,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IAEnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,GAAG,EAAE,CAAC,4DAA4D;SAC5E,CAAC;IACJ,CAAC;IAED,+CAA+C;IAC/C,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IAC3C,MAAM,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC;IACjE,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC;IAEjD,MAAM,IAAI,GAAG,SAAS,IAAI,iBAAiB,IAAI,WAAW,CAAC;IAE3D,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE,CACZ,IAAI;YACF,CAAC,CAAC,6CAA6C;YAC/C,CAAC,CAAC,yFAAyF;KAChG,CAAC;AACJ,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,cAAc,GAAyC,UAAU,QAAQ,EAAE,SAAS;IACxF,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAErC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,GAAG,EAAE,CAAC,oCAAoC,SAAS,8BAA8B;SAC3F,CAAC;IACJ,CAAC;IAED,mEAAmE;IACnE,4EAA4E;IAC5E,MAAM,UAAU,GAAG,IAAI,MAAM,CAAC,uCAAuC,WAAW,CAAC,SAAS,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;IACjH,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEnC,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE,CACZ,IAAI,CAAC,CAAC,CAAC,wCAAwC,SAAS,GAAG,CAAC,CAAC,CAAC,oCAAoC,SAAS,GAAG;KACjH,CAAC;AACJ,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,sBAAsB,GAAuC,UAAU,QAAQ,EAAE,OAAO;IAC5F,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAErC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,IAAI,EAAE,IAAI;YACV,OAAO,EAAE,GAAG,EAAE,CAAC,yCAAyC,OAAO,sBAAsB;SACtF,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAErC,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE,CACZ,IAAI;YACF,CAAC,CAAC,yCAAyC,OAAO,GAAG;YACrD,CAAC,CAAC,6CAA6C,OAAO,oCAAoC;KAC/F,CAAC;AACJ,CAAC,CAAC;AAEF;;GAEG;AACH,MAAM,yBAAyB,GAAwB,UAAU,QAAQ;IACvE,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAErC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,GAAG,EAAE,CAAC,2DAA2D;SAC3E,CAAC;IACJ,CAAC;IAED,uEAAuE;IACvE,MAAM,cAAc,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAEtE,2CAA2C;IAC3C,MAAM,WAAW,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEzC,MAAM,IAAI,GAAG,WAAW,IAAI,CAAC,cAAc,CAAC;IAE5C,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE;YACZ,IAAI,cAAc,EAAE,CAAC;gBACnB,OAAO,sGAAsG,CAAC;YAChH,CAAC;YACD,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,OAAO,wDAAwD,CAAC;YAClE,CAAC;YACD,OAAO,mDAAmD,CAAC;QAC7D,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEF,sEAAsE;AACtE,yBAAyB;AACzB,sEAAsE;AAEtE;;;;;;;;GAQG;AACH,MAAM,kBAAkB,GAAkD,UAAU,QAAQ,EAAE,QAAQ;IACpG,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IAEnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,GAAG,EAAE,CAAC,6CAA6C,QAAQ,uBAAuB;SAC5F,CAAC;IACJ,CAAC;IAED,MAAM,gBAAgB,GAAG,IAAA,wCAAuB,EAAC,QAAQ,CAAC,CAAC;IAC3D,MAAM,iBAAiB,GAAG,IAAA,yCAAwB,EAAC,QAAQ,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEnC,8BAA8B;IAC9B,MAAM,iBAAiB,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAE5G,+BAA+B;IAC/B,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAE3G,MAAM,IAAI,GAAG,iBAAiB,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,CAAC;IAE7D,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE;YACZ,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBACvB,OAAO,8CAA8C,gBAAgB,CAAC,IAAI,CACxE,IAAI,CACL,mBAAmB,QAAQ,kBAAkB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;YACvE,CAAC;YACD,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC7B,OAAO,oCAAoC,aAAa,CAAC,IAAI,CAC3D,IAAI,CACL,mBAAmB,QAAQ,2BAA2B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YAC1F,CAAC;YACD,OAAO,kDAAkD,QAAQ,GAAG,CAAC;QACvE,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,aAAa,GAAmC,UAAU,QAAQ,EAAE,GAAG;IAC3E,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IAEnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,GAAG,EAAE,CAAC,+BAA+B,GAAG,uBAAuB;SACzE,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,CAAC;IAEzB,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,mCAAmC,GAAG,GAAG,CAAC,CAAC,CAAC,+BAA+B,GAAG,GAAG,CAAC;KAC1G,CAAC;AACJ,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,eAAe,GAAmD,UAAU,QAAQ,EAAE,GAAG,EAAE,KAAK;IACpG,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IAEnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,GAAG,EAAE,CAAC,mBAAmB,GAAG,eAAe,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,sBAAsB;SAChG,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAEnE,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE,CACZ,IAAI;YACF,CAAC,CAAC,mBAAmB,GAAG,mBAAmB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE;YAClE,CAAC,CAAC,mBAAmB,GAAG,eAAe,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,aAAa,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE;KAC3G,CAAC;AACJ,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,gBAAgB,GAAmC,UAAU,QAAQ,EAAE,GAAG;IAC9E,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IAEnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,yCAAyC;QACzC,OAAO;YACL,IAAI,EAAE,IAAI;YACV,OAAO,EAAE,GAAG,EAAE,CAAC,+BAA+B,GAAG,uBAAuB;SACzE,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;IAE5B,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,+BAA+B,GAAG,GAAG,CAAC,CAAC,CAAC,mCAAmC,GAAG,GAAG,CAAC;KAC1G,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,wBAAwB,GAAyC,UAAU,QAAQ,EAAE,SAAS;IAClG,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IAEnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,GAAG,EAAE,CAAC,+CAA+C,SAAS,uBAAuB;SAC/F,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IACvE,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IAE3D,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE;YACZ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC1B,OAAO,+CAA+C,SAAS,uBAAuB,CAAC;YACzF,CAAC;YACD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzB,OAAO,oDAAoD,SAAS,kBAAkB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;YAChH,CAAC;YACD,OAAO,wDAAwD,SAAS,GAAG,CAAC;QAC9E,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,sBAAsB,GAAkD,UAAU,QAAQ,EAAE,QAAQ;IACxG,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,gBAAgB,GAAG,IAAA,oCAAmB,EAAC,QAAQ,CAAC,CAAC;IAEvD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,GAAG,EAAE,CACZ,qCAAqC,gBAAgB,mBAAmB,QAAQ,uBAAuB;SAC1G,CAAC;IACJ,CAAC;IAED,iDAAiD;IACjD,IAAI,WAAmB,CAAC;IACxB,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ;YACX,WAAW,GAAG,iBAAiB,CAAC;YAChC,MAAM;QACR,KAAK,UAAU;YACb,WAAW,GAAG,aAAa,CAAC;YAC5B,MAAM;QACR;YACE,wFAAwF;YACxF,WAAW,GAAG,mBAAmB,CAAC;IACtC,CAAC;IAED,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,cAAc,KAAK,gBAAgB,CAAC;IAEjD,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE,CACZ,IAAI;YACF,CAAC,CAAC,mBAAmB,WAAW,iBAAiB,gBAAgB,GAAG;YACpE,CAAC,CAAC,mBAAmB,WAAW,aAAa,gBAAgB,mBAAmB,QAAQ,eAAe,cAAc,GAAG;KAC7H,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,kBAAkB,GAAkD,UAAU,QAAQ,EAAE,QAAQ;IACpG,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IAEnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,GAAG,EAAE,CAAC,6CAA6C,QAAQ,uBAAuB;SAC5F,CAAC;IACJ,CAAC;IAED,iDAAiD;IACjD,IAAI,OAAe,CAAC;IACpB,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,GAAG,aAAa,CAAC;YACxB,MAAM;QACR,KAAK,UAAU;YACb,OAAO,GAAG,SAAS,CAAC;YACpB,MAAM;QACR;YACE,OAAO,GAAG,eAAe,CAAC;IAC9B,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3B,MAAM,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAEzD,OAAO;QACL,IAAI;QACJ,OAAO,EAAE,GAAG,EAAE,CACZ,IAAI;YACF,CAAC,CAAC,gDAAgD,OAAO,GAAG;YAC5D,CAAC,CAAC,mBAAmB,OAAO,oCAAoC,QAAQ,UACpE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,OAAO,OAAO,IAAI,EAC3D,EAAE;KACT,CAAC;AACJ,CAAC,CAAC;AAEF,sEAAsE;AACtE,UAAU;AACV,sEAAsE;AAEtE;;GAEG;AACU,QAAA,UAAU,GAAG;IACxB,yBAAyB;IACzB,kBAAkB;IAClB,oBAAoB;IACpB,mBAAmB;IACnB,WAAW;IACX,oBAAoB;IACpB,cAAc;IACd,sBAAsB;IACtB,yBAAyB;IACzB,yBAAyB;IACzB,kBAAkB;IAClB,aAAa;IACb,eAAe;IACf,gBAAgB;IAChB,wBAAwB;IACxB,sBAAsB;IACtB,kBAAkB;CACnB,CAAC","sourcesContent":["/**\n * @file ui-matchers.ts\n * @description UI-specific Jest matchers for validating tool UI responses\n *\n * The metadata keys used in these matchers 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 { test, expect } from '@frontmcp/testing';\n *\n * test('tool has rendered UI', async ({ mcp }) => {\n * const result = await mcp.tools.call('my-tool', {});\n * expect(result).toHaveRenderedHtml();\n * expect(result).toBeXssSafe();\n * expect(result).toContainBoundValue('expected-value');\n * });\n * ```\n */\n\nimport type { MatcherFunction } from 'expect';\nimport type { ToolResultWrapper } from '../client/mcp-test-client.types';\nimport type { TestPlatformType } from '../platform/platform-types';\nimport { getForbiddenMetaPrefixes, getToolCallMetaPrefixes, getPlatformMimeType } from '../platform/platform-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 * Extract UI HTML from a tool result wrapper or raw result.\n */\nfunction extractUiHtml(received: unknown): string | undefined {\n if (typeof received === 'string') {\n return received;\n }\n\n // ToolResultWrapper has raw._meta\n const wrapper = received as ToolResultWrapper & { _meta?: Record<string, unknown> };\n const meta = wrapper?.raw?._meta || wrapper?._meta;\n\n if (meta && typeof meta === 'object') {\n const uiHtml = (meta as Record<string, unknown>)['ui/html'];\n if (typeof uiHtml === 'string') {\n return uiHtml;\n }\n }\n\n return undefined;\n}\n\n/**\n * Extract _meta object from a tool result wrapper.\n */\nfunction extractMeta(received: unknown): Record<string, unknown> | undefined {\n const wrapper = received as ToolResultWrapper & { _meta?: Record<string, unknown> };\n const meta = wrapper?.raw?._meta || wrapper?._meta;\n\n if (meta && typeof meta === 'object') {\n return meta as Record<string, unknown>;\n }\n\n return undefined;\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// UI MATCHERS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Check if tool result has rendered HTML in _meta['ui/html'].\n * Fails if the HTML is the mdx-fallback (escaped raw content).\n */\nconst toHaveRenderedHtml: MatcherFunction<[]> = function (received) {\n const html = extractUiHtml(received);\n const hasHtml = html !== undefined && html.length > 0;\n const isFallback = hasHtml && html.includes('mdx-fallback');\n\n const pass = hasHtml && !isFallback;\n\n return {\n pass,\n message: () => {\n if (isFallback) {\n return 'Expected rendered HTML but got mdx-fallback (raw escaped content). MDX rendering may have failed.';\n }\n if (!hasHtml) {\n return 'Expected _meta to have ui/html property with rendered HTML';\n }\n return 'Expected result not to have rendered HTML';\n },\n };\n};\n\n/**\n * Check if HTML contains a specific HTML element tag.\n * @param tag - The HTML tag name to look for (e.g., 'div', 'h1', 'span')\n */\nconst toContainHtmlElement: MatcherFunction<[tag: string]> = function (received, tag) {\n const html = extractUiHtml(received);\n\n if (!html) {\n return {\n pass: false,\n message: () => `Expected to find <${tag}> element, but no HTML content found`,\n };\n }\n\n // Match opening tags: <tag> or <tag attributes>\n // Escape regex metacharacters to prevent user input from breaking the regex\n const regex = new RegExp(`<${escapeRegex(tag)}[\\\\s>]`, 'i');\n const pass = regex.test(html);\n\n return {\n pass,\n message: () =>\n pass ? `Expected HTML not to contain <${tag}> element` : `Expected HTML to contain <${tag}> element`,\n };\n};\n\n/**\n * Check if a bound value from tool output appears in the rendered HTML.\n * @param value - The value to look for (string or number)\n */\nconst toContainBoundValue: MatcherFunction<[value: string | number]> = function (received, value) {\n const html = extractUiHtml(received);\n\n if (!html) {\n return {\n pass: false,\n message: () => `Expected HTML to contain bound value \"${value}\", but no HTML content found`,\n };\n }\n\n const stringValue = String(value);\n const pass = html.includes(stringValue);\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected HTML not to contain bound value \"${stringValue}\"`\n : `Expected HTML to contain bound value \"${stringValue}\"`,\n };\n};\n\n/**\n * Check if HTML is XSS-safe (no script tags, event handlers, or javascript: URIs).\n */\nconst toBeXssSafe: MatcherFunction<[]> = function (received) {\n const html = extractUiHtml(received);\n\n if (!html) {\n // No HTML means nothing to exploit\n return {\n pass: true,\n message: () => 'Expected HTML to be XSS unsafe (no HTML found)',\n };\n }\n\n const hasScript = /<script[\\s>]/i.test(html);\n const hasOnHandler = /\\son\\w+\\s*=/i.test(html);\n const hasJavascriptUri = /javascript:/i.test(html);\n\n const issues: string[] = [];\n if (hasScript) issues.push('<script> tag');\n if (hasOnHandler) issues.push('inline event handler (onclick, etc.)');\n if (hasJavascriptUri) issues.push('javascript: URI');\n\n const pass = !hasScript && !hasOnHandler && !hasJavascriptUri;\n\n return {\n pass,\n message: () =>\n pass ? 'Expected HTML not to be XSS safe' : `Expected HTML to be XSS safe, but found: ${issues.join(', ')}`,\n };\n};\n\n/**\n * Check if tool result has widget metadata.\n * Checks for ui/html (universal), openai/outputTemplate, or ui/mimeType.\n */\nconst toHaveWidgetMetadata: MatcherFunction<[]> = function (received) {\n const meta = extractMeta(received);\n\n if (!meta) {\n return {\n pass: false,\n message: () => 'Expected _meta to have widget metadata, but no _meta found',\n };\n }\n\n // Check for any widget-related metadata fields\n const hasUiHtml = Boolean(meta['ui/html']);\n const hasOutputTemplate = Boolean(meta['openai/outputTemplate']);\n const hasMimeType = Boolean(meta['ui/mimeType']);\n\n const pass = hasUiHtml || hasOutputTemplate || hasMimeType;\n\n return {\n pass,\n message: () =>\n pass\n ? 'Expected result not to have widget metadata'\n : 'Expected _meta to have widget metadata (ui/html, openai/outputTemplate, or ui/mimeType)',\n };\n};\n\n/**\n * Check if HTML has CSS classes (for styling validation).\n * @param className - The CSS class name to look for\n */\nconst toHaveCssClass: MatcherFunction<[className: string]> = function (received, className) {\n const html = extractUiHtml(received);\n\n if (!html) {\n return {\n pass: false,\n message: () => `Expected HTML to have CSS class \"${className}\", but no HTML content found`,\n };\n }\n\n // Match class=\"... className ...\" or className=\"... className ...\"\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 const pass = classRegex.test(html);\n\n return {\n pass,\n message: () =>\n pass ? `Expected HTML not to have CSS class \"${className}\"` : `Expected HTML to have CSS class \"${className}\"`,\n };\n};\n\n/**\n * Check that HTML does NOT contain specific content (useful for fallback checks).\n * @param content - The content that should NOT be in the HTML\n */\nconst toNotContainRawContent: MatcherFunction<[content: string]> = function (received, content) {\n const html = extractUiHtml(received);\n\n if (!html) {\n return {\n pass: true,\n message: () => `Expected HTML to contain raw content \"${content}\", but no HTML found`,\n };\n }\n\n const pass = !html.includes(content);\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected HTML to contain raw content \"${content}\"`\n : `Expected HTML not to contain raw content \"${content}\" (may indicate rendering failure)`,\n };\n};\n\n/**\n * Check if HTML has proper structure (not just escaped text).\n */\nconst toHaveProperHtmlStructure: MatcherFunction<[]> = function (received) {\n const html = extractUiHtml(received);\n\n if (!html) {\n return {\n pass: false,\n message: () => 'Expected proper HTML structure, but no HTML content found',\n };\n }\n\n // Check for escaped HTML entities that suggest content wasn't rendered\n const hasEscapedTags = html.includes('&lt;') && html.includes('&gt;');\n\n // Check that there's at least one HTML tag\n const hasHtmlTags = /<[a-z]/i.test(html);\n\n const pass = hasHtmlTags && !hasEscapedTags;\n\n return {\n pass,\n message: () => {\n if (hasEscapedTags) {\n return 'Expected proper HTML structure, but found escaped HTML entities - content may not have been rendered';\n }\n if (!hasHtmlTags) {\n return 'Expected proper HTML structure, but found no HTML tags';\n }\n return 'Expected result not to have proper HTML structure';\n },\n };\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// PLATFORM META MATCHERS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Check if tool result has correct meta keys for a specific platform.\n * @param platform - The platform type to check for\n *\n * This matcher verifies:\n * - OpenAI: Has openai/* keys, no ui/* or frontmcp/* keys\n * - ext-apps: Has ui/* keys, no openai/* or frontmcp/* keys\n * - Others: Has frontmcp/* + ui/* keys, no openai/* keys\n */\nconst toHavePlatformMeta: MatcherFunction<[platform: TestPlatformType]> = function (received, platform) {\n const meta = extractMeta(received);\n\n if (!meta) {\n return {\n pass: false,\n message: () => `Expected _meta to have platform meta for \"${platform}\", but no _meta found`,\n };\n }\n\n const expectedPrefixes = getToolCallMetaPrefixes(platform);\n const forbiddenPrefixes = getForbiddenMetaPrefixes(platform);\n const metaKeys = Object.keys(meta);\n\n // Check for expected prefixes\n const hasExpectedPrefix = metaKeys.some((key) => expectedPrefixes.some((prefix) => key.startsWith(prefix)));\n\n // Check for forbidden prefixes\n const forbiddenKeys = metaKeys.filter((key) => forbiddenPrefixes.some((prefix) => key.startsWith(prefix)));\n\n const pass = hasExpectedPrefix && forbiddenKeys.length === 0;\n\n return {\n pass,\n message: () => {\n if (!hasExpectedPrefix) {\n return `Expected _meta to have keys with prefixes [${expectedPrefixes.join(\n ', ',\n )}] for platform \"${platform}\", but found: [${metaKeys.join(', ')}]`;\n }\n if (forbiddenKeys.length > 0) {\n return `Expected _meta NOT to have keys [${forbiddenKeys.join(\n ', ',\n )}] for platform \"${platform}\" (forbidden prefixes: [${forbiddenPrefixes.join(', ')}])`;\n }\n return `Expected result not to have platform meta for \"${platform}\"`;\n },\n };\n};\n\n/**\n * Check if _meta has a specific key.\n * @param key - The meta key to look for (e.g., 'ui/html', 'openai/mimeType')\n */\nconst toHaveMetaKey: MatcherFunction<[key: string]> = function (received, key) {\n const meta = extractMeta(received);\n\n if (!meta) {\n return {\n pass: false,\n message: () => `Expected _meta to have key \"${key}\", but no _meta found`,\n };\n }\n\n const pass = key in meta;\n\n return {\n pass,\n message: () => (pass ? `Expected _meta not to have key \"${key}\"` : `Expected _meta to have key \"${key}\"`),\n };\n};\n\n/**\n * Check if _meta has a specific key with a specific value.\n * @param key - The meta key to look for\n * @param value - The expected value\n */\nconst toHaveMetaValue: MatcherFunction<[key: string, value: unknown]> = function (received, key, value) {\n const meta = extractMeta(received);\n\n if (!meta) {\n return {\n pass: false,\n message: () => `Expected _meta[\"${key}\"] to equal ${JSON.stringify(value)}, but no _meta found`,\n };\n }\n\n const actualValue = meta[key];\n const pass = JSON.stringify(actualValue) === JSON.stringify(value);\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected _meta[\"${key}\"] not to equal ${JSON.stringify(value)}`\n : `Expected _meta[\"${key}\"] to equal ${JSON.stringify(value)}, but got ${JSON.stringify(actualValue)}`,\n };\n};\n\n/**\n * Check if _meta does NOT have a specific key.\n * @param key - The meta key that should be absent\n */\nconst toNotHaveMetaKey: MatcherFunction<[key: string]> = function (received, key) {\n const meta = extractMeta(received);\n\n if (!meta) {\n // No meta means key is definitely absent\n return {\n pass: true,\n message: () => `Expected _meta to have key \"${key}\", but no _meta found`,\n };\n }\n\n const pass = !(key in meta);\n\n return {\n pass,\n message: () => (pass ? `Expected _meta to have key \"${key}\"` : `Expected _meta not to have key \"${key}\"`),\n };\n};\n\n/**\n * Check if _meta ONLY has keys with a specific namespace prefix.\n * @param namespace - The namespace prefix (e.g., 'openai/', 'ui/', 'frontmcp/')\n *\n * Note: For platforms like Claude that use both frontmcp/* and ui/*,\n * use toHavePlatformMeta() instead.\n */\nconst toHaveOnlyNamespacedMeta: MatcherFunction<[namespace: string]> = function (received, namespace) {\n const meta = extractMeta(received);\n\n if (!meta) {\n return {\n pass: false,\n message: () => `Expected _meta to have keys with namespace \"${namespace}\", but no _meta found`,\n };\n }\n\n const metaKeys = Object.keys(meta);\n const wrongKeys = metaKeys.filter((key) => !key.startsWith(namespace));\n const pass = wrongKeys.length === 0 && metaKeys.length > 0;\n\n return {\n pass,\n message: () => {\n if (metaKeys.length === 0) {\n return `Expected _meta to have keys with namespace \"${namespace}\", but _meta is empty`;\n }\n if (wrongKeys.length > 0) {\n return `Expected _meta to ONLY have keys with namespace \"${namespace}\", but found: [${wrongKeys.join(', ')}]`;\n }\n return `Expected _meta not to have only keys with namespace \"${namespace}\"`;\n },\n };\n};\n\n/**\n * Check if _meta has the correct MIME type for a platform.\n * @param platform - The platform type to check for\n *\n * MIME types:\n * - OpenAI: text/html+skybridge\n * - Others: text/html+mcp\n */\nconst toHavePlatformMimeType: MatcherFunction<[platform: TestPlatformType]> = function (received, platform) {\n const meta = extractMeta(received);\n const expectedMimeType = getPlatformMimeType(platform);\n\n if (!meta) {\n return {\n pass: false,\n message: () =>\n `Expected _meta to have MIME type \"${expectedMimeType}\" for platform \"${platform}\", but no _meta found`,\n };\n }\n\n // Determine which key to check based on platform\n let mimeTypeKey: string;\n switch (platform) {\n case 'openai':\n mimeTypeKey = 'openai/mimeType';\n break;\n case 'ext-apps':\n mimeTypeKey = 'ui/mimeType';\n break;\n default:\n // For other platforms, check frontmcp/mimeType (primary) or ui/mimeType (compatibility)\n mimeTypeKey = 'frontmcp/mimeType';\n }\n\n const actualMimeType = meta[mimeTypeKey];\n const pass = actualMimeType === expectedMimeType;\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected _meta[\"${mimeTypeKey}\"] not to be \"${expectedMimeType}\"`\n : `Expected _meta[\"${mimeTypeKey}\"] to be \"${expectedMimeType}\" for platform \"${platform}\", but got \"${actualMimeType}\"`,\n };\n};\n\n/**\n * Check if _meta has HTML content in the correct platform-specific key.\n * @param platform - The platform type to check for\n *\n * HTML keys:\n * - OpenAI: openai/html\n * - ext-apps: ui/html\n * - Others: frontmcp/html (or ui/html for compatibility)\n */\nconst toHavePlatformHtml: MatcherFunction<[platform: TestPlatformType]> = function (received, platform) {\n const meta = extractMeta(received);\n\n if (!meta) {\n return {\n pass: false,\n message: () => `Expected _meta to have platform HTML for \"${platform}\", but no _meta found`,\n };\n }\n\n // Determine which key to check based on platform\n let htmlKey: string;\n switch (platform) {\n case 'openai':\n htmlKey = 'openai/html';\n break;\n case 'ext-apps':\n htmlKey = 'ui/html';\n break;\n default:\n htmlKey = 'frontmcp/html';\n }\n\n const html = meta[htmlKey];\n const pass = typeof html === 'string' && html.length > 0;\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected _meta not to have platform HTML in \"${htmlKey}\"`\n : `Expected _meta[\"${htmlKey}\"] to contain HTML for platform \"${platform}\", but ${\n html === undefined ? 'key not found' : `got ${typeof html}`\n }`,\n };\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// EXPORTS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * All UI matchers as an object for expect.extend()\n */\nexport const uiMatchers = {\n // Existing HTML matchers\n toHaveRenderedHtml,\n toContainHtmlElement,\n toContainBoundValue,\n toBeXssSafe,\n toHaveWidgetMetadata,\n toHaveCssClass,\n toNotContainRawContent,\n toHaveProperHtmlStructure,\n // Platform meta matchers\n toHavePlatformMeta,\n toHaveMetaKey,\n toHaveMetaValue,\n toNotHaveMetaKey,\n toHaveOnlyNamespacedMeta,\n toHavePlatformMimeType,\n toHavePlatformHtml,\n};\n"]}