mcp-maestro-mobile-ai 1.1.0 → 1.3.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.
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Report Generator
3
+ *
4
+ * Generates HTML and JSON reports from test results
5
+ */
6
+
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import os from 'os';
10
+
11
+ // Report storage location
12
+ const USER_HOME = os.homedir();
13
+ const REPORTS_DIR = path.join(USER_HOME, '.maestro-mcp', 'reports');
14
+
15
+ /**
16
+ * Ensure reports directory exists
17
+ */
18
+ async function ensureReportsDir() {
19
+ await fs.mkdir(REPORTS_DIR, { recursive: true });
20
+ }
21
+
22
+ /**
23
+ * Generate a unique report ID
24
+ */
25
+ function generateReportId() {
26
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
27
+ return `report-${timestamp}`;
28
+ }
29
+
30
+ /**
31
+ * Generate JSON report
32
+ */
33
+ export async function generateJsonReport(results, metadata = {}) {
34
+ await ensureReportsDir();
35
+
36
+ const reportId = generateReportId();
37
+ const report = {
38
+ reportId,
39
+ generatedAt: new Date().toISOString(),
40
+ metadata: {
41
+ promptFile: metadata.promptFile || 'unknown',
42
+ appId: metadata.appId || 'unknown',
43
+ totalTests: results.length,
44
+ ...metadata,
45
+ },
46
+ summary: {
47
+ total: results.length,
48
+ passed: results.filter(r => r.success).length,
49
+ failed: results.filter(r => !r.success).length,
50
+ passRate: results.length > 0
51
+ ? Math.round((results.filter(r => r.success).length / results.length) * 100)
52
+ : 0,
53
+ },
54
+ tests: results.map((result, index) => ({
55
+ index: index + 1,
56
+ name: result.name,
57
+ success: result.success,
58
+ duration: result.duration || 'N/A',
59
+ attempts: result.attempts || 1,
60
+ error: result.error || null,
61
+ screenshot: result.screenshot || null,
62
+ })),
63
+ };
64
+
65
+ const jsonPath = path.join(REPORTS_DIR, `${reportId}.json`);
66
+ await fs.writeFile(jsonPath, JSON.stringify(report, null, 2), 'utf8');
67
+
68
+ return {
69
+ reportId,
70
+ jsonPath,
71
+ report,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Generate HTML report
77
+ */
78
+ export async function generateHtmlReport(results, metadata = {}) {
79
+ await ensureReportsDir();
80
+
81
+ const reportId = generateReportId();
82
+ const timestamp = new Date().toLocaleString();
83
+
84
+ const total = results.length;
85
+ const passed = results.filter(r => r.success).length;
86
+ const failed = total - passed;
87
+ const passRate = total > 0 ? Math.round((passed / total) * 100) : 0;
88
+
89
+ // Generate test rows
90
+ const testRows = results.map((result, index) => {
91
+ const statusClass = result.success ? 'passed' : 'failed';
92
+ const statusIcon = result.success ? '✅' : '❌';
93
+ const errorSection = result.error
94
+ ? `<div class="error-details">${escapeHtml(result.error)}</div>`
95
+ : '';
96
+
97
+ return `
98
+ <tr class="${statusClass}">
99
+ <td>${index + 1}</td>
100
+ <td>${escapeHtml(result.name)}</td>
101
+ <td class="status-${statusClass}">${statusIcon} ${result.success ? 'PASSED' : 'FAILED'}</td>
102
+ <td>${result.duration || 'N/A'}</td>
103
+ <td>${result.attempts || 1}</td>
104
+ <td>${errorSection}</td>
105
+ </tr>
106
+ `;
107
+ }).join('\n');
108
+
109
+ const html = `<!DOCTYPE html>
110
+ <html lang="en">
111
+ <head>
112
+ <meta charset="UTF-8">
113
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
114
+ <title>Test Report - ${reportId}</title>
115
+ <style>
116
+ :root {
117
+ --bg-color: #0d1117;
118
+ --card-bg: #161b22;
119
+ --text-color: #c9d1d9;
120
+ --text-muted: #8b949e;
121
+ --border-color: #30363d;
122
+ --success-color: #238636;
123
+ --success-bg: #1a4721;
124
+ --error-color: #f85149;
125
+ --error-bg: #4a1d1d;
126
+ --primary-color: #58a6ff;
127
+ --header-bg: #21262d;
128
+ }
129
+
130
+ * {
131
+ box-sizing: border-box;
132
+ margin: 0;
133
+ padding: 0;
134
+ }
135
+
136
+ body {
137
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
138
+ background-color: var(--bg-color);
139
+ color: var(--text-color);
140
+ line-height: 1.6;
141
+ padding: 20px;
142
+ }
143
+
144
+ .container {
145
+ max-width: 1200px;
146
+ margin: 0 auto;
147
+ }
148
+
149
+ header {
150
+ background: var(--card-bg);
151
+ border: 1px solid var(--border-color);
152
+ border-radius: 12px;
153
+ padding: 24px;
154
+ margin-bottom: 24px;
155
+ }
156
+
157
+ h1 {
158
+ font-size: 24px;
159
+ font-weight: 600;
160
+ margin-bottom: 8px;
161
+ color: var(--primary-color);
162
+ }
163
+
164
+ .meta {
165
+ color: var(--text-muted);
166
+ font-size: 14px;
167
+ }
168
+
169
+ .summary-cards {
170
+ display: grid;
171
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
172
+ gap: 16px;
173
+ margin-bottom: 24px;
174
+ }
175
+
176
+ .card {
177
+ background: var(--card-bg);
178
+ border: 1px solid var(--border-color);
179
+ border-radius: 12px;
180
+ padding: 20px;
181
+ text-align: center;
182
+ }
183
+
184
+ .card-value {
185
+ font-size: 36px;
186
+ font-weight: 700;
187
+ margin-bottom: 4px;
188
+ }
189
+
190
+ .card-label {
191
+ color: var(--text-muted);
192
+ font-size: 14px;
193
+ text-transform: uppercase;
194
+ letter-spacing: 0.5px;
195
+ }
196
+
197
+ .card-passed .card-value { color: var(--success-color); }
198
+ .card-failed .card-value { color: var(--error-color); }
199
+ .card-rate .card-value { color: var(--primary-color); }
200
+
201
+ .results-table {
202
+ background: var(--card-bg);
203
+ border: 1px solid var(--border-color);
204
+ border-radius: 12px;
205
+ overflow: hidden;
206
+ }
207
+
208
+ table {
209
+ width: 100%;
210
+ border-collapse: collapse;
211
+ }
212
+
213
+ th {
214
+ background: var(--header-bg);
215
+ padding: 14px 16px;
216
+ text-align: left;
217
+ font-weight: 600;
218
+ font-size: 13px;
219
+ text-transform: uppercase;
220
+ letter-spacing: 0.5px;
221
+ color: var(--text-muted);
222
+ border-bottom: 1px solid var(--border-color);
223
+ }
224
+
225
+ td {
226
+ padding: 14px 16px;
227
+ border-bottom: 1px solid var(--border-color);
228
+ font-size: 14px;
229
+ }
230
+
231
+ tr:last-child td {
232
+ border-bottom: none;
233
+ }
234
+
235
+ tr.passed {
236
+ background: var(--success-bg);
237
+ }
238
+
239
+ tr.failed {
240
+ background: var(--error-bg);
241
+ }
242
+
243
+ .status-passed {
244
+ color: var(--success-color);
245
+ font-weight: 600;
246
+ }
247
+
248
+ .status-failed {
249
+ color: var(--error-color);
250
+ font-weight: 600;
251
+ }
252
+
253
+ .error-details {
254
+ background: rgba(0,0,0,0.3);
255
+ padding: 8px 12px;
256
+ border-radius: 6px;
257
+ font-size: 12px;
258
+ font-family: 'Monaco', 'Menlo', monospace;
259
+ color: var(--error-color);
260
+ margin-top: 8px;
261
+ max-width: 400px;
262
+ overflow: auto;
263
+ }
264
+
265
+ footer {
266
+ margin-top: 24px;
267
+ text-align: center;
268
+ color: var(--text-muted);
269
+ font-size: 12px;
270
+ }
271
+
272
+ .progress-bar {
273
+ height: 8px;
274
+ background: var(--border-color);
275
+ border-radius: 4px;
276
+ overflow: hidden;
277
+ margin-top: 10px;
278
+ }
279
+
280
+ .progress-fill {
281
+ height: 100%;
282
+ background: linear-gradient(90deg, var(--success-color), #2ea043);
283
+ transition: width 0.3s ease;
284
+ }
285
+ </style>
286
+ </head>
287
+ <body>
288
+ <div class="container">
289
+ <header>
290
+ <h1>🧪 MCP Maestro Test Report</h1>
291
+ <div class="meta">
292
+ <p><strong>Report ID:</strong> ${reportId}</p>
293
+ <p><strong>Generated:</strong> ${timestamp}</p>
294
+ <p><strong>Prompt File:</strong> ${escapeHtml(metadata.promptFile || 'N/A')}</p>
295
+ <p><strong>App ID:</strong> ${escapeHtml(metadata.appId || 'N/A')}</p>
296
+ </div>
297
+ </header>
298
+
299
+ <div class="summary-cards">
300
+ <div class="card">
301
+ <div class="card-value">${total}</div>
302
+ <div class="card-label">Total Tests</div>
303
+ </div>
304
+ <div class="card card-passed">
305
+ <div class="card-value">${passed}</div>
306
+ <div class="card-label">Passed</div>
307
+ </div>
308
+ <div class="card card-failed">
309
+ <div class="card-value">${failed}</div>
310
+ <div class="card-label">Failed</div>
311
+ </div>
312
+ <div class="card card-rate">
313
+ <div class="card-value">${passRate}%</div>
314
+ <div class="card-label">Pass Rate</div>
315
+ <div class="progress-bar">
316
+ <div class="progress-fill" style="width: ${passRate}%"></div>
317
+ </div>
318
+ </div>
319
+ </div>
320
+
321
+ <div class="results-table">
322
+ <table>
323
+ <thead>
324
+ <tr>
325
+ <th>#</th>
326
+ <th>Test Name</th>
327
+ <th>Status</th>
328
+ <th>Duration</th>
329
+ <th>Attempts</th>
330
+ <th>Details</th>
331
+ </tr>
332
+ </thead>
333
+ <tbody>
334
+ ${testRows}
335
+ </tbody>
336
+ </table>
337
+ </div>
338
+
339
+ <footer>
340
+ <p>Generated by MCP Maestro Mobile AI</p>
341
+ <p>Report Location: ${REPORTS_DIR}</p>
342
+ </footer>
343
+ </div>
344
+ </body>
345
+ </html>`;
346
+
347
+ const htmlPath = path.join(REPORTS_DIR, `${reportId}.html`);
348
+ await fs.writeFile(htmlPath, html, 'utf8');
349
+
350
+ return {
351
+ reportId,
352
+ htmlPath,
353
+ summary: {
354
+ total,
355
+ passed,
356
+ failed,
357
+ passRate,
358
+ },
359
+ };
360
+ }
361
+
362
+ /**
363
+ * Generate both JSON and HTML reports
364
+ */
365
+ export async function generateReport(results, metadata = {}) {
366
+ const jsonResult = await generateJsonReport(results, metadata);
367
+ const htmlResult = await generateHtmlReport(results, metadata);
368
+
369
+ return {
370
+ reportId: jsonResult.reportId,
371
+ jsonPath: jsonResult.jsonPath,
372
+ htmlPath: htmlResult.htmlPath,
373
+ summary: htmlResult.summary,
374
+ report: jsonResult.report,
375
+ };
376
+ }
377
+
378
+ /**
379
+ * Get reports directory path
380
+ */
381
+ export function getReportsDir() {
382
+ return REPORTS_DIR;
383
+ }
384
+
385
+ /**
386
+ * List all generated reports
387
+ */
388
+ export async function listReports() {
389
+ await ensureReportsDir();
390
+
391
+ try {
392
+ const files = await fs.readdir(REPORTS_DIR);
393
+ const htmlReports = files.filter(f => f.endsWith('.html'));
394
+
395
+ const reports = [];
396
+ for (const file of htmlReports) {
397
+ const reportId = file.replace('.html', '');
398
+ const jsonPath = path.join(REPORTS_DIR, `${reportId}.json`);
399
+ const htmlPath = path.join(REPORTS_DIR, file);
400
+
401
+ try {
402
+ const jsonContent = await fs.readFile(jsonPath, 'utf8');
403
+ const data = JSON.parse(jsonContent);
404
+ reports.push({
405
+ reportId,
406
+ generatedAt: data.generatedAt,
407
+ promptFile: data.metadata?.promptFile,
408
+ summary: data.summary,
409
+ htmlPath,
410
+ jsonPath,
411
+ });
412
+ } catch {
413
+ // JSON file might not exist
414
+ reports.push({
415
+ reportId,
416
+ htmlPath,
417
+ });
418
+ }
419
+ }
420
+
421
+ // Sort by date (newest first)
422
+ reports.sort((a, b) => {
423
+ if (a.generatedAt && b.generatedAt) {
424
+ return new Date(b.generatedAt) - new Date(a.generatedAt);
425
+ }
426
+ return 0;
427
+ });
428
+
429
+ return reports;
430
+ } catch {
431
+ return [];
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Escape HTML special characters
437
+ */
438
+ function escapeHtml(text) {
439
+ if (!text) return '';
440
+ return String(text)
441
+ .replace(/&/g, '&amp;')
442
+ .replace(/</g, '&lt;')
443
+ .replace(/>/g, '&gt;')
444
+ .replace(/"/g, '&quot;')
445
+ .replace(/'/g, '&#039;');
446
+ }
447
+
448
+ export default {
449
+ generateJsonReport,
450
+ generateHtmlReport,
451
+ generateReport,
452
+ getReportsDir,
453
+ listReports,
454
+ };
455
+