slapify 0.0.14 โ†’ 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +342 -258
  2. package/dist/ai/interpreter.d.ts +13 -0
  3. package/dist/ai/interpreter.js +1 -293
  4. package/dist/browser/agent.js +1 -485
  5. package/dist/cli.js +1 -1315
  6. package/dist/config/loader.js +1 -305
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +1 -260
  9. package/dist/parser/flow.js +1 -117
  10. package/dist/perf/audit.d.ts +215 -0
  11. package/dist/perf/audit.js +1 -0
  12. package/dist/report/generator.d.ts +1 -0
  13. package/dist/report/generator.js +1 -549
  14. package/dist/runner/index.d.ts +14 -1
  15. package/dist/runner/index.js +1 -584
  16. package/dist/task/index.d.ts +5 -0
  17. package/dist/task/index.js +1 -0
  18. package/dist/task/report.d.ts +9 -0
  19. package/dist/task/report.js +1 -0
  20. package/dist/task/runner.d.ts +3 -0
  21. package/dist/task/runner.js +1 -0
  22. package/dist/task/session.d.ts +18 -0
  23. package/dist/task/session.js +1 -0
  24. package/dist/task/tools.d.ts +253 -0
  25. package/dist/task/tools.js +1 -0
  26. package/dist/task/types.d.ts +153 -0
  27. package/dist/task/types.js +1 -0
  28. package/dist/types.d.ts +2 -0
  29. package/dist/types.js +1 -2
  30. package/package.json +25 -15
  31. package/dist/ai/interpreter.d.ts.map +0 -1
  32. package/dist/ai/interpreter.js.map +0 -1
  33. package/dist/browser/agent.d.ts.map +0 -1
  34. package/dist/browser/agent.js.map +0 -1
  35. package/dist/cli.d.ts.map +0 -1
  36. package/dist/cli.js.map +0 -1
  37. package/dist/config/loader.d.ts.map +0 -1
  38. package/dist/config/loader.js.map +0 -1
  39. package/dist/index.d.ts.map +0 -1
  40. package/dist/index.js.map +0 -1
  41. package/dist/parser/flow.d.ts.map +0 -1
  42. package/dist/parser/flow.js.map +0 -1
  43. package/dist/report/generator.d.ts.map +0 -1
  44. package/dist/report/generator.js.map +0 -1
  45. package/dist/runner/index.d.ts.map +0 -1
  46. package/dist/runner/index.js.map +0 -1
  47. package/dist/types.d.ts.map +0 -1
  48. package/dist/types.js.map +0 -1
