slapify 0.0.16 → 0.0.18

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 (52) hide show
  1. package/README.md +38 -4
  2. package/dist/ai/interpreter.js +1 -331
  3. package/dist/browser/agent.js +1 -485
  4. package/dist/cli.js +1 -1553
  5. package/dist/config/loader.js +1 -305
  6. package/dist/index.js +1 -262
  7. package/dist/parser/flow.js +1 -117
  8. package/dist/perf/audit.js +1 -635
  9. package/dist/report/generator.js +1 -641
  10. package/dist/runner/index.js +1 -744
  11. package/dist/task/index.js +1 -4
  12. package/dist/task/report.js +1 -740
  13. package/dist/task/runner.js +1 -1362
  14. package/dist/task/session.js +1 -153
  15. package/dist/task/tools.d.ts +12 -0
  16. package/dist/task/tools.js +1 -258
  17. package/dist/task/types.d.ts +18 -0
  18. package/dist/task/types.js +1 -2
  19. package/dist/types.js +1 -2
  20. package/package.json +6 -3
  21. package/dist/ai/interpreter.d.ts.map +0 -1
  22. package/dist/ai/interpreter.js.map +0 -1
  23. package/dist/browser/agent.d.ts.map +0 -1
  24. package/dist/browser/agent.js.map +0 -1
  25. package/dist/cli.d.ts.map +0 -1
  26. package/dist/cli.js.map +0 -1
  27. package/dist/config/loader.d.ts.map +0 -1
  28. package/dist/config/loader.js.map +0 -1
  29. package/dist/index.d.ts.map +0 -1
  30. package/dist/index.js.map +0 -1
  31. package/dist/parser/flow.d.ts.map +0 -1
  32. package/dist/parser/flow.js.map +0 -1
  33. package/dist/perf/audit.d.ts.map +0 -1
  34. package/dist/perf/audit.js.map +0 -1
  35. package/dist/report/generator.d.ts.map +0 -1
  36. package/dist/report/generator.js.map +0 -1
  37. package/dist/runner/index.d.ts.map +0 -1
  38. package/dist/runner/index.js.map +0 -1
  39. package/dist/task/index.d.ts.map +0 -1
  40. package/dist/task/index.js.map +0 -1
  41. package/dist/task/report.d.ts.map +0 -1
  42. package/dist/task/report.js.map +0 -1
  43. package/dist/task/runner.d.ts.map +0 -1
  44. package/dist/task/runner.js.map +0 -1
  45. package/dist/task/session.d.ts.map +0 -1
  46. package/dist/task/session.js.map +0 -1
  47. package/dist/task/tools.d.ts.map +0 -1
  48. package/dist/task/tools.js.map +0 -1
  49. package/dist/task/types.d.ts.map +0 -1
  50. package/dist/task/types.js.map +0 -1
  51. package/dist/types.d.ts.map +0 -1
  52. package/dist/types.js.map +0 -1
