@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.
- package/LICENSE +15 -0
- package/README.md +423 -0
- package/browserControl.js +113 -0
- package/cli.js +187 -0
- package/docs/api/tool-reference.md +317 -0
- package/docs/api_tools_usage.md +477 -0
- package/docs/development/adding-tools.md +274 -0
- package/docs/development/configuration.md +332 -0
- package/docs/examples/authentication.md +124 -0
- package/docs/examples/basic-automation.md +105 -0
- package/docs/getting-started.md +214 -0
- package/docs/index.md +61 -0
- package/mcpServer.js +280 -0
- package/package.json +83 -0
- package/run-server.js +140 -0
- package/src/config/environments/api-only.js +53 -0
- package/src/config/environments/development.js +54 -0
- package/src/config/environments/production.js +69 -0
- package/src/config/index.js +341 -0
- package/src/config/server.js +41 -0
- package/src/config/tools/api.js +67 -0
- package/src/config/tools/browser.js +90 -0
- package/src/config/tools/default.js +32 -0
- package/src/services/browserService.js +325 -0
- package/src/tools/api/api-request.js +641 -0
- package/src/tools/api/api-session-report.js +1262 -0
- package/src/tools/api/api-session-status.js +395 -0
- package/src/tools/base/ToolBase.js +230 -0
- package/src/tools/base/ToolRegistry.js +269 -0
- package/src/tools/browser/advanced/browser-console.js +384 -0
- package/src/tools/browser/advanced/browser-dialog.js +319 -0
- package/src/tools/browser/advanced/browser-evaluate.js +337 -0
- package/src/tools/browser/advanced/browser-file.js +480 -0
- package/src/tools/browser/advanced/browser-keyboard.js +343 -0
- package/src/tools/browser/advanced/browser-mouse.js +332 -0
- package/src/tools/browser/advanced/browser-network.js +421 -0
- package/src/tools/browser/advanced/browser-pdf.js +407 -0
- package/src/tools/browser/advanced/browser-tabs.js +497 -0
- package/src/tools/browser/advanced/browser-wait.js +378 -0
- package/src/tools/browser/click.js +168 -0
- package/src/tools/browser/close.js +60 -0
- package/src/tools/browser/dom.js +70 -0
- package/src/tools/browser/launch.js +67 -0
- package/src/tools/browser/navigate.js +270 -0
- package/src/tools/browser/screenshot.js +351 -0
- package/src/tools/browser/type.js +174 -0
- package/src/tools/index.js +95 -0
- 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
|
+
'&': '&',
|
|
1253
|
+
'<': '<',
|
|
1254
|
+
'>': '>',
|
|
1255
|
+
'"': '"',
|
|
1256
|
+
"'": '''
|
|
1257
|
+
};
|
|
1258
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
module.exports = ApiSessionReportTool;
|