@democratize-quality/mcp-server 1.1.9 → 1.2.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 (62) hide show
  1. package/dist/server.d.ts +41 -0
  2. package/dist/server.d.ts.map +1 -0
  3. package/dist/server.js +225 -0
  4. package/dist/server.js.map +1 -0
  5. package/package.json +23 -24
  6. package/browserControl.js +0 -113
  7. package/cli.js +0 -228
  8. package/mcpServer.js +0 -335
  9. package/run-server.js +0 -140
  10. package/src/chatmodes//360/237/214/220 api-generator.chatmode.md" +0 -409
  11. package/src/chatmodes//360/237/214/220 api-healer.chatmode.md" +0 -494
  12. package/src/chatmodes//360/237/214/220 api-planner.chatmode.md" +0 -954
  13. package/src/config/environments/api-only.js +0 -53
  14. package/src/config/environments/development.js +0 -54
  15. package/src/config/environments/production.js +0 -69
  16. package/src/config/index.js +0 -341
  17. package/src/config/server.js +0 -41
  18. package/src/config/tools/api.js +0 -67
  19. package/src/config/tools/browser.js +0 -90
  20. package/src/config/tools/default.js +0 -32
  21. package/src/docs/Agent_README.md +0 -310
  22. package/src/docs/QUICK_REFERENCE.md +0 -111
  23. package/src/services/browserService.js +0 -325
  24. package/src/skills/api-planning/SKILL.md +0 -224
  25. package/src/skills/test-execution/SKILL.md +0 -777
  26. package/src/skills/test-generation/SKILL.md +0 -309
  27. package/src/skills/test-healing/SKILL.md +0 -405
  28. package/src/tools/api/api-generator.js +0 -1865
  29. package/src/tools/api/api-healer.js +0 -617
  30. package/src/tools/api/api-planner.js +0 -2598
  31. package/src/tools/api/api-project-setup.js +0 -313
  32. package/src/tools/api/api-request.js +0 -641
  33. package/src/tools/api/api-session-report.js +0 -1278
  34. package/src/tools/api/api-session-status.js +0 -395
  35. package/src/tools/api/prompts/README.md +0 -293
  36. package/src/tools/api/prompts/generation-prompts.js +0 -703
  37. package/src/tools/api/prompts/healing-prompts.js +0 -195
  38. package/src/tools/api/prompts/index.js +0 -25
  39. package/src/tools/api/prompts/orchestrator.js +0 -334
  40. package/src/tools/api/prompts/validation-rules.js +0 -339
  41. package/src/tools/base/ToolBase.js +0 -230
  42. package/src/tools/base/ToolRegistry.js +0 -269
  43. package/src/tools/browser/advanced/browser-console.js +0 -384
  44. package/src/tools/browser/advanced/browser-dialog.js +0 -319
  45. package/src/tools/browser/advanced/browser-evaluate.js +0 -337
  46. package/src/tools/browser/advanced/browser-file.js +0 -480
  47. package/src/tools/browser/advanced/browser-keyboard.js +0 -343
  48. package/src/tools/browser/advanced/browser-mouse.js +0 -332
  49. package/src/tools/browser/advanced/browser-network.js +0 -421
  50. package/src/tools/browser/advanced/browser-pdf.js +0 -407
  51. package/src/tools/browser/advanced/browser-tabs.js +0 -497
  52. package/src/tools/browser/advanced/browser-wait.js +0 -378
  53. package/src/tools/browser/click.js +0 -168
  54. package/src/tools/browser/close.js +0 -60
  55. package/src/tools/browser/dom.js +0 -70
  56. package/src/tools/browser/launch.js +0 -67
  57. package/src/tools/browser/navigate.js +0 -270
  58. package/src/tools/browser/screenshot.js +0 -351
  59. package/src/tools/browser/type.js +0 -174
  60. package/src/tools/index.js +0 -95
  61. package/src/utils/agentInstaller.js +0 -418
  62. package/src/utils/browserHelpers.js +0 -83
