cbrowser 6.4.0 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.js CHANGED
@@ -33,6 +33,14 @@ exports.loadPerformanceBaseline = loadPerformanceBaseline;
33
33
  exports.deletePerformanceBaseline = deletePerformanceBaseline;
34
34
  exports.detectPerformanceRegression = detectPerformanceRegression;
35
35
  exports.formatPerformanceRegressionReport = formatPerformanceRegressionReport;
36
+ exports.parseTestFilesForCoverage = parseTestFilesForCoverage;
37
+ exports.parseSitemap = parseSitemap;
38
+ exports.crawlSiteForCoverage = crawlSiteForCoverage;
39
+ exports.identifyCoverageGaps = identifyCoverageGaps;
40
+ exports.calculateCoverageAnalysis = calculateCoverageAnalysis;
41
+ exports.generateCoverageMap = generateCoverageMap;
42
+ exports.formatCoverageReport = formatCoverageReport;
43
+ exports.generateCoverageHtmlReport = generateCoverageHtmlReport;
36
44
  const playwright_1 = require("playwright");
37
45
  const fs_1 = require("fs");
38
46
  const path_1 = require("path");
@@ -4506,4 +4514,702 @@ function formatPerformanceRegressionReport(result) {
4506
4514
  }
4507
4515
  return lines.join("\n");
4508
4516
  }
