@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,391 @@
1
+ "use strict";
2
+ /**
3
+ * @file mcp-matchers.ts
4
+ * @description Custom Jest matchers for MCP testing
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { test, expect } from '@frontmcp/testing';
9
+ *
10
+ * test('tools work', async ({ mcp }) => {
11
+ * const tools = await mcp.tools.list();
12
+ * expect(tools).toContainTool('my-tool');
13
+ *
14
+ * const result = await mcp.tools.call('my-tool', {});
15
+ * expect(result).toBeSuccessful();
16
+ * expect(result).toHaveTextContent();
17
+ * });
18
+ * ```
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.mcpMatchers = void 0;
22
+ const ui_matchers_1 = require("../ui/ui-matchers");
23
+ // ═══════════════════════════════════════════════════════════════════
24
+ // TOOL MATCHERS
25
+ // ═══════════════════════════════════════════════════════════════════
26
+ /**
27
+ * Check if tools array contains a tool with the given name
28
+ */
29
+ const toContainTool = function (received, toolName) {
30
+ const tools = received;
31
+ if (!Array.isArray(tools)) {
32
+ return {
33
+ pass: false,
34
+ message: () => `Expected an array of tools, but received ${typeof received}`,
35
+ };
36
+ }
37
+ const pass = tools.some((t) => t.name === toolName);
38
+ const availableTools = tools.map((t) => t.name).join(', ');
39
+ return {
40
+ pass,
41
+ message: () => pass
42
+ ? `Expected tools not to contain "${toolName}"`
43
+ : `Expected tools to contain "${toolName}", but got: [${availableTools}]`,
44
+ };
45
+ };
46
+ /**
47
+ * Check if result is successful (not an error)
48
+ */
49
+ const toBeSuccessful = function (received) {
50
+ const result = received;
51
+ if (typeof result !== 'object' || result === null || !('isSuccess' in result)) {
52
+ return {
53
+ pass: false,
54
+ message: () => `Expected a result wrapper object with isSuccess property`,
55
+ };
56
+ }
57
+ const pass = result.isSuccess;
58
+ return {
59
+ pass,
60
+ message: () => pass
61
+ ? 'Expected result not to be successful'
62
+ : `Expected result to be successful, but got error: ${result.error?.message ?? 'unknown error'}`,
63
+ };
64
+ };
65
+ /**
66
+ * Check if result is an error, optionally with a specific error code
67
+ */
68
+ const toBeError = function (received, expectedCode) {
69
+ const result = received;
70
+ if (typeof result !== 'object' || result === null || !('isError' in result)) {
71
+ return {
72
+ pass: false,
73
+ message: () => `Expected a result wrapper object with isError property`,
74
+ };
75
+ }
76
+ let pass = result.isError;
77
+ if (pass && expectedCode !== undefined) {
78
+ pass = result.error?.code === expectedCode;
79
+ }
80
+ return {
81
+ pass,
82
+ message: () => {
83
+ if (!result.isError) {
84
+ return 'Expected result to be an error, but it was successful';
85
+ }
86
+ if (expectedCode !== undefined && result.error?.code !== expectedCode) {
87
+ return `Expected error code ${expectedCode}, but got ${result.error?.code}`;
88
+ }
89
+ return 'Expected result not to be an error';
90
+ },
91
+ };
92
+ };
93
+ /**
94
+ * Check if tool result has text content, optionally containing specific text
95
+ */
96
+ const toHaveTextContent = function (received, expectedText) {
97
+ const result = received;
98
+ if (typeof result !== 'object' || result === null || !('hasTextContent' in result)) {
99
+ return {
100
+ pass: false,
101
+ message: () => `Expected a ToolResultWrapper object with hasTextContent method`,
102
+ };
103
+ }
104
+ const hasText = result.hasTextContent();
105
+ const text = result.text();
106
+ let pass = hasText;
107
+ if (pass && expectedText !== undefined) {
108
+ pass = text?.includes(expectedText) ?? false;
109
+ }
110
+ return {
111
+ pass,
112
+ message: () => {
113
+ if (!hasText) {
114
+ return 'Expected result to have text content';
115
+ }
116
+ if (expectedText !== undefined && !text?.includes(expectedText)) {
117
+ return `Expected text to contain "${expectedText}", but got: "${text}"`;
118
+ }
119
+ return 'Expected result not to have text content';
120
+ },
121
+ };
122
+ };
123
+ /**
124
+ * Check if tool result has image content
125
+ */
126
+ const toHaveImageContent = function (received) {
127
+ const result = received;
128
+ if (typeof result !== 'object' || result === null || !('hasImageContent' in result)) {
129
+ return {
130
+ pass: false,
131
+ message: () => `Expected a ToolResultWrapper object with hasImageContent method`,
132
+ };
133
+ }
134
+ const pass = result.hasImageContent();
135
+ return {
136
+ pass,
137
+ message: () => (pass ? 'Expected result not to have image content' : 'Expected result to have image content'),
138
+ };
139
+ };
140
+ /**
141
+ * Check if tool result has resource content
142
+ */
143
+ const toHaveResourceContent = function (received) {
144
+ const result = received;
145
+ if (typeof result !== 'object' || result === null || !('hasResourceContent' in result)) {
146
+ return {
147
+ pass: false,
148
+ message: () => `Expected a ToolResultWrapper object with hasResourceContent method`,
149
+ };
150
+ }
151
+ const pass = result.hasResourceContent();
152
+ return {
153
+ pass,
154
+ message: () => (pass ? 'Expected result not to have resource content' : 'Expected result to have resource content'),
155
+ };
156
+ };
157
+ // ═══════════════════════════════════════════════════════════════════
158
+ // RESOURCE MATCHERS
159
+ // ═══════════════════════════════════════════════════════════════════
160
+ /**
161
+ * Check if resources array contains a resource with the given URI
162
+ */
163
+ const toContainResource = function (received, uri) {
164
+ const resources = received;
165
+ if (!Array.isArray(resources)) {
166
+ return {
167
+ pass: false,
168
+ message: () => `Expected an array of resources, but received ${typeof received}`,
169
+ };
170
+ }
171
+ const pass = resources.some((r) => r.uri === uri);
172
+ const availableUris = resources.map((r) => r.uri).join(', ');
173
+ return {
174
+ pass,
175
+ message: () => pass
176
+ ? `Expected resources not to contain "${uri}"`
177
+ : `Expected resources to contain "${uri}", but got: [${availableUris}]`,
178
+ };
179
+ };
180
+ /**
181
+ * Check if resource templates array contains a template with the given URI template
182
+ */
183
+ const toContainResourceTemplate = function (received, uriTemplate) {
184
+ const templates = received;
185
+ if (!Array.isArray(templates)) {
186
+ return {
187
+ pass: false,
188
+ message: () => `Expected an array of resource templates, but received ${typeof received}`,
189
+ };
190
+ }
191
+ const pass = templates.some((t) => t.uriTemplate === uriTemplate);
192
+ const availableTemplates = templates.map((t) => t.uriTemplate).join(', ');
193
+ return {
194
+ pass,
195
+ message: () => pass
196
+ ? `Expected templates not to contain "${uriTemplate}"`
197
+ : `Expected templates to contain "${uriTemplate}", but got: [${availableTemplates}]`,
198
+ };
199
+ };
200
+ /**
201
+ * Check if resource content has a specific MIME type
202
+ */
203
+ const toHaveMimeType = function (received, mimeType) {
204
+ const result = received;
205
+ if (typeof result !== 'object' || result === null || !('hasMimeType' in result)) {
206
+ return {
207
+ pass: false,
208
+ message: () => `Expected a ResourceContentWrapper object with hasMimeType method`,
209
+ };
210
+ }
211
+ const pass = result.hasMimeType(mimeType);
212
+ const actualMimeType = result.mimeType();
213
+ return {
214
+ pass,
215
+ message: () => pass
216
+ ? `Expected content not to have MIME type "${mimeType}"`
217
+ : `Expected MIME type "${mimeType}", but got "${actualMimeType}"`,
218
+ };
219
+ };
220
+ // ═══════════════════════════════════════════════════════════════════
221
+ // PROMPT MATCHERS
222
+ // ═══════════════════════════════════════════════════════════════════
223
+ /**
224
+ * Check if prompts array contains a prompt with the given name
225
+ */
226
+ const toContainPrompt = function (received, name) {
227
+ const prompts = received;
228
+ if (!Array.isArray(prompts)) {
229
+ return {
230
+ pass: false,
231
+ message: () => `Expected an array of prompts, but received ${typeof received}`,
232
+ };
233
+ }
234
+ const pass = prompts.some((p) => p.name === name);
235
+ const availablePrompts = prompts.map((p) => p.name).join(', ');
236
+ return {
237
+ pass,
238
+ message: () => pass
239
+ ? `Expected prompts not to contain "${name}"`
240
+ : `Expected prompts to contain "${name}", but got: [${availablePrompts}]`,
241
+ };
242
+ };
243
+ /**
244
+ * Check if prompt result has a specific number of messages
245
+ */
246
+ const toHaveMessages = function (received, count) {
247
+ const result = received;
248
+ if (typeof result !== 'object' || result === null || !('messages' in result)) {
249
+ return {
250
+ pass: false,
251
+ message: () => `Expected a PromptResultWrapper object with messages property`,
252
+ };
253
+ }
254
+ const actualCount = result.messages?.length ?? 0;
255
+ const pass = actualCount === count;
256
+ return {
257
+ pass,
258
+ message: () => pass
259
+ ? `Expected prompt not to have ${count} messages`
260
+ : `Expected prompt to have ${count} messages, but got ${actualCount}`,
261
+ };
262
+ };
263
+ // ═══════════════════════════════════════════════════════════════════
264
+ // PROTOCOL MATCHERS
265
+ // ═══════════════════════════════════════════════════════════════════
266
+ /**
267
+ * Check if response is valid JSON-RPC 2.0
268
+ * A valid JSON-RPC 2.0 response must have:
269
+ * - jsonrpc: "2.0"
270
+ * - id (matching the request, can be null for notifications)
271
+ * - Either result OR error (but not both)
272
+ */
273
+ const toBeValidJsonRpc = function (received) {
274
+ const response = received;
275
+ if (typeof response !== 'object' || response === null) {
276
+ return {
277
+ pass: false,
278
+ message: () => `Expected an object, but received ${typeof received}`,
279
+ };
280
+ }
281
+ const hasJsonRpc = response['jsonrpc'] === '2.0';
282
+ const hasId = 'id' in response;
283
+ const hasResult = 'result' in response;
284
+ const hasError = 'error' in response;
285
+ const hasExactlyOneResultOrError = (hasResult || hasError) && !(hasResult && hasError);
286
+ const pass = hasJsonRpc && hasId && hasExactlyOneResultOrError;
287
+ return {
288
+ pass,
289
+ message: () => {
290
+ if (pass) {
291
+ return 'Expected response not to be valid JSON-RPC';
292
+ }
293
+ const issues = [];
294
+ if (!hasJsonRpc)
295
+ issues.push('missing or invalid "jsonrpc": "2.0"');
296
+ if (!hasId)
297
+ issues.push('missing "id" field');
298
+ if (!hasExactlyOneResultOrError) {
299
+ if (!hasResult && !hasError)
300
+ issues.push('missing "result" or "error"');
301
+ else
302
+ issues.push('cannot have both "result" and "error"');
303
+ }
304
+ return `Expected valid JSON-RPC 2.0 response: ${issues.join(', ')}`;
305
+ },
306
+ };
307
+ };
308
+ /**
309
+ * Check if JSON-RPC response has a result
310
+ */
311
+ const toHaveResult = function (received) {
312
+ const response = received;
313
+ if (typeof response !== 'object' || response === null) {
314
+ return {
315
+ pass: false,
316
+ message: () => `Expected an object, but received ${typeof received}`,
317
+ };
318
+ }
319
+ const pass = 'result' in response;
320
+ return {
321
+ pass,
322
+ message: () => (pass ? 'Expected response not to have result' : 'Expected response to have result'),
323
+ };
324
+ };
325
+ /**
326
+ * Check if JSON-RPC response has an error
327
+ */
328
+ const toHaveError = function (received) {
329
+ const response = received;
330
+ if (typeof response !== 'object' || response === null) {
331
+ return {
332
+ pass: false,
333
+ message: () => `Expected an object, but received ${typeof received}`,
334
+ };
335
+ }
336
+ const pass = 'error' in response;
337
+ return {
338
+ pass,
339
+ message: () => (pass ? 'Expected response not to have error' : 'Expected response to have error'),
340
+ };
341
+ };
342
+ /**
343
+ * Check if JSON-RPC response has a specific error code
344
+ */
345
+ const toHaveErrorCode = function (received, code) {
346
+ const response = received;
347
+ if (typeof response !== 'object' || response === null) {
348
+ return {
349
+ pass: false,
350
+ message: () => `Expected an object, but received ${typeof received}`,
351
+ };
352
+ }
353
+ const actualCode = response.error?.code;
354
+ const pass = actualCode === code;
355
+ return {
356
+ pass,
357
+ message: () => pass
358
+ ? `Expected response not to have error code ${code}`
359
+ : `Expected error code ${code}, but got ${actualCode ?? 'no error'}`,
360
+ };
361
+ };
362
+ // ═══════════════════════════════════════════════════════════════════
363
+ // EXPORTS
364
+ // ═══════════════════════════════════════════════════════════════════
365
+ /**
366
+ * All MCP matchers as an object for expect.extend()
367
+ */
368
+ exports.mcpMatchers = {
369
+ // Tool matchers
370
+ toContainTool,
371
+ toBeSuccessful,
372
+ toBeError,
373
+ toHaveTextContent,
374
+ toHaveImageContent,
375
+ toHaveResourceContent,
376
+ // Resource matchers
377
+ toContainResource,
378
+ toContainResourceTemplate,
379
+ toHaveMimeType,
380
+ // Prompt matchers
381
+ toContainPrompt,
382
+ toHaveMessages,
383
+ // Protocol matchers
384
+ toBeValidJsonRpc,
385
+ toHaveResult,
386
+ toHaveError,
387
+ toHaveErrorCode,
388
+ // UI matchers (for testing tool UI responses)
389
+ ...ui_matchers_1.uiMatchers,
390
+ };
391
+ //# sourceMappingURL=mcp-matchers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-matchers.js","sourceRoot":"","sources":["../../../src/matchers/mcp-matchers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file mcp-matchers.ts\n * @description Custom Jest matchers for MCP testing\n *\n * @example\n * ```typescript\n * import { test, expect } from '@frontmcp/testing';\n *\n * test('tools work', async ({ mcp }) => {\n * const tools = await mcp.tools.list();\n * expect(tools).toContainTool('my-tool');\n *\n * const result = await mcp.tools.call('my-tool', {});\n * expect(result).toBeSuccessful();\n * expect(result).toHaveTextContent();\n * });\n * ```\n */\n\nimport type { MatcherFunction } from 'expect';\nimport type { Tool, Resource, ResourceTemplate, Prompt } from '@modelcontextprotocol/sdk/types.js';\nimport type { ToolResultWrapper, ResourceContentWrapper, PromptResultWrapper } from '../client/mcp-test-client.types';\nimport { uiMatchers } from '../ui/ui-matchers';\n\n// ═══════════════════════════════════════════════════════════════════\n// HELPER TYPES\n// ═══════════════════════════════════════════════════════════════════\n\ntype ResultWrapper = ToolResultWrapper | ResourceContentWrapper;\n\n// ═══════════════════════════════════════════════════════════════════\n// TOOL MATCHERS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Check if tools array contains a tool with the given name\n */\nconst toContainTool: MatcherFunction<[toolName: string]> = function (received, toolName) {\n const tools = received as Tool[];\n\n if (!Array.isArray(tools)) {\n return {\n pass: false,\n message: () => `Expected an array of tools, but received ${typeof received}`,\n };\n }\n\n const pass = tools.some((t) => t.name === toolName);\n const availableTools = tools.map((t) => t.name).join(', ');\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected tools not to contain \"${toolName}\"`\n : `Expected tools to contain \"${toolName}\", but got: [${availableTools}]`,\n };\n};\n\n/**\n * Check if result is successful (not an error)\n */\nconst toBeSuccessful: MatcherFunction<[]> = function (received) {\n const result = received as ResultWrapper;\n\n if (typeof result !== 'object' || result === null || !('isSuccess' in result)) {\n return {\n pass: false,\n message: () => `Expected a result wrapper object with isSuccess property`,\n };\n }\n\n const pass = result.isSuccess;\n\n return {\n pass,\n message: () =>\n pass\n ? 'Expected result not to be successful'\n : `Expected result to be successful, but got error: ${result.error?.message ?? 'unknown error'}`,\n };\n};\n\n/**\n * Check if result is an error, optionally with a specific error code\n */\nconst toBeError: MatcherFunction<[expectedCode?: number]> = function (received, expectedCode) {\n const result = received as ResultWrapper;\n\n if (typeof result !== 'object' || result === null || !('isError' in result)) {\n return {\n pass: false,\n message: () => `Expected a result wrapper object with isError property`,\n };\n }\n\n let pass = result.isError;\n\n if (pass && expectedCode !== undefined) {\n pass = result.error?.code === expectedCode;\n }\n\n return {\n pass,\n message: () => {\n if (!result.isError) {\n return 'Expected result to be an error, but it was successful';\n }\n if (expectedCode !== undefined && result.error?.code !== expectedCode) {\n return `Expected error code ${expectedCode}, but got ${result.error?.code}`;\n }\n return 'Expected result not to be an error';\n },\n };\n};\n\n/**\n * Check if tool result has text content, optionally containing specific text\n */\nconst toHaveTextContent: MatcherFunction<[expectedText?: string]> = function (received, expectedText) {\n const result = received as ToolResultWrapper;\n\n if (typeof result !== 'object' || result === null || !('hasTextContent' in result)) {\n return {\n pass: false,\n message: () => `Expected a ToolResultWrapper object with hasTextContent method`,\n };\n }\n\n const hasText = result.hasTextContent();\n const text = result.text();\n let pass = hasText;\n\n if (pass && expectedText !== undefined) {\n pass = text?.includes(expectedText) ?? false;\n }\n\n return {\n pass,\n message: () => {\n if (!hasText) {\n return 'Expected result to have text content';\n }\n if (expectedText !== undefined && !text?.includes(expectedText)) {\n return `Expected text to contain \"${expectedText}\", but got: \"${text}\"`;\n }\n return 'Expected result not to have text content';\n },\n };\n};\n\n/**\n * Check if tool result has image content\n */\nconst toHaveImageContent: MatcherFunction<[]> = function (received) {\n const result = received as ToolResultWrapper;\n\n if (typeof result !== 'object' || result === null || !('hasImageContent' in result)) {\n return {\n pass: false,\n message: () => `Expected a ToolResultWrapper object with hasImageContent method`,\n };\n }\n\n const pass = result.hasImageContent();\n\n return {\n pass,\n message: () => (pass ? 'Expected result not to have image content' : 'Expected result to have image content'),\n };\n};\n\n/**\n * Check if tool result has resource content\n */\nconst toHaveResourceContent: MatcherFunction<[]> = function (received) {\n const result = received as ToolResultWrapper;\n\n if (typeof result !== 'object' || result === null || !('hasResourceContent' in result)) {\n return {\n pass: false,\n message: () => `Expected a ToolResultWrapper object with hasResourceContent method`,\n };\n }\n\n const pass = result.hasResourceContent();\n\n return {\n pass,\n message: () => (pass ? 'Expected result not to have resource content' : 'Expected result to have resource content'),\n };\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// RESOURCE MATCHERS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Check if resources array contains a resource with the given URI\n */\nconst toContainResource: MatcherFunction<[uri: string]> = function (received, uri) {\n const resources = received as Resource[];\n\n if (!Array.isArray(resources)) {\n return {\n pass: false,\n message: () => `Expected an array of resources, but received ${typeof received}`,\n };\n }\n\n const pass = resources.some((r) => r.uri === uri);\n const availableUris = resources.map((r) => r.uri).join(', ');\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected resources not to contain \"${uri}\"`\n : `Expected resources to contain \"${uri}\", but got: [${availableUris}]`,\n };\n};\n\n/**\n * Check if resource templates array contains a template with the given URI template\n */\nconst toContainResourceTemplate: MatcherFunction<[uriTemplate: string]> = function (received, uriTemplate) {\n const templates = received as ResourceTemplate[];\n\n if (!Array.isArray(templates)) {\n return {\n pass: false,\n message: () => `Expected an array of resource templates, but received ${typeof received}`,\n };\n }\n\n const pass = templates.some((t) => t.uriTemplate === uriTemplate);\n const availableTemplates = templates.map((t) => t.uriTemplate).join(', ');\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected templates not to contain \"${uriTemplate}\"`\n : `Expected templates to contain \"${uriTemplate}\", but got: [${availableTemplates}]`,\n };\n};\n\n/**\n * Check if resource content has a specific MIME type\n */\nconst toHaveMimeType: MatcherFunction<[mimeType: string]> = function (received, mimeType) {\n const result = received as ResourceContentWrapper;\n\n if (typeof result !== 'object' || result === null || !('hasMimeType' in result)) {\n return {\n pass: false,\n message: () => `Expected a ResourceContentWrapper object with hasMimeType method`,\n };\n }\n\n const pass = result.hasMimeType(mimeType);\n const actualMimeType = result.mimeType();\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected content not to have MIME type \"${mimeType}\"`\n : `Expected MIME type \"${mimeType}\", but got \"${actualMimeType}\"`,\n };\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// PROMPT MATCHERS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Check if prompts array contains a prompt with the given name\n */\nconst toContainPrompt: MatcherFunction<[name: string]> = function (received, name) {\n const prompts = received as Prompt[];\n\n if (!Array.isArray(prompts)) {\n return {\n pass: false,\n message: () => `Expected an array of prompts, but received ${typeof received}`,\n };\n }\n\n const pass = prompts.some((p) => p.name === name);\n const availablePrompts = prompts.map((p) => p.name).join(', ');\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected prompts not to contain \"${name}\"`\n : `Expected prompts to contain \"${name}\", but got: [${availablePrompts}]`,\n };\n};\n\n/**\n * Check if prompt result has a specific number of messages\n */\nconst toHaveMessages: MatcherFunction<[count: number]> = function (received, count) {\n const result = received as PromptResultWrapper;\n\n if (typeof result !== 'object' || result === null || !('messages' in result)) {\n return {\n pass: false,\n message: () => `Expected a PromptResultWrapper object with messages property`,\n };\n }\n\n const actualCount = result.messages?.length ?? 0;\n const pass = actualCount === count;\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected prompt not to have ${count} messages`\n : `Expected prompt to have ${count} messages, but got ${actualCount}`,\n };\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// PROTOCOL MATCHERS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Check if response is valid JSON-RPC 2.0\n * A valid JSON-RPC 2.0 response must have:\n * - jsonrpc: \"2.0\"\n * - id (matching the request, can be null for notifications)\n * - Either result OR error (but not both)\n */\nconst toBeValidJsonRpc: MatcherFunction<[]> = function (received) {\n const response = received as Record<string, unknown>;\n\n if (typeof response !== 'object' || response === null) {\n return {\n pass: false,\n message: () => `Expected an object, but received ${typeof received}`,\n };\n }\n\n const hasJsonRpc = response['jsonrpc'] === '2.0';\n const hasId = 'id' in response;\n const hasResult = 'result' in response;\n const hasError = 'error' in response;\n const hasExactlyOneResultOrError = (hasResult || hasError) && !(hasResult && hasError);\n\n const pass = hasJsonRpc && hasId && hasExactlyOneResultOrError;\n\n return {\n pass,\n message: () => {\n if (pass) {\n return 'Expected response not to be valid JSON-RPC';\n }\n const issues: string[] = [];\n if (!hasJsonRpc) issues.push('missing or invalid \"jsonrpc\": \"2.0\"');\n if (!hasId) issues.push('missing \"id\" field');\n if (!hasExactlyOneResultOrError) {\n if (!hasResult && !hasError) issues.push('missing \"result\" or \"error\"');\n else issues.push('cannot have both \"result\" and \"error\"');\n }\n return `Expected valid JSON-RPC 2.0 response: ${issues.join(', ')}`;\n },\n };\n};\n\n/**\n * Check if JSON-RPC response has a result\n */\nconst toHaveResult: MatcherFunction<[]> = function (received) {\n const response = received as Record<string, unknown>;\n\n if (typeof response !== 'object' || response === null) {\n return {\n pass: false,\n message: () => `Expected an object, but received ${typeof received}`,\n };\n }\n\n const pass = 'result' in response;\n\n return {\n pass,\n message: () => (pass ? 'Expected response not to have result' : 'Expected response to have result'),\n };\n};\n\n/**\n * Check if JSON-RPC response has an error\n */\nconst toHaveError: MatcherFunction<[]> = function (received) {\n const response = received as Record<string, unknown>;\n\n if (typeof response !== 'object' || response === null) {\n return {\n pass: false,\n message: () => `Expected an object, but received ${typeof received}`,\n };\n }\n\n const pass = 'error' in response;\n\n return {\n pass,\n message: () => (pass ? 'Expected response not to have error' : 'Expected response to have error'),\n };\n};\n\n/**\n * Check if JSON-RPC response has a specific error code\n */\nconst toHaveErrorCode: MatcherFunction<[code: number]> = function (received, code) {\n const response = received as { error?: { code: number } };\n\n if (typeof response !== 'object' || response === null) {\n return {\n pass: false,\n message: () => `Expected an object, but received ${typeof received}`,\n };\n }\n\n const actualCode = response.error?.code;\n const pass = actualCode === code;\n\n return {\n pass,\n message: () =>\n pass\n ? `Expected response not to have error code ${code}`\n : `Expected error code ${code}, but got ${actualCode ?? 'no error'}`,\n };\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// EXPORTS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * All MCP matchers as an object for expect.extend()\n */\nexport const mcpMatchers = {\n // Tool matchers\n toContainTool,\n toBeSuccessful,\n toBeError,\n toHaveTextContent,\n toHaveImageContent,\n toHaveResourceContent,\n\n // Resource matchers\n toContainResource,\n toContainResourceTemplate,\n toHaveMimeType,\n\n // Prompt matchers\n toContainPrompt,\n toHaveMessages,\n\n // Protocol matchers\n toBeValidJsonRpc,\n toHaveResult,\n toHaveError,\n toHaveErrorCode,\n\n // UI matchers (for testing tool UI responses)\n ...uiMatchers,\n};\n"]}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @file playwright/index.ts
3
+ * @description Playwright integration for OAuth flow testing (Future Phase)
4
+ *
5
+ * This module will provide Playwright-based testing utilities for:
6
+ * - OAuth consent flow testing
7
+ * - Login page interactions
8
+ * - Browser-based authentication flows
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { test, expect } from '@frontmcp/testing/playwright';
13
+ * import MyServer from './src/main';
14
+ *
15
+ * test.describe('OAuth Flow', () => {
16
+ * test.use({
17
+ * server: MyServer,
18
+ * auth: { mode: 'orchestrated' }
19
+ * });
20
+ *
21
+ * test('completes OAuth flow', async ({ page, oauth }) => {
22
+ * const { authorizeUrl } = await oauth.startFlow({ ... });
23
+ * await page.goto(authorizeUrl);
24
+ * // Test login and consent pages
25
+ * });
26
+ * });
27
+ * ```
28
+ */
29
+ export declare const playwrightIntegration: {
30
+ version: string;
31
+ status: string;
32
+ description: string;
33
+ };
34
+ export declare function test(_name: string, _fn: () => Promise<void>): void;
35
+ export declare const expect: {
36
+ notImplemented: boolean;
37
+ };
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * @file playwright/index.ts
4
+ * @description Playwright integration for OAuth flow testing (Future Phase)
5
+ *
6
+ * This module will provide Playwright-based testing utilities for:
7
+ * - OAuth consent flow testing
8
+ * - Login page interactions
9
+ * - Browser-based authentication flows
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { test, expect } from '@frontmcp/testing/playwright';
14
+ * import MyServer from './src/main';
15
+ *
16
+ * test.describe('OAuth Flow', () => {
17
+ * test.use({
18
+ * server: MyServer,
19
+ * auth: { mode: 'orchestrated' }
20
+ * });
21
+ *
22
+ * test('completes OAuth flow', async ({ page, oauth }) => {
23
+ * const { authorizeUrl } = await oauth.startFlow({ ... });
24
+ * await page.goto(authorizeUrl);
25
+ * // Test login and consent pages
26
+ * });
27
+ * });
28
+ * ```
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.expect = exports.playwrightIntegration = void 0;
32
+ exports.test = test;
33
+ // Placeholder exports - will be implemented in future phase
34
+ exports.playwrightIntegration = {
35
+ version: '0.4.0',
36
+ status: 'planned',
37
+ description: 'Playwright integration for OAuth flow testing - coming in a future release',
38
+ };
39
+ // Export placeholder test function
40
+ function test(_name, _fn) {
41
+ throw new Error('Playwright integration not yet implemented. ' +
42
+ 'Use @frontmcp/testing for non-browser tests, ' +
43
+ 'or wait for a future release that includes playwright support.');
44
+ }
45
+ // Export placeholder expect
46
+ exports.expect = {
47
+ notImplemented: true,
48
+ };
49
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/playwright/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;;;AAUH,oBAMC;AAdD,4DAA4D;AAC/C,QAAA,qBAAqB,GAAG;IACnC,OAAO,EAAE,OAAO;IAChB,MAAM,EAAE,SAAS;IACjB,WAAW,EAAE,4EAA4E;CAC1F,CAAC;AAEF,mCAAmC;AACnC,SAAgB,IAAI,CAAC,KAAa,EAAE,GAAwB;IAC1D,MAAM,IAAI,KAAK,CACb,8CAA8C;QAC5C,+CAA+C;QAC/C,gEAAgE,CACnE,CAAC;AACJ,CAAC;AAED,4BAA4B;AACf,QAAA,MAAM,GAAG;IACpB,cAAc,EAAE,IAAI;CACrB,CAAC","sourcesContent":["/**\n * @file playwright/index.ts\n * @description Playwright integration for OAuth flow testing (Future Phase)\n *\n * This module will provide Playwright-based testing utilities for:\n * - OAuth consent flow testing\n * - Login page interactions\n * - Browser-based authentication flows\n *\n * @example\n * ```typescript\n * import { test, expect } from '@frontmcp/testing/playwright';\n * import MyServer from './src/main';\n *\n * test.describe('OAuth Flow', () => {\n * test.use({\n * server: MyServer,\n * auth: { mode: 'orchestrated' }\n * });\n *\n * test('completes OAuth flow', async ({ page, oauth }) => {\n * const { authorizeUrl } = await oauth.startFlow({ ... });\n * await page.goto(authorizeUrl);\n * // Test login and consent pages\n * });\n * });\n * ```\n */\n\n// Placeholder exports - will be implemented in future phase\nexport const playwrightIntegration = {\n version: '0.4.0',\n status: 'planned',\n description: 'Playwright integration for OAuth flow testing - coming in a future release',\n};\n\n// Export placeholder test function\nexport function test(_name: string, _fn: () => Promise<void>): void {\n throw new Error(\n 'Playwright integration not yet implemented. ' +\n 'Use @frontmcp/testing for non-browser tests, ' +\n 'or wait for a future release that includes playwright support.',\n );\n}\n\n// Export placeholder expect\nexport const expect = {\n notImplemented: true,\n};\n"]}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @file server/index.ts
3
+ * @description Test server management exports
4
+ */
5
+ export { TestServer } from './test-server';
6
+ export type { TestServerOptions, TestServerInfo } from './test-server';
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * @file server/index.ts
4
+ * @description Test server management exports
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.TestServer = void 0;
8
+ var test_server_1 = require("./test-server");
9
+ Object.defineProperty(exports, "TestServer", { enumerable: true, get: function () { return test_server_1.TestServer; } });
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,6CAA2C;AAAlC,yGAAA,UAAU,OAAA","sourcesContent":["/**\n * @file server/index.ts\n * @description Test server management exports\n */\n\nexport { TestServer } from './test-server';\nexport type { TestServerOptions, TestServerInfo } from './test-server';\n"]}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @file test-server.ts
3
+ * @description Test server management for E2E testing
4
+ */
5
+ export interface TestServerOptions {
6
+ /** Port to run the server on (default: random available port) */
7
+ port?: number;
8
+ /** Command to start the server */
9
+ command?: string;
10
+ /** Working directory */
11
+ cwd?: string;
12
+ /** Environment variables */
13
+ env?: Record<string, string>;
14
+ /** Timeout for server startup in milliseconds (default: 30000) */
15
+ startupTimeout?: number;
16
+ /** Path to check for server readiness (default: /health) */
17
+ healthCheckPath?: string;
18
+ /** Enable debug logging */
19
+ debug?: boolean;
20
+ }
21
+ export interface TestServerInfo {
22
+ /** Base URL of the server */
23
+ baseUrl: string;
24
+ /** Port the server is running on */
25
+ port: number;
26
+ /** Process ID (if available) */
27
+ pid?: number;
28
+ }
29
+ /**
30
+ * Manages test server lifecycle for E2E testing
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * // Start a server with custom command
35
+ * const server = await TestServer.start({
36
+ * command: 'node dist/main.js',
37
+ * port: 3003,
38
+ * cwd: './apps/my-server',
39
+ * });
40
+ *
41
+ * // Or start an Nx project
42
+ * const server = await TestServer.startNx('demo-public', { port: 3003 });
43
+ *
44
+ * // Use the server
45
+ * console.log(server.info.baseUrl); // http://localhost:3003
46
+ *
47
+ * // Stop when done
48
+ * await server.stop();
49
+ * ```
50
+ */
51
+ export declare class TestServer {
52
+ private process;
53
+ private readonly options;
54
+ private _info;
55
+ private logs;
56
+ private constructor();
57
+ /**
58
+ * Start a test server with custom command
59
+ */
60
+ static start(options: TestServerOptions): Promise<TestServer>;
61
+ /**
62
+ * Start an Nx project as test server
63
+ */
64
+ static startNx(project: string, options?: Partial<TestServerOptions>): Promise<TestServer>;
65
+ /**
66
+ * Create a test server connected to an already running server
67
+ */
68
+ static connect(baseUrl: string): TestServer;
69
+ /**
70
+ * Get server information
71
+ */
72
+ get info(): TestServerInfo;
73
+ /**
74
+ * Stop the test server
75
+ */
76
+ stop(): Promise<void>;
77
+ /**
78
+ * Wait for server to be ready
79
+ */
80
+ waitForReady(timeout?: number): Promise<void>;
81
+ /**
82
+ * Restart the server
83
+ */
84
+ restart(): Promise<void>;
85
+ /**
86
+ * Get captured server logs
87
+ */
88
+ getLogs(): string[];
89
+ /**
90
+ * Clear captured logs
91
+ */
92
+ clearLogs(): void;
93
+ private startProcess;
94
+ private log;
95
+ }
96
+ /**
97
+ * Find an available port
98
+ */
99
+ export declare function findAvailablePort(): Promise<number>;