@@ -1,1278 +0,0 @@
1
- const ToolBase = require('../base/ToolBase');
2
- const fs = require('fs');
3
- const path = require('path');
4
- const os = require('os');
5
-
6
- /**
7
- * API Session Report Tool - Generate comprehensive HTML reports for API test sessions
8
- */
9
- class ApiSessionReportTool extends ToolBase {
10
- static definition = {
11
- name: "api_session_report",
12
- description: "Generate comprehensive HTML report for API test session with detailed request/response logs, validation results, and timing analysis.",
13
- input_schema: {
14
- type: "object",
15
- properties: {
16
- sessionId: {
17
- type: "string",
18
- description: "The session ID to generate report for"
19
- },
20
- outputPath: {
21
- type: "string",
22
- description: "Path where to save the HTML report (relative to output directory)"
23
- },
24
- title: {
25
- type: "string",
26
- default: "API Test Session Report",
27
- description: "Title for the HTML report"
28
- },
29
- includeRequestData: {
30
- type: "boolean",
31
- default: true,
32
- description: "Whether to include request data in the report"
33
- },
34
- includeResponseData: {
35
- type: "boolean",
36
- default: true,
37
- description: "Whether to include response data in the report"
38
- },
39
- includeTiming: {
40
- type: "boolean",
41
- default: true,
42
- description: "Whether to include timing analysis"
43
- },
44
- theme: {
45
- type: "string",
46
- enum: ["light", "dark", "auto"],
47
- default: "light",
48
- description: "Report theme"
49
- }
50
- },
51
- required: ["sessionId", "outputPath"]
52
- },
53
- output_schema: {
54
- type: "object",
55
- properties: {
56
- success: { type: "boolean", description: "Whether report was generated successfully" },
57
- reportPath: { type: "string", description: "Path to the generated report" },
58
- fileSize: { type: "number", description: "Size of generated report in bytes" },
59
- sessionSummary: {
60
- type: "object",
61
- properties: {
62
- requestCount: { type: "number" },
63
- successRate: { type: "number" },
64
- totalDuration: { type: "number" }
65
- },
66
- description: "Summary of session metrics"
67
- },
68
- reportUrl: { type: "string", description: "URL to view the report" }
69
- },
70
- required: ["success"]
71
- }
72
- };
73
-
74
- constructor() {
75
- super();
76
- // Access the global session store
77
- if (!global.__API_SESSION_STORE__) {
78
- global.__API_SESSION_STORE__ = new Map();
79
- }
80
- this.sessionStore = global.__API_SESSION_STORE__;
81
-
82
- // Try to use reports directory in current working directory first
83
- let defaultOutputDir;
84
- try {
85
- defaultOutputDir = path.join(process.cwd(), 'reports');
86
- } catch (error) {
87
- // Fallback to existing logic if current working directory is not accessible
88
- defaultOutputDir = process.env.HOME
89
- ? path.join(process.env.HOME, '.mcp-browser-control')
90
- : path.join(os.tmpdir(), 'mcp-browser-control');
91
- }
92
-
93
- this.outputDir = process.env.OUTPUT_DIR || defaultOutputDir;
94
-
95
- // Ensure output directory exists
96
- try {
97
- if (!fs.existsSync(this.outputDir)) {
98
- fs.mkdirSync(this.outputDir, { recursive: true });
99
- }
100
- } catch (error) {
101
- console.warn(`Warning: Could not create output directory ${this.outputDir}:`, error.message);
102
- }
103
- }
104
-
105
- async execute(parameters) {
106
- const {
107
- sessionId,
108
- outputPath,
109
- title = "API Test Session Report",
110
- includeRequestData = true,
111
- includeResponseData = true,
112
- includeTiming = true,
113
- theme = "light"
114
- } = parameters;
115
-
116
- const session = this.sessionStore.get(sessionId);
117
-
118
- if (!session) {
119
- return {
120
- success: false,
121
- error: `Session not found: ${sessionId}`,
122
- availableSessions: Array.from(this.sessionStore.keys())
123
- };
124
- }
125
-
126
- try {
127
- // Ensure output directory exists
128
- if (!fs.existsSync(this.outputDir)) {
129
- fs.mkdirSync(this.outputDir, { recursive: true });
130
- }
131
-
132
- // Generate report data
133
- const reportData = this.generateReportData(
134
- session,
135
- includeRequestData,
136
- includeResponseData,
137
- includeTiming
138
- );
139
-
140
- // Generate HTML content
141
- const htmlContent = this.generateHtmlReport(reportData, title, theme);
142
-
143
- // Write report to file
144
- const fullOutputPath = path.join(this.outputDir, outputPath);
145
- const outputDir = path.dirname(fullOutputPath);
146
-
147
- if (!fs.existsSync(outputDir)) {
148
- fs.mkdirSync(outputDir, { recursive: true });
149
- }
150
-
151
- fs.writeFileSync(fullOutputPath, htmlContent, 'utf8');
152
-
153
- // Get file size
154
- const stats = fs.statSync(fullOutputPath);
155
-
156
- return {
157
- success: true,
158
- reportPath: fullOutputPath,
159
- fileSize: stats.size,
160
- sessionSummary: {
161
- requestCount: reportData.summary.totalRequests,
162
- successRate: reportData.summary.successRate,
163
- totalDuration: reportData.session.executionTime || 0
164
- },
165
- reportUrl: `file://${fullOutputPath}`
166
- };
167
-
168
- } catch (error) {
169
- return {
170
- success: false,
171
- error: `Failed to generate report: ${error.message}`
172
- };
173
- }
174
- }
175
-
176
- /**
177
- * Generate structured report data from session
178
- */
179
- generateReportData(session, includeRequestData, includeResponseData, includeTiming) {
180
- const logs = session.logs || [];
181
-
182
- // Generate summary
183
- const summary = this.generateSummary(logs);
184
-
185
- // Process logs for display
186
- const processedLogs = logs.map(log => this.processLogForReport(
187
- log,
188
- includeRequestData,
189
- includeResponseData
190
- ));
191
-
192
- // Generate timing data if requested
193
- const timingData = includeTiming ? this.generateTimingData(logs, session) : null;
194
-
195
- return {
196
- session: {
197
- sessionId: session.sessionId,
198
- status: session.status,
199
- startTime: session.startTime,
200
- endTime: session.endTime,
201
- executionTime: session.executionTime,
202
- error: session.error
203
- },
204
- summary,
205
- logs: processedLogs,
206
- timing: timingData,
207
- metadata: {
208
- generatedAt: new Date().toISOString(),
209
- includeRequestData,
210
- includeResponseData,
211
- includeTiming
212
- }
213
- };
214
- }
215
-
216
- /**
217
- * Generate summary statistics
218
- */
219
- generateSummary(logs) {
220
- let totalRequests = 0;
221
- let successfulRequests = 0;
222
- let failedRequests = 0;
223
- let validationsPassed = 0;
224
- let validationsFailed = 0;
225
- let chainStepCount = 0;
226
- let singleRequestCount = 0;
227
-
228
- // Process chain steps and single requests separately
229
- const chainSteps = logs
230
- .filter(log => log.type === 'chain' && log.steps)
231
- .flatMap(log => log.steps || []);
232
-
233
- const singleRequests = logs.filter(
234
- log => (log.type === 'single' || log.type === 'request') &&
235
- log.request && log.response
236
- );
237
-
238
- // Process chain steps
239
- for (const step of chainSteps) {
240
- if (step.request && step.response) {
241
- totalRequests++;
242
- chainStepCount++;
243
-
244
- const isValid = step.validation &&
245
- step.bodyValidation &&
246
- step.validation.status &&
247
- step.validation.contentType &&
248
- step.bodyValidation.matched;
249
-
250
- if (isValid) {
251
- successfulRequests++;
252
- validationsPassed++;
253
- } else {
254
- failedRequests++;
255
- validationsFailed++;
256
- }
257
- }
258
- }
259
-
260
- // Process single requests
261
- for (const request of singleRequests) {
262
- totalRequests++;
263
- singleRequestCount++;
264
-
265
- const isValid = request.validation &&
266
- request.bodyValidation &&
267
- request.validation.status &&
268
- request.validation.contentType &&
269
- request.bodyValidation.matched;
270
-
271
- if (isValid) {
272
- successfulRequests++;
273
- validationsPassed++;
274
- } else {
275
- failedRequests++;
276
- validationsFailed++;
277
- }
278
- }
279
-
280
- return {
281
- totalRequests,
282
- successfulRequests,
283
- failedRequests,
284
- successRate: totalRequests > 0 ? Math.round((successfulRequests / totalRequests) * 100) / 100 : 0,
285
- validationsPassed,
286
- validationsFailed,
287
- validationRate: (validationsPassed + validationsFailed) > 0
288
- ? Math.round((validationsPassed / (validationsPassed + validationsFailed)) * 100) / 100
289
- : 0,
290
- logEntries: logs.length,
291
- chainSteps: chainStepCount,
292
- singleRequests: singleRequestCount
293
- };
294
- }
295
-
296
- /**
297
- * Process individual log entry for report display
298
- */
299
- processLogForReport(log, includeRequestData, includeResponseData) {
300
- const processed = {
301
- type: log.type,
302
- timestamp: log.timestamp,
303
- formattedTime: new Date(log.timestamp).toLocaleString()
304
- };
305
-
306
- // Process request data with better error handling
307
- if (log.request && includeRequestData) {
308
- processed.request = {
309
- method: log.request.method || 'UNKNOWN',
310
- url: log.request.url || 'UNKNOWN',
311
- headers: log.request.headers || {},
312
- data: log.request.data || null
313
- };
314
- } else if (log.request) {
315
- processed.request = {
316
- method: log.request.method || 'UNKNOWN',
317
- url: log.request.url || 'UNKNOWN',
318
- hasHeaders: !!(log.request.headers && Object.keys(log.request.headers).length > 0),
319
- hasData: !!log.request.data
320
- };
321
- }
322
-
323
- // Process response data with better error handling
324
- if (log.response && includeResponseData) {
325
- processed.response = {
326
- status: log.response.status || 0,
327
- statusText: log.response.statusText || this.getStatusTextFromCode(log.response.status),
328
- contentType: log.response.contentType || 'UNKNOWN',
329
- headers: log.response.headers || {},
330
- body: log.response.body || null
331
- };
332
- } else if (log.response) {
333
- processed.response = {
334
- status: log.response.status || 0,
335
- statusText: log.response.statusText || this.getStatusTextFromCode(log.response.status),
336
- contentType: log.response.contentType || 'UNKNOWN',
337
- hasHeaders: !!(log.response.headers && Object.keys(log.response.headers).length > 0),
338
- bodySize: log.response.body ? log.response.body.length : 0
339
- };
340
- }
341
-
342
- // Process chain steps with better data handling
343
- if (log.steps) {
344
- processed.steps = log.steps.map(step => ({
345
- method: step.method || 'UNKNOWN',
346
- url: step.url || 'UNKNOWN',
347
- status: step.status || 0,
348
- timestamp: step.timestamp || log.timestamp,
349
- data: step.data || null,
350
- headers: step.headers || {},
351
- expectations: step.expectations || {},
352
- validation: step.validation || {},
353
- bodyValidation: step.bodyValidation || {}
354
- }));
355
- }
356
-
357
- // Process validation data for single requests
358
- if (log.validation || log.bodyValidation) {
359
- processed.validation = log.validation || {};
360
- processed.bodyValidation = log.bodyValidation || {};
361
- processed.expectations = log.expectations || {};
362
- }
363
-
364
- return processed;
365
- }
366
-
367
- /**
368
- * Generate timing analysis data
369
- */
370
- generateTimingData(logs, session) {
371
- // Process chain steps and single requests separately
372
- const chainSteps = logs
373
- .filter(log => log.type === 'chain' && log.steps)
374
- .flatMap(log => log.steps || [])
375
- .filter(step => step.timestamp && step.method && step.url && step.status);
376
-
377
- const singleRequests = logs
378
- .filter(log => (log.type === 'single' || log.type === 'request') &&
379
- log.request && log.response)
380
- .map(req => ({
381
- timestamp: req.timestamp,
382
- method: req.request.method,
383
- url: req.request.url,
384
- status: req.response.status
385
- }));
386
-
387
- const allRequests = [...chainSteps, ...singleRequests];
388
-
389
- if (allRequests.length === 0) {
390
- return null;
391
- }
392
-
393
- // Sort requests by timestamp to ensure correct timing
394
- allRequests.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
395
-
396
- const sessionStart = new Date(session.startTime).getTime();
397
- const timings = allRequests.map((req, i) => {
398
- const requestTime = new Date(req.timestamp).getTime();
399
- const relativeTime = requestTime - sessionStart;
400
-
401
- return {
402
- index: i,
403
- timestamp: req.timestamp,
404
- relativeTimeMs: relativeTime,
405
- method: req.method || 'UNKNOWN',
406
- url: req.url || 'UNKNOWN',
407
- status: req.status || 0
408
- };
409
- });
410
-
411
- // Calculate actual intervals between requests
412
- const intervals = timings.map((t, i) => i === 0 ? 0 : t.relativeTimeMs - timings[i - 1].relativeTimeMs);
413
- const averageInterval = intervals.length > 1 ? Math.round(intervals.reduce((a, b) => a + b) / (intervals.length - 1)) : 0;
414
-
415
- // Calculate session duration using first and last request timestamps
416
- const firstRequest = allRequests[0];
417
- const lastRequest = allRequests[allRequests.length - 1];
418
- const sessionDuration = lastRequest
419
- ? (new Date(lastRequest.timestamp).getTime() - new Date(firstRequest.timestamp).getTime())
420
- : 0;
421
-
422
- return {
423
- sessionDurationMs: sessionDuration,
424
- requestCount: allRequests.length,
425
- averageIntervalMs: averageInterval,
426
- timings
427
- };
428
- }
429
-
430
- /**
431
- * Generate complete HTML report
432
- */
433
- generateHtmlReport(reportData, title, theme) {
434
- const css = this.generateCSS(theme);
435
- const jsCode = this.generateJavaScript();
436
-
437
- return `<!DOCTYPE html>
438
- <html lang="en">
439
- <head>
440
- <meta charset="UTF-8">
441
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
442
- <title>${this.escapeHtml(title)}</title>
443
- <style>${css}</style>
444
- </head>
445
- <body class="theme-${theme}">
446
- <div class="container">
447
- <header class="header">
448
- <h1>${this.escapeHtml(title)}</h1>
449
- <div class="session-info">
450
- <span class="session-id">Session: ${this.escapeHtml(reportData.session.sessionId)}</span>
451
- <span class="status status-${reportData.session.status}">${reportData.session.status}</span>
452
- </div>
453
- </header>
454
-
455
- <section class="summary">
456
- <h2>Summary</h2>
457
- <div class="summary-grid">
458
- <div class="summary-card">
459
- <div class="summary-value">${reportData.summary.totalRequests}</div>
460
- <div class="summary-label">Total Requests</div>
461
- </div>
462
- <div class="summary-card success">
463
- <div class="summary-value">${reportData.summary.successfulRequests}</div>
464
- <div class="summary-label">Successful</div>
465
- </div>
466
- <div class="summary-card failure">
467
- <div class="summary-value">${reportData.summary.failedRequests}</div>
468
- <div class="summary-label">Failed</div>
469
- </div>
470
- <div class="summary-card">
471
- <div class="summary-value">${Math.round(reportData.summary.successRate * 100)}%</div>
472
- <div class="summary-label">Success Rate</div>
473
- </div>
474
- <div class="summary-card">
475
- <div class="summary-value">${reportData.session.executionTime || 0}ms</div>
476
- <div class="summary-label">Duration</div>
477
- </div>
478
- <div class="summary-card">
479
- <div class="summary-value">${Math.round(reportData.summary.validationRate * 100)}%</div>
480
- <div class="summary-label">Validation Rate</div>
481
- </div>
482
- </div>
483
- </section>
484
-
485
- ${reportData.timing ? this.generateTimingSection(reportData.timing) : ''}
486
-
487
- <section class="logs">
488
- <h2>Request Logs</h2>
489
- <div class="logs-container">
490
- ${reportData.logs.map((log, index) => this.generateLogEntry(log, index)).join('')}
491
- </div>
492
- </section>
493
-
494
- <footer class="footer">
495
- <p>Report generated at ${new Date(reportData.metadata.generatedAt).toLocaleString()}</p>
496
- </footer>
497
- </div>
498
-
499
- <script>${jsCode}</script>
500
- </body>
501
- </html>`;
502
- }
503
-
504
- /**
505
- * Generate CSS styles for the report
506
- */
507
- generateCSS(theme) {
508
- return `
509
- * {
510
- margin: 0;
511
- padding: 0;
512
- box-sizing: border-box;
513
- }
514
-
515
- body {
516
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
517
- line-height: 1.6;
518
- color: #333;
519
- background-color: #f5f5f5;
520
- }
521
-
522
- .theme-dark {
523
- color: #e0e0e0;
524
- background-color: #1a1a1a;
525
- }
526
-
527
- .container {
528
- max-width: 1200px;
529
- margin: 0 auto;
530
- padding: 20px;
531
- }
532
-
533
- .header {
534
- background: white;
535
- padding: 30px;
536
- border-radius: 8px;
537
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
538
- margin-bottom: 20px;
539
- display: flex;
540
- justify-content: space-between;
541
- align-items: center;
542
- }
543
-
544
- .theme-dark .header {
545
- background: #2d2d2d;
546
- }
547
-
548
- .header h1 {
549
- color: #2c3e50;
550
- font-size: 2em;
551
- }
552
-
553
- .theme-dark .header h1 {
554
- color: #ecf0f1;
555
- }
556
-
557
- .session-info {
558
- display: flex;
559
- gap: 15px;
560
- align-items: center;
561
- }
562
-
563
- .session-id {
564
- font-family: monospace;
565
- background: #f8f9fa;
566
- padding: 5px 10px;
567
- border-radius: 4px;
568
- font-size: 0.9em;
569
- }
570
-
571
- .theme-dark .session-id {
572
- background: #3a3a3a;
573
- }
574
-
575
- .status {
576
- padding: 5px 12px;
577
- border-radius: 20px;
578
- font-size: 0.8em;
579
- font-weight: bold;
580
- text-transform: uppercase;
581
- }
582
-
583
- .status-completed {
584
- background: #d4edda;
585
- color: #155724;
586
- }
587
-
588
- .status-running {
589
- background: #fff3cd;
590
- color: #856404;
591
- }
592
-
593
- .status-failed {
594
- background: #f8d7da;
595
- color: #721c24;
596
- }
597
-
598
- .summary {
599
- background: white;
600
- padding: 30px;
601
- border-radius: 8px;
602
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
603
- margin-bottom: 20px;
604
- }
605
-
606
- .theme-dark .summary {
607
- background: #2d2d2d;
608
- }
609
-
610
- .summary h2 {
611
- margin-bottom: 20px;
612
- color: #2c3e50;
613
- }
614
-
615
- .theme-dark .summary h2 {
616
- color: #ecf0f1;
617
- }
618
-
619
- .summary-grid {
620
- display: grid;
621
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
622
- gap: 20px;
623
- }
624
-
625
- .summary-card {
626
- text-align: center;
627
- padding: 20px;
628
- background: #f8f9fa;
629
- border-radius: 8px;
630
- border-left: 4px solid #6c757d;
631
- }
632
-
633
- .theme-dark .summary-card {
634
- background: #3a3a3a;
635
- }
636
-
637
- .summary-card.success {
638
- border-left-color: #28a745;
639
- }
640
-
641
- .summary-card.failure {
642
- border-left-color: #dc3545;
643
- }
644
-
645
- .summary-value {
646
- font-size: 2em;
647
- font-weight: bold;
648
- color: #2c3e50;
649
- }
650
-
651
- .theme-dark .summary-value {
652
- color: #ecf0f1;
653
- }
654
-
655
- .summary-label {
656
- font-size: 0.9em;
657
- color: #6c757d;
658
- margin-top: 5px;
659
- }
660
-
661
- .logs {
662
- background: white;
663
- padding: 30px;
664
- border-radius: 8px;
665
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
666
- margin-bottom: 20px;
667
- }
668
-
669
- .theme-dark .logs {
670
- background: #2d2d2d;
671
- }
672
-
673
- .logs h2 {
674
- margin-bottom: 20px;
675
- color: #2c3e50;
676
- }
677
-
678
- .theme-dark .logs h2 {
679
- color: #ecf0f1;
680
- }
681
-
682
- .log-entry {
683
- border: 1px solid #e9ecef;
684
- border-radius: 8px;
685
- margin-bottom: 15px;
686
- overflow: hidden;
687
- }
688
-
689
- .theme-dark .log-entry {
690
- border-color: #4a4a4a;
691
- }
692
-
693
- .log-header {
694
- background: #f8f9fa;
695
- padding: 15px;
696
- cursor: pointer;
697
- display: flex;
698
- justify-content: space-between;
699
- align-items: center;
700
- }
701
-
702
- .theme-dark .log-header {
703
- background: #3a3a3a;
704
- }
705
-
706
- .log-header:hover {
707
- background: #e9ecef;
708
- }
709
-
710
- .theme-dark .log-header:hover {
711
- background: #4a4a4a;
712
- }
713
-
714
- .log-title {
715
- font-weight: bold;
716
- display: flex;
717
- align-items: center;
718
- gap: 10px;
719
- }
720
-
721
- .method {
722
- padding: 3px 8px;
723
- border-radius: 4px;
724
- font-size: 0.8em;
725
- font-weight: bold;
726
- }
727
-
728
- .method-get { background: #28a745; color: white; }
729
- .method-post { background: #007bff; color: white; }
730
- .method-put { background: #ffc107; color: black; }
731
- .method-delete { background: #dc3545; color: white; }
732
- .method-patch { background: #6f42c1; color: white; }
733
-
734
- .status-code {
735
- padding: 3px 8px;
736
- border-radius: 4px;
737
- font-size: 0.8em;
738
- font-weight: bold;
739
- }
740
-
741
- .status-2xx { background: #28a745; color: white; }
742
- .status-3xx { background: #ffc107; color: black; }
743
- .status-4xx { background: #fd7e14; color: white; }
744
- .status-5xx { background: #dc3545; color: white; }
745
-
746
- .validation-badge {
747
- padding: 3px 8px;
748
- border-radius: 4px;
749
- font-size: 0.8em;
750
- font-weight: bold;
751
- }
752
-
753
- .validation-badge.passed {
754
- background: #28a745;
755
- color: white;
756
- }
757
-
758
- .validation-badge.failed {
759
- background: #dc3545;
760
- color: white;
761
- }
762
-
763
- .log-body {
764
- padding: 20px;
765
- background: white;
766
- display: none;
767
- }
768
-
769
- .theme-dark .log-body {
770
- background: #2d2d2d;
771
- }
772
-
773
- .log-body.expanded {
774
- display: block;
775
- }
776
-
777
- .request-response-grid {
778
- display: grid;
779
- grid-template-columns: 1fr 1fr;
780
- gap: 20px;
781
- }
782
-
783
- .request-section, .response-section {
784
- background: #f8f9fa;
785
- padding: 15px;
786
- border-radius: 6px;
787
- }
788
-
789
- .theme-dark .request-section,
790
- .theme-dark .response-section {
791
- background: #3a3a3a;
792
- }
793
-
794
- .section-title {
795
- font-weight: bold;
796
- margin-bottom: 10px;
797
- color: #2c3e50;
798
- }
799
-
800
- .theme-dark .section-title {
801
- color: #ecf0f1;
802
- }
803
-
804
- .code-block {
805
- background: #2d3748;
806
- color: #e2e8f0;
807
- padding: 15px;
808
- border-radius: 6px;
809
- font-family: 'Courier New', monospace;
810
- font-size: 0.9em;
811
- overflow-x: auto;
812
- white-space: pre-wrap;
813
- word-break: break-all;
814
- }
815
-
816
- .validation-results {
817
- margin-top: 15px;
818
- padding: 15px;
819
- border-radius: 6px;
820
- }
821
-
822
- .validation-passed {
823
- background: #d4edda;
824
- border-left: 4px solid #28a745;
825
- }
826
-
827
- .validation-failed {
828
- background: #f8d7da;
829
- border-left: 4px solid #dc3545;
830
- }
831
-
832
- .validation-comparison {
833
- margin-top: 10px;
834
- display: grid;
835
- grid-template-columns: 1fr 1fr;
836
- gap: 15px;
837
- }
838
-
839
- .expected-section, .actual-section {
840
- background: rgba(255, 255, 255, 0.5);
841
- padding: 10px;
842
- border-radius: 4px;
843
- }
844
-
845
- .theme-dark .expected-section,
846
- .theme-dark .actual-section {
847
- background: rgba(0, 0, 0, 0.2);
848
- }
849
-
850
- .comparison-title {
851
- font-weight: bold;
852
- margin-bottom: 5px;
853
- font-size: 0.9em;
854
- }
855
-
856
- .comparison-value {
857
- font-family: 'Courier New', monospace;
858
- font-size: 0.85em;
859
- padding: 5px;
860
- background: #f8f9fa;
861
- border-radius: 3px;
862
- word-break: break-all;
863
- }
864
-
865
- .theme-dark .comparison-value {
866
- background: #2d2d2d;
867
- }
868
-
869
- .timing-section {
870
- background: white;
871
- padding: 30px;
872
- border-radius: 8px;
873
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
874
- margin-bottom: 20px;
875
- }
876
-
877
- .theme-dark .timing-section {
878
- background: #2d2d2d;
879
- }
880
-
881
- .timing-chart {
882
- margin-top: 20px;
883
- padding: 20px;
884
- background: #f8f9fa;
885
- border-radius: 6px;
886
- }
887
-
888
- .theme-dark .timing-chart {
889
- background: #3a3a3a;
890
- }
891
-
892
- .footer {
893
- text-align: center;
894
- padding: 20px;
895
- color: #6c757d;
896
- font-size: 0.9em;
897
- }
898
-
899
- @media (max-width: 768px) {
900
- .header {
901
- flex-direction: column;
902
- gap: 15px;
903
- text-align: center;
904
- }
905
-
906
- .request-response-grid {
907
- grid-template-columns: 1fr;
908
- }
909
-
910
- .summary-grid {
911
- grid-template-columns: repeat(2, 1fr);
912
- }
913
- }
914
- `;
915
- }
916
-
917
- /**
918
- * Generate JavaScript for interactivity
919
- */
920
- generateJavaScript() {
921
- return `
922
- document.addEventListener('DOMContentLoaded', function() {
923
- // Toggle log entry expansion
924
- const logHeaders = document.querySelectorAll('.log-header');
925
- logHeaders.forEach(header => {
926
- header.addEventListener('click', function() {
927
- const logBody = this.nextElementSibling;
928
- logBody.classList.toggle('expanded');
929
-
930
- const arrow = this.querySelector('.arrow');
931
- if (arrow) {
932
- arrow.textContent = logBody.classList.contains('expanded') ? '▼' : '▶';
933
- }
934
- });
935
- });
936
-
937
- // Pretty print JSON in code blocks
938
- const codeBlocks = document.querySelectorAll('.code-block');
939
- codeBlocks.forEach(block => {
940
- try {
941
- const content = block.textContent;
942
- const parsed = JSON.parse(content);
943
- block.textContent = JSON.stringify(parsed, null, 2);
944
- } catch (e) {
945
- // Not JSON, leave as is
946
- }
947
- });
948
- });
949
- `;
950
- }
951
-
952
- /**
953
- * Generate timing section HTML
954
- */
955
- generateTimingSection(timing) {
956
- return `
957
- <section class="timing-section">
958
- <h2>Timing Analysis</h2>
959
- <div class="summary-grid">
960
- <div class="summary-card">
961
- <div class="summary-value">${timing.sessionDurationMs}ms</div>
962
- <div class="summary-label">Total Duration</div>
963
- </div>
964
- <div class="summary-card">
965
- <div class="summary-value">${timing.requestCount}</div>
966
- <div class="summary-label">Requests</div>
967
- </div>
968
- <div class="summary-card">
969
- <div class="summary-value">${timing.averageIntervalMs}ms</div>
970
- <div class="summary-label">Avg Interval</div>
971
- </div>
972
- </div>
973
- <div class="timing-chart">
974
- <h3>Request Timeline</h3>
975
- ${timing.timings.map(t => `
976
- <div style="margin: 5px 0; padding: 5px; background: rgba(0,123,255,0.1); border-radius: 3px;">
977
- <strong>${t.method} ${this.escapeHtml(t.url)}</strong>
978
- - ${t.relativeTimeMs}ms
979
- <span class="status-code status-${Math.floor(t.status / 100)}xx">${t.status}</span>
980
- </div>
981
- `).join('')}
982
- </div>
983
- </section>
984
- `;
985
- }
986
-
987
- /**
988
- * Generate individual log entry HTML
989
- */
990
- generateLogEntry(log, index) {
991
- // Handle chain type logs
992
- if (log.type === 'chain') {
993
- if (!log.steps || log.steps.length === 0) {
994
- return '';
995
- }
996
-
997
- return `
998
- <div class="log-entry">
999
- <div class="log-header">
1000
- <div class="log-title">
1001
- <span class="arrow">▶</span>
1002
- <span style="font-weight: bold;">Chain Request (${log.steps.length} steps)</span>
1003
- </div>
1004
- <div class="log-time">${log.formattedTime || ''}</div>
1005
- </div>
1006
- <div class="log-body">
1007
- ${this.generateChainStepsHtml(log.steps)}
1008
- </div>
1009
- </div>
1010
- `;
1011
- }
1012
-
1013
- const hasValidation = (log.validation && Object.keys(log.validation).length > 0) ||
1014
- (log.bodyValidation && Object.keys(log.bodyValidation).length > 0);
1015
- const isValidationPassed = hasValidation &&
1016
- log.validation &&
1017
- log.bodyValidation &&
1018
- log.validation.status &&
1019
- log.validation.contentType &&
1020
- log.bodyValidation.matched;
1021
-
1022
- let method = 'UNKNOWN';
1023
- let url = 'UNKNOWN';
1024
- let statusCode = 0;
1025
- let statusText = '';
1026
-
1027
- if (log.request) {
1028
- method = log.request.method || 'UNKNOWN';
1029
- url = log.request.url || 'UNKNOWN';
1030
- }
1031
-
1032
- if (log.response) {
1033
- statusCode = log.response.status || 0;
1034
- statusText = log.response.statusText || this.getStatusTextFromCode(statusCode);
1035
- }
1036
-
1037
- const statusClass = statusCode > 0 ? `status-${Math.floor(statusCode / 100)}xx` : '';
1038
-
1039
- return `
1040
- <div class="log-entry">
1041
- <div class="log-header">
1042
- <div class="log-title">
1043
- <span class="arrow">▶</span>
1044
- <span class="method method-${method.toLowerCase()}">${method}</span>
1045
- <span>${this.escapeHtml(url)}</span>
1046
- ${statusCode > 0 ? `<span class="status-code ${statusClass}">${statusCode} ${statusText}</span>` : ''}
1047
- ${hasValidation ?
1048
- `<span class="validation-badge ${isValidationPassed ? 'passed' : 'failed'}">
1049
- ${isValidationPassed ? '✓' : '✗'} Validation
1050
- </span>` : ''
1051
- }
1052
- </div>
1053
- <div class="log-time">${log.formattedTime || ''}</div>
1054
- </div>
1055
- <div class="log-body">
1056
- ${this.generateRequestResponseHtml(log)}
1057
- ${hasValidation ? this.generateValidationHtml(log.validation, log.bodyValidation, log.expectations, log.response) : ''}
1058
- ${!hasValidation ? '<!-- No validation data available -->' : ''}
1059
- </div>
1060
- </div>
1061
- `;
1062
- }
1063
-
1064
- /**
1065
- * Generate chain steps HTML
1066
- */
1067
- generateChainStepsHtml(steps) {
1068
- if (!steps || steps.length === 0) {
1069
- return '<p>No steps found in chain.</p>';
1070
- }
1071
-
1072
- return `
1073
- <div class="chain-steps">
1074
- <h4>Chain Steps (${steps.length})</h4>
1075
- ${steps.map((step, index) => {
1076
- const hasStepValidation = (step.validation && Object.keys(step.validation).length > 0) ||
1077
- (step.bodyValidation && Object.keys(step.bodyValidation).length > 0);
1078
- return `
1079
- <div class="chain-step">
1080
- <h5>Step ${index + 1}: ${step.name || 'Unnamed'}</h5>
1081
- ${this.generateRequestResponseHtml(step)}
1082
- ${hasStepValidation ? this.generateValidationHtml(
1083
- step.validation,
1084
- step.bodyValidation,
1085
- step.expectations,
1086
- {
1087
- status: step.status,
1088
- contentType: step.contentType,
1089
- body: step.body
1090
- }
1091
- ) : ''}
1092
- </div>
1093
- `;
1094
- }).join('')}
1095
- </div>
1096
- `;
1097
- }
1098
-
1099
- /**
1100
- * Generate request/response HTML
1101
- */
1102
- generateRequestResponseHtml(log) {
1103
- // Skip if no request or response data
1104
- if (!log.request && !log.response) {
1105
- return '<p>No request/response data available</p>';
1106
- }
1107
-
1108
- return `
1109
- <div class="request-response-grid">
1110
- <div class="request-section">
1111
- <div class="section-title">Request</div>
1112
- ${log.request ? `
1113
- <p><strong>Method:</strong> ${log.request.method || 'UNKNOWN'}</p>
1114
- <p><strong>URL:</strong> ${this.escapeHtml(log.request.url || 'UNKNOWN')}</p>
1115
- ${Object.keys(log.request.headers || {}).length > 0 ? `
1116
- <p><strong>Headers:</strong></p>
1117
- <div class="code-block">${this.escapeHtml(JSON.stringify(log.request.headers, null, 2))}</div>
1118
- ` : ''}
1119
- ${log.request.data ? `
1120
- <p><strong>Body:</strong></p>
1121
- <div class="code-block">${this.escapeHtml(JSON.stringify(log.request.data, null, 2))}</div>
1122
- ` : ''}
1123
- ` : '<p>No request data available</p>'}
1124
- </div>
1125
- <div class="response-section">
1126
- <div class="section-title">Response</div>
1127
- ${log.response ? `
1128
- <p><strong>Status:</strong> ${log.response.status || 'UNKNOWN'}${log.response.statusText || this.getStatusTextFromCode(log.response.status) ? ` - ${log.response.statusText || this.getStatusTextFromCode(log.response.status)}` : ''}</p>
1129
- ${log.response.contentType ? `<p><strong>Content Type:</strong> ${log.response.contentType}</p>` : ''}
1130
- ${Object.keys(log.response.headers || {}).length > 0 ? `
1131
- <p><strong>Headers:</strong></p>
1132
- <div class="code-block">${this.escapeHtml(JSON.stringify(log.response.headers, null, 2))}</div>
1133
- ` : ''}
1134
- ${log.response.body !== undefined ? `
1135
- <p><strong>Body:</strong></p>
1136
- <div class="code-block">${
1137
- typeof log.response.body === 'string'
1138
- ? this.escapeHtml(log.response.body)
1139
- : this.escapeHtml(JSON.stringify(log.response.body, null, 2))
1140
- }</div>
1141
- ` : ''}
1142
- ` : '<p>No response data available</p>'}
1143
- </div>
1144
- </div>
1145
- `;
1146
- }
1147
-
1148
- /**
1149
- * Generate validation results HTML
1150
- */
1151
- generateValidationHtml(validation, bodyValidation, expectations, actualResponse) {
1152
- // Handle undefined or empty validation objects
1153
- validation = validation || {};
1154
- bodyValidation = bodyValidation || {};
1155
- expectations = expectations || {};
1156
-
1157
- const isPassed = validation.status && validation.contentType && bodyValidation.matched;
1158
-
1159
- return `
1160
- <div class="validation-results ${isPassed ? 'validation-passed' : 'validation-failed'}">
1161
- <h4>Validation Results</h4>
1162
-
1163
- <!-- Status Code Validation -->
1164
- <div style="margin-bottom: 15px;">
1165
- <p><strong>Status Code:</strong> ${validation.status ? '✓ Passed' : '✗ Failed'}</p>
1166
- ${expectations.status !== undefined ? `
1167
- <div class="validation-comparison">
1168
- <div class="expected-section">
1169
- <div class="comparison-title">Expected:</div>
1170
- <div class="comparison-value">${expectations.status}</div>
1171
- </div>
1172
- <div class="actual-section">
1173
- <div class="comparison-title">Actual:</div>
1174
- <div class="comparison-value">${actualResponse?.status || 'N/A'}</div>
1175
- </div>
1176
- </div>
1177
- ` : ''}
1178
- </div>
1179
-
1180
- <!-- Content Type Validation -->
1181
- <div style="margin-bottom: 15px;">
1182
- <p><strong>Content Type:</strong> ${validation.contentType ? '✓ Passed' : '✗ Failed'}</p>
1183
- ${expectations.contentType !== undefined ? `
1184
- <div class="validation-comparison">
1185
- <div class="expected-section">
1186
- <div class="comparison-title">Expected:</div>
1187
- <div class="comparison-value">${this.escapeHtml(expectations.contentType)}</div>
1188
- </div>
1189
- <div class="actual-section">
1190
- <div class="comparison-title">Actual:</div>
1191
- <div class="comparison-value">${this.escapeHtml(actualResponse?.contentType || 'N/A')}</div>
1192
- </div>
1193
- </div>
1194
- ` : ''}
1195
- </div>
1196
-
1197
- <!-- Body Validation -->
1198
- <div style="margin-bottom: 15px;">
1199
- <p><strong>Body Validation:</strong> ${bodyValidation.matched ? '✓ Passed' : '✗ Failed'}</p>
1200
- ${bodyValidation.reason ? `<p><strong>Reason:</strong> ${this.escapeHtml(bodyValidation.reason)}</p>` : ''}
1201
-
1202
- ${(expectations.body !== undefined || expectations.bodyRegex !== undefined) ? `
1203
- <div class="validation-comparison">
1204
- <div class="expected-section">
1205
- <div class="comparison-title">Expected:</div>
1206
- <div class="comparison-value">${
1207
- expectations.bodyRegex
1208
- ? `Regex: ${this.escapeHtml(expectations.bodyRegex)}`
1209
- : this.escapeHtml(typeof expectations.body === 'object'
1210
- ? JSON.stringify(expectations.body, null, 2)
1211
- : String(expectations.body || ''))
1212
- }</div>
1213
- </div>
1214
- <div class="actual-section">
1215
- <div class="comparison-title">Actual:</div>
1216
- <div class="comparison-value">${
1217
- actualResponse?.body !== undefined
1218
- ? this.escapeHtml(typeof actualResponse.body === 'object'
1219
- ? JSON.stringify(actualResponse.body, null, 2)
1220
- : String(actualResponse.body))
1221
- : 'N/A'
1222
- }</div>
1223
- </div>
1224
- </div>
1225
- ` : ''}
1226
- </div>
1227
- </div>
1228
- `;
1229
- }
1230
-
1231
- /**
1232
- * Get status text from status code
1233
- */
1234
- getStatusTextFromCode(statusCode) {
1235
- const statusTexts = {
1236
- 200: 'OK',
1237
- 201: 'Created',
1238
- 202: 'Accepted',
1239
- 204: 'No Content',
1240
- 300: 'Multiple Choices',
1241
- 301: 'Moved Permanently',
1242
- 302: 'Found',
1243
- 304: 'Not Modified',
1244
- 400: 'Bad Request',
1245
- 401: 'Unauthorized',
1246
- 403: 'Forbidden',
1247
- 404: 'Not Found',
1248
- 405: 'Method Not Allowed',
1249
- 409: 'Conflict',
1250
- 422: 'Unprocessable Entity',
1251
- 429: 'Too Many Requests',
1252
- 500: 'Internal Server Error',
1253
- 502: 'Bad Gateway',
1254
- 503: 'Service Unavailable',
1255
- 504: 'Gateway Timeout'
1256
- };
1257
- return statusTexts[statusCode] || '';
1258
- }
1259
-
1260
- /**
1261
- * Escape HTML to prevent XSS
1262
- */
1263
- escapeHtml(text) {
1264
- if (typeof text !== 'string') {
1265
- return String(text);
1266
- }
1267
- const map = {
1268
- '&': '&amp;',
1269
- '<': '&lt;',
1270
- '>': '&gt;',
1271
- '"': '&quot;',
1272
- "'": '&#039;'
1273
- };
1274
- return text.replace(/[&<>"']/g, m => map[m]);
1275
- }
1276
- }
1277
-
1278
- module.exports = ApiSessionReportTool;