4517
+ // ============================================================================
4518
+ // Test Coverage Map (v6.5.0)
4519
+ // ============================================================================
4520
+ /**
4521
+ * Parse test files to extract tested URLs and actions
4522
+ */
4523
+ function parseTestFilesForCoverage(testFiles) {
4524
+ const pageMap = new Map();
4525
+ for (const testFile of testFiles) {
4526
+ if (!(0, fs_1.existsSync)(testFile))
4527
+ continue;
4528
+ const content = (0, fs_1.readFileSync)(testFile, "utf-8");
4529
+ const lines = content.split("\n");
4530
+ let currentUrl = null;
4531
+ let lineNumber = 0;
4532
+ for (const line of lines) {
4533
+ lineNumber++;
4534
+ const trimmed = line.trim().toLowerCase();
4535
+ // Skip comments and empty lines
4536
+ if (trimmed.startsWith("#") || !trimmed)
4537
+ continue;
4538
+ // Detect navigation
4539
+ const navMatch = line.match(/(?:go to|navigate to|open|visit)\s+["']?([^"'\s]+)["']?/i);
4540
+ if (navMatch) {
4541
+ currentUrl = navMatch[1];
4542
+ const path = normalizeUrlToPath(currentUrl);
4543
+ if (!pageMap.has(path)) {
4544
+ pageMap.set(path, {
4545
+ url: currentUrl,
4546
+ path,
4547
+ testFiles: [],
4548
+ actions: [],
4549
+ testCount: 0,
4550
+ coverageScore: 0,
4551
+ });
4552
+ }
4553
+ const page = pageMap.get(path);
4554
+ if (!page.testFiles.includes(testFile)) {
4555
+ page.testFiles.push(testFile);
4556
+ page.testCount++;
4557
+ }
4558
+ page.actions.push({
4559
+ type: "navigate",
4560
+ target: currentUrl,
4561
+ testFile,
4562
+ lineNumber,
4563
+ });
4564
+ }
4565
+ // Detect click actions
4566
+ const clickMatch = line.match(/click\s+(?:on\s+)?(?:the\s+)?["']?([^"'\n]+)["']?/i);
4567
+ if (clickMatch && currentUrl) {
4568
+ const path = normalizeUrlToPath(currentUrl);
4569
+ const page = pageMap.get(path);
4570
+ if (page) {
4571
+ page.actions.push({
4572
+ type: "click",
4573
+ target: clickMatch[1].trim(),
4574
+ testFile,
4575
+ lineNumber,
4576
+ });
4577
+ }
4578
+ }
4579
+ // Detect fill/type actions
4580
+ const fillMatch = line.match(/(?:type|fill|enter)\s+["']([^"']+)["']\s+(?:in|into)\s+(?:the\s+)?["']?([^"'\n]+)["']?/i);
4581
+ if (fillMatch && currentUrl) {
4582
+ const path = normalizeUrlToPath(currentUrl);
4583
+ const page = pageMap.get(path);
4584
+ if (page) {
4585
+ page.actions.push({
4586
+ type: "fill",
4587
+ target: fillMatch[2].trim(),
4588
+ value: fillMatch[1],
4589
+ testFile,
4590
+ lineNumber,
4591
+ });
4592
+ }
4593
+ }
4594
+ // Detect verify actions
4595
+ const verifyMatch = line.match(/(?:verify|assert|check|expect|should)\s+(.+)/i);
4596
+ if (verifyMatch && currentUrl) {
4597
+ const path = normalizeUrlToPath(currentUrl);
4598
+ const page = pageMap.get(path);
4599
+ if (page) {
4600
+ page.actions.push({
4601
+ type: "verify",
4602
+ target: verifyMatch[1].trim(),
4603
+ testFile,
4604
+ lineNumber,
4605
+ });
4606
+ }
4607
+ }
4608
+ // Detect wait actions
4609
+ const waitMatch = line.match(/wait\s+(?:for\s+)?(.+)/i);
4610
+ if (waitMatch && currentUrl) {
4611
+ const path = normalizeUrlToPath(currentUrl);
4612
+ const page = pageMap.get(path);
4613
+ if (page) {
4614
+ page.actions.push({
4615
+ type: "wait",
4616
+ target: waitMatch[1].trim(),
4617
+ testFile,
4618
+ lineNumber,
4619
+ });
4620
+ }
4621
+ }
4622
+ }
4623
+ }
4624
+ // Calculate coverage scores
4625
+ for (const page of pageMap.values()) {
4626
+ const hasClicks = page.actions.some(a => a.type === "click");
4627
+ const hasFills = page.actions.some(a => a.type === "fill");
4628
+ const hasVerifies = page.actions.some(a => a.type === "verify");
4629
+ let score = 20; // Base score for visiting
4630
+ if (hasClicks)
4631
+ score += 25;
4632
+ if (hasFills)
4633
+ score += 25;
4634
+ if (hasVerifies)
4635
+ score += 30;
4636
+ page.coverageScore = Math.min(100, score);
4637
+ }
4638
+ return Array.from(pageMap.values());
4639
+ }
4640
+ /**
4641
+ * Normalize URL to a path for comparison
4642
+ */
4643
+ function normalizeUrlToPath(url) {
4644
+ try {
4645
+ const parsed = new URL(url);
4646
+ return parsed.pathname.replace(/\/$/, "") || "/";
4647
+ }
4648
+ catch {
4649
+ // Not a full URL, treat as path
4650
+ return url.replace(/\/$/, "") || "/";
4651
+ }
4652
+ }
4653
+ /**
4654
+ * Fetch and parse sitemap.xml
4655
+ */
4656
+ async function parseSitemap(sitemapUrl) {
4657
+ const pages = [];
4658
+ try {
4659
+ const response = await fetch(sitemapUrl);
4660
+ const xml = await response.text();
4661
+ // Simple XML parsing for sitemap
4662
+ const locMatches = xml.matchAll(/<loc>([^<]+)<\/loc>/g);
4663
+ for (const match of locMatches) {
4664
+ const url = match[1].trim();
4665
+ pages.push({
4666
+ url,
4667
+ path: normalizeUrlToPath(url),
4668
+ source: "sitemap",
4669
+ });
4670
+ }
4671
+ }
4672
+ catch (err) {
4673
+ console.error(`Failed to fetch sitemap: ${err}`);
4674
+ }
4675
+ return pages;
4676
+ }
4677
+ /**
4678
+ * Crawl a site to discover pages
4679
+ */
4680
+ async function crawlSiteForCoverage(startUrl, maxPages = 100, includePattern, excludePattern) {
4681
+ const pages = [];
4682
+ const visited = new Set();
4683
+ const queue = [startUrl];
4684
+ const browser = new CBrowser({
4685
+ headless: true,
4686
+ browser: "chromium",
4687
+ });
4688
+ const baseUrl = new URL(startUrl);
4689
+ const includeRegex = includePattern ? new RegExp(includePattern) : null;
4690
+ const excludeRegex = excludePattern ? new RegExp(excludePattern) : null;
4691
+ try {
4692
+ while (queue.length > 0 && pages.length < maxPages) {
4693
+ const url = queue.shift();
4694
+ const path = normalizeUrlToPath(url);
4695
+ if (visited.has(path))
4696
+ continue;
4697
+ visited.add(path);
4698
+ // Check patterns
4699
+ if (includeRegex && !includeRegex.test(path))
4700
+ continue;
4701
+ if (excludeRegex && excludeRegex.test(path))
4702
+ continue;
4703
+ try {
4704
+ const result = await browser.navigate(url);
4705
+ // Count interactive elements
4706
+ const page = await browser.getPage();
4707
+ const interactiveElements = await page.locator("button, a, input, select, textarea, [onclick], [role='button']").count();
4708
+ const formCount = await page.locator("form").count();
4709
+ // Get outbound links
4710
+ const links = await page.locator("a[href]").evaluateAll((els) => els.map(el => el.href).filter(href => href && !href.startsWith("javascript:")));
4711
+ const sitePage = {
4712
+ url,
4713
+ path,
4714
+ title: result.title,
4715
+ source: pages.length === 0 ? "crawl" : "link",
4716
+ status: 200,
4717
+ outboundLinks: links,
4718
+ interactiveElements,
4719
+ formCount,
4720
+ };
4721
+ pages.push(sitePage);
4722
+ // Add internal links to queue
4723
+ for (const link of links) {
4724
+ try {
4725
+ const linkUrl = new URL(link);
4726
+ if (linkUrl.hostname === baseUrl.hostname && !visited.has(normalizeUrlToPath(link))) {
4727
+ queue.push(link);
4728
+ }
4729
+ }
4730
+ catch {
4731
+ // Invalid URL, skip
4732
+ }
4733
+ }
4734
+ }
4735
+ catch (err) {
4736
+ // Page failed to load
4737
+ pages.push({
4738
+ url,
4739
+ path,
4740
+ source: "link",
4741
+ status: 0,
4742
+ });
4743
+ }
4744
+ }
4745
+ }
4746
+ finally {
4747
+ await browser.close();
4748
+ }
4749
+ return pages;
4750
+ }
4751
+ /**
4752
+ * Identify coverage gaps
4753
+ */
4754
+ function identifyCoverageGaps(sitePages, testedPages, minCoverage = 50) {
4755
+ const gaps = [];
4756
+ const testedPaths = new Set(testedPages.map(p => p.path));
4757
+ for (const sitePage of sitePages) {
4758
+ const testedPage = testedPages.find(p => p.path === sitePage.path);
4759
+ // Completely untested
4760
+ if (!testedPage) {
4761
+ const priority = determinePriority(sitePage);
4762
+ gaps.push({
4763
+ page: sitePage,
4764
+ reason: "untested",
4765
+ priority,
4766
+ suggestedTests: generateSuggestedTests(sitePage),
4767
+ similarTestedPages: findSimilarTestedPages(sitePage.path, testedPages),
4768
+ });
4769
+ continue;
4770
+ }
4771
+ // Low coverage
4772
+ if (testedPage.coverageScore < minCoverage) {
4773
+ gaps.push({
4774
+ page: sitePage,
4775
+ reason: "low-coverage",
4776
+ priority: "medium",
4777
+ suggestedTests: generateSuggestedTests(sitePage, testedPage),
4778
+ });
4779
+ continue;
4780
+ }
4781
+ // No interactions tested
4782
+ const hasInteractions = testedPage.actions.some(a => a.type === "click" || a.type === "fill");
4783
+ if (!hasInteractions && sitePage.interactiveElements && sitePage.interactiveElements > 5) {
4784
+ gaps.push({
4785
+ page: sitePage,
4786
+ reason: "no-interactions",
4787
+ priority: "low",
4788
+ suggestedTests: [`Test interactive elements on ${sitePage.path}`],
4789
+ });
4790
+ }
4791
+ // No verifications
4792
+ const hasVerifications = testedPage.actions.some(a => a.type === "verify");
4793
+ if (!hasVerifications) {
4794
+ gaps.push({
4795
+ page: sitePage,
4796
+ reason: "no-verifications",
4797
+ priority: "low",
4798
+ suggestedTests: [`Add assertions to verify ${sitePage.path} content`],
4799
+ });
4800
+ }
4801
+ }
4802
+ // Sort by priority
4803
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
4804
+ gaps.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
4805
+ return gaps;
4806
+ }
4807
+ /**
4808
+ * Determine priority of an untested page
4809
+ */
4810
+ function determinePriority(page) {
4811
+ const path = page.path.toLowerCase();
4812
+ // Critical paths
4813
+ if (path.includes("checkout") || path.includes("payment") || path.includes("login") ||
4814
+ path.includes("register") || path.includes("signup") || path.includes("auth")) {
4815
+ return "critical";
4816
+ }
4817
+ // High priority - user account, settings
4818
+ if (path.includes("account") || path.includes("profile") || path.includes("settings") ||
4819
+ path.includes("dashboard") || path.includes("admin")) {
4820
+ return "high";
4821
+ }
4822
+ // Medium - has forms or many interactive elements
4823
+ if (page.formCount && page.formCount > 0)
4824
+ return "medium";
4825
+ if (page.interactiveElements && page.interactiveElements > 10)
4826
+ return "medium";
4827
+ return "low";
4828
+ }
4829
+ /**
4830
+ * Generate suggested test steps for a page
4831
+ */
4832
+ function generateSuggestedTests(sitePage, existingTests) {
4833
+ const suggestions = [];
4834
+ suggestions.push(`go to ${sitePage.url}`);
4835
+ if (sitePage.formCount && sitePage.formCount > 0) {
4836
+ suggestions.push(`fill form fields with test data`);
4837
+ suggestions.push(`submit form and verify success`);
4838
+ }
4839
+ if (sitePage.interactiveElements && sitePage.interactiveElements > 0) {
4840
+ suggestions.push(`click primary call-to-action`);
4841
+ }
4842
+ suggestions.push(`verify page contains expected content`);
4843
+ suggestions.push(`verify no console errors`);
4844
+ if (existingTests) {
4845
+ // Add specific suggestions based on what's missing
4846
+ const hasClicks = existingTests.actions.some(a => a.type === "click");
4847
+ const hasFills = existingTests.actions.some(a => a.type === "fill");
4848
+ const hasVerifies = existingTests.actions.some(a => a.type === "verify");
4849
+ if (!hasClicks)
4850
+ suggestions.unshift(`# Add click interactions`);
4851
+ if (!hasFills && sitePage.formCount)
4852
+ suggestions.unshift(`# Add form fill tests`);
4853
+ if (!hasVerifies)
4854
+ suggestions.unshift(`# Add verification assertions`);
4855
+ }
4856
+ return suggestions;
4857
+ }
4858
+ /**
4859
+ * Find similar tested pages for reference
4860
+ */
4861
+ function findSimilarTestedPages(path, testedPages) {
4862
+ const segments = path.split("/").filter(Boolean);
4863
+ if (segments.length === 0)
4864
+ return [];
4865
+ const similar = [];
4866
+ const prefix = "/" + segments[0];
4867
+ for (const tested of testedPages) {
4868
+ if (tested.path.startsWith(prefix) && tested.path !== path) {
4869
+ similar.push(tested.path);
4870
+ if (similar.length >= 3)
4871
+ break;
4872
+ }
4873
+ }
4874
+ return similar;
4875
+ }
4876
+ /**
4877
+ * Calculate overall coverage analysis
4878
+ */
4879
+ function calculateCoverageAnalysis(sitePages, testedPages) {
4880
+ const testedPaths = new Set(testedPages.map(p => p.path));
4881
+ // Section coverage
4882
+ const sections = {};
4883
+ for (const page of sitePages) {
4884
+ const segments = page.path.split("/").filter(Boolean);
4885
+ const section = segments.length > 0 ? "/" + segments[0] : "/";
4886
+ if (!sections[section]) {
4887
+ sections[section] = { total: 0, tested: 0 };
4888
+ }
4889
+ sections[section].total++;
4890
+ if (testedPaths.has(page.path)) {
4891
+ sections[section].tested++;
4892
+ }
4893
+ }
4894
+ const sectionCoverage = {};
4895
+ for (const [section, data] of Object.entries(sections)) {
4896
+ sectionCoverage[section] = {
4897
+ ...data,
4898
+ percent: data.total > 0 ? Math.round((data.tested / data.total) * 100) : 0,
4899
+ };
4900
+ }
4901
+ const totalPages = sitePages.length;
4902
+ const testedCount = sitePages.filter(p => testedPaths.has(p.path)).length;
4903
+ return {
4904
+ totalPages,
4905
+ testedPages: testedCount,
4906
+ untestedPages: totalPages - testedCount,
4907
+ coveragePercent: totalPages > 0 ? Math.round((testedCount / totalPages) * 100) : 0,
4908
+ sectionCoverage,
4909
+ };
4910
+ }
4911
+ /**
4912
+ * Generate complete coverage map
4913
+ */
4914
+ async function generateCoverageMap(baseUrl, testFiles, options = {}) {
4915
+ const startTime = Date.now();
4916
+ // Parse test files
4917
+ const testedPages = parseTestFilesForCoverage(testFiles);
4918
+ // Get site pages
4919
+ let sitePages;
4920
+ if (options.sitemapUrl) {
4921
+ sitePages = await parseSitemap(options.sitemapUrl);
4922
+ }
4923
+ else {
4924
+ sitePages = await crawlSiteForCoverage(baseUrl, options.maxPages || 100, options.includePattern, options.excludePattern);
4925
+ }
4926
+ // Identify gaps
4927
+ const gaps = identifyCoverageGaps(sitePages, testedPages, options.minCoverage || 50);
4928
+ // Calculate analysis
4929
+ const analysis = calculateCoverageAnalysis(sitePages, testedPages);
4930
+ // Generate recommendations
4931
+ const recommendations = [];
4932
+ if (analysis.coveragePercent < 50) {
4933
+ recommendations.push("Coverage is below 50% - prioritize testing critical paths");
4934
+ }
4935
+ const criticalGaps = gaps.filter(g => g.priority === "critical");
4936
+ if (criticalGaps.length > 0) {
4937
+ recommendations.push(`${criticalGaps.length} critical pages have no tests (checkout, auth, etc.)`);
4938
+ }
4939
+ const lowCoverageSections = Object.entries(analysis.sectionCoverage)
4940
+ .filter(([_, data]) => data.percent < 30 && data.total > 2)
4941
+ .map(([section]) => section);
4942
+ if (lowCoverageSections.length > 0) {
4943
+ recommendations.push(`Sections with low coverage: ${lowCoverageSections.join(", ")}`);
4944
+ }
4945
+ if (gaps.filter(g => g.reason === "no-verifications").length > 3) {
4946
+ recommendations.push("Many tests lack assertions - add verification steps");
4947
+ }
4948
+ return {
4949
+ baseUrl,
4950
+ timestamp: new Date().toISOString(),
4951
+ duration: Date.now() - startTime,
4952
+ testFiles,
4953
+ sitePages,
4954
+ testedPages,
4955
+ gaps,
4956
+ analysis,
4957
+ recommendations,
4958
+ };
4959
+ }
4960
+ /**
4961
+ * Format coverage map as text report
4962
+ */
4963
+ function formatCoverageReport(result) {
4964
+ const lines = [];
4965
+ lines.push("╔══════════════════════════════════════════════════════════════════════════════╗");
4966
+ lines.push("║ TEST COVERAGE MAP REPORT ║");
4967
+ lines.push("╚══════════════════════════════════════════════════════════════════════════════╝");
4968
+ lines.push("");
4969
+ lines.push(`📊 Site: ${result.baseUrl}`);
4970
+ lines.push(`📅 Generated: ${result.timestamp}`);
4971
+ lines.push(`⏱️ Analysis time: ${(result.duration / 1000).toFixed(1)}s`);
4972
+ lines.push(`📝 Test files analyzed: ${result.testFiles.length}`);
4973
+ lines.push("");
4974
+ // Overall coverage
4975
+ lines.push("═══════════════════════════════════════════════════════════════════════════════");
4976
+ lines.push("📈 OVERALL COVERAGE");
4977
+ lines.push("═══════════════════════════════════════════════════════════════════════════════");
4978
+ lines.push("");
4979
+ const { analysis } = result;
4980
+ const coverageBar = generateCoverageProgressBar(analysis.coveragePercent);
4981
+ lines.push(` Coverage: ${coverageBar} ${analysis.coveragePercent}%`);
4982
+ lines.push("");
4983
+ lines.push(` Total pages: ${analysis.totalPages}`);
4984
+ lines.push(` Tested pages: ${analysis.testedPages}`);
4985
+ lines.push(` Untested pages: ${analysis.untestedPages}`);
4986
+ lines.push("");
4987
+ // Section coverage
4988
+ lines.push("───────────────────────────────────────────────────────────────────────────────");
4989
+ lines.push("📁 COVERAGE BY SECTION");
4990
+ lines.push("───────────────────────────────────────────────────────────────────────────────");
4991
+ lines.push("");
4992
+ const sections = Object.entries(analysis.sectionCoverage)
4993
+ .sort((a, b) => b[1].total - a[1].total);
4994
+ for (const [section, data] of sections) {
4995
+ const bar = generateCoverageProgressBar(data.percent, 20);
4996
+ const status = data.percent >= 70 ? "✅" : data.percent >= 40 ? "⚠️" : "❌";
4997
+ lines.push(` ${status} ${section.padEnd(20)} ${bar} ${data.tested}/${data.total} (${data.percent}%)`);
4998
+ }
4999
+ lines.push("");
5000
+ // Coverage gaps
5001
+ if (result.gaps.length > 0) {
5002
+ lines.push("═══════════════════════════════════════════════════════════════════════════════");
5003
+ lines.push("🕳️ COVERAGE GAPS");
5004
+ lines.push("═══════════════════════════════════════════════════════════════════════════════");
5005
+ lines.push("");
5006
+ const priorityEmoji = { critical: "🚨", high: "🔴", medium: "🟡", low: "🟢" };
5007
+ for (const gap of result.gaps.slice(0, 15)) {
5008
+ const emoji = priorityEmoji[gap.priority];
5009
+ lines.push(` ${emoji} ${gap.page.path}`);
5010
+ lines.push(` Reason: ${gap.reason} | Priority: ${gap.priority}`);
5011
+ if (gap.suggestedTests.length > 0) {
5012
+ lines.push(` Suggested: ${gap.suggestedTests[0]}`);
5013
+ }
5014
+ lines.push("");
5015
+ }
5016
+ if (result.gaps.length > 15) {
5017
+ lines.push(` ... and ${result.gaps.length - 15} more gaps`);
5018
+ lines.push("");
5019
+ }
5020
+ }
5021
+ // Recommendations
5022
+ if (result.recommendations.length > 0) {
5023
+ lines.push("═══════════════════════════════════════════════════════════════════════════════");
5024
+ lines.push("💡 RECOMMENDATIONS");
5025
+ lines.push("═══════════════════════════════════════════════════════════════════════════════");
5026
+ lines.push("");
5027
+ for (const rec of result.recommendations) {
5028
+ lines.push(` ${rec}`);
5029
+ }
5030
+ lines.push("");
5031
+ }
5032
+ // Tested pages summary
5033
+ lines.push("───────────────────────────────────────────────────────────────────────────────");
5034
+ lines.push("✅ TESTED PAGES (Top 10 by coverage)");
5035
+ lines.push("───────────────────────────────────────────────────────────────────────────────");
5036
+ lines.push("");
5037
+ const topTested = [...result.testedPages]
5038
+ .sort((a, b) => b.coverageScore - a.coverageScore)
5039
+ .slice(0, 10);
5040
+ for (const page of topTested) {
5041
+ const bar = generateCoverageProgressBar(page.coverageScore, 15);
5042
+ lines.push(` ${bar} ${page.coverageScore}% ${page.path}`);
5043
+ lines.push(` Actions: ${page.actions.length} | Tests: ${page.testCount}`);
5044
+ }
5045
+ return lines.join("\n");
5046
+ }
5047
+ /**
5048
+ * Generate HTML coverage report
5049
+ */
5050
+ function generateCoverageHtmlReport(result) {
5051
+ const { analysis, gaps, testedPages } = result;
5052
+ const coverageColor = analysis.coveragePercent >= 70 ? "#22c55e" :
5053
+ analysis.coveragePercent >= 40 ? "#eab308" : "#ef4444";
5054
+ return `<!DOCTYPE html>
5055
+ <html lang="en">
5056
+ <head>
5057
+ <meta charset="UTF-8">
5058
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
5059
+ <title>Test Coverage Map - ${result.baseUrl}</title>
5060
+ <style>
5061
+ * { box-sizing: border-box; margin: 0; padding: 0; }
5062
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; padding: 2rem; }
5063
+ .container { max-width: 1200px; margin: 0 auto; }
5064
+ h1 { color: #fff; margin-bottom: 0.5rem; }
5065
+ .subtitle { color: #888; margin-bottom: 2rem; }
5066
+ .card { background: #252540; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
5067
+ .card h2 { color: #fff; font-size: 1.1rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
5068
+ .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; }
5069
+ .stat { text-align: center; }
5070
+ .stat-value { font-size: 2rem; font-weight: bold; color: ${coverageColor}; }
5071
+ .stat-label { color: #888; font-size: 0.875rem; }
5072
+ .progress-bar { height: 8px; background: #333; border-radius: 4px; overflow: hidden; margin: 1rem 0; }
5073
+ .progress-fill { height: 100%; background: ${coverageColor}; transition: width 0.5s; }
5074
+ .section-list { list-style: none; }
5075
+ .section-item { display: flex; align-items: center; gap: 1rem; padding: 0.75rem 0; border-bottom: 1px solid #333; }
5076
+ .section-name { flex: 1; }
5077
+ .section-bar { width: 150px; height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
5078
+ .section-bar-fill { height: 100%; border-radius: 3px; }
5079
+ .section-percent { width: 60px; text-align: right; font-weight: 500; }
5080
+ .gap-list { list-style: none; }
5081
+ .gap-item { padding: 1rem; margin-bottom: 0.75rem; background: #1a1a2e; border-radius: 8px; border-left: 4px solid; }
5082
+ .gap-critical { border-color: #ef4444; }
5083
+ .gap-high { border-color: #f97316; }
5084
+ .gap-medium { border-color: #eab308; }
5085
+ .gap-low { border-color: #22c55e; }
5086
+ .gap-path { font-weight: 600; color: #fff; }
5087
+ .gap-reason { color: #888; font-size: 0.875rem; margin-top: 0.25rem; }
5088
+ .badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
5089
+ .badge-critical { background: #ef4444; color: #fff; }
5090
+ .badge-high { background: #f97316; color: #fff; }
5091
+ .badge-medium { background: #eab308; color: #000; }
5092
+ .badge-low { background: #22c55e; color: #fff; }
5093
+ .recommendations { list-style: none; }
5094
+ .recommendations li { padding: 0.75rem; background: #1a1a2e; border-radius: 6px; margin-bottom: 0.5rem; }
5095
+ .page-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; }
5096
+ .page-card { background: #1a1a2e; border-radius: 8px; padding: 1rem; }
5097
+ .page-score { font-size: 1.5rem; font-weight: bold; }
5098
+ .page-path { color: #888; font-size: 0.875rem; word-break: break-all; }
5099
+ </style>
5100
+ </head>
5101
+ <body>
5102
+ <div class="container">
5103
+ <h1>Test Coverage Map</h1>
5104
+ <p class="subtitle">${result.baseUrl} | Generated ${new Date(result.timestamp).toLocaleString()}</p>
5105
+
5106
+ <div class="card">
5107
+ <h2>📊 Overall Coverage</h2>
5108
+ <div class="stats">
5109
+ <div class="stat">
5110
+ <div class="stat-value">${analysis.coveragePercent}%</div>
5111
+ <div class="stat-label">Coverage</div>
5112
+ </div>
5113
+ <div class="stat">
5114
+ <div class="stat-value">${analysis.totalPages}</div>
5115
+ <div class="stat-label">Total Pages</div>
5116
+ </div>
5117
+ <div class="stat">
5118
+ <div class="stat-value">${analysis.testedPages}</div>
5119
+ <div class="stat-label">Tested</div>
5120
+ </div>
5121
+ <div class="stat">
5122
+ <div class="stat-value">${analysis.untestedPages}</div>
5123
+ <div class="stat-label">Untested</div>
5124
+ </div>
5125
+ </div>
5126
+ <div class="progress-bar">
5127
+ <div class="progress-fill" style="width: ${analysis.coveragePercent}%"></div>
5128
+ </div>
5129
+ </div>
5130
+
5131
+ <div class="card">
5132
+ <h2>📁 Coverage by Section</h2>
5133
+ <ul class="section-list">
5134
+ ${Object.entries(analysis.sectionCoverage)
5135
+ .sort((a, b) => b[1].total - a[1].total)
5136
+ .map(([section, data]) => {
5137
+ const color = data.percent >= 70 ? "#22c55e" : data.percent >= 40 ? "#eab308" : "#ef4444";
5138
+ return `
5139
+ <li class="section-item">
5140
+ <span class="section-name">${section}</span>
5141
+ <div class="section-bar">
5142
+ <div class="section-bar-fill" style="width: ${data.percent}%; background: ${color}"></div>
5143
+ </div>
5144
+ <span class="section-percent" style="color: ${color}">${data.percent}%</span>
5145
+ <span style="color: #666; font-size: 0.875rem">${data.tested}/${data.total}</span>
5146
+ </li>
5147
+ `;
5148
+ }).join("")}
5149
+ </ul>
5150
+ </div>
5151
+
5152
+ ${gaps.length > 0 ? `
5153
+ <div class="card">
5154
+ <h2>🕳️ Coverage Gaps (${gaps.length})</h2>
5155
+ <ul class="gap-list">
5156
+ ${gaps.slice(0, 20).map(gap => `
5157
+ <li class="gap-item gap-${gap.priority}">
5158
+ <div style="display: flex; justify-content: space-between; align-items: center;">
5159
+ <span class="gap-path">${gap.page.path}</span>
5160
+ <span class="badge badge-${gap.priority}">${gap.priority}</span>
5161
+ </div>
5162
+ <div class="gap-reason">Reason: ${gap.reason}</div>
5163
+ </li>
5164
+ `).join("")}
5165
+ </ul>
5166
+ ${gaps.length > 20 ? `<p style="color: #666; text-align: center;">...and ${gaps.length - 20} more gaps</p>` : ""}
5167
+ </div>
5168
+ ` : ""}
5169
+
5170
+ ${result.recommendations.length > 0 ? `
5171
+ <div class="card">
5172
+ <h2>💡 Recommendations</h2>
5173
+ <ul class="recommendations">
5174
+ ${result.recommendations.map(rec => `<li>${rec}</li>`).join("")}
5175
+ </ul>
5176
+ </div>
5177
+ ` : ""}
5178
+
5179
+ <div class="card">
5180
+ <h2>✅ Tested Pages (Top 12)</h2>
5181
+ <div class="page-grid">
5182
+ ${testedPages
5183
+ .sort((a, b) => b.coverageScore - a.coverageScore)
5184
+ .slice(0, 12)
5185
+ .map(page => {
5186
+ const color = page.coverageScore >= 70 ? "#22c55e" : page.coverageScore >= 40 ? "#eab308" : "#ef4444";
5187
+ return `
5188
+ <div class="page-card">
5189
+ <div class="page-score" style="color: ${color}">${page.coverageScore}%</div>
5190
+ <div class="page-path">${page.path}</div>
5191
+ <div style="color: #666; font-size: 0.75rem; margin-top: 0.5rem;">
5192
+ ${page.actions.length} actions | ${page.testCount} test(s)
5193
+ </div>
5194
+ </div>
5195
+ `;
5196
+ }).join("")}
5197
+ </div>
5198
+ </div>
5199
+
5200
+ <footer style="text-align: center; color: #666; margin-top: 2rem; font-size: 0.875rem;">
5201
+ Generated by CBrowser v6.5.0 | Analysis took ${(result.duration / 1000).toFixed(1)}s
5202
+ </footer>
5203
+ </div>
5204
+ </body>
5205
+ </html>`;
5206
+ }
5207
+ /**
5208
+ * Generate a text progress bar for coverage
5209
+ */
5210
+ function generateCoverageProgressBar(percent, width = 30) {
5211
+ const filled = Math.round((percent / 100) * width);
5212
+ const empty = width - filled;
5213
+ return "█".repeat(filled) + "░".repeat(empty);
5214
+ }
4509
5215
  //# sourceMappingURL=browser.js.map