@@ -1,641 +1 @@
1
- import fs from "fs";
2
- import path from "path";
3
- /**
4
- * Generate test reports in various formats
5
- */
6
- export class ReportGenerator {
7
- config;
8
- constructor(config = {}) {
9
- this.config = {
10
- format: "html",
11
- screenshots: true,
12
- output_dir: "./test-reports",
13
- ...config,
14
- };
15
- }
16
- /**
17
- * Generate a report from test results
18
- */
19
- generate(result) {
20
- switch (this.config.format) {
21
- case "markdown":
22
- return this.generateMarkdown(result);
23
- case "html":
24
- return this.generateHTML(result);
25
- case "json":
26
- return JSON.stringify(result, null, 2);
27
- default:
28
- return this.generateHTML(result);
29
- }
30
- }
31
- /**
32
- * Generate a suite report from multiple test results
33
- */
34
- generateSuiteReport(results) {
35
- return this.generateSuiteHTML(results);
36
- }
37
- /**
38
- * Save report to file
39
- */
40
- save(result, filename) {
41
- const report = this.generate(result);
42
- // Ensure output directory exists
43
- const outputDir = this.config.output_dir || "./test-reports";
44
- if (!fs.existsSync(outputDir)) {
45
- fs.mkdirSync(outputDir, { recursive: true });
46
- }
47
- // Generate filename
48
- const ext = this.config.format === "html"
49
- ? "html"
50
- : this.config.format === "json"
51
- ? "json"
52
- : "md";
53
- const defaultName = `${path.basename(result.flowFile, ".flow")}-${Date.now()}.${ext}`;
54
- const outputPath = path.join(outputDir, filename || defaultName);
55
- fs.writeFileSync(outputPath, report);
56
- return outputPath;
57
- }
58
- /**
59
- * Save report as a folder with embedded images
60
- */
61
- saveAsFolder(result) {
62
- const outputDir = this.config.output_dir || "./test-reports";
63
- const folderName = `${path.basename(result.flowFile, ".flow")}-${Date.now()}`;
64
- const reportFolder = path.join(outputDir, folderName);
65
- // Create report folder
66
- fs.mkdirSync(reportFolder, { recursive: true });
67
- // Copy screenshots to folder and build map
68
- const screenshotMap = {};
69
- for (const step of result.steps) {
70
- if (step.screenshot && fs.existsSync(step.screenshot)) {
71
- const screenshotName = path.basename(step.screenshot);
72
- const destPath = path.join(reportFolder, screenshotName);
73
- fs.copyFileSync(step.screenshot, destPath);
74
- screenshotMap[step.screenshot] = screenshotName;
75
- // Clean up original screenshot
76
- try {
77
- fs.unlinkSync(step.screenshot);
78
- }
79
- catch {
80
- // Ignore cleanup errors
81
- }
82
- }
83
- }
84
- // Update result with relative paths
85
- const updatedResult = {
86
- ...result,
87
- steps: result.steps.map((step) => ({
88
- ...step,
89
- screenshot: step.screenshot
90
- ? screenshotMap[step.screenshot] || step.screenshot
91
- : undefined,
92
- })),
93
- };
94
- // Generate report
95
- const report = this.generateHTML(updatedResult);
96
- const reportPath = path.join(reportFolder, "report.html");
97
- fs.writeFileSync(reportPath, report);
98
- return reportFolder;
99
- }
100
- /**
101
- * Save suite report as a folder
102
- */
103
- saveSuiteAsFolder(results) {
104
- const outputDir = this.config.output_dir || "./test-reports";
105
- const folderName = `test-suite-${Date.now()}`;
106
- const reportFolder = path.join(outputDir, folderName);
107
- // Create report folder
108
- fs.mkdirSync(reportFolder, { recursive: true });
109
- // Copy all screenshots and update paths
110
- const updatedResults = results.map((result) => {
111
- const screenshotMap = {};
112
- for (const step of result.steps) {
113
- if (step.screenshot && fs.existsSync(step.screenshot)) {
114
- const screenshotName = `${path.basename(result.flowFile, ".flow")}-${path.basename(step.screenshot)}`;
115
- const destPath = path.join(reportFolder, screenshotName);
116
- fs.copyFileSync(step.screenshot, destPath);
117
- screenshotMap[step.screenshot] = screenshotName;
118
- try {
119
- fs.unlinkSync(step.screenshot);
120
- }
121
- catch { }
122
- }
123
- }
124
- return {
125
- ...result,
126
- steps: result.steps.map((step) => ({
127
- ...step,
128
- screenshot: step.screenshot
129
- ? screenshotMap[step.screenshot] || step.screenshot
130
- : undefined,
131
- })),
132
- };
133
- });
134
- // Generate suite report
135
- const report = this.generateSuiteHTML(updatedResults);
136
- const reportPath = path.join(reportFolder, "report.html");
137
- fs.writeFileSync(reportPath, report);
138
- return reportFolder;
139
- }
140
- /**
141
- * Generate Markdown report
142
- */
143
- generateMarkdown(result) {
144
- const lines = [];
145
- const statusEmoji = result.status === "passed" ? "✅" : "❌";
146
- lines.push(`# ${path.basename(result.flowFile, ".flow")}`);
147
- lines.push("");
148
- lines.push(`**Status:** ${statusEmoji} ${result.status.toUpperCase()}`);
149
- lines.push(`**Duration:** ${this.formatDuration(result.duration)}`);
150
- lines.push(`**Started:** ${result.startTime.toISOString()}`);
151
- lines.push("");
152
- lines.push("## Summary");
153
- lines.push("");
154
- lines.push(`| Metric | Value |`);
155
- lines.push(`|--------|-------|`);
156
- lines.push(`| Total Steps | ${result.totalSteps} |`);
157
- lines.push(`| Passed | ${result.passedSteps} |`);
158
- lines.push(`| Failed | ${result.failedSteps} |`);
159
- lines.push(`| Skipped | ${result.skippedSteps} |`);
160
- lines.push("");
161
- if (result.autoHandled.length > 0) {
162
- lines.push("## Auto-Handled");
163
- for (const handled of result.autoHandled) {
164
- lines.push(`- ${handled}`);
165
- }
166
- lines.push("");
167
- }
168
- lines.push("## Steps");
169
- lines.push("");
170
- for (const stepResult of result.steps) {
171
- const step = stepResult.step;
172
- const statusIcon = stepResult.status === "passed"
173
- ? "✅"
174
- : stepResult.status === "failed"
175
- ? "❌"
176
- : "⏭️";
177
- const optional = step.optional ? " [Optional]" : "";
178
- lines.push(`### ${statusIcon} ${step.text}${optional}`);
179
- lines.push(`Duration: ${this.formatDuration(stepResult.duration)}`);
180
- lines.push("");
181
- if (stepResult.actions.length > 0) {
182
- for (const action of stepResult.actions) {
183
- lines.push(`- ${action.description}`);
184
- }
185
- lines.push("");
186
- }
187
- if (stepResult.error) {
188
- lines.push(`**Error:** ${stepResult.error}`);
189
- lines.push("");
190
- }
191
- if (stepResult.screenshot) {
192
- lines.push(`![Screenshot](./${stepResult.screenshot})`);
193
- lines.push("");
194
- }
195
- }
196
- return lines.join("\n");
197
- }
198
- /**
199
- * Generate HTML report with Tailwind CSS
200
- */
201
- generateHTML(result) {
202
- const statusIcon = result.status === "passed" ? "✓" : "✗";
203
- return `<!DOCTYPE html>
204
- <html lang="en">
205
- <head>
206
- <meta charset="UTF-8">
207
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
208
- <title>${path.basename(result.flowFile, ".flow")} - Test Report</title>
209
- <script src="https://cdn.tailwindcss.com"></script>
210
- <style>
211
- .step-details { display: none; }
212
- .step.expanded .step-details { display: block; }
213
- .step.expanded .chevron { transform: rotate(180deg); }
214
- .modal { display: none; }
215
- .modal.active { display: flex; }
216
- </style>
217
- </head>
218
- <body class="bg-gray-50 text-gray-900 min-h-screen">
219
- <div class="max-w-4xl mx-auto p-6">
220
-
221
- <!-- Header -->
222
- <div class="bg-white rounded-xl shadow-sm p-6 mb-6">
223
- <div class="flex items-center justify-between mb-4">
224
- <h1 class="text-2xl font-bold">${path.basename(result.flowFile, ".flow")}</h1>
225
- <span class="px-4 py-2 rounded-full font-semibold text-sm ${result.status === "passed"
226
- ? "bg-green-100 text-green-800"
227
- : "bg-red-100 text-red-800"}">
228
- ${statusIcon} ${result.status.toUpperCase()}
229
- </span>
230
- </div>
231
- <p class="text-gray-500 text-sm mb-6">Duration: ${this.formatDuration(result.duration)} · ${result.startTime.toLocaleString()}</p>
232
-
233
- <!-- Stats -->
234
- <div class="grid grid-cols-4 gap-4">
235
- <div class="bg-gray-100 rounded-lg p-4 text-center">
236
- <div class="text-3xl font-bold">${result.totalSteps}</div>
237
- <div class="text-xs text-gray-500 uppercase tracking-wide">Total</div>
238
- </div>
239
- <div class="bg-gray-100 rounded-lg p-4 text-center">
240
- <div class="text-3xl font-bold text-green-600">${result.passedSteps}</div>
241
- <div class="text-xs text-gray-500 uppercase tracking-wide">Passed</div>
242
- </div>
243
- <div class="bg-gray-100 rounded-lg p-4 text-center">
244
- <div class="text-3xl font-bold text-red-600">${result.failedSteps}</div>
245
- <div class="text-xs text-gray-500 uppercase tracking-wide">Failed</div>
246
- </div>
247
- <div class="bg-gray-100 rounded-lg p-4 text-center">
248
- <div class="text-3xl font-bold text-yellow-600">${result.skippedSteps}</div>
249
- <div class="text-xs text-gray-500 uppercase tracking-wide">Skipped</div>
250
- </div>
251
- </div>
252
- </div>
253
-
254
- ${result.autoHandled.length > 0
255
- ? `
256
- <!-- Auto-handled -->
257
- <div class="bg-blue-50 border border-blue-200 rounded-xl p-4 mb-6">
258
- <div class="font-semibold text-blue-800 mb-2">ℹ️ Auto-Handled Interruptions</div>
259
- ${result.autoHandled
260
- .map((h) => `<div class="text-blue-700 text-sm py-1">• ${h}</div>`)
261
- .join("")}
262
- </div>
263
- `
264
- : ""}
265
-
266
- ${result.perfAudit ? this.formatPerfSectionHTML(result.perfAudit) : ""}
267
-
268
- <!-- Steps -->
269
- <div class="bg-white rounded-xl shadow-sm overflow-hidden">
270
- <div class="px-6 py-4 border-b border-gray-200 font-semibold">Steps</div>
271
- ${result.steps.map((s, i) => this.formatStepHTML(s, i)).join("")}
272
- </div>
273
-
274
- <!-- Footer -->
275
- <div class="text-center text-gray-400 text-sm py-8">
276
- Generated by Slapify · ${new Date().toISOString()}
277
- </div>
278
- </div>
279
-
280
- <!-- Modal -->
281
- <div class="modal fixed inset-0 bg-black/90 z-50 items-center justify-center cursor-pointer" id="modal">
282
- <img src="" alt="Screenshot" id="modal-img" class="max-w-[95%] max-h-[95%] rounded-lg">
283
- </div>
284
-
285
- <script>
286
- document.querySelectorAll('.step-header').forEach(header => {
287
- header.addEventListener('click', () => header.parentElement.classList.toggle('expanded'));
288
- });
289
-
290
- const modal = document.getElementById('modal');
291
- const modalImg = document.getElementById('modal-img');
292
-
293
- document.querySelectorAll('.screenshot-img').forEach(img => {
294
- img.addEventListener('click', (e) => {
295
- e.stopPropagation();
296
- modalImg.src = img.src;
297
- modal.classList.add('active');
298
- });
299
- });
300
-
301
- modal.addEventListener('click', () => modal.classList.remove('active'));
302
- document.querySelectorAll('.step.failed').forEach(step => step.classList.add('expanded'));
303
- </script>
304
- </body>
305
- </html>`;
306
- }
307
- /**
308
- * Format a single step for HTML with Tailwind
309
- */
310
- formatStepHTML(result, index) {
311
- const step = result.step;
312
- const statusIcon = result.status === "passed" ? "✓" : result.status === "failed" ? "✗" : "○";
313
- const statusColors = {
314
- passed: "bg-green-100 text-green-600",
315
- failed: "bg-red-100 text-red-600",
316
- skipped: "bg-yellow-100 text-yellow-600",
317
- };
318
- const hasScreenshot = !!result.screenshot;
319
- const wasRetried = result.retried;
320
- return `
321
- <div class="step ${result.status} border-b border-gray-100 last:border-b-0">
322
- <div class="step-header flex items-center px-6 py-4 cursor-pointer hover:bg-gray-50 transition-colors">
323
- <div class="w-8 h-8 rounded-full flex items-center justify-center font-semibold text-sm mr-4 flex-shrink-0 ${statusColors[result.status]}">${statusIcon}</div>
324
- <div class="flex-1 min-w-0">
325
- <div class="font-medium flex items-center flex-wrap gap-2">
326
- <span>${step.text}</span>
327
- ${step.optional
328
- ? '<span class="px-2 py-0.5 bg-gray-100 text-gray-500 text-xs rounded">Optional</span>'
329
- : ""}
330
- ${wasRetried
331
- ? '<span class="px-2 py-0.5 bg-orange-100 text-orange-600 text-xs rounded">Retried</span>'
332
- : ""}
333
- </div>
334
- ${result.actions.length > 0
335
- ? `<div class="text-sm text-gray-500 mt-1 truncate">${result.actions[0]?.description || ""}</div>`
336
- : ""}
337
- </div>
338
- <div class="flex items-center gap-3 ml-4">
339
- ${hasScreenshot ? '<span class="text-gray-400" title="Has screenshot">📷</span>' : ""}
340
- <span class="text-gray-400 text-sm">${this.formatDuration(result.duration)}</span>
341
- </div>
342
- <div class="chevron ml-2 text-gray-400 transition-transform">▼</div>
343
- </div>
344
- <div class="step-details px-6 pb-6 pl-16">
345
- ${result.actions.length > 0
346
- ? `
347
- <div class="bg-gray-50 rounded-lg p-4 mb-4">
348
- ${result.actions
349
- .map((a) => `<div class="py-1 text-sm"><span class="mr-2">${this.getActionIcon(a.type)}</span>${a.description}</div>`)
350
- .join("")}
351
- </div>
352
- `
353
- : ""}
354
- ${result.assumptions && result.assumptions.length > 0
355
- ? `
356
- <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
357
- <div class="font-semibold text-yellow-800 mb-2">💡 Assumptions</div>
358
- ${result.assumptions
359
- .map((a) => `<div class="text-yellow-700 text-sm py-1">• ${a}</div>`)
360
- .join("")}
361
- </div>
362
- `
363
- : ""}
364
- ${result.error
365
- ? `<div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-4 font-mono text-sm text-red-700 whitespace-pre-wrap break-words">${result.error}</div>`
366
- : ""}
367
- ${result.screenshot
368
- ? `
369
- <div class="mt-4">
370
- <img src="./${result.screenshot}" alt="Step ${index + 1}" loading="lazy" class="screenshot-img max-w-full rounded-lg border border-gray-200 cursor-pointer hover:shadow-lg transition-shadow">
371
- </div>
372
- `
373
- : ""}
374
- </div>
375
- </div>`;
376
- }
377
- /**
378
- * Generate suite report HTML with Tailwind
379
- */
380
- generateSuiteHTML(results) {
381
- const totalTests = results.length;
382
- const passedTests = results.filter((r) => r.status === "passed").length;
383
- const failedTests = results.filter((r) => r.status === "failed").length;
384
- const totalSteps = results.reduce((sum, r) => sum + r.totalSteps, 0);
385
- const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
386
- const overallStatus = failedTests === 0 ? "passed" : "failed";
387
- return `<!DOCTYPE html>
388
- <html lang="en">
389
- <head>
390
- <meta charset="UTF-8">
391
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
392
- <title>Test Suite Report</title>
393
- <script src="https://cdn.tailwindcss.com"></script>
394
- <style>
395
- .test-details { display: none; }
396
- .test-item.expanded .test-details { display: block; }
397
- .test-item.expanded .chevron { transform: rotate(180deg); }
398
- </style>
399
- </head>
400
- <body class="bg-gray-50 text-gray-900 min-h-screen">
401
- <div class="max-w-5xl mx-auto p-6">
402
-
403
- <!-- Header -->
404
- <div class="bg-white rounded-xl shadow-sm p-8 mb-6">
405
- <div class="flex items-center justify-between mb-6">
406
- <h1 class="text-3xl font-bold">Test Suite Report</h1>
407
- <span class="px-6 py-3 rounded-full font-bold ${overallStatus === "passed"
408
- ? "bg-green-100 text-green-800"
409
- : "bg-red-100 text-red-800"}">
410
- ${overallStatus === "passed" ? "✓ ALL PASSED" : "✗ FAILURES"}
411
- </span>
412
- </div>
413
-
414
- <!-- Stats -->
415
- <div class="grid grid-cols-5 gap-4">
416
- <div class="bg-gray-100 rounded-lg p-5 text-center">
417
- <div class="text-4xl font-bold">${totalTests}</div>
418
- <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Test Files</div>
419
- </div>
420
- <div class="bg-gray-100 rounded-lg p-5 text-center">
421
- <div class="text-4xl font-bold text-green-600">${passedTests}</div>
422
- <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Passed</div>
423
- </div>
424
- <div class="bg-gray-100 rounded-lg p-5 text-center">
425
- <div class="text-4xl font-bold text-red-600">${failedTests}</div>
426
- <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Failed</div>
427
- </div>
428
- <div class="bg-gray-100 rounded-lg p-5 text-center">
429
- <div class="text-4xl font-bold">${totalSteps}</div>
430
- <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Total Steps</div>
431
- </div>
432
- <div class="bg-gray-100 rounded-lg p-5 text-center">
433
- <div class="text-2xl font-bold">${this.formatDuration(totalDuration)}</div>
434
- <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Duration</div>
435
- </div>
436
- </div>
437
- </div>
438
-
439
- <!-- Test List -->
440
- <div class="bg-white rounded-xl shadow-sm overflow-hidden">
441
- <div class="px-6 py-4 border-b border-gray-200 font-semibold text-lg">Test Files (${totalTests})</div>
442
- ${results.map((r, i) => this.formatTestItemHTML(r, i)).join("")}
443
- </div>
444
-
445
- <!-- Footer -->
446
- <div class="text-center text-gray-400 text-sm py-8">
447
- Generated by Slapify · ${new Date().toISOString()}
448
- </div>
449
- </div>
450
-
451
- <script>
452
- document.querySelectorAll('.test-header').forEach(header => {
453
- header.addEventListener('click', () => header.parentElement.classList.toggle('expanded'));
454
- });
455
- document.querySelectorAll('.test-item.failed').forEach(item => item.classList.add('expanded'));
456
- </script>
457
- </body>
458
- </html>`;
459
- }
460
- /**
461
- * Format a test item for suite report with Tailwind
462
- */
463
- formatTestItemHTML(result, index) {
464
- const statusIcon = result.status === "passed" ? "✓" : "✗";
465
- const statusColors = result.status === "passed"
466
- ? "bg-green-100 text-green-600"
467
- : "bg-red-100 text-red-600";
468
- return `
469
- <div class="test-item ${result.status} border-b border-gray-100 last:border-b-0">
470
- <div class="test-header flex items-center px-6 py-5 cursor-pointer hover:bg-gray-50 transition-colors">
471
- <div class="w-10 h-10 rounded-full flex items-center justify-center font-bold mr-4 ${statusColors}">${statusIcon}</div>
472
- <div class="flex-1">
473
- <div class="font-semibold text-lg">${path.basename(result.flowFile, ".flow")}</div>
474
- <div class="text-sm text-gray-500 mt-1">${result.totalSteps} steps · ${this.formatDuration(result.duration)}</div>
475
- </div>
476
- <div class="flex gap-2 ml-4">
477
- <span class="px-3 py-1 bg-green-100 text-green-700 rounded text-sm font-medium">${result.passedSteps} passed</span>
478
- ${result.failedSteps > 0
479
- ? `<span class="px-3 py-1 bg-red-100 text-red-700 rounded text-sm font-medium">${result.failedSteps} failed</span>`
480
- : ""}
481
- </div>
482
- <div class="chevron ml-4 text-gray-400 transition-transform">▼</div>
483
- </div>
484
- <div class="test-details px-6 pb-6 bg-gray-50">
485
- ${result.steps
486
- .map((s) => {
487
- const stepColors = {
488
- passed: "bg-green-100 text-green-600",
489
- failed: "bg-red-100 text-red-600",
490
- skipped: "bg-yellow-100 text-yellow-600",
491
- };
492
- const stepIcon = s.status === "passed" ? "✓" : s.status === "failed" ? "✗" : "○";
493
- return `
494
- <div class="flex items-center py-2 text-sm">
495
- <div class="w-6 h-6 rounded-full flex items-center justify-center text-xs mr-3 ${stepColors[s.status]}">${stepIcon}</div>
496
- <div class="flex-1">${s.step.text}</div>
497
- <div class="text-gray-400">${this.formatDuration(s.duration)}</div>
498
- </div>
499
- ${s.error
500
- ? `<div class="ml-9 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700 mb-2">${s.error}</div>`
501
- : ""}
502
- `;
503
- })
504
- .join("")}
505
- </div>
506
- </div>`;
507
- }
508
- /**
509
- * Format duration in human-readable form
510
- */
511
- formatPerfSectionHTML(perf) {
512
- const scoreGauge = (score, label) => {
513
- const color = score >= 90 ? "#16a34a" : score >= 50 ? "#d97706" : "#dc2626";
514
- const bg = score >= 90 ? "bg-green-50 border-green-200" : score >= 50 ? "bg-yellow-50 border-yellow-200" : "bg-red-50 border-red-200";
515
- const text = score >= 90 ? "text-green-700" : score >= 50 ? "text-yellow-700" : "text-red-700";
516
- const c = 2 * Math.PI * 28;
517
- return `<div class="flex flex-col items-center p-4 rounded-xl border ${bg} min-w-[100px]">
518
- <svg width="64" height="64" viewBox="0 0 72 72" class="mb-2">
519
- <circle cx="36" cy="36" r="28" fill="none" stroke="#e5e7eb" stroke-width="8"/>
520
- <circle cx="36" cy="36" r="28" fill="none" stroke="${color}" stroke-width="8"
521
- stroke-dasharray="${((score / 100) * c).toFixed(1)} ${c.toFixed(1)}"
522
- stroke-linecap="round" transform="rotate(-90 36 36)"/>
523
- <text x="36" y="41" text-anchor="middle" fill="${color}" font-size="16" font-weight="700">${score}</text>
524
- </svg>
525
- <div class="text-xs font-semibold text-gray-600">${label}</div>
526
- <div class="text-xs ${text}">${score >= 90 ? "Good" : score >= 50 ? "Needs Work" : "Poor"}</div>
527
- </div>`;
528
- };
529
- const vitalBadge = (name, val, unit, thresholds) => {
530
- if (val == null)
531
- return "";
532
- const color = val <= thresholds[0] ? "text-green-700 bg-green-50 border-green-200"
533
- : val <= thresholds[1] ? "text-yellow-700 bg-yellow-50 border-yellow-200"
534
- : "text-red-700 bg-red-50 border-red-200";
535
- const display = unit === "" ? val.toFixed(4) : `${val}${unit}`;
536
- return `<div class="flex items-center justify-between px-4 py-2 rounded-lg border ${color}">
537
- <span class="text-xs font-medium text-gray-600">${name}</span>
538
- <span class="text-sm font-bold ml-4">${display}</span>
539
- </div>`;
540
- };
541
- const lhGauges = perf.lighthouse
542
- ? [
543
- scoreGauge(perf.lighthouse.performance, "Performance"),
544
- scoreGauge(perf.lighthouse.accessibility, "Accessibility"),
545
- scoreGauge(perf.lighthouse.bestPractices, "Best Practices"),
546
- scoreGauge(perf.lighthouse.seo, "SEO"),
547
- ].join("")
548
- : "";
549
- const vitals = [
550
- vitalBadge("FCP", perf.vitals.fcp, "ms", [1800, 3000]),
551
- vitalBadge("LCP", perf.vitals.lcp, "ms", [2500, 4000]),
552
- vitalBadge("CLS", perf.vitals.cls, "", [0.1, 0.25]),
553
- vitalBadge("TTFB", perf.vitals.ttfb, "ms", [800, 1800]),
554
- vitalBadge("DOM Ready", perf.vitals.domContentLoaded, "ms", [2000, 4000]),
555
- ].filter(Boolean).join("");
556
- const lhMetrics = perf.lighthouse
557
- ? [
558
- ["FCP", perf.lighthouse.fcp, "ms"],
559
- ["LCP", perf.lighthouse.lcp, "ms"],
560
- ["TBT", perf.lighthouse.tbt, "ms"],
561
- ["Speed Index", perf.lighthouse.speedIndex, "ms"],
562
- ["TTI", perf.lighthouse.tti, "ms"],
563
- ].filter(([, v]) => v != null)
564
- .map(([l, v, u]) => `<div class="bg-gray-100 rounded-lg p-3 text-center">
565
- <div class="text-xs text-gray-500">${l}</div>
566
- <div class="text-lg font-bold">${v}${u}</div>
567
- </div>`).join("")
568
- : "";
569
- const reactSection = perf.react
570
- ? perf.react.detected
571
- ? `<div class="mt-4 pt-4 border-t border-gray-200">
572
- <div class="font-semibold text-sm mb-3">⚛️ React Scan${perf.react.version ? ` <span class="text-gray-400 font-normal">v${perf.react.version}</span>` : ""}</div>
573
- ${perf.react.issues.length === 0
574
- ? `<p class="text-green-600 text-sm">✅ No unnecessary re-renders detected</p>`
575
- : `<div class="space-y-1">${perf.react.issues.map(i => `<div class="flex items-center justify-between text-sm px-3 py-2 bg-yellow-50 rounded-lg border border-yellow-200">
576
- <code class="text-yellow-800">${i.component}</code>
577
- <span class="text-red-600 font-semibold">${i.renderCount} renders${i.avgMs != null ? ` · ${i.avgMs}ms avg` : ""}</span>
578
- </div>`).join("")}</div>`}
579
- </div>`
580
- : `<p class="text-gray-400 text-sm mt-4">ℹ️ React not detected on this page</p>`
581
- : "";
582
- return `
583
- <!-- Performance -->
584
- <div class="bg-white rounded-xl shadow-sm p-6 mb-6">
585
- <div class="font-semibold text-lg mb-1">⚡ Performance Audit</div>
586
- <div class="text-xs text-gray-400 mb-4">${perf.url}</div>
587
-
588
- ${lhGauges ? `<div class="flex flex-wrap gap-3 mb-6">${lhGauges}</div>` : ""}
589
-
590
- ${lhMetrics ? `<div class="grid grid-cols-3 gap-3 mb-4">${lhMetrics}</div>` : ""}
591
-
592
- ${vitals ? `
593
- <div class="mb-2 text-xs font-semibold text-gray-500 uppercase tracking-wide">Core Web Vitals (live)</div>
594
- <div class="grid grid-cols-2 gap-2 mb-2">${vitals}</div>` : ""}
595
-
596
- ${reactSection}
597
-
598
- ${perf.lighthouseReportPath ? `<div class="mt-4 text-xs text-gray-400">Full Lighthouse report: <a href="${perf.lighthouseReportPath}" class="text-blue-500 underline">${perf.lighthouseReportPath}</a></div>` : ""}
599
- </div>`;
600
- }
601
- formatDuration(ms) {
602
- if (ms < 1000)
603
- return `${ms}ms`;
604
- if (ms < 60000)
605
- return `${(ms / 1000).toFixed(1)}s`;
606
- const mins = Math.floor(ms / 60000);
607
- const secs = Math.floor((ms % 60000) / 1000);
608
- return `${mins}m ${secs}s`;
609
- }
610
- /**
611
- * Get icon for action type
612
- */
613
- getActionIcon(type) {
614
- switch (type) {
615
- case "navigate":
616
- return "🔗";
617
- case "click":
618
- return "👆";
619
- case "fill":
620
- return "✏️";
621
- case "verify":
622
- return "✓";
623
- case "wait":
624
- return "⏳";
625
- case "auto-handle":
626
- return "ℹ️";
627
- default:
628
- return "•";
629
- }
630
- }
631
- /**
632
- * Print summary to console
633
- */
634
- printSummary(result) {
635
- const statusEmoji = result.status === "passed" ? "✅" : "❌";
636
- console.log("");
637
- console.log(`${statusEmoji} ${path.basename(result.flowFile, ".flow")} - ${result.status.toUpperCase()}`);
638
- console.log(` ${result.passedSteps}/${result.totalSteps} steps passed (${this.formatDuration(result.duration)})`);
639
- }
640
- }
641
- //# sourceMappingURL=generator.js.map
1
+ import e from"fs";import t from"path";export class ReportGenerator{config;constructor(e={}){this.config={format:"html",screenshots:!0,output_dir:"./test-reports",...e}}generate(e){switch(this.config.format){case"markdown":return this.generateMarkdown(e);case"html":default:return this.generateHTML(e);case"json":return JSON.stringify(e,null,2)}}generateSuiteReport(e){return this.generateSuiteHTML(e)}save(s,n){const r=this.generate(s),a=this.config.output_dir||"./test-reports";e.existsSync(a)||e.mkdirSync(a,{recursive:!0});const i="html"===this.config.format?"html":"json"===this.config.format?"json":"md",d=`${t.basename(s.flowFile,".flow")}-${Date.now()}.${i}`,o=t.join(a,n||d);return e.writeFileSync(o,r),o}saveAsFolder(s){const n=this.config.output_dir||"./test-reports",r=`${t.basename(s.flowFile,".flow")}-${Date.now()}`,a=t.join(n,r);e.mkdirSync(a,{recursive:!0});const i={};for(const n of s.steps)if(n.screenshot&&e.existsSync(n.screenshot)){const s=t.basename(n.screenshot),r=t.join(a,s);e.copyFileSync(n.screenshot,r),i[n.screenshot]=s;try{e.unlinkSync(n.screenshot)}catch{}}const d={...s,steps:s.steps.map(e=>({...e,screenshot:e.screenshot?i[e.screenshot]||e.screenshot:void 0}))},o=this.generateHTML(d),l=t.join(a,"report.html");return e.writeFileSync(l,o),a}saveSuiteAsFolder(s){const n=this.config.output_dir||"./test-reports",r=`test-suite-${Date.now()}`,a=t.join(n,r);e.mkdirSync(a,{recursive:!0});const i=s.map(s=>{const n={};for(const r of s.steps)if(r.screenshot&&e.existsSync(r.screenshot)){const i=`${t.basename(s.flowFile,".flow")}-${t.basename(r.screenshot)}`,d=t.join(a,i);e.copyFileSync(r.screenshot,d),n[r.screenshot]=i;try{e.unlinkSync(r.screenshot)}catch{}}return{...s,steps:s.steps.map(e=>({...e,screenshot:e.screenshot?n[e.screenshot]||e.screenshot:void 0}))}}),d=this.generateSuiteHTML(i),o=t.join(a,"report.html");return e.writeFileSync(o,d),a}generateMarkdown(e){const s=[],n="passed"===e.status?"✅":"❌";if(s.push(`# ${t.basename(e.flowFile,".flow")}`),s.push(""),s.push(`**Status:** ${n} ${e.status.toUpperCase()}`),s.push(`**Duration:** ${this.formatDuration(e.duration)}`),s.push(`**Started:** ${e.startTime.toISOString()}`),s.push(""),s.push("## Summary"),s.push(""),s.push("| Metric | Value |"),s.push("|--------|-------|"),s.push(`| Total Steps | ${e.totalSteps} |`),s.push(`| Passed | ${e.passedSteps} |`),s.push(`| Failed | ${e.failedSteps} |`),s.push(`| Skipped | ${e.skippedSteps} |`),s.push(""),e.autoHandled.length>0){s.push("## Auto-Handled");for(const t of e.autoHandled)s.push(`- ${t}`);s.push("")}s.push("## Steps"),s.push("");for(const t of e.steps){const e=t.step,n="passed"===t.status?"✅":"failed"===t.status?"❌":"⏭️",r=e.optional?" [Optional]":"";if(s.push(`### ${n} ${e.text}${r}`),s.push(`Duration: ${this.formatDuration(t.duration)}`),s.push(""),t.actions.length>0){for(const e of t.actions)s.push(`- ${e.description}`);s.push("")}t.error&&(s.push(`**Error:** ${t.error}`),s.push("")),t.screenshot&&(s.push(`![Screenshot](./${t.screenshot})`),s.push(""))}return s.join("\n")}generateHTML(e){const s="passed"===e.status?"✓":"✗";return`<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>${t.basename(e.flowFile,".flow")} - Test Report</title>\n <script src="https://cdn.tailwindcss.com"><\/script>\n <style>\n .step-details { display: none; }\n .step.expanded .step-details { display: block; }\n .step.expanded .chevron { transform: rotate(180deg); }\n .modal { display: none; }\n .modal.active { display: flex; }\n </style>\n</head>\n<body class="bg-gray-50 text-gray-900 min-h-screen">\n <div class="max-w-4xl mx-auto p-6">\n \n \x3c!-- Header --\x3e\n <div class="bg-white rounded-xl shadow-sm p-6 mb-6">\n <div class="flex items-center justify-between mb-4">\n <h1 class="text-2xl font-bold">${t.basename(e.flowFile,".flow")}</h1>\n <span class="px-4 py-2 rounded-full font-semibold text-sm ${"passed"===e.status?"bg-green-100 text-green-800":"bg-red-100 text-red-800"}">\n ${s} ${e.status.toUpperCase()}\n </span>\n </div>\n <p class="text-gray-500 text-sm mb-6">Duration: ${this.formatDuration(e.duration)} · ${e.startTime.toLocaleString()}</p>\n \n \x3c!-- Stats --\x3e\n <div class="grid grid-cols-4 gap-4">\n <div class="bg-gray-100 rounded-lg p-4 text-center">\n <div class="text-3xl font-bold">${e.totalSteps}</div>\n <div class="text-xs text-gray-500 uppercase tracking-wide">Total</div>\n </div>\n <div class="bg-gray-100 rounded-lg p-4 text-center">\n <div class="text-3xl font-bold text-green-600">${e.passedSteps}</div>\n <div class="text-xs text-gray-500 uppercase tracking-wide">Passed</div>\n </div>\n <div class="bg-gray-100 rounded-lg p-4 text-center">\n <div class="text-3xl font-bold text-red-600">${e.failedSteps}</div>\n <div class="text-xs text-gray-500 uppercase tracking-wide">Failed</div>\n </div>\n <div class="bg-gray-100 rounded-lg p-4 text-center">\n <div class="text-3xl font-bold text-yellow-600">${e.skippedSteps}</div>\n <div class="text-xs text-gray-500 uppercase tracking-wide">Skipped</div>\n </div>\n </div>\n </div>\n\n ${e.autoHandled.length>0?`\n \x3c!-- Auto-handled --\x3e\n <div class="bg-blue-50 border border-blue-200 rounded-xl p-4 mb-6">\n <div class="font-semibold text-blue-800 mb-2">ℹ️ Auto-Handled Interruptions</div>\n ${e.autoHandled.map(e=>`<div class="text-blue-700 text-sm py-1">• ${e}</div>`).join("")}\n </div>\n `:""}\n\n ${e.perfAudit?this.formatPerfSectionHTML(e.perfAudit):""}\n\n \x3c!-- Steps --\x3e\n <div class="bg-white rounded-xl shadow-sm overflow-hidden">\n <div class="px-6 py-4 border-b border-gray-200 font-semibold">Steps</div>\n ${e.steps.map((e,t)=>this.formatStepHTML(e,t)).join("")}\n </div>\n \n \x3c!-- Footer --\x3e\n <div class="text-center text-gray-400 text-sm py-8">\n Generated by Slapify · ${(new Date).toISOString()}\n </div>\n </div>\n\n \x3c!-- Modal --\x3e\n <div class="modal fixed inset-0 bg-black/90 z-50 items-center justify-center cursor-pointer" id="modal">\n <img src="" alt="Screenshot" id="modal-img" class="max-w-[95%] max-h-[95%] rounded-lg">\n </div>\n\n <script>\n document.querySelectorAll('.step-header').forEach(header => {\n header.addEventListener('click', () => header.parentElement.classList.toggle('expanded'));\n });\n \n const modal = document.getElementById('modal');\n const modalImg = document.getElementById('modal-img');\n \n document.querySelectorAll('.screenshot-img').forEach(img => {\n img.addEventListener('click', (e) => {\n e.stopPropagation();\n modalImg.src = img.src;\n modal.classList.add('active');\n });\n });\n \n modal.addEventListener('click', () => modal.classList.remove('active'));\n document.querySelectorAll('.step.failed').forEach(step => step.classList.add('expanded'));\n <\/script>\n</body>\n</html>`}formatStepHTML(e,t){const s=e.step,n="passed"===e.status?"✓":"failed"===e.status?"✗":"○",r=!!e.screenshot,a=e.retried;return`\n <div class="step ${e.status} border-b border-gray-100 last:border-b-0">\n <div class="step-header flex items-center px-6 py-4 cursor-pointer hover:bg-gray-50 transition-colors">\n <div class="w-8 h-8 rounded-full flex items-center justify-center font-semibold text-sm mr-4 flex-shrink-0 ${{passed:"bg-green-100 text-green-600",failed:"bg-red-100 text-red-600",skipped:"bg-yellow-100 text-yellow-600"}[e.status]}">${n}</div>\n <div class="flex-1 min-w-0">\n <div class="font-medium flex items-center flex-wrap gap-2">\n <span>${s.text}</span>\n ${s.optional?'<span class="px-2 py-0.5 bg-gray-100 text-gray-500 text-xs rounded">Optional</span>':""}\n ${a?'<span class="px-2 py-0.5 bg-orange-100 text-orange-600 text-xs rounded">Retried</span>':""}\n </div>\n ${e.actions.length>0?`<div class="text-sm text-gray-500 mt-1 truncate">${e.actions[0]?.description||""}</div>`:""}\n </div>\n <div class="flex items-center gap-3 ml-4">\n ${r?'<span class="text-gray-400" title="Has screenshot">📷</span>':""}\n <span class="text-gray-400 text-sm">${this.formatDuration(e.duration)}</span>\n </div>\n <div class="chevron ml-2 text-gray-400 transition-transform">▼</div>\n </div>\n <div class="step-details px-6 pb-6 pl-16">\n ${e.actions.length>0?`\n <div class="bg-gray-50 rounded-lg p-4 mb-4">\n ${e.actions.map(e=>`<div class="py-1 text-sm"><span class="mr-2">${this.getActionIcon(e.type)}</span>${e.description}</div>`).join("")}\n </div>\n `:""}\n ${e.assumptions&&e.assumptions.length>0?`\n <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">\n <div class="font-semibold text-yellow-800 mb-2">💡 Assumptions</div>\n ${e.assumptions.map(e=>`<div class="text-yellow-700 text-sm py-1">• ${e}</div>`).join("")}\n </div>\n `:""}\n ${e.error?`<div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-4 font-mono text-sm text-red-700 whitespace-pre-wrap break-words">${e.error}</div>`:""}\n ${e.screenshot?`\n <div class="mt-4">\n <img src="./${e.screenshot}" alt="Step ${t+1}" loading="lazy" class="screenshot-img max-w-full rounded-lg border border-gray-200 cursor-pointer hover:shadow-lg transition-shadow">\n </div>\n `:""}\n </div>\n </div>`}generateSuiteHTML(e){const t=e.length,s=e.filter(e=>"passed"===e.status).length,n=e.filter(e=>"failed"===e.status).length,r=e.reduce((e,t)=>e+t.totalSteps,0),a=e.reduce((e,t)=>e+t.duration,0),i=0===n?"passed":"failed";return`<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>Test Suite Report</title>\n <script src="https://cdn.tailwindcss.com"><\/script>\n <style>\n .test-details { display: none; }\n .test-item.expanded .test-details { display: block; }\n .test-item.expanded .chevron { transform: rotate(180deg); }\n </style>\n</head>\n<body class="bg-gray-50 text-gray-900 min-h-screen">\n <div class="max-w-5xl mx-auto p-6">\n \n \x3c!-- Header --\x3e\n <div class="bg-white rounded-xl shadow-sm p-8 mb-6">\n <div class="flex items-center justify-between mb-6">\n <h1 class="text-3xl font-bold">Test Suite Report</h1>\n <span class="px-6 py-3 rounded-full font-bold ${"passed"===i?"bg-green-100 text-green-800":"bg-red-100 text-red-800"}">\n ${"passed"===i?"✓ ALL PASSED":"✗ FAILURES"}\n </span>\n </div>\n \n \x3c!-- Stats --\x3e\n <div class="grid grid-cols-5 gap-4">\n <div class="bg-gray-100 rounded-lg p-5 text-center">\n <div class="text-4xl font-bold">${t}</div>\n <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Test Files</div>\n </div>\n <div class="bg-gray-100 rounded-lg p-5 text-center">\n <div class="text-4xl font-bold text-green-600">${s}</div>\n <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Passed</div>\n </div>\n <div class="bg-gray-100 rounded-lg p-5 text-center">\n <div class="text-4xl font-bold text-red-600">${n}</div>\n <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Failed</div>\n </div>\n <div class="bg-gray-100 rounded-lg p-5 text-center">\n <div class="text-4xl font-bold">${r}</div>\n <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Total Steps</div>\n </div>\n <div class="bg-gray-100 rounded-lg p-5 text-center">\n <div class="text-2xl font-bold">${this.formatDuration(a)}</div>\n <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Duration</div>\n </div>\n </div>\n </div>\n\n \x3c!-- Test List --\x3e\n <div class="bg-white rounded-xl shadow-sm overflow-hidden">\n <div class="px-6 py-4 border-b border-gray-200 font-semibold text-lg">Test Files (${t})</div>\n ${e.map((e,t)=>this.formatTestItemHTML(e,t)).join("")}\n </div>\n \n \x3c!-- Footer --\x3e\n <div class="text-center text-gray-400 text-sm py-8">\n Generated by Slapify · ${(new Date).toISOString()}\n </div>\n </div>\n\n <script>\n document.querySelectorAll('.test-header').forEach(header => {\n header.addEventListener('click', () => header.parentElement.classList.toggle('expanded'));\n });\n document.querySelectorAll('.test-item.failed').forEach(item => item.classList.add('expanded'));\n <\/script>\n</body>\n</html>`}formatTestItemHTML(e,s){const n="passed"===e.status?"✓":"✗",r="passed"===e.status?"bg-green-100 text-green-600":"bg-red-100 text-red-600";return`\n <div class="test-item ${e.status} border-b border-gray-100 last:border-b-0">\n <div class="test-header flex items-center px-6 py-5 cursor-pointer hover:bg-gray-50 transition-colors">\n <div class="w-10 h-10 rounded-full flex items-center justify-center font-bold mr-4 ${r}">${n}</div>\n <div class="flex-1">\n <div class="font-semibold text-lg">${t.basename(e.flowFile,".flow")}</div>\n <div class="text-sm text-gray-500 mt-1">${e.totalSteps} steps · ${this.formatDuration(e.duration)}</div>\n </div>\n <div class="flex gap-2 ml-4">\n <span class="px-3 py-1 bg-green-100 text-green-700 rounded text-sm font-medium">${e.passedSteps} passed</span>\n ${e.failedSteps>0?`<span class="px-3 py-1 bg-red-100 text-red-700 rounded text-sm font-medium">${e.failedSteps} failed</span>`:""}\n </div>\n <div class="chevron ml-4 text-gray-400 transition-transform">▼</div>\n </div>\n <div class="test-details px-6 pb-6 bg-gray-50">\n ${e.steps.map(e=>{const t="passed"===e.status?"✓":"failed"===e.status?"✗":"○";return`\n <div class="flex items-center py-2 text-sm">\n <div class="w-6 h-6 rounded-full flex items-center justify-center text-xs mr-3 ${{passed:"bg-green-100 text-green-600",failed:"bg-red-100 text-red-600",skipped:"bg-yellow-100 text-yellow-600"}[e.status]}">${t}</div>\n <div class="flex-1">${e.step.text}</div>\n <div class="text-gray-400">${this.formatDuration(e.duration)}</div>\n </div>\n ${e.error?`<div class="ml-9 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700 mb-2">${e.error}</div>`:""}\n `}).join("")}\n </div>\n </div>`}formatPerfSectionHTML(e){const t=(e,t)=>{const s=e>=90?"#16a34a":e>=50?"#d97706":"#dc2626",n=e>=90?"bg-green-50 border-green-200":e>=50?"bg-yellow-50 border-yellow-200":"bg-red-50 border-red-200",r=e>=90?"text-green-700":e>=50?"text-yellow-700":"text-red-700",a=2*Math.PI*28;return`<div class="flex flex-col items-center p-4 rounded-xl border ${n} min-w-[100px]">\n <svg width="64" height="64" viewBox="0 0 72 72" class="mb-2">\n <circle cx="36" cy="36" r="28" fill="none" stroke="#e5e7eb" stroke-width="8"/>\n <circle cx="36" cy="36" r="28" fill="none" stroke="${s}" stroke-width="8"\n stroke-dasharray="${(e/100*a).toFixed(1)} ${a.toFixed(1)}"\n stroke-linecap="round" transform="rotate(-90 36 36)"/>\n <text x="36" y="41" text-anchor="middle" fill="${s}" font-size="16" font-weight="700">${e}</text>\n </svg>\n <div class="text-xs font-semibold text-gray-600">${t}</div>\n <div class="text-xs ${r}">${e>=90?"Good":e>=50?"Needs Work":"Poor"}</div>\n </div>`},s=(e,t,s,n)=>{if(null==t)return"";return`<div class="flex items-center justify-between px-4 py-2 rounded-lg border ${t<=n[0]?"text-green-700 bg-green-50 border-green-200":t<=n[1]?"text-yellow-700 bg-yellow-50 border-yellow-200":"text-red-700 bg-red-50 border-red-200"}">\n <span class="text-xs font-medium text-gray-600">${e}</span>\n <span class="text-sm font-bold ml-4">${""===s?t.toFixed(4):`${t}${s}`}</span>\n </div>`},n=e.lighthouse?[t(e.lighthouse.performance,"Performance"),t(e.lighthouse.accessibility,"Accessibility"),t(e.lighthouse.bestPractices,"Best Practices"),t(e.lighthouse.seo,"SEO")].join(""):"",r=[s("FCP",e.vitals.fcp,"ms",[1800,3e3]),s("LCP",e.vitals.lcp,"ms",[2500,4e3]),s("CLS",e.vitals.cls,"",[.1,.25]),s("TTFB",e.vitals.ttfb,"ms",[800,1800]),s("DOM Ready",e.vitals.domContentLoaded,"ms",[2e3,4e3])].filter(Boolean).join(""),a=e.lighthouse?[["FCP",e.lighthouse.fcp,"ms"],["LCP",e.lighthouse.lcp,"ms"],["TBT",e.lighthouse.tbt,"ms"],["Speed Index",e.lighthouse.speedIndex,"ms"],["TTI",e.lighthouse.tti,"ms"]].filter(([,e])=>null!=e).map(([e,t,s])=>`<div class="bg-gray-100 rounded-lg p-3 text-center">\n <div class="text-xs text-gray-500">${e}</div>\n <div class="text-lg font-bold">${t}${s}</div>\n </div>`).join(""):"",i=e.react?e.react.detected?`<div class="mt-4 pt-4 border-t border-gray-200">\n <div class="font-semibold text-sm mb-3">⚛️ React Scan${e.react.version?` <span class="text-gray-400 font-normal">v${e.react.version}</span>`:""}</div>\n ${0===e.react.issues.length?'<p class="text-green-600 text-sm">✅ No unnecessary re-renders detected</p>':`<div class="space-y-1">${e.react.issues.map(e=>`<div class="flex items-center justify-between text-sm px-3 py-2 bg-yellow-50 rounded-lg border border-yellow-200">\n <code class="text-yellow-800">${e.component}</code>\n <span class="text-red-600 font-semibold">${e.renderCount} renders${null!=e.avgMs?` · ${e.avgMs}ms avg`:""}</span>\n </div>`).join("")}</div>`}\n </div>`:'<p class="text-gray-400 text-sm mt-4">ℹ️ React not detected on this page</p>':"";return`\n \x3c!-- Performance --\x3e\n <div class="bg-white rounded-xl shadow-sm p-6 mb-6">\n <div class="font-semibold text-lg mb-1">⚡ Performance Audit</div>\n <div class="text-xs text-gray-400 mb-4">${e.url}</div>\n\n ${n?`<div class="flex flex-wrap gap-3 mb-6">${n}</div>`:""}\n\n ${a?`<div class="grid grid-cols-3 gap-3 mb-4">${a}</div>`:""}\n\n ${r?`\n <div class="mb-2 text-xs font-semibold text-gray-500 uppercase tracking-wide">Core Web Vitals (live)</div>\n <div class="grid grid-cols-2 gap-2 mb-2">${r}</div>`:""}\n\n ${i}\n\n ${e.lighthouseReportPath?`<div class="mt-4 text-xs text-gray-400">Full Lighthouse report: <a href="${e.lighthouseReportPath}" class="text-blue-500 underline">${e.lighthouseReportPath}</a></div>`:""}\n </div>`}formatDuration(e){if(e<1e3)return`${e}ms`;if(e<6e4)return`${(e/1e3).toFixed(1)}s`;return`${Math.floor(e/6e4)}m ${Math.floor(e%6e4/1e3)}s`}getActionIcon(e){switch(e){case"navigate":return"🔗";case"click":return"👆";case"fill":return"✏️";case"verify":return"✓";case"wait":return"⏳";case"auto-handle":return"ℹ️";default:return"•"}}printSummary(e){const s="passed"===e.status?"✅":"❌";console.log(""),console.log(`${s} ${t.basename(e.flowFile,".flow")} - ${e.status.toUpperCase()}`),console.log(` ${e.passedSteps}/${e.totalSteps} steps passed (${this.formatDuration(e.duration)})`)}}