@@ -1,549 +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
- <!-- Steps -->
267
- <div class="bg-white rounded-xl shadow-sm overflow-hidden">
268
- <div class="px-6 py-4 border-b border-gray-200 font-semibold">Steps</div>
269
- ${result.steps.map((s, i) => this.formatStepHTML(s, i)).join("")}
270
- </div>
271
-
272
- <!-- Footer -->
273
- <div class="text-center text-gray-400 text-sm py-8">
274
- Generated by Slapify ยท ${new Date().toISOString()}
275
- </div>
276
- </div>
277
-
278
- <!-- Modal -->
279
- <div class="modal fixed inset-0 bg-black/90 z-50 items-center justify-center cursor-pointer" id="modal">
280
- <img src="" alt="Screenshot" id="modal-img" class="max-w-[95%] max-h-[95%] rounded-lg">
281
- </div>
282
-
283
- <script>
284
- document.querySelectorAll('.step-header').forEach(header => {
285
- header.addEventListener('click', () => header.parentElement.classList.toggle('expanded'));
286
- });
287
-
288
- const modal = document.getElementById('modal');
289
- const modalImg = document.getElementById('modal-img');
290
-
291
- document.querySelectorAll('.screenshot-img').forEach(img => {
292
- img.addEventListener('click', (e) => {
293
- e.stopPropagation();
294
- modalImg.src = img.src;
295
- modal.classList.add('active');
296
- });
297
- });
298
-
299
- modal.addEventListener('click', () => modal.classList.remove('active'));
300
- document.querySelectorAll('.step.failed').forEach(step => step.classList.add('expanded'));
301
- </script>
302
- </body>
303
- </html>`;
304
- }
305
- /**
306
- * Format a single step for HTML with Tailwind
307
- */
308
- formatStepHTML(result, index) {
309
- const step = result.step;
310
- const statusIcon = result.status === "passed" ? "โœ“" : result.status === "failed" ? "โœ—" : "โ—‹";
311
- const statusColors = {
312
- passed: "bg-green-100 text-green-600",
313
- failed: "bg-red-100 text-red-600",
314
- skipped: "bg-yellow-100 text-yellow-600",
315
- };
316
- const hasScreenshot = !!result.screenshot;
317
- const wasRetried = result.retried;
318
- return `
319
- <div class="step ${result.status} border-b border-gray-100 last:border-b-0">
320
- <div class="step-header flex items-center px-6 py-4 cursor-pointer hover:bg-gray-50 transition-colors">
321
- <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>
322
- <div class="flex-1 min-w-0">
323
- <div class="font-medium flex items-center flex-wrap gap-2">
324
- <span>${step.text}</span>
325
- ${step.optional
326
- ? '<span class="px-2 py-0.5 bg-gray-100 text-gray-500 text-xs rounded">Optional</span>'
327
- : ""}
328
- ${wasRetried
329
- ? '<span class="px-2 py-0.5 bg-orange-100 text-orange-600 text-xs rounded">Retried</span>'
330
- : ""}
331
- </div>
332
- ${result.actions.length > 0
333
- ? `<div class="text-sm text-gray-500 mt-1 truncate">${result.actions[0]?.description || ""}</div>`
334
- : ""}
335
- </div>
336
- <div class="flex items-center gap-3 ml-4">
337
- ${hasScreenshot ? '<span class="text-gray-400" title="Has screenshot">๐Ÿ“ท</span>' : ""}
338
- <span class="text-gray-400 text-sm">${this.formatDuration(result.duration)}</span>
339
- </div>
340
- <div class="chevron ml-2 text-gray-400 transition-transform">โ–ผ</div>
341
- </div>
342
- <div class="step-details px-6 pb-6 pl-16">
343
- ${result.actions.length > 0
344
- ? `
345
- <div class="bg-gray-50 rounded-lg p-4 mb-4">
346
- ${result.actions
347
- .map((a) => `<div class="py-1 text-sm"><span class="mr-2">${this.getActionIcon(a.type)}</span>${a.description}</div>`)
348
- .join("")}
349
- </div>
350
- `
351
- : ""}
352
- ${result.assumptions && result.assumptions.length > 0
353
- ? `
354
- <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
355
- <div class="font-semibold text-yellow-800 mb-2">๐Ÿ’ก Assumptions</div>
356
- ${result.assumptions
357
- .map((a) => `<div class="text-yellow-700 text-sm py-1">โ€ข ${a}</div>`)
358
- .join("")}
359
- </div>
360
- `
361
- : ""}
362
- ${result.error
363
- ? `<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>`
364
- : ""}
365
- ${result.screenshot
366
- ? `
367
- <div class="mt-4">
368
- <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">
369
- </div>
370
- `
371
- : ""}
372
- </div>
373
- </div>`;
374
- }
375
- /**
376
- * Generate suite report HTML with Tailwind
377
- */
378
- generateSuiteHTML(results) {
379
- const totalTests = results.length;
380
- const passedTests = results.filter((r) => r.status === "passed").length;
381
- const failedTests = results.filter((r) => r.status === "failed").length;
382
- const totalSteps = results.reduce((sum, r) => sum + r.totalSteps, 0);
383
- const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
384
- const overallStatus = failedTests === 0 ? "passed" : "failed";
385
- return `<!DOCTYPE html>
386
- <html lang="en">
387
- <head>
388
- <meta charset="UTF-8">
389
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
390
- <title>Test Suite Report</title>
391
- <script src="https://cdn.tailwindcss.com"></script>
392
- <style>
393
- .test-details { display: none; }
394
- .test-item.expanded .test-details { display: block; }
395
- .test-item.expanded .chevron { transform: rotate(180deg); }
396
- </style>
397
- </head>
398
- <body class="bg-gray-50 text-gray-900 min-h-screen">
399
- <div class="max-w-5xl mx-auto p-6">
400
-
401
- <!-- Header -->
402
- <div class="bg-white rounded-xl shadow-sm p-8 mb-6">
403
- <div class="flex items-center justify-between mb-6">
404
- <h1 class="text-3xl font-bold">Test Suite Report</h1>
405
- <span class="px-6 py-3 rounded-full font-bold ${overallStatus === "passed"
406
- ? "bg-green-100 text-green-800"
407
- : "bg-red-100 text-red-800"}">
408
- ${overallStatus === "passed" ? "โœ“ ALL PASSED" : "โœ— FAILURES"}
409
- </span>
410
- </div>
411
-
412
- <!-- Stats -->
413
- <div class="grid grid-cols-5 gap-4">
414
- <div class="bg-gray-100 rounded-lg p-5 text-center">
415
- <div class="text-4xl font-bold">${totalTests}</div>
416
- <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Test Files</div>
417
- </div>
418
- <div class="bg-gray-100 rounded-lg p-5 text-center">
419
- <div class="text-4xl font-bold text-green-600">${passedTests}</div>
420
- <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Passed</div>
421
- </div>
422
- <div class="bg-gray-100 rounded-lg p-5 text-center">
423
- <div class="text-4xl font-bold text-red-600">${failedTests}</div>
424
- <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Failed</div>
425
- </div>
426
- <div class="bg-gray-100 rounded-lg p-5 text-center">
427
- <div class="text-4xl font-bold">${totalSteps}</div>
428
- <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Total Steps</div>
429
- </div>
430
- <div class="bg-gray-100 rounded-lg p-5 text-center">
431
- <div class="text-2xl font-bold">${this.formatDuration(totalDuration)}</div>
432
- <div class="text-xs text-gray-500 uppercase tracking-wide mt-1">Duration</div>
433
- </div>
434
- </div>
435
- </div>
436
-
437
- <!-- Test List -->
438
- <div class="bg-white rounded-xl shadow-sm overflow-hidden">
439
- <div class="px-6 py-4 border-b border-gray-200 font-semibold text-lg">Test Files (${totalTests})</div>
440
- ${results.map((r, i) => this.formatTestItemHTML(r, i)).join("")}
441
- </div>
442
-
443
- <!-- Footer -->
444
- <div class="text-center text-gray-400 text-sm py-8">
445
- Generated by Slapify ยท ${new Date().toISOString()}
446
- </div>
447
- </div>
448
-
449
- <script>
450
- document.querySelectorAll('.test-header').forEach(header => {
451
- header.addEventListener('click', () => header.parentElement.classList.toggle('expanded'));
452
- });
453
- document.querySelectorAll('.test-item.failed').forEach(item => item.classList.add('expanded'));
454
- </script>
455
- </body>
456
- </html>`;
457
- }
458
- /**
459
- * Format a test item for suite report with Tailwind
460
- */
461
- formatTestItemHTML(result, index) {
462
- const statusIcon = result.status === "passed" ? "โœ“" : "โœ—";
463
- const statusColors = result.status === "passed"
464
- ? "bg-green-100 text-green-600"
465
- : "bg-red-100 text-red-600";
466
- return `
467
- <div class="test-item ${result.status} border-b border-gray-100 last:border-b-0">
468
- <div class="test-header flex items-center px-6 py-5 cursor-pointer hover:bg-gray-50 transition-colors">
469
- <div class="w-10 h-10 rounded-full flex items-center justify-center font-bold mr-4 ${statusColors}">${statusIcon}</div>
470
- <div class="flex-1">
471
- <div class="font-semibold text-lg">${path.basename(result.flowFile, ".flow")}</div>
472
- <div class="text-sm text-gray-500 mt-1">${result.totalSteps} steps ยท ${this.formatDuration(result.duration)}</div>
473
- </div>
474
- <div class="flex gap-2 ml-4">
475
- <span class="px-3 py-1 bg-green-100 text-green-700 rounded text-sm font-medium">${result.passedSteps} passed</span>
476
- ${result.failedSteps > 0
477
- ? `<span class="px-3 py-1 bg-red-100 text-red-700 rounded text-sm font-medium">${result.failedSteps} failed</span>`
478
- : ""}
479
- </div>
480
- <div class="chevron ml-4 text-gray-400 transition-transform">โ–ผ</div>
481
- </div>
482
- <div class="test-details px-6 pb-6 bg-gray-50">
483
- ${result.steps
484
- .map((s) => {
485
- const stepColors = {
486
- passed: "bg-green-100 text-green-600",
487
- failed: "bg-red-100 text-red-600",
488
- skipped: "bg-yellow-100 text-yellow-600",
489
- };
490
- const stepIcon = s.status === "passed" ? "โœ“" : s.status === "failed" ? "โœ—" : "โ—‹";
491
- return `
492
- <div class="flex items-center py-2 text-sm">
493
- <div class="w-6 h-6 rounded-full flex items-center justify-center text-xs mr-3 ${stepColors[s.status]}">${stepIcon}</div>
494
- <div class="flex-1">${s.step.text}</div>
495
- <div class="text-gray-400">${this.formatDuration(s.duration)}</div>
496
- </div>
497
- ${s.error
498
- ? `<div class="ml-9 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700 mb-2">${s.error}</div>`
499
- : ""}
500
- `;
501
- })
502
- .join("")}
503
- </div>
504
- </div>`;
505
- }
506
- /**
507
- * Format duration in human-readable form
508
- */
509
- formatDuration(ms) {
510
- if (ms < 1000)
511
- return `${ms}ms`;
512
- if (ms < 60000)
513
- return `${(ms / 1000).toFixed(1)}s`;
514
- const mins = Math.floor(ms / 60000);
515
- const secs = Math.floor((ms % 60000) / 1000);
516
- return `${mins}m ${secs}s`;
517
- }
518
- /**
519
- * Get icon for action type
520
- */
521
- getActionIcon(type) {
522
- switch (type) {
523
- case "navigate":
524
- return "๐Ÿ”—";
525
- case "click":
526
- return "๐Ÿ‘†";
527
- case "fill":
528
- return "โœ๏ธ";
529
- case "verify":
530
- return "โœ“";
531
- case "wait":
532
- return "โณ";
533
- case "auto-handle":
534
- return "โ„น๏ธ";
535
- default:
536
- return "โ€ข";
537
- }
538
- }
539
- /**
540
- * Print summary to console
541
- */
542
- printSummary(result) {
543
- const statusEmoji = result.status === "passed" ? "โœ…" : "โŒ";
544
- console.log("");
545
- console.log(`${statusEmoji} ${path.basename(result.flowFile, ".flow")} - ${result.status.toUpperCase()}`);
546
- console.log(` ${result.passedSteps}/${result.totalSteps} steps passed (${this.formatDuration(result.duration)})`);
547
- }
548
- }
549
- //# 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)})`)}}
@@ -14,8 +14,9 @@ export declare class TestRunner {
14
14
  * Run a flow file
15
15
  * @param flow The flow file to run
16
16
  * @param onStep Optional callback for real-time step progress
17
+ * @param runPerformanceAudit If true, collect perf metrics before closing the browser
17
18
  */
18
- runFlow(flow: FlowFile, onStep?: (result: StepResult) => void): Promise<TestResult>;
19
+ runFlow(flow: FlowFile, onStep?: (result: StepResult) => void, runPerformanceAudit?: boolean): Promise<TestResult>;
19
20
  /**
20
21
  * Execute a single step
21
22
  */
@@ -24,6 +25,18 @@ export declare class TestRunner {
24
25
  * Execute a browser command
25
26
  */
26
27
  private executeCommand;
28
+ /**
29
+ * Pick the best credential profile for a given URL.
30
+ * Prefers profiles whose name matches the current domain, falls back to "default".
31
+ * Returns null when no profiles exist.
32
+ */
33
+ private pickBestCredentialProfile;
34
+ /**
35
+ * Detect and solve captchas on the current page.
36
+ * Handles common captcha patterns โ€” checkbox reCAPTCHA, hCaptcha, Cloudflare Turnstile.
37
+ * Runs silently; any failure is swallowed so the main flow continues.
38
+ */
39
+ private handleCaptcha;
27
40
  /**
28
41
  * Handle automatic dismissal of interruptions
29
42
  */