@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":"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"]}