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