cbrowser 7.3.0 → 7.4.1

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 (85) hide show
  1. package/dist/analysis/bug-hunter.d.ts +32 -0
  2. package/dist/analysis/bug-hunter.d.ts.map +1 -0
  3. package/dist/analysis/bug-hunter.js +106 -0
  4. package/dist/analysis/bug-hunter.js.map +1 -0
  5. package/dist/analysis/chaos-testing.d.ts +41 -0
  6. package/dist/analysis/chaos-testing.d.ts.map +1 -0
  7. package/dist/analysis/chaos-testing.js +87 -0
  8. package/dist/analysis/chaos-testing.js.map +1 -0
  9. package/dist/analysis/index.d.ts +10 -0
  10. package/dist/analysis/index.d.ts.map +1 -0
  11. package/dist/analysis/index.js +26 -0
  12. package/dist/analysis/index.js.map +1 -0
  13. package/dist/analysis/natural-language.d.ts +43 -0
  14. package/dist/analysis/natural-language.d.ts.map +1 -0
  15. package/dist/analysis/natural-language.js +205 -0
  16. package/dist/analysis/natural-language.js.map +1 -0
  17. package/dist/analysis/persona-comparison.d.ts +31 -0
  18. package/dist/analysis/persona-comparison.d.ts.map +1 -0
  19. package/dist/analysis/persona-comparison.js +217 -0
  20. package/dist/analysis/persona-comparison.js.map +1 -0
  21. package/dist/browser.d.ts +1 -411
  22. package/dist/browser.d.ts.map +1 -1
  23. package/dist/browser.js +0 -4745
  24. package/dist/browser.js.map +1 -1
  25. package/dist/cli.js +64 -56
  26. package/dist/cli.js.map +1 -1
  27. package/dist/daemon.d.ts.map +1 -1
  28. package/dist/daemon.js +2 -1
  29. package/dist/daemon.js.map +1 -1
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +9 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/mcp-server.d.ts.map +1 -1
  35. package/dist/mcp-server.js +406 -1
  36. package/dist/mcp-server.js.map +1 -1
  37. package/dist/performance/index.d.ts +7 -0
  38. package/dist/performance/index.d.ts.map +1 -0
  39. package/dist/performance/index.js +23 -0
  40. package/dist/performance/index.js.map +1 -0
  41. package/dist/performance/metrics.d.ts +49 -0
  42. package/dist/performance/metrics.d.ts.map +1 -0
  43. package/dist/performance/metrics.js +386 -0
  44. package/dist/performance/metrics.js.map +1 -0
  45. package/dist/testing/coverage.d.ts +39 -0
  46. package/dist/testing/coverage.d.ts.map +1 -0
  47. package/dist/testing/coverage.js +713 -0
  48. package/dist/testing/coverage.js.map +1 -0
  49. package/dist/testing/flaky-detection.d.ts +28 -0
  50. package/dist/testing/flaky-detection.d.ts.map +1 -0
  51. package/dist/testing/flaky-detection.js +332 -0
  52. package/dist/testing/flaky-detection.js.map +1 -0
  53. package/dist/testing/index.d.ts +10 -0
  54. package/dist/testing/index.d.ts.map +1 -0
  55. package/dist/testing/index.js +26 -0
  56. package/dist/testing/index.js.map +1 -0
  57. package/dist/testing/nl-test-suite.d.ts +70 -0
  58. package/dist/testing/nl-test-suite.d.ts.map +1 -0
  59. package/dist/testing/nl-test-suite.js +427 -0
  60. package/dist/testing/nl-test-suite.js.map +1 -0
  61. package/dist/testing/test-repair.d.ts +36 -0
  62. package/dist/testing/test-repair.d.ts.map +1 -0
  63. package/dist/testing/test-repair.js +528 -0
  64. package/dist/testing/test-repair.js.map +1 -0
  65. package/dist/visual/ab-comparison.d.ts +23 -0
  66. package/dist/visual/ab-comparison.d.ts.map +1 -0
  67. package/dist/visual/ab-comparison.js +366 -0
  68. package/dist/visual/ab-comparison.js.map +1 -0
  69. package/dist/visual/cross-browser.d.ts +41 -0
  70. package/dist/visual/cross-browser.d.ts.map +1 -0
  71. package/dist/visual/cross-browser.js +442 -0
  72. package/dist/visual/cross-browser.js.map +1 -0
  73. package/dist/visual/index.d.ts +10 -0
  74. package/dist/visual/index.d.ts.map +1 -0
  75. package/dist/visual/index.js +26 -0
  76. package/dist/visual/index.js.map +1 -0
  77. package/dist/visual/regression.d.ts +55 -0
  78. package/dist/visual/regression.d.ts.map +1 -0
  79. package/dist/visual/regression.js +616 -0
  80. package/dist/visual/regression.js.map +1 -0
  81. package/dist/visual/responsive.d.ts +27 -0
  82. package/dist/visual/responsive.d.ts.map +1 -0
  83. package/dist/visual/responsive.js +450 -0
  84. package/dist/visual/responsive.js.map +1 -0
  85. package/package.json +32 -3
@@ -0,0 +1,713 @@
1
+ "use strict";
2
+ /**
3
+ * Test Coverage Map (v6.5.0)
4
+ *
5
+ * Analyze test coverage across your site.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.parseTestFilesForCoverage = parseTestFilesForCoverage;
9
+ exports.parseSitemap = parseSitemap;
10
+ exports.crawlSiteForCoverage = crawlSiteForCoverage;
11
+ exports.identifyCoverageGaps = identifyCoverageGaps;
12
+ exports.calculateCoverageAnalysis = calculateCoverageAnalysis;
13
+ exports.generateCoverageMap = generateCoverageMap;
14
+ exports.formatCoverageReport = formatCoverageReport;
15
+ exports.generateCoverageHtmlReport = generateCoverageHtmlReport;
16
+ const fs_1 = require("fs");
17
+ const browser_js_1 = require("../browser.js");
18
+ /**
19
+ * Parse test files to extract tested URLs and actions
20
+ */
21
+ function parseTestFilesForCoverage(testFiles) {
22
+ const pageMap = new Map();
23
+ for (const testFile of testFiles) {
24
+ if (!(0, fs_1.existsSync)(testFile))
25
+ continue;
26
+ const content = (0, fs_1.readFileSync)(testFile, "utf-8");
27
+ const lines = content.split("\n");
28
+ let currentUrl = null;
29
+ let lineNumber = 0;
30
+ for (const line of lines) {
31
+ lineNumber++;
32
+ const trimmed = line.trim().toLowerCase();
33
+ // Skip comments and empty lines
34
+ if (trimmed.startsWith("#") || !trimmed)
35
+ continue;
36
+ // Detect navigation
37
+ const navMatch = line.match(/(?:go to|navigate to|open|visit)\s+["']?([^"'\s]+)["']?/i);
38
+ if (navMatch) {
39
+ currentUrl = navMatch[1];
40
+ const path = normalizeUrlToPath(currentUrl);
41
+ if (!pageMap.has(path)) {
42
+ pageMap.set(path, {
43
+ url: currentUrl,
44
+ path,
45
+ testFiles: [],
46
+ actions: [],
47
+ testCount: 0,
48
+ coverageScore: 0,
49
+ });
50
+ }
51
+ const page = pageMap.get(path);
52
+ if (!page.testFiles.includes(testFile)) {
53
+ page.testFiles.push(testFile);
54
+ page.testCount++;
55
+ }
56
+ page.actions.push({
57
+ type: "navigate",
58
+ target: currentUrl,
59
+ testFile,
60
+ lineNumber,
61
+ });
62
+ }
63
+ // Detect click actions
64
+ const clickMatch = line.match(/click\s+(?:on\s+)?(?:the\s+)?["']?([^"'\n]+)["']?/i);
65
+ if (clickMatch && currentUrl) {
66
+ const path = normalizeUrlToPath(currentUrl);
67
+ const page = pageMap.get(path);
68
+ if (page) {
69
+ page.actions.push({
70
+ type: "click",
71
+ target: clickMatch[1].trim(),
72
+ testFile,
73
+ lineNumber,
74
+ });
75
+ }
76
+ }
77
+ // Detect fill/type actions
78
+ const fillMatch = line.match(/(?:type|fill|enter)\s+["']([^"']+)["']\s+(?:in|into)\s+(?:the\s+)?["']?([^"'\n]+)["']?/i);
79
+ if (fillMatch && currentUrl) {
80
+ const path = normalizeUrlToPath(currentUrl);
81
+ const page = pageMap.get(path);
82
+ if (page) {
83
+ page.actions.push({
84
+ type: "fill",
85
+ target: fillMatch[2].trim(),
86
+ value: fillMatch[1],
87
+ testFile,
88
+ lineNumber,
89
+ });
90
+ }
91
+ }
92
+ // Detect verify actions
93
+ const verifyMatch = line.match(/(?:verify|assert|check|expect|should)\s+(.+)/i);
94
+ if (verifyMatch && currentUrl) {
95
+ const path = normalizeUrlToPath(currentUrl);
96
+ const page = pageMap.get(path);
97
+ if (page) {
98
+ page.actions.push({
99
+ type: "verify",
100
+ target: verifyMatch[1].trim(),
101
+ testFile,
102
+ lineNumber,
103
+ });
104
+ }
105
+ }
106
+ // Detect wait actions
107
+ const waitMatch = line.match(/wait\s+(?:for\s+)?(.+)/i);
108
+ if (waitMatch && currentUrl) {
109
+ const path = normalizeUrlToPath(currentUrl);
110
+ const page = pageMap.get(path);
111
+ if (page) {
112
+ page.actions.push({
113
+ type: "wait",
114
+ target: waitMatch[1].trim(),
115
+ testFile,
116
+ lineNumber,
117
+ });
118
+ }
119
+ }
120
+ }
121
+ }
122
+ // Calculate coverage scores
123
+ for (const page of pageMap.values()) {
124
+ const hasClicks = page.actions.some(a => a.type === "click");
125
+ const hasFills = page.actions.some(a => a.type === "fill");
126
+ const hasVerifies = page.actions.some(a => a.type === "verify");
127
+ let score = 20; // Base score for visiting
128
+ if (hasClicks)
129
+ score += 25;
130
+ if (hasFills)
131
+ score += 25;
132
+ if (hasVerifies)
133
+ score += 30;
134
+ page.coverageScore = Math.min(100, score);
135
+ }
136
+ return Array.from(pageMap.values());
137
+ }
138
+ /**
139
+ * Normalize URL to a path for comparison
140
+ */
141
+ function normalizeUrlToPath(url) {
142
+ try {
143
+ const parsed = new URL(url);
144
+ return parsed.pathname.replace(/\/$/, "") || "/";
145
+ }
146
+ catch {
147
+ // Not a full URL, treat as path
148
+ return url.replace(/\/$/, "") || "/";
149
+ }
150
+ }
151
+ /**
152
+ * Fetch and parse sitemap.xml
153
+ */
154
+ async function parseSitemap(sitemapUrl) {
155
+ const pages = [];
156
+ try {
157
+ const response = await fetch(sitemapUrl);
158
+ const xml = await response.text();
159
+ // Simple XML parsing for sitemap
160
+ const locMatches = xml.matchAll(/<loc>([^<]+)<\/loc>/g);
161
+ for (const match of locMatches) {
162
+ const url = match[1].trim();
163
+ pages.push({
164
+ url,
165
+ path: normalizeUrlToPath(url),
166
+ source: "sitemap",
167
+ });
168
+ }
169
+ }
170
+ catch (err) {
171
+ console.error(`Failed to fetch sitemap: ${err}`);
172
+ }
173
+ return pages;
174
+ }
175
+ /**
176
+ * Crawl a site to discover pages
177
+ */
178
+ async function crawlSiteForCoverage(startUrl, maxPages = 100, includePattern, excludePattern) {
179
+ const pages = [];
180
+ const visited = new Set();
181
+ const queue = [startUrl];
182
+ const browser = new browser_js_1.CBrowser({
183
+ headless: true,
184
+ browser: "chromium",
185
+ });
186
+ const baseUrl = new URL(startUrl);
187
+ const includeRegex = includePattern ? new RegExp(includePattern) : null;
188
+ const excludeRegex = excludePattern ? new RegExp(excludePattern) : null;
189
+ try {
190
+ while (queue.length > 0 && pages.length < maxPages) {
191
+ const url = queue.shift();
192
+ const path = normalizeUrlToPath(url);
193
+ if (visited.has(path))
194
+ continue;
195
+ visited.add(path);
196
+ // Check patterns
197
+ if (includeRegex && !includeRegex.test(path))
198
+ continue;
199
+ if (excludeRegex && excludeRegex.test(path))
200
+ continue;
201
+ try {
202
+ const result = await browser.navigate(url);
203
+ // Count interactive elements
204
+ const page = await browser.getPage();
205
+ const interactiveElements = await page.locator("button, a, input, select, textarea, [onclick], [role='button']").count();
206
+ const formCount = await page.locator("form").count();
207
+ // Get outbound links
208
+ const links = await page.locator("a[href]").evaluateAll((els) => els.map(el => el.href).filter(href => href && !href.startsWith("javascript:")));
209
+ const sitePage = {
210
+ url,
211
+ path,
212
+ title: result.title,
213
+ source: pages.length === 0 ? "crawl" : "link",
214
+ status: 200,
215
+ outboundLinks: links,
216
+ interactiveElements,
217
+ formCount,
218
+ };
219
+ pages.push(sitePage);
220
+ // Add internal links to queue
221
+ for (const link of links) {
222
+ try {
223
+ const linkUrl = new URL(link);
224
+ if (linkUrl.hostname === baseUrl.hostname && !visited.has(normalizeUrlToPath(link))) {
225
+ queue.push(link);
226
+ }
227
+ }
228
+ catch {
229
+ // Invalid URL, skip
230
+ }
231
+ }
232
+ }
233
+ catch (err) {
234
+ // Page failed to load
235
+ pages.push({
236
+ url,
237
+ path,
238
+ source: "link",
239
+ status: 0,
240
+ });
241
+ }
242
+ }
243
+ }
244
+ finally {
245
+ await browser.close();
246
+ }
247
+ return pages;
248
+ }
249
+ /**
250
+ * Identify coverage gaps
251
+ */
252
+ function identifyCoverageGaps(sitePages, testedPages, minCoverage = 50) {
253
+ const gaps = [];
254
+ const testedPaths = new Set(testedPages.map(p => p.path));
255
+ for (const sitePage of sitePages) {
256
+ const testedPage = testedPages.find(p => p.path === sitePage.path);
257
+ // Completely untested
258
+ if (!testedPage) {
259
+ const priority = determinePriority(sitePage);
260
+ gaps.push({
261
+ page: sitePage,
262
+ reason: "untested",
263
+ priority,
264
+ suggestedTests: generateSuggestedTests(sitePage),
265
+ similarTestedPages: findSimilarTestedPages(sitePage.path, testedPages),
266
+ });
267
+ continue;
268
+ }
269
+ // Low coverage
270
+ if (testedPage.coverageScore < minCoverage) {
271
+ gaps.push({
272
+ page: sitePage,
273
+ reason: "low-coverage",
274
+ priority: "medium",
275
+ suggestedTests: generateSuggestedTests(sitePage, testedPage),
276
+ });
277
+ continue;
278
+ }
279
+ // No interactions tested
280
+ const hasInteractions = testedPage.actions.some(a => a.type === "click" || a.type === "fill");
281
+ if (!hasInteractions && sitePage.interactiveElements && sitePage.interactiveElements > 5) {
282
+ gaps.push({
283
+ page: sitePage,
284
+ reason: "no-interactions",
285
+ priority: "low",
286
+ suggestedTests: [`Test interactive elements on ${sitePage.path}`],
287
+ });
288
+ }
289
+ // No verifications
290
+ const hasVerifications = testedPage.actions.some(a => a.type === "verify");
291
+ if (!hasVerifications) {
292
+ gaps.push({
293
+ page: sitePage,
294
+ reason: "no-verifications",
295
+ priority: "low",
296
+ suggestedTests: [`Add assertions to verify ${sitePage.path} content`],
297
+ });
298
+ }
299
+ }
300
+ // Sort by priority
301
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
302
+ gaps.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
303
+ return gaps;
304
+ }
305
+ /**
306
+ * Determine priority of an untested page
307
+ */
308
+ function determinePriority(page) {
309
+ const path = page.path.toLowerCase();
310
+ // Critical paths
311
+ if (path.includes("checkout") || path.includes("payment") || path.includes("login") ||
312
+ path.includes("register") || path.includes("signup") || path.includes("auth")) {
313
+ return "critical";
314
+ }
315
+ // High priority - user account, settings
316
+ if (path.includes("account") || path.includes("profile") || path.includes("settings") ||
317
+ path.includes("dashboard") || path.includes("admin")) {
318
+ return "high";
319
+ }
320
+ // Medium - has forms or many interactive elements
321
+ if (page.formCount && page.formCount > 0)
322
+ return "medium";
323
+ if (page.interactiveElements && page.interactiveElements > 10)
324
+ return "medium";
325
+ return "low";
326
+ }
327
+ /**
328
+ * Generate suggested test steps for a page
329
+ */
330
+ function generateSuggestedTests(sitePage, existingTests) {
331
+ const suggestions = [];
332
+ suggestions.push(`go to ${sitePage.url}`);
333
+ if (sitePage.formCount && sitePage.formCount > 0) {
334
+ suggestions.push(`fill form fields with test data`);
335
+ suggestions.push(`submit form and verify success`);
336
+ }
337
+ if (sitePage.interactiveElements && sitePage.interactiveElements > 0) {
338
+ suggestions.push(`click primary call-to-action`);
339
+ }
340
+ suggestions.push(`verify page contains expected content`);
341
+ suggestions.push(`verify no console errors`);
342
+ if (existingTests) {
343
+ // Add specific suggestions based on what's missing
344
+ const hasClicks = existingTests.actions.some(a => a.type === "click");
345
+ const hasFills = existingTests.actions.some(a => a.type === "fill");
346
+ const hasVerifies = existingTests.actions.some(a => a.type === "verify");
347
+ if (!hasClicks)
348
+ suggestions.unshift(`# Add click interactions`);
349
+ if (!hasFills && sitePage.formCount)
350
+ suggestions.unshift(`# Add form fill tests`);
351
+ if (!hasVerifies)
352
+ suggestions.unshift(`# Add verification assertions`);
353
+ }
354
+ return suggestions;
355
+ }
356
+ /**
357
+ * Find similar tested pages for reference
358
+ */
359
+ function findSimilarTestedPages(path, testedPages) {
360
+ const segments = path.split("/").filter(Boolean);
361
+ if (segments.length === 0)
362
+ return [];
363
+ const similar = [];
364
+ const prefix = "/" + segments[0];
365
+ for (const tested of testedPages) {
366
+ if (tested.path.startsWith(prefix) && tested.path !== path) {
367
+ similar.push(tested.path);
368
+ if (similar.length >= 3)
369
+ break;
370
+ }
371
+ }
372
+ return similar;
373
+ }
374
+ /**
375
+ * Calculate overall coverage analysis
376
+ */
377
+ function calculateCoverageAnalysis(sitePages, testedPages) {
378
+ const testedPaths = new Set(testedPages.map(p => p.path));
379
+ // Section coverage
380
+ const sections = {};
381
+ for (const page of sitePages) {
382
+ const segments = page.path.split("/").filter(Boolean);
383
+ const section = segments.length > 0 ? "/" + segments[0] : "/";
384
+ if (!sections[section]) {
385
+ sections[section] = { total: 0, tested: 0 };
386
+ }
387
+ sections[section].total++;
388
+ if (testedPaths.has(page.path)) {
389
+ sections[section].tested++;
390
+ }
391
+ }
392
+ const sectionCoverage = {};
393
+ for (const [section, data] of Object.entries(sections)) {
394
+ sectionCoverage[section] = {
395
+ ...data,
396
+ percent: data.total > 0 ? Math.round((data.tested / data.total) * 100) : 0,
397
+ };
398
+ }
399
+ const totalPages = sitePages.length;
400
+ const testedCount = sitePages.filter(p => testedPaths.has(p.path)).length;
401
+ return {
402
+ totalPages,
403
+ testedPages: testedCount,
404
+ untestedPages: totalPages - testedCount,
405
+ coveragePercent: totalPages > 0 ? Math.round((testedCount / totalPages) * 100) : 0,
406
+ sectionCoverage,
407
+ };
408
+ }
409
+ /**
410
+ * Generate complete coverage map
411
+ */
412
+ async function generateCoverageMap(baseUrl, testFiles, options = {}) {
413
+ const startTime = Date.now();
414
+ // Parse test files
415
+ const testedPages = parseTestFilesForCoverage(testFiles);
416
+ // Get site pages
417
+ let sitePages;
418
+ if (options.sitemapUrl) {
419
+ sitePages = await parseSitemap(options.sitemapUrl);
420
+ }
421
+ else {
422
+ sitePages = await crawlSiteForCoverage(baseUrl, options.maxPages || 100, options.includePattern, options.excludePattern);
423
+ }
424
+ // Identify gaps
425
+ const gaps = identifyCoverageGaps(sitePages, testedPages, options.minCoverage || 50);
426
+ // Calculate analysis
427
+ const analysis = calculateCoverageAnalysis(sitePages, testedPages);
428
+ // Generate recommendations
429
+ const recommendations = [];
430
+ if (analysis.coveragePercent < 50) {
431
+ recommendations.push("Coverage is below 50% - prioritize testing critical paths");
432
+ }
433
+ const criticalGaps = gaps.filter(g => g.priority === "critical");
434
+ if (criticalGaps.length > 0) {
435
+ recommendations.push(`${criticalGaps.length} critical pages have no tests (checkout, auth, etc.)`);
436
+ }
437
+ const lowCoverageSections = Object.entries(analysis.sectionCoverage)
438
+ .filter(([_, data]) => data.percent < 30 && data.total > 2)
439
+ .map(([section]) => section);
440
+ if (lowCoverageSections.length > 0) {
441
+ recommendations.push(`Sections with low coverage: ${lowCoverageSections.join(", ")}`);
442
+ }
443
+ if (gaps.filter(g => g.reason === "no-verifications").length > 3) {
444
+ recommendations.push("Many tests lack assertions - add verification steps");
445
+ }
446
+ return {
447
+ baseUrl,
448
+ timestamp: new Date().toISOString(),
449
+ duration: Date.now() - startTime,
450
+ testFiles,
451
+ sitePages,
452
+ testedPages,
453
+ gaps,
454
+ analysis,
455
+ recommendations,
456
+ };
457
+ }
458
+ /**
459
+ * Format coverage map as text report
460
+ */
461
+ function formatCoverageReport(result) {
462
+ const lines = [];
463
+ lines.push("╔══════════════════════════════════════════════════════════════════════════════╗");
464
+ lines.push("║ TEST COVERAGE MAP REPORT ║");
465
+ lines.push("╚══════════════════════════════════════════════════════════════════════════════╝");
466
+ lines.push("");
467
+ lines.push(`📊 Site: ${result.baseUrl}`);
468
+ lines.push(`📅 Generated: ${result.timestamp}`);
469
+ lines.push(`⏱️ Analysis time: ${(result.duration / 1000).toFixed(1)}s`);
470
+ lines.push(`📝 Test files analyzed: ${result.testFiles.length}`);
471
+ lines.push("");
472
+ // Overall coverage
473
+ lines.push("═══════════════════════════════════════════════════════════════════════════════");
474
+ lines.push("📈 OVERALL COVERAGE");
475
+ lines.push("═══════════════════════════════════════════════════════════════════════════════");
476
+ lines.push("");
477
+ const { analysis } = result;
478
+ const coverageBar = generateCoverageProgressBar(analysis.coveragePercent);
479
+ lines.push(` Coverage: ${coverageBar} ${analysis.coveragePercent}%`);
480
+ lines.push("");
481
+ lines.push(` Total pages: ${analysis.totalPages}`);
482
+ lines.push(` Tested pages: ${analysis.testedPages}`);
483
+ lines.push(` Untested pages: ${analysis.untestedPages}`);
484
+ lines.push("");
485
+ // Section coverage
486
+ lines.push("───────────────────────────────────────────────────────────────────────────────");
487
+ lines.push("📁 COVERAGE BY SECTION");
488
+ lines.push("───────────────────────────────────────────────────────────────────────────────");
489
+ lines.push("");
490
+ const sections = Object.entries(analysis.sectionCoverage)
491
+ .sort((a, b) => b[1].total - a[1].total);
492
+ for (const [section, data] of sections) {
493
+ const bar = generateCoverageProgressBar(data.percent, 20);
494
+ const status = data.percent >= 70 ? "✅" : data.percent >= 40 ? "⚠️" : "❌";
495
+ lines.push(` ${status} ${section.padEnd(20)} ${bar} ${data.tested}/${data.total} (${data.percent}%)`);
496
+ }
497
+ lines.push("");
498
+ // Coverage gaps
499
+ if (result.gaps.length > 0) {
500
+ lines.push("═══════════════════════════════════════════════════════════════════════════════");
501
+ lines.push("🕳️ COVERAGE GAPS");
502
+ lines.push("═══════════════════════════════════════════════════════════════════════════════");
503
+ lines.push("");
504
+ const priorityEmoji = { critical: "🚨", high: "🔴", medium: "🟡", low: "🟢" };
505
+ for (const gap of result.gaps.slice(0, 15)) {
506
+ const emoji = priorityEmoji[gap.priority];
507
+ lines.push(` ${emoji} ${gap.page.path}`);
508
+ lines.push(` Reason: ${gap.reason} | Priority: ${gap.priority}`);
509
+ if (gap.suggestedTests.length > 0) {
510
+ lines.push(` Suggested: ${gap.suggestedTests[0]}`);
511
+ }
512
+ lines.push("");
513
+ }
514
+ if (result.gaps.length > 15) {
515
+ lines.push(` ... and ${result.gaps.length - 15} more gaps`);
516
+ lines.push("");
517
+ }
518
+ }
519
+ // Recommendations
520
+ if (result.recommendations.length > 0) {
521
+ lines.push("═══════════════════════════════════════════════════════════════════════════════");
522
+ lines.push("💡 RECOMMENDATIONS");
523
+ lines.push("═══════════════════════════════════════════════════════════════════════════════");
524
+ lines.push("");
525
+ for (const rec of result.recommendations) {
526
+ lines.push(` ${rec}`);
527
+ }
528
+ lines.push("");
529
+ }
530
+ // Tested pages summary
531
+ lines.push("───────────────────────────────────────────────────────────────────────────────");
532
+ lines.push("✅ TESTED PAGES (Top 10 by coverage)");
533
+ lines.push("───────────────────────────────────────────────────────────────────────────────");
534
+ lines.push("");
535
+ const topTested = [...result.testedPages]
536
+ .sort((a, b) => b.coverageScore - a.coverageScore)
537
+ .slice(0, 10);
538
+ for (const page of topTested) {
539
+ const bar = generateCoverageProgressBar(page.coverageScore, 15);
540
+ lines.push(` ${bar} ${page.coverageScore}% ${page.path}`);
541
+ lines.push(` Actions: ${page.actions.length} | Tests: ${page.testCount}`);
542
+ }
543
+ return lines.join("\n");
544
+ }
545
+ /**
546
+ * Generate HTML coverage report
547
+ */
548
+ function generateCoverageHtmlReport(result) {
549
+ const { analysis, gaps, testedPages } = result;
550
+ const coverageColor = analysis.coveragePercent >= 70 ? "#22c55e" :
551
+ analysis.coveragePercent >= 40 ? "#eab308" : "#ef4444";
552
+ return `<!DOCTYPE html>
553
+ <html lang="en">
554
+ <head>
555
+ <meta charset="UTF-8">
556
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
557
+ <title>Test Coverage Map - ${result.baseUrl}</title>
558
+ <style>
559
+ * { box-sizing: border-box; margin: 0; padding: 0; }
560
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; padding: 2rem; }
561
+ .container { max-width: 1200px; margin: 0 auto; }
562
+ h1 { color: #fff; margin-bottom: 0.5rem; }
563
+ .subtitle { color: #888; margin-bottom: 2rem; }
564
+ .card { background: #252540; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
565
+ .card h2 { color: #fff; font-size: 1.1rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
566
+ .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; }
567
+ .stat { text-align: center; }
568
+ .stat-value { font-size: 2rem; font-weight: bold; color: ${coverageColor}; }
569
+ .stat-label { color: #888; font-size: 0.875rem; }
570
+ .progress-bar { height: 8px; background: #333; border-radius: 4px; overflow: hidden; margin: 1rem 0; }
571
+ .progress-fill { height: 100%; background: ${coverageColor}; transition: width 0.5s; }
572
+ .section-list { list-style: none; }
573
+ .section-item { display: flex; align-items: center; gap: 1rem; padding: 0.75rem 0; border-bottom: 1px solid #333; }
574
+ .section-name { flex: 1; }
575
+ .section-bar { width: 150px; height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
576
+ .section-bar-fill { height: 100%; border-radius: 3px; }
577
+ .section-percent { width: 60px; text-align: right; font-weight: 500; }
578
+ .gap-list { list-style: none; }
579
+ .gap-item { padding: 1rem; margin-bottom: 0.75rem; background: #1a1a2e; border-radius: 8px; border-left: 4px solid; }
580
+ .gap-critical { border-color: #ef4444; }
581
+ .gap-high { border-color: #f97316; }
582
+ .gap-medium { border-color: #eab308; }
583
+ .gap-low { border-color: #22c55e; }
584
+ .gap-path { font-weight: 600; color: #fff; }
585
+ .gap-reason { color: #888; font-size: 0.875rem; margin-top: 0.25rem; }
586
+ .badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
587
+ .badge-critical { background: #ef4444; color: #fff; }
588
+ .badge-high { background: #f97316; color: #fff; }
589
+ .badge-medium { background: #eab308; color: #000; }
590
+ .badge-low { background: #22c55e; color: #fff; }
591
+ .recommendations { list-style: none; }
592
+ .recommendations li { padding: 0.75rem; background: #1a1a2e; border-radius: 6px; margin-bottom: 0.5rem; }
593
+ .page-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; }
594
+ .page-card { background: #1a1a2e; border-radius: 8px; padding: 1rem; }
595
+ .page-score { font-size: 1.5rem; font-weight: bold; }
596
+ .page-path { color: #888; font-size: 0.875rem; word-break: break-all; }
597
+ </style>
598
+ </head>
599
+ <body>
600
+ <div class="container">
601
+ <h1>Test Coverage Map</h1>
602
+ <p class="subtitle">${result.baseUrl} | Generated ${new Date(result.timestamp).toLocaleString()}</p>
603
+
604
+ <div class="card">
605
+ <h2>📊 Overall Coverage</h2>
606
+ <div class="stats">
607
+ <div class="stat">
608
+ <div class="stat-value">${analysis.coveragePercent}%</div>
609
+ <div class="stat-label">Coverage</div>
610
+ </div>
611
+ <div class="stat">
612
+ <div class="stat-value">${analysis.totalPages}</div>
613
+ <div class="stat-label">Total Pages</div>
614
+ </div>
615
+ <div class="stat">
616
+ <div class="stat-value">${analysis.testedPages}</div>
617
+ <div class="stat-label">Tested</div>
618
+ </div>
619
+ <div class="stat">
620
+ <div class="stat-value">${analysis.untestedPages}</div>
621
+ <div class="stat-label">Untested</div>
622
+ </div>
623
+ </div>
624
+ <div class="progress-bar">
625
+ <div class="progress-fill" style="width: ${analysis.coveragePercent}%"></div>
626
+ </div>
627
+ </div>
628
+
629
+ <div class="card">
630
+ <h2>📁 Coverage by Section</h2>
631
+ <ul class="section-list">
632
+ ${Object.entries(analysis.sectionCoverage)
633
+ .sort((a, b) => b[1].total - a[1].total)
634
+ .map(([section, data]) => {
635
+ const color = data.percent >= 70 ? "#22c55e" : data.percent >= 40 ? "#eab308" : "#ef4444";
636
+ return `
637
+ <li class="section-item">
638
+ <span class="section-name">${section}</span>
639
+ <div class="section-bar">
640
+ <div class="section-bar-fill" style="width: ${data.percent}%; background: ${color}"></div>
641
+ </div>
642
+ <span class="section-percent" style="color: ${color}">${data.percent}%</span>
643
+ <span style="color: #666; font-size: 0.875rem">${data.tested}/${data.total}</span>
644
+ </li>
645
+ `;
646
+ }).join("")}
647
+ </ul>
648
+ </div>
649
+
650
+ ${gaps.length > 0 ? `
651
+ <div class="card">
652
+ <h2>🕳️ Coverage Gaps (${gaps.length})</h2>
653
+ <ul class="gap-list">
654
+ ${gaps.slice(0, 20).map(gap => `
655
+ <li class="gap-item gap-${gap.priority}">
656
+ <div style="display: flex; justify-content: space-between; align-items: center;">
657
+ <span class="gap-path">${gap.page.path}</span>
658
+ <span class="badge badge-${gap.priority}">${gap.priority}</span>
659
+ </div>
660
+ <div class="gap-reason">Reason: ${gap.reason}</div>
661
+ </li>
662
+ `).join("")}
663
+ </ul>
664
+ ${gaps.length > 20 ? `<p style="color: #666; text-align: center;">...and ${gaps.length - 20} more gaps</p>` : ""}
665
+ </div>
666
+ ` : ""}
667
+
668
+ ${result.recommendations.length > 0 ? `
669
+ <div class="card">
670
+ <h2>💡 Recommendations</h2>
671
+ <ul class="recommendations">
672
+ ${result.recommendations.map(rec => `<li>${rec}</li>`).join("")}
673
+ </ul>
674
+ </div>
675
+ ` : ""}
676
+
677
+ <div class="card">
678
+ <h2>✅ Tested Pages (Top 12)</h2>
679
+ <div class="page-grid">
680
+ ${testedPages
681
+ .sort((a, b) => b.coverageScore - a.coverageScore)
682
+ .slice(0, 12)
683
+ .map(page => {
684
+ const color = page.coverageScore >= 70 ? "#22c55e" : page.coverageScore >= 40 ? "#eab308" : "#ef4444";
685
+ return `
686
+ <div class="page-card">
687
+ <div class="page-score" style="color: ${color}">${page.coverageScore}%</div>
688
+ <div class="page-path">${page.path}</div>
689
+ <div style="color: #666; font-size: 0.75rem; margin-top: 0.5rem;">
690
+ ${page.actions.length} actions | ${page.testCount} test(s)
691
+ </div>
692
+ </div>
693
+ `;
694
+ }).join("")}
695
+ </div>
696
+ </div>
697
+
698
+ <footer style="text-align: center; color: #666; margin-top: 2rem; font-size: 0.875rem;">
699
+ Generated by CBrowser v6.5.0 | Analysis took ${(result.duration / 1000).toFixed(1)}s
700
+ </footer>
701
+ </div>
702
+ </body>
703
+ </html>`;
704
+ }
705
+ /**
706
+ * Generate a text progress bar for coverage
707
+ */
708
+ function generateCoverageProgressBar(percent, width = 30) {
709
+ const filled = Math.round((percent / 100) * width);
710
+ const empty = width - filled;
711
+ return "█".repeat(filled) + "░".repeat(empty);
712
+ }
713
+ //# sourceMappingURL=coverage.js.map