cbrowser 6.4.0 â 7.0.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.d.ts +79 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1313 -0
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +375 -2
- package/dist/cli.js.map +1 -1
- package/dist/types.d.ts +236 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -3
package/dist/browser.js
CHANGED
|
@@ -33,6 +33,23 @@ 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;
|
|
44
|
+
exports.loadVisualBaselines = loadVisualBaselines;
|
|
45
|
+
exports.captureVisualBaseline = captureVisualBaseline;
|
|
46
|
+
exports.listVisualBaselines = listVisualBaselines;
|
|
47
|
+
exports.getVisualBaseline = getVisualBaseline;
|
|
48
|
+
exports.deleteVisualBaseline = deleteVisualBaseline;
|
|
49
|
+
exports.runVisualRegression = runVisualRegression;
|
|
50
|
+
exports.runVisualRegressionSuite = runVisualRegressionSuite;
|
|
51
|
+
exports.formatVisualRegressionReport = formatVisualRegressionReport;
|
|
52
|
+
exports.generateVisualRegressionHtmlReport = generateVisualRegressionHtmlReport;
|
|
36
53
|
const playwright_1 = require("playwright");
|
|
37
54
|
const fs_1 = require("fs");
|
|
38
55
|
const path_1 = require("path");
|
|
@@ -4506,4 +4523,1300 @@ function formatPerformanceRegressionReport(result) {
|
|
|
4506
4523
|
}
|
|
4507
4524
|
return lines.join("\n");
|
|
4508
4525
|
}
|
|
4526
|
+
// ============================================================================
|
|
4527
|
+
// Test Coverage Map (v6.5.0)
|
|
4528
|
+
// ============================================================================
|
|
4529
|
+
/**
|
|
4530
|
+
* Parse test files to extract tested URLs and actions
|
|
4531
|
+
*/
|
|
4532
|
+
function parseTestFilesForCoverage(testFiles) {
|
|
4533
|
+
const pageMap = new Map();
|
|
4534
|
+
for (const testFile of testFiles) {
|
|
4535
|
+
if (!(0, fs_1.existsSync)(testFile))
|
|
4536
|
+
continue;
|
|
4537
|
+
const content = (0, fs_1.readFileSync)(testFile, "utf-8");
|
|
4538
|
+
const lines = content.split("\n");
|
|
4539
|
+
let currentUrl = null;
|
|
4540
|
+
let lineNumber = 0;
|
|
4541
|
+
for (const line of lines) {
|
|
4542
|
+
lineNumber++;
|
|
4543
|
+
const trimmed = line.trim().toLowerCase();
|
|
4544
|
+
// Skip comments and empty lines
|
|
4545
|
+
if (trimmed.startsWith("#") || !trimmed)
|
|
4546
|
+
continue;
|
|
4547
|
+
// Detect navigation
|
|
4548
|
+
const navMatch = line.match(/(?:go to|navigate to|open|visit)\s+["']?([^"'\s]+)["']?/i);
|
|
4549
|
+
if (navMatch) {
|
|
4550
|
+
currentUrl = navMatch[1];
|
|
4551
|
+
const path = normalizeUrlToPath(currentUrl);
|
|
4552
|
+
if (!pageMap.has(path)) {
|
|
4553
|
+
pageMap.set(path, {
|
|
4554
|
+
url: currentUrl,
|
|
4555
|
+
path,
|
|
4556
|
+
testFiles: [],
|
|
4557
|
+
actions: [],
|
|
4558
|
+
testCount: 0,
|
|
4559
|
+
coverageScore: 0,
|
|
4560
|
+
});
|
|
4561
|
+
}
|
|
4562
|
+
const page = pageMap.get(path);
|
|
4563
|
+
if (!page.testFiles.includes(testFile)) {
|
|
4564
|
+
page.testFiles.push(testFile);
|
|
4565
|
+
page.testCount++;
|
|
4566
|
+
}
|
|
4567
|
+
page.actions.push({
|
|
4568
|
+
type: "navigate",
|
|
4569
|
+
target: currentUrl,
|
|
4570
|
+
testFile,
|
|
4571
|
+
lineNumber,
|
|
4572
|
+
});
|
|
4573
|
+
}
|
|
4574
|
+
// Detect click actions
|
|
4575
|
+
const clickMatch = line.match(/click\s+(?:on\s+)?(?:the\s+)?["']?([^"'\n]+)["']?/i);
|
|
4576
|
+
if (clickMatch && currentUrl) {
|
|
4577
|
+
const path = normalizeUrlToPath(currentUrl);
|
|
4578
|
+
const page = pageMap.get(path);
|
|
4579
|
+
if (page) {
|
|
4580
|
+
page.actions.push({
|
|
4581
|
+
type: "click",
|
|
4582
|
+
target: clickMatch[1].trim(),
|
|
4583
|
+
testFile,
|
|
4584
|
+
lineNumber,
|
|
4585
|
+
});
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
// Detect fill/type actions
|
|
4589
|
+
const fillMatch = line.match(/(?:type|fill|enter)\s+["']([^"']+)["']\s+(?:in|into)\s+(?:the\s+)?["']?([^"'\n]+)["']?/i);
|
|
4590
|
+
if (fillMatch && currentUrl) {
|
|
4591
|
+
const path = normalizeUrlToPath(currentUrl);
|
|
4592
|
+
const page = pageMap.get(path);
|
|
4593
|
+
if (page) {
|
|
4594
|
+
page.actions.push({
|
|
4595
|
+
type: "fill",
|
|
4596
|
+
target: fillMatch[2].trim(),
|
|
4597
|
+
value: fillMatch[1],
|
|
4598
|
+
testFile,
|
|
4599
|
+
lineNumber,
|
|
4600
|
+
});
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4603
|
+
// Detect verify actions
|
|
4604
|
+
const verifyMatch = line.match(/(?:verify|assert|check|expect|should)\s+(.+)/i);
|
|
4605
|
+
if (verifyMatch && currentUrl) {
|
|
4606
|
+
const path = normalizeUrlToPath(currentUrl);
|
|
4607
|
+
const page = pageMap.get(path);
|
|
4608
|
+
if (page) {
|
|
4609
|
+
page.actions.push({
|
|
4610
|
+
type: "verify",
|
|
4611
|
+
target: verifyMatch[1].trim(),
|
|
4612
|
+
testFile,
|
|
4613
|
+
lineNumber,
|
|
4614
|
+
});
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
// Detect wait actions
|
|
4618
|
+
const waitMatch = line.match(/wait\s+(?:for\s+)?(.+)/i);
|
|
4619
|
+
if (waitMatch && currentUrl) {
|
|
4620
|
+
const path = normalizeUrlToPath(currentUrl);
|
|
4621
|
+
const page = pageMap.get(path);
|
|
4622
|
+
if (page) {
|
|
4623
|
+
page.actions.push({
|
|
4624
|
+
type: "wait",
|
|
4625
|
+
target: waitMatch[1].trim(),
|
|
4626
|
+
testFile,
|
|
4627
|
+
lineNumber,
|
|
4628
|
+
});
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
// Calculate coverage scores
|
|
4634
|
+
for (const page of pageMap.values()) {
|
|
4635
|
+
const hasClicks = page.actions.some(a => a.type === "click");
|
|
4636
|
+
const hasFills = page.actions.some(a => a.type === "fill");
|
|
4637
|
+
const hasVerifies = page.actions.some(a => a.type === "verify");
|
|
4638
|
+
let score = 20; // Base score for visiting
|
|
4639
|
+
if (hasClicks)
|
|
4640
|
+
score += 25;
|
|
4641
|
+
if (hasFills)
|
|
4642
|
+
score += 25;
|
|
4643
|
+
if (hasVerifies)
|
|
4644
|
+
score += 30;
|
|
4645
|
+
page.coverageScore = Math.min(100, score);
|
|
4646
|
+
}
|
|
4647
|
+
return Array.from(pageMap.values());
|
|
4648
|
+
}
|
|
4649
|
+
/**
|
|
4650
|
+
* Normalize URL to a path for comparison
|
|
4651
|
+
*/
|
|
4652
|
+
function normalizeUrlToPath(url) {
|
|
4653
|
+
try {
|
|
4654
|
+
const parsed = new URL(url);
|
|
4655
|
+
return parsed.pathname.replace(/\/$/, "") || "/";
|
|
4656
|
+
}
|
|
4657
|
+
catch {
|
|
4658
|
+
// Not a full URL, treat as path
|
|
4659
|
+
return url.replace(/\/$/, "") || "/";
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
/**
|
|
4663
|
+
* Fetch and parse sitemap.xml
|
|
4664
|
+
*/
|
|
4665
|
+
async function parseSitemap(sitemapUrl) {
|
|
4666
|
+
const pages = [];
|
|
4667
|
+
try {
|
|
4668
|
+
const response = await fetch(sitemapUrl);
|
|
4669
|
+
const xml = await response.text();
|
|
4670
|
+
// Simple XML parsing for sitemap
|
|
4671
|
+
const locMatches = xml.matchAll(/<loc>([^<]+)<\/loc>/g);
|
|
4672
|
+
for (const match of locMatches) {
|
|
4673
|
+
const url = match[1].trim();
|
|
4674
|
+
pages.push({
|
|
4675
|
+
url,
|
|
4676
|
+
path: normalizeUrlToPath(url),
|
|
4677
|
+
source: "sitemap",
|
|
4678
|
+
});
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
catch (err) {
|
|
4682
|
+
console.error(`Failed to fetch sitemap: ${err}`);
|
|
4683
|
+
}
|
|
4684
|
+
return pages;
|
|
4685
|
+
}
|
|
4686
|
+
/**
|
|
4687
|
+
* Crawl a site to discover pages
|
|
4688
|
+
*/
|
|
4689
|
+
async function crawlSiteForCoverage(startUrl, maxPages = 100, includePattern, excludePattern) {
|
|
4690
|
+
const pages = [];
|
|
4691
|
+
const visited = new Set();
|
|
4692
|
+
const queue = [startUrl];
|
|
4693
|
+
const browser = new CBrowser({
|
|
4694
|
+
headless: true,
|
|
4695
|
+
browser: "chromium",
|
|
4696
|
+
});
|
|
4697
|
+
const baseUrl = new URL(startUrl);
|
|
4698
|
+
const includeRegex = includePattern ? new RegExp(includePattern) : null;
|
|
4699
|
+
const excludeRegex = excludePattern ? new RegExp(excludePattern) : null;
|
|
4700
|
+
try {
|
|
4701
|
+
while (queue.length > 0 && pages.length < maxPages) {
|
|
4702
|
+
const url = queue.shift();
|
|
4703
|
+
const path = normalizeUrlToPath(url);
|
|
4704
|
+
if (visited.has(path))
|
|
4705
|
+
continue;
|
|
4706
|
+
visited.add(path);
|
|
4707
|
+
// Check patterns
|
|
4708
|
+
if (includeRegex && !includeRegex.test(path))
|
|
4709
|
+
continue;
|
|
4710
|
+
if (excludeRegex && excludeRegex.test(path))
|
|
4711
|
+
continue;
|
|
4712
|
+
try {
|
|
4713
|
+
const result = await browser.navigate(url);
|
|
4714
|
+
// Count interactive elements
|
|
4715
|
+
const page = await browser.getPage();
|
|
4716
|
+
const interactiveElements = await page.locator("button, a, input, select, textarea, [onclick], [role='button']").count();
|
|
4717
|
+
const formCount = await page.locator("form").count();
|
|
4718
|
+
// Get outbound links
|
|
4719
|
+
const links = await page.locator("a[href]").evaluateAll((els) => els.map(el => el.href).filter(href => href && !href.startsWith("javascript:")));
|
|
4720
|
+
const sitePage = {
|
|
4721
|
+
url,
|
|
4722
|
+
path,
|
|
4723
|
+
title: result.title,
|
|
4724
|
+
source: pages.length === 0 ? "crawl" : "link",
|
|
4725
|
+
status: 200,
|
|
4726
|
+
outboundLinks: links,
|
|
4727
|
+
interactiveElements,
|
|
4728
|
+
formCount,
|
|
4729
|
+
};
|
|
4730
|
+
pages.push(sitePage);
|
|
4731
|
+
// Add internal links to queue
|
|
4732
|
+
for (const link of links) {
|
|
4733
|
+
try {
|
|
4734
|
+
const linkUrl = new URL(link);
|
|
4735
|
+
if (linkUrl.hostname === baseUrl.hostname && !visited.has(normalizeUrlToPath(link))) {
|
|
4736
|
+
queue.push(link);
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
catch {
|
|
4740
|
+
// Invalid URL, skip
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
catch (err) {
|
|
4745
|
+
// Page failed to load
|
|
4746
|
+
pages.push({
|
|
4747
|
+
url,
|
|
4748
|
+
path,
|
|
4749
|
+
source: "link",
|
|
4750
|
+
status: 0,
|
|
4751
|
+
});
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
}
|
|
4755
|
+
finally {
|
|
4756
|
+
await browser.close();
|
|
4757
|
+
}
|
|
4758
|
+
return pages;
|
|
4759
|
+
}
|
|
4760
|
+
/**
|
|
4761
|
+
* Identify coverage gaps
|
|
4762
|
+
*/
|
|
4763
|
+
function identifyCoverageGaps(sitePages, testedPages, minCoverage = 50) {
|
|
4764
|
+
const gaps = [];
|
|
4765
|
+
const testedPaths = new Set(testedPages.map(p => p.path));
|
|
4766
|
+
for (const sitePage of sitePages) {
|
|
4767
|
+
const testedPage = testedPages.find(p => p.path === sitePage.path);
|
|
4768
|
+
// Completely untested
|
|
4769
|
+
if (!testedPage) {
|
|
4770
|
+
const priority = determinePriority(sitePage);
|
|
4771
|
+
gaps.push({
|
|
4772
|
+
page: sitePage,
|
|
4773
|
+
reason: "untested",
|
|
4774
|
+
priority,
|
|
4775
|
+
suggestedTests: generateSuggestedTests(sitePage),
|
|
4776
|
+
similarTestedPages: findSimilarTestedPages(sitePage.path, testedPages),
|
|
4777
|
+
});
|
|
4778
|
+
continue;
|
|
4779
|
+
}
|
|
4780
|
+
// Low coverage
|
|
4781
|
+
if (testedPage.coverageScore < minCoverage) {
|
|
4782
|
+
gaps.push({
|
|
4783
|
+
page: sitePage,
|
|
4784
|
+
reason: "low-coverage",
|
|
4785
|
+
priority: "medium",
|
|
4786
|
+
suggestedTests: generateSuggestedTests(sitePage, testedPage),
|
|
4787
|
+
});
|
|
4788
|
+
continue;
|
|
4789
|
+
}
|
|
4790
|
+
// No interactions tested
|
|
4791
|
+
const hasInteractions = testedPage.actions.some(a => a.type === "click" || a.type === "fill");
|
|
4792
|
+
if (!hasInteractions && sitePage.interactiveElements && sitePage.interactiveElements > 5) {
|
|
4793
|
+
gaps.push({
|
|
4794
|
+
page: sitePage,
|
|
4795
|
+
reason: "no-interactions",
|
|
4796
|
+
priority: "low",
|
|
4797
|
+
suggestedTests: [`Test interactive elements on ${sitePage.path}`],
|
|
4798
|
+
});
|
|
4799
|
+
}
|
|
4800
|
+
// No verifications
|
|
4801
|
+
const hasVerifications = testedPage.actions.some(a => a.type === "verify");
|
|
4802
|
+
if (!hasVerifications) {
|
|
4803
|
+
gaps.push({
|
|
4804
|
+
page: sitePage,
|
|
4805
|
+
reason: "no-verifications",
|
|
4806
|
+
priority: "low",
|
|
4807
|
+
suggestedTests: [`Add assertions to verify ${sitePage.path} content`],
|
|
4808
|
+
});
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
// Sort by priority
|
|
4812
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
4813
|
+
gaps.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
4814
|
+
return gaps;
|
|
4815
|
+
}
|
|
4816
|
+
/**
|
|
4817
|
+
* Determine priority of an untested page
|
|
4818
|
+
*/
|
|
4819
|
+
function determinePriority(page) {
|
|
4820
|
+
const path = page.path.toLowerCase();
|
|
4821
|
+
// Critical paths
|
|
4822
|
+
if (path.includes("checkout") || path.includes("payment") || path.includes("login") ||
|
|
4823
|
+
path.includes("register") || path.includes("signup") || path.includes("auth")) {
|
|
4824
|
+
return "critical";
|
|
4825
|
+
}
|
|
4826
|
+
// High priority - user account, settings
|
|
4827
|
+
if (path.includes("account") || path.includes("profile") || path.includes("settings") ||
|
|
4828
|
+
path.includes("dashboard") || path.includes("admin")) {
|
|
4829
|
+
return "high";
|
|
4830
|
+
}
|
|
4831
|
+
// Medium - has forms or many interactive elements
|
|
4832
|
+
if (page.formCount && page.formCount > 0)
|
|
4833
|
+
return "medium";
|
|
4834
|
+
if (page.interactiveElements && page.interactiveElements > 10)
|
|
4835
|
+
return "medium";
|
|
4836
|
+
return "low";
|
|
4837
|
+
}
|
|
4838
|
+
/**
|
|
4839
|
+
* Generate suggested test steps for a page
|
|
4840
|
+
*/
|
|
4841
|
+
function generateSuggestedTests(sitePage, existingTests) {
|
|
4842
|
+
const suggestions = [];
|
|
4843
|
+
suggestions.push(`go to ${sitePage.url}`);
|
|
4844
|
+
if (sitePage.formCount && sitePage.formCount > 0) {
|
|
4845
|
+
suggestions.push(`fill form fields with test data`);
|
|
4846
|
+
suggestions.push(`submit form and verify success`);
|
|
4847
|
+
}
|
|
4848
|
+
if (sitePage.interactiveElements && sitePage.interactiveElements > 0) {
|
|
4849
|
+
suggestions.push(`click primary call-to-action`);
|
|
4850
|
+
}
|
|
4851
|
+
suggestions.push(`verify page contains expected content`);
|
|
4852
|
+
suggestions.push(`verify no console errors`);
|
|
4853
|
+
if (existingTests) {
|
|
4854
|
+
// Add specific suggestions based on what's missing
|
|
4855
|
+
const hasClicks = existingTests.actions.some(a => a.type === "click");
|
|
4856
|
+
const hasFills = existingTests.actions.some(a => a.type === "fill");
|
|
4857
|
+
const hasVerifies = existingTests.actions.some(a => a.type === "verify");
|
|
4858
|
+
if (!hasClicks)
|
|
4859
|
+
suggestions.unshift(`# Add click interactions`);
|
|
4860
|
+
if (!hasFills && sitePage.formCount)
|
|
4861
|
+
suggestions.unshift(`# Add form fill tests`);
|
|
4862
|
+
if (!hasVerifies)
|
|
4863
|
+
suggestions.unshift(`# Add verification assertions`);
|
|
4864
|
+
}
|
|
4865
|
+
return suggestions;
|
|
4866
|
+
}
|
|
4867
|
+
/**
|
|
4868
|
+
* Find similar tested pages for reference
|
|
4869
|
+
*/
|
|
4870
|
+
function findSimilarTestedPages(path, testedPages) {
|
|
4871
|
+
const segments = path.split("/").filter(Boolean);
|
|
4872
|
+
if (segments.length === 0)
|
|
4873
|
+
return [];
|
|
4874
|
+
const similar = [];
|
|
4875
|
+
const prefix = "/" + segments[0];
|
|
4876
|
+
for (const tested of testedPages) {
|
|
4877
|
+
if (tested.path.startsWith(prefix) && tested.path !== path) {
|
|
4878
|
+
similar.push(tested.path);
|
|
4879
|
+
if (similar.length >= 3)
|
|
4880
|
+
break;
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
return similar;
|
|
4884
|
+
}
|
|
4885
|
+
/**
|
|
4886
|
+
* Calculate overall coverage analysis
|
|
4887
|
+
*/
|
|
4888
|
+
function calculateCoverageAnalysis(sitePages, testedPages) {
|
|
4889
|
+
const testedPaths = new Set(testedPages.map(p => p.path));
|
|
4890
|
+
// Section coverage
|
|
4891
|
+
const sections = {};
|
|
4892
|
+
for (const page of sitePages) {
|
|
4893
|
+
const segments = page.path.split("/").filter(Boolean);
|
|
4894
|
+
const section = segments.length > 0 ? "/" + segments[0] : "/";
|
|
4895
|
+
if (!sections[section]) {
|
|
4896
|
+
sections[section] = { total: 0, tested: 0 };
|
|
4897
|
+
}
|
|
4898
|
+
sections[section].total++;
|
|
4899
|
+
if (testedPaths.has(page.path)) {
|
|
4900
|
+
sections[section].tested++;
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
const sectionCoverage = {};
|
|
4904
|
+
for (const [section, data] of Object.entries(sections)) {
|
|
4905
|
+
sectionCoverage[section] = {
|
|
4906
|
+
...data,
|
|
4907
|
+
percent: data.total > 0 ? Math.round((data.tested / data.total) * 100) : 0,
|
|
4908
|
+
};
|
|
4909
|
+
}
|
|
4910
|
+
const totalPages = sitePages.length;
|
|
4911
|
+
const testedCount = sitePages.filter(p => testedPaths.has(p.path)).length;
|
|
4912
|
+
return {
|
|
4913
|
+
totalPages,
|
|
4914
|
+
testedPages: testedCount,
|
|
4915
|
+
untestedPages: totalPages - testedCount,
|
|
4916
|
+
coveragePercent: totalPages > 0 ? Math.round((testedCount / totalPages) * 100) : 0,
|
|
4917
|
+
sectionCoverage,
|
|
4918
|
+
};
|
|
4919
|
+
}
|
|
4920
|
+
/**
|
|
4921
|
+
* Generate complete coverage map
|
|
4922
|
+
*/
|
|
4923
|
+
async function generateCoverageMap(baseUrl, testFiles, options = {}) {
|
|
4924
|
+
const startTime = Date.now();
|
|
4925
|
+
// Parse test files
|
|
4926
|
+
const testedPages = parseTestFilesForCoverage(testFiles);
|
|
4927
|
+
// Get site pages
|
|
4928
|
+
let sitePages;
|
|
4929
|
+
if (options.sitemapUrl) {
|
|
4930
|
+
sitePages = await parseSitemap(options.sitemapUrl);
|
|
4931
|
+
}
|
|
4932
|
+
else {
|
|
4933
|
+
sitePages = await crawlSiteForCoverage(baseUrl, options.maxPages || 100, options.includePattern, options.excludePattern);
|
|
4934
|
+
}
|
|
4935
|
+
// Identify gaps
|
|
4936
|
+
const gaps = identifyCoverageGaps(sitePages, testedPages, options.minCoverage || 50);
|
|
4937
|
+
// Calculate analysis
|
|
4938
|
+
const analysis = calculateCoverageAnalysis(sitePages, testedPages);
|
|
4939
|
+
// Generate recommendations
|
|
4940
|
+
const recommendations = [];
|
|
4941
|
+
if (analysis.coveragePercent < 50) {
|
|
4942
|
+
recommendations.push("Coverage is below 50% - prioritize testing critical paths");
|
|
4943
|
+
}
|
|
4944
|
+
const criticalGaps = gaps.filter(g => g.priority === "critical");
|
|
4945
|
+
if (criticalGaps.length > 0) {
|
|
4946
|
+
recommendations.push(`${criticalGaps.length} critical pages have no tests (checkout, auth, etc.)`);
|
|
4947
|
+
}
|
|
4948
|
+
const lowCoverageSections = Object.entries(analysis.sectionCoverage)
|
|
4949
|
+
.filter(([_, data]) => data.percent < 30 && data.total > 2)
|
|
4950
|
+
.map(([section]) => section);
|
|
4951
|
+
if (lowCoverageSections.length > 0) {
|
|
4952
|
+
recommendations.push(`Sections with low coverage: ${lowCoverageSections.join(", ")}`);
|
|
4953
|
+
}
|
|
4954
|
+
if (gaps.filter(g => g.reason === "no-verifications").length > 3) {
|
|
4955
|
+
recommendations.push("Many tests lack assertions - add verification steps");
|
|
4956
|
+
}
|
|
4957
|
+
return {
|
|
4958
|
+
baseUrl,
|
|
4959
|
+
timestamp: new Date().toISOString(),
|
|
4960
|
+
duration: Date.now() - startTime,
|
|
4961
|
+
testFiles,
|
|
4962
|
+
sitePages,
|
|
4963
|
+
testedPages,
|
|
4964
|
+
gaps,
|
|
4965
|
+
analysis,
|
|
4966
|
+
recommendations,
|
|
4967
|
+
};
|
|
4968
|
+
}
|
|
4969
|
+
/**
|
|
4970
|
+
* Format coverage map as text report
|
|
4971
|
+
*/
|
|
4972
|
+
function formatCoverageReport(result) {
|
|
4973
|
+
const lines = [];
|
|
4974
|
+
lines.push("ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
4975
|
+
lines.push("â TEST COVERAGE MAP REPORT â");
|
|
4976
|
+
lines.push("ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
4977
|
+
lines.push("");
|
|
4978
|
+
lines.push(`đ Site: ${result.baseUrl}`);
|
|
4979
|
+
lines.push(`đ
Generated: ${result.timestamp}`);
|
|
4980
|
+
lines.push(`âąī¸ Analysis time: ${(result.duration / 1000).toFixed(1)}s`);
|
|
4981
|
+
lines.push(`đ Test files analyzed: ${result.testFiles.length}`);
|
|
4982
|
+
lines.push("");
|
|
4983
|
+
// Overall coverage
|
|
4984
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
4985
|
+
lines.push("đ OVERALL COVERAGE");
|
|
4986
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
4987
|
+
lines.push("");
|
|
4988
|
+
const { analysis } = result;
|
|
4989
|
+
const coverageBar = generateCoverageProgressBar(analysis.coveragePercent);
|
|
4990
|
+
lines.push(` Coverage: ${coverageBar} ${analysis.coveragePercent}%`);
|
|
4991
|
+
lines.push("");
|
|
4992
|
+
lines.push(` Total pages: ${analysis.totalPages}`);
|
|
4993
|
+
lines.push(` Tested pages: ${analysis.testedPages}`);
|
|
4994
|
+
lines.push(` Untested pages: ${analysis.untestedPages}`);
|
|
4995
|
+
lines.push("");
|
|
4996
|
+
// Section coverage
|
|
4997
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
4998
|
+
lines.push("đ COVERAGE BY SECTION");
|
|
4999
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5000
|
+
lines.push("");
|
|
5001
|
+
const sections = Object.entries(analysis.sectionCoverage)
|
|
5002
|
+
.sort((a, b) => b[1].total - a[1].total);
|
|
5003
|
+
for (const [section, data] of sections) {
|
|
5004
|
+
const bar = generateCoverageProgressBar(data.percent, 20);
|
|
5005
|
+
const status = data.percent >= 70 ? "â
" : data.percent >= 40 ? "â ī¸" : "â";
|
|
5006
|
+
lines.push(` ${status} ${section.padEnd(20)} ${bar} ${data.tested}/${data.total} (${data.percent}%)`);
|
|
5007
|
+
}
|
|
5008
|
+
lines.push("");
|
|
5009
|
+
// Coverage gaps
|
|
5010
|
+
if (result.gaps.length > 0) {
|
|
5011
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5012
|
+
lines.push("đŗī¸ COVERAGE GAPS");
|
|
5013
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5014
|
+
lines.push("");
|
|
5015
|
+
const priorityEmoji = { critical: "đ¨", high: "đ´", medium: "đĄ", low: "đĸ" };
|
|
5016
|
+
for (const gap of result.gaps.slice(0, 15)) {
|
|
5017
|
+
const emoji = priorityEmoji[gap.priority];
|
|
5018
|
+
lines.push(` ${emoji} ${gap.page.path}`);
|
|
5019
|
+
lines.push(` Reason: ${gap.reason} | Priority: ${gap.priority}`);
|
|
5020
|
+
if (gap.suggestedTests.length > 0) {
|
|
5021
|
+
lines.push(` Suggested: ${gap.suggestedTests[0]}`);
|
|
5022
|
+
}
|
|
5023
|
+
lines.push("");
|
|
5024
|
+
}
|
|
5025
|
+
if (result.gaps.length > 15) {
|
|
5026
|
+
lines.push(` ... and ${result.gaps.length - 15} more gaps`);
|
|
5027
|
+
lines.push("");
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
// Recommendations
|
|
5031
|
+
if (result.recommendations.length > 0) {
|
|
5032
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5033
|
+
lines.push("đĄ RECOMMENDATIONS");
|
|
5034
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5035
|
+
lines.push("");
|
|
5036
|
+
for (const rec of result.recommendations) {
|
|
5037
|
+
lines.push(` ${rec}`);
|
|
5038
|
+
}
|
|
5039
|
+
lines.push("");
|
|
5040
|
+
}
|
|
5041
|
+
// Tested pages summary
|
|
5042
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5043
|
+
lines.push("â
TESTED PAGES (Top 10 by coverage)");
|
|
5044
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5045
|
+
lines.push("");
|
|
5046
|
+
const topTested = [...result.testedPages]
|
|
5047
|
+
.sort((a, b) => b.coverageScore - a.coverageScore)
|
|
5048
|
+
.slice(0, 10);
|
|
5049
|
+
for (const page of topTested) {
|
|
5050
|
+
const bar = generateCoverageProgressBar(page.coverageScore, 15);
|
|
5051
|
+
lines.push(` ${bar} ${page.coverageScore}% ${page.path}`);
|
|
5052
|
+
lines.push(` Actions: ${page.actions.length} | Tests: ${page.testCount}`);
|
|
5053
|
+
}
|
|
5054
|
+
return lines.join("\n");
|
|
5055
|
+
}
|
|
5056
|
+
/**
|
|
5057
|
+
* Generate HTML coverage report
|
|
5058
|
+
*/
|
|
5059
|
+
function generateCoverageHtmlReport(result) {
|
|
5060
|
+
const { analysis, gaps, testedPages } = result;
|
|
5061
|
+
const coverageColor = analysis.coveragePercent >= 70 ? "#22c55e" :
|
|
5062
|
+
analysis.coveragePercent >= 40 ? "#eab308" : "#ef4444";
|
|
5063
|
+
return `<!DOCTYPE html>
|
|
5064
|
+
<html lang="en">
|
|
5065
|
+
<head>
|
|
5066
|
+
<meta charset="UTF-8">
|
|
5067
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5068
|
+
<title>Test Coverage Map - ${result.baseUrl}</title>
|
|
5069
|
+
<style>
|
|
5070
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
5071
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; padding: 2rem; }
|
|
5072
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
5073
|
+
h1 { color: #fff; margin-bottom: 0.5rem; }
|
|
5074
|
+
.subtitle { color: #888; margin-bottom: 2rem; }
|
|
5075
|
+
.card { background: #252540; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
|
|
5076
|
+
.card h2 { color: #fff; font-size: 1.1rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
|
|
5077
|
+
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; }
|
|
5078
|
+
.stat { text-align: center; }
|
|
5079
|
+
.stat-value { font-size: 2rem; font-weight: bold; color: ${coverageColor}; }
|
|
5080
|
+
.stat-label { color: #888; font-size: 0.875rem; }
|
|
5081
|
+
.progress-bar { height: 8px; background: #333; border-radius: 4px; overflow: hidden; margin: 1rem 0; }
|
|
5082
|
+
.progress-fill { height: 100%; background: ${coverageColor}; transition: width 0.5s; }
|
|
5083
|
+
.section-list { list-style: none; }
|
|
5084
|
+
.section-item { display: flex; align-items: center; gap: 1rem; padding: 0.75rem 0; border-bottom: 1px solid #333; }
|
|
5085
|
+
.section-name { flex: 1; }
|
|
5086
|
+
.section-bar { width: 150px; height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
|
|
5087
|
+
.section-bar-fill { height: 100%; border-radius: 3px; }
|
|
5088
|
+
.section-percent { width: 60px; text-align: right; font-weight: 500; }
|
|
5089
|
+
.gap-list { list-style: none; }
|
|
5090
|
+
.gap-item { padding: 1rem; margin-bottom: 0.75rem; background: #1a1a2e; border-radius: 8px; border-left: 4px solid; }
|
|
5091
|
+
.gap-critical { border-color: #ef4444; }
|
|
5092
|
+
.gap-high { border-color: #f97316; }
|
|
5093
|
+
.gap-medium { border-color: #eab308; }
|
|
5094
|
+
.gap-low { border-color: #22c55e; }
|
|
5095
|
+
.gap-path { font-weight: 600; color: #fff; }
|
|
5096
|
+
.gap-reason { color: #888; font-size: 0.875rem; margin-top: 0.25rem; }
|
|
5097
|
+
.badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
|
|
5098
|
+
.badge-critical { background: #ef4444; color: #fff; }
|
|
5099
|
+
.badge-high { background: #f97316; color: #fff; }
|
|
5100
|
+
.badge-medium { background: #eab308; color: #000; }
|
|
5101
|
+
.badge-low { background: #22c55e; color: #fff; }
|
|
5102
|
+
.recommendations { list-style: none; }
|
|
5103
|
+
.recommendations li { padding: 0.75rem; background: #1a1a2e; border-radius: 6px; margin-bottom: 0.5rem; }
|
|
5104
|
+
.page-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; }
|
|
5105
|
+
.page-card { background: #1a1a2e; border-radius: 8px; padding: 1rem; }
|
|
5106
|
+
.page-score { font-size: 1.5rem; font-weight: bold; }
|
|
5107
|
+
.page-path { color: #888; font-size: 0.875rem; word-break: break-all; }
|
|
5108
|
+
</style>
|
|
5109
|
+
</head>
|
|
5110
|
+
<body>
|
|
5111
|
+
<div class="container">
|
|
5112
|
+
<h1>Test Coverage Map</h1>
|
|
5113
|
+
<p class="subtitle">${result.baseUrl} | Generated ${new Date(result.timestamp).toLocaleString()}</p>
|
|
5114
|
+
|
|
5115
|
+
<div class="card">
|
|
5116
|
+
<h2>đ Overall Coverage</h2>
|
|
5117
|
+
<div class="stats">
|
|
5118
|
+
<div class="stat">
|
|
5119
|
+
<div class="stat-value">${analysis.coveragePercent}%</div>
|
|
5120
|
+
<div class="stat-label">Coverage</div>
|
|
5121
|
+
</div>
|
|
5122
|
+
<div class="stat">
|
|
5123
|
+
<div class="stat-value">${analysis.totalPages}</div>
|
|
5124
|
+
<div class="stat-label">Total Pages</div>
|
|
5125
|
+
</div>
|
|
5126
|
+
<div class="stat">
|
|
5127
|
+
<div class="stat-value">${analysis.testedPages}</div>
|
|
5128
|
+
<div class="stat-label">Tested</div>
|
|
5129
|
+
</div>
|
|
5130
|
+
<div class="stat">
|
|
5131
|
+
<div class="stat-value">${analysis.untestedPages}</div>
|
|
5132
|
+
<div class="stat-label">Untested</div>
|
|
5133
|
+
</div>
|
|
5134
|
+
</div>
|
|
5135
|
+
<div class="progress-bar">
|
|
5136
|
+
<div class="progress-fill" style="width: ${analysis.coveragePercent}%"></div>
|
|
5137
|
+
</div>
|
|
5138
|
+
</div>
|
|
5139
|
+
|
|
5140
|
+
<div class="card">
|
|
5141
|
+
<h2>đ Coverage by Section</h2>
|
|
5142
|
+
<ul class="section-list">
|
|
5143
|
+
${Object.entries(analysis.sectionCoverage)
|
|
5144
|
+
.sort((a, b) => b[1].total - a[1].total)
|
|
5145
|
+
.map(([section, data]) => {
|
|
5146
|
+
const color = data.percent >= 70 ? "#22c55e" : data.percent >= 40 ? "#eab308" : "#ef4444";
|
|
5147
|
+
return `
|
|
5148
|
+
<li class="section-item">
|
|
5149
|
+
<span class="section-name">${section}</span>
|
|
5150
|
+
<div class="section-bar">
|
|
5151
|
+
<div class="section-bar-fill" style="width: ${data.percent}%; background: ${color}"></div>
|
|
5152
|
+
</div>
|
|
5153
|
+
<span class="section-percent" style="color: ${color}">${data.percent}%</span>
|
|
5154
|
+
<span style="color: #666; font-size: 0.875rem">${data.tested}/${data.total}</span>
|
|
5155
|
+
</li>
|
|
5156
|
+
`;
|
|
5157
|
+
}).join("")}
|
|
5158
|
+
</ul>
|
|
5159
|
+
</div>
|
|
5160
|
+
|
|
5161
|
+
${gaps.length > 0 ? `
|
|
5162
|
+
<div class="card">
|
|
5163
|
+
<h2>đŗī¸ Coverage Gaps (${gaps.length})</h2>
|
|
5164
|
+
<ul class="gap-list">
|
|
5165
|
+
${gaps.slice(0, 20).map(gap => `
|
|
5166
|
+
<li class="gap-item gap-${gap.priority}">
|
|
5167
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
5168
|
+
<span class="gap-path">${gap.page.path}</span>
|
|
5169
|
+
<span class="badge badge-${gap.priority}">${gap.priority}</span>
|
|
5170
|
+
</div>
|
|
5171
|
+
<div class="gap-reason">Reason: ${gap.reason}</div>
|
|
5172
|
+
</li>
|
|
5173
|
+
`).join("")}
|
|
5174
|
+
</ul>
|
|
5175
|
+
${gaps.length > 20 ? `<p style="color: #666; text-align: center;">...and ${gaps.length - 20} more gaps</p>` : ""}
|
|
5176
|
+
</div>
|
|
5177
|
+
` : ""}
|
|
5178
|
+
|
|
5179
|
+
${result.recommendations.length > 0 ? `
|
|
5180
|
+
<div class="card">
|
|
5181
|
+
<h2>đĄ Recommendations</h2>
|
|
5182
|
+
<ul class="recommendations">
|
|
5183
|
+
${result.recommendations.map(rec => `<li>${rec}</li>`).join("")}
|
|
5184
|
+
</ul>
|
|
5185
|
+
</div>
|
|
5186
|
+
` : ""}
|
|
5187
|
+
|
|
5188
|
+
<div class="card">
|
|
5189
|
+
<h2>â
Tested Pages (Top 12)</h2>
|
|
5190
|
+
<div class="page-grid">
|
|
5191
|
+
${testedPages
|
|
5192
|
+
.sort((a, b) => b.coverageScore - a.coverageScore)
|
|
5193
|
+
.slice(0, 12)
|
|
5194
|
+
.map(page => {
|
|
5195
|
+
const color = page.coverageScore >= 70 ? "#22c55e" : page.coverageScore >= 40 ? "#eab308" : "#ef4444";
|
|
5196
|
+
return `
|
|
5197
|
+
<div class="page-card">
|
|
5198
|
+
<div class="page-score" style="color: ${color}">${page.coverageScore}%</div>
|
|
5199
|
+
<div class="page-path">${page.path}</div>
|
|
5200
|
+
<div style="color: #666; font-size: 0.75rem; margin-top: 0.5rem;">
|
|
5201
|
+
${page.actions.length} actions | ${page.testCount} test(s)
|
|
5202
|
+
</div>
|
|
5203
|
+
</div>
|
|
5204
|
+
`;
|
|
5205
|
+
}).join("")}
|
|
5206
|
+
</div>
|
|
5207
|
+
</div>
|
|
5208
|
+
|
|
5209
|
+
<footer style="text-align: center; color: #666; margin-top: 2rem; font-size: 0.875rem;">
|
|
5210
|
+
Generated by CBrowser v6.5.0 | Analysis took ${(result.duration / 1000).toFixed(1)}s
|
|
5211
|
+
</footer>
|
|
5212
|
+
</div>
|
|
5213
|
+
</body>
|
|
5214
|
+
</html>`;
|
|
5215
|
+
}
|
|
5216
|
+
/**
|
|
5217
|
+
* Generate a text progress bar for coverage
|
|
5218
|
+
*/
|
|
5219
|
+
function generateCoverageProgressBar(percent, width = 30) {
|
|
5220
|
+
const filled = Math.round((percent / 100) * width);
|
|
5221
|
+
const empty = width - filled;
|
|
5222
|
+
return "â".repeat(filled) + "â".repeat(empty);
|
|
5223
|
+
}
|
|
5224
|
+
// =========================================================================
|
|
5225
|
+
// Tier 7: AI Visual Regression (v7.0.0)
|
|
5226
|
+
// =========================================================================
|
|
5227
|
+
/**
|
|
5228
|
+
* Get the path to visual baselines storage
|
|
5229
|
+
*/
|
|
5230
|
+
function getVisualBaselinesPath() {
|
|
5231
|
+
const baseDir = process.env.CBROWSER_DATA_DIR || (0, path_1.join)(process.cwd(), ".cbrowser");
|
|
5232
|
+
const baselinesDir = (0, path_1.join)(baseDir, "visual-baselines");
|
|
5233
|
+
if (!(0, fs_1.existsSync)(baselinesDir)) {
|
|
5234
|
+
(0, fs_1.mkdirSync)(baselinesDir, { recursive: true });
|
|
5235
|
+
}
|
|
5236
|
+
return baselinesDir;
|
|
5237
|
+
}
|
|
5238
|
+
/**
|
|
5239
|
+
* Get the path to visual baseline screenshots
|
|
5240
|
+
*/
|
|
5241
|
+
function getVisualScreenshotsPath() {
|
|
5242
|
+
const baselinesDir = getVisualBaselinesPath();
|
|
5243
|
+
const screenshotsDir = (0, path_1.join)(baselinesDir, "screenshots");
|
|
5244
|
+
if (!(0, fs_1.existsSync)(screenshotsDir)) {
|
|
5245
|
+
(0, fs_1.mkdirSync)(screenshotsDir, { recursive: true });
|
|
5246
|
+
}
|
|
5247
|
+
return screenshotsDir;
|
|
5248
|
+
}
|
|
5249
|
+
/**
|
|
5250
|
+
* Load all visual baselines from storage
|
|
5251
|
+
*/
|
|
5252
|
+
function loadVisualBaselines() {
|
|
5253
|
+
const baselinesPath = getVisualBaselinesPath();
|
|
5254
|
+
const indexPath = (0, path_1.join)(baselinesPath, "baselines.json");
|
|
5255
|
+
if (!(0, fs_1.existsSync)(indexPath)) {
|
|
5256
|
+
return [];
|
|
5257
|
+
}
|
|
5258
|
+
try {
|
|
5259
|
+
const data = JSON.parse((0, fs_1.readFileSync)(indexPath, "utf-8"));
|
|
5260
|
+
return data.baselines || [];
|
|
5261
|
+
}
|
|
5262
|
+
catch {
|
|
5263
|
+
return [];
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
/**
|
|
5267
|
+
* Save visual baselines to storage
|
|
5268
|
+
*/
|
|
5269
|
+
function saveVisualBaselines(baselines) {
|
|
5270
|
+
const baselinesPath = getVisualBaselinesPath();
|
|
5271
|
+
const indexPath = (0, path_1.join)(baselinesPath, "baselines.json");
|
|
5272
|
+
(0, fs_1.writeFileSync)(indexPath, JSON.stringify({ baselines, updated: new Date().toISOString() }, null, 2));
|
|
5273
|
+
}
|
|
5274
|
+
/**
|
|
5275
|
+
* Capture a visual baseline screenshot
|
|
5276
|
+
*/
|
|
5277
|
+
async function captureVisualBaseline(url, name, options = {}) {
|
|
5278
|
+
const browser = new CBrowser({
|
|
5279
|
+
device: options.device,
|
|
5280
|
+
viewportWidth: options.viewport?.width || 1920,
|
|
5281
|
+
viewportHeight: options.viewport?.height || 1080,
|
|
5282
|
+
});
|
|
5283
|
+
try {
|
|
5284
|
+
await browser.launch();
|
|
5285
|
+
await browser.navigate(url);
|
|
5286
|
+
// Wait if specified
|
|
5287
|
+
if (options.waitFor) {
|
|
5288
|
+
if (typeof options.waitFor === "number") {
|
|
5289
|
+
await new Promise(resolve => setTimeout(resolve, options.waitFor));
|
|
5290
|
+
}
|
|
5291
|
+
else {
|
|
5292
|
+
const page = await browser.getPage();
|
|
5293
|
+
await page.waitForSelector(options.waitFor, { timeout: 10000 }).catch(() => { });
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
// Take screenshot
|
|
5297
|
+
const screenshotsPath = getVisualScreenshotsPath();
|
|
5298
|
+
const id = `${name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${Date.now()}`;
|
|
5299
|
+
const screenshotPath = (0, path_1.join)(screenshotsPath, `${id}.png`);
|
|
5300
|
+
const page = await browser.getPage();
|
|
5301
|
+
if (options.selector) {
|
|
5302
|
+
const element = page.locator(options.selector).first();
|
|
5303
|
+
await element.screenshot({ path: screenshotPath });
|
|
5304
|
+
}
|
|
5305
|
+
else {
|
|
5306
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
5307
|
+
}
|
|
5308
|
+
// Get dimensions
|
|
5309
|
+
const viewport = page.viewportSize() || { width: 1920, height: 1080 };
|
|
5310
|
+
const baseline = {
|
|
5311
|
+
id,
|
|
5312
|
+
name,
|
|
5313
|
+
url,
|
|
5314
|
+
screenshotPath,
|
|
5315
|
+
dimensions: viewport,
|
|
5316
|
+
viewport,
|
|
5317
|
+
device: options.device,
|
|
5318
|
+
timestamp: new Date().toISOString(),
|
|
5319
|
+
selector: options.selector,
|
|
5320
|
+
};
|
|
5321
|
+
// Save to index
|
|
5322
|
+
const baselines = loadVisualBaselines();
|
|
5323
|
+
// Remove existing baseline with same name (update)
|
|
5324
|
+
const filtered = baselines.filter(b => b.name !== name);
|
|
5325
|
+
filtered.push(baseline);
|
|
5326
|
+
saveVisualBaselines(filtered);
|
|
5327
|
+
return baseline;
|
|
5328
|
+
}
|
|
5329
|
+
finally {
|
|
5330
|
+
await browser.close();
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
5333
|
+
/**
|
|
5334
|
+
* List all visual baselines
|
|
5335
|
+
*/
|
|
5336
|
+
function listVisualBaselines() {
|
|
5337
|
+
return loadVisualBaselines();
|
|
5338
|
+
}
|
|
5339
|
+
/**
|
|
5340
|
+
* Get a visual baseline by name
|
|
5341
|
+
*/
|
|
5342
|
+
function getVisualBaseline(name) {
|
|
5343
|
+
const baselines = loadVisualBaselines();
|
|
5344
|
+
return baselines.find(b => b.name === name);
|
|
5345
|
+
}
|
|
5346
|
+
/**
|
|
5347
|
+
* Delete a visual baseline
|
|
5348
|
+
*/
|
|
5349
|
+
function deleteVisualBaseline(name) {
|
|
5350
|
+
const baselines = loadVisualBaselines();
|
|
5351
|
+
const baseline = baselines.find(b => b.name === name);
|
|
5352
|
+
if (!baseline) {
|
|
5353
|
+
return false;
|
|
5354
|
+
}
|
|
5355
|
+
// Delete screenshot file
|
|
5356
|
+
if ((0, fs_1.existsSync)(baseline.screenshotPath)) {
|
|
5357
|
+
(0, fs_1.unlinkSync)(baseline.screenshotPath);
|
|
5358
|
+
}
|
|
5359
|
+
// Update index
|
|
5360
|
+
const filtered = baselines.filter(b => b.name !== name);
|
|
5361
|
+
saveVisualBaselines(filtered);
|
|
5362
|
+
return true;
|
|
5363
|
+
}
|
|
5364
|
+
/**
|
|
5365
|
+
* Analyze visual differences using AI
|
|
5366
|
+
*/
|
|
5367
|
+
async function analyzeVisualDifferences(baselinePath, currentPath, options = {}) {
|
|
5368
|
+
// Read both images as base64
|
|
5369
|
+
const baselineImage = (0, fs_1.readFileSync)(baselinePath).toString("base64");
|
|
5370
|
+
const currentImage = (0, fs_1.readFileSync)(currentPath).toString("base64");
|
|
5371
|
+
// Build the AI prompt for analysis
|
|
5372
|
+
const sensitivityDesc = {
|
|
5373
|
+
low: "Only flag significant, obvious changes that would clearly impact users",
|
|
5374
|
+
medium: "Flag notable changes in layout, content, or style",
|
|
5375
|
+
high: "Flag any visible differences, including subtle spacing or color changes",
|
|
5376
|
+
};
|
|
5377
|
+
const prompt = `You are a visual regression testing AI. Compare these two screenshots and identify any differences.
|
|
5378
|
+
|
|
5379
|
+
BASELINE IMAGE: The first/reference screenshot
|
|
5380
|
+
CURRENT IMAGE: The second/new screenshot
|
|
5381
|
+
|
|
5382
|
+
Sensitivity level: ${options.sensitivity || "medium"} - ${sensitivityDesc[options.sensitivity || "medium"]}
|
|
5383
|
+
|
|
5384
|
+
${options.ignoreRegions?.length ? `Ignore changes in these regions: ${JSON.stringify(options.ignoreRegions)}` : ""}
|
|
5385
|
+
|
|
5386
|
+
Analyze the visual differences and respond in this exact JSON format:
|
|
5387
|
+
{
|
|
5388
|
+
"overallStatus": "pass" | "warning" | "fail",
|
|
5389
|
+
"summary": "Brief 1-2 sentence summary of changes found",
|
|
5390
|
+
"changes": [
|
|
5391
|
+
{
|
|
5392
|
+
"type": "layout" | "content" | "style" | "missing" | "added" | "moved",
|
|
5393
|
+
"severity": "breaking" | "warning" | "info" | "acceptable",
|
|
5394
|
+
"region": { "x": 0, "y": 0, "width": 100, "height": 100 },
|
|
5395
|
+
"description": "What changed",
|
|
5396
|
+
"reasoning": "Why this matters",
|
|
5397
|
+
"confidence": 0.95,
|
|
5398
|
+
"suggestion": "Optional suggestion to fix or accept"
|
|
5399
|
+
}
|
|
5400
|
+
],
|
|
5401
|
+
"similarityScore": 0.85,
|
|
5402
|
+
"productionReady": true | false,
|
|
5403
|
+
"confidence": 0.9
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
Change severity guidelines:
|
|
5407
|
+
- "breaking": Layout shifts, missing critical elements, broken functionality indicators
|
|
5408
|
+
- "warning": Noticeable content changes, significant style differences
|
|
5409
|
+
- "info": Minor spacing changes, subtle color adjustments
|
|
5410
|
+
- "acceptable": Expected dynamic content (timestamps, ads), minor rendering differences
|
|
5411
|
+
|
|
5412
|
+
For overallStatus:
|
|
5413
|
+
- "pass": No changes or only acceptable/info-level changes
|
|
5414
|
+
- "warning": Some warning-level changes that should be reviewed
|
|
5415
|
+
- "fail": Any breaking changes detected
|
|
5416
|
+
|
|
5417
|
+
Respond ONLY with the JSON, no other text.`;
|
|
5418
|
+
// Use Claude to analyze the images
|
|
5419
|
+
// For now, we'll use a simulated response since we don't have direct API access
|
|
5420
|
+
// In production, this would call the Anthropic API with vision
|
|
5421
|
+
try {
|
|
5422
|
+
// Try to use the inference tool if available
|
|
5423
|
+
const { execSync } = await import("child_process");
|
|
5424
|
+
const inferenceScript = (0, path_1.join)(process.env.HOME || "", ".claude/skills/Tools/Inference.ts");
|
|
5425
|
+
if ((0, fs_1.existsSync)(inferenceScript)) {
|
|
5426
|
+
// Create a temporary file with the images and prompt
|
|
5427
|
+
const tempDir = (0, path_1.join)(getVisualBaselinesPath(), "temp");
|
|
5428
|
+
if (!(0, fs_1.existsSync)(tempDir)) {
|
|
5429
|
+
(0, fs_1.mkdirSync)(tempDir, { recursive: true });
|
|
5430
|
+
}
|
|
5431
|
+
const requestPath = (0, path_1.join)(tempDir, `analysis-${Date.now()}.json`);
|
|
5432
|
+
(0, fs_1.writeFileSync)(requestPath, JSON.stringify({
|
|
5433
|
+
prompt,
|
|
5434
|
+
images: [
|
|
5435
|
+
{ type: "base64", media_type: "image/png", data: baselineImage },
|
|
5436
|
+
{ type: "base64", media_type: "image/png", data: currentImage },
|
|
5437
|
+
],
|
|
5438
|
+
}));
|
|
5439
|
+
// For now, perform a heuristic comparison since we can't easily call Claude with images
|
|
5440
|
+
// This can be enhanced when proper API integration is available
|
|
5441
|
+
const analysis = performHeuristicAnalysis(baselinePath, currentPath, options);
|
|
5442
|
+
// Clean up
|
|
5443
|
+
if ((0, fs_1.existsSync)(requestPath)) {
|
|
5444
|
+
(0, fs_1.unlinkSync)(requestPath);
|
|
5445
|
+
}
|
|
5446
|
+
return analysis;
|
|
5447
|
+
}
|
|
5448
|
+
}
|
|
5449
|
+
catch {
|
|
5450
|
+
// Fall back to heuristic analysis
|
|
5451
|
+
}
|
|
5452
|
+
// Fallback: Heuristic analysis based on file comparison
|
|
5453
|
+
return performHeuristicAnalysis(baselinePath, currentPath, options);
|
|
5454
|
+
}
|
|
5455
|
+
/**
|
|
5456
|
+
* Perform heuristic visual analysis when AI is not available
|
|
5457
|
+
*/
|
|
5458
|
+
function performHeuristicAnalysis(baselinePath, currentPath, options = {}) {
|
|
5459
|
+
const baselineStats = (0, fs_1.statSync)(baselinePath);
|
|
5460
|
+
const currentStats = (0, fs_1.statSync)(currentPath);
|
|
5461
|
+
// Simple heuristic: compare file sizes
|
|
5462
|
+
const sizeDiff = Math.abs(baselineStats.size - currentStats.size);
|
|
5463
|
+
const sizeRatio = sizeDiff / baselineStats.size;
|
|
5464
|
+
const changes = [];
|
|
5465
|
+
let overallStatus = "pass";
|
|
5466
|
+
let similarityScore = 1.0;
|
|
5467
|
+
// Size-based heuristics
|
|
5468
|
+
if (sizeRatio > 0.3) {
|
|
5469
|
+
changes.push({
|
|
5470
|
+
type: "layout",
|
|
5471
|
+
severity: "breaking",
|
|
5472
|
+
region: { x: 0, y: 0, width: 1920, height: 1080 },
|
|
5473
|
+
description: "Significant visual change detected (>30% size difference)",
|
|
5474
|
+
reasoning: "Large file size difference indicates substantial visual changes",
|
|
5475
|
+
confidence: 0.7,
|
|
5476
|
+
suggestion: "Review the visual changes manually",
|
|
5477
|
+
});
|
|
5478
|
+
overallStatus = "fail";
|
|
5479
|
+
similarityScore = 0.5;
|
|
5480
|
+
}
|
|
5481
|
+
else if (sizeRatio > 0.1) {
|
|
5482
|
+
changes.push({
|
|
5483
|
+
type: "content",
|
|
5484
|
+
severity: "warning",
|
|
5485
|
+
region: { x: 0, y: 0, width: 1920, height: 1080 },
|
|
5486
|
+
description: "Moderate visual change detected (10-30% size difference)",
|
|
5487
|
+
reasoning: "Moderate file size difference suggests some visual changes",
|
|
5488
|
+
confidence: 0.6,
|
|
5489
|
+
suggestion: "Review to confirm changes are expected",
|
|
5490
|
+
});
|
|
5491
|
+
overallStatus = "warning";
|
|
5492
|
+
similarityScore = 0.75;
|
|
5493
|
+
}
|
|
5494
|
+
else if (sizeRatio > 0.02) {
|
|
5495
|
+
changes.push({
|
|
5496
|
+
type: "style",
|
|
5497
|
+
severity: "info",
|
|
5498
|
+
region: { x: 0, y: 0, width: 1920, height: 1080 },
|
|
5499
|
+
description: "Minor visual change detected (2-10% size difference)",
|
|
5500
|
+
reasoning: "Small file size difference indicates minor rendering differences",
|
|
5501
|
+
confidence: 0.5,
|
|
5502
|
+
});
|
|
5503
|
+
similarityScore = 0.9;
|
|
5504
|
+
}
|
|
5505
|
+
// Apply sensitivity adjustments
|
|
5506
|
+
if (options.sensitivity === "low" && overallStatus === "warning") {
|
|
5507
|
+
overallStatus = "pass";
|
|
5508
|
+
}
|
|
5509
|
+
else if (options.sensitivity === "high" && changes.length === 0 && sizeRatio > 0.005) {
|
|
5510
|
+
changes.push({
|
|
5511
|
+
type: "style",
|
|
5512
|
+
severity: "info",
|
|
5513
|
+
region: { x: 0, y: 0, width: 1920, height: 1080 },
|
|
5514
|
+
description: "Very minor visual change detected",
|
|
5515
|
+
reasoning: "Slight file size difference at high sensitivity",
|
|
5516
|
+
confidence: 0.4,
|
|
5517
|
+
});
|
|
5518
|
+
similarityScore = 0.95;
|
|
5519
|
+
}
|
|
5520
|
+
return {
|
|
5521
|
+
overallStatus,
|
|
5522
|
+
summary: changes.length === 0
|
|
5523
|
+
? "No significant visual changes detected"
|
|
5524
|
+
: `Found ${changes.length} visual change(s) with ${overallStatus} status`,
|
|
5525
|
+
changes,
|
|
5526
|
+
similarityScore,
|
|
5527
|
+
productionReady: overallStatus !== "fail",
|
|
5528
|
+
confidence: 0.6, // Lower confidence for heuristic analysis
|
|
5529
|
+
rawAnalysis: "Heuristic analysis based on file comparison (AI analysis not available)",
|
|
5530
|
+
};
|
|
5531
|
+
}
|
|
5532
|
+
/**
|
|
5533
|
+
* Run visual regression test against a baseline
|
|
5534
|
+
*/
|
|
5535
|
+
async function runVisualRegression(url, baselineName, options = {}) {
|
|
5536
|
+
const startTime = Date.now();
|
|
5537
|
+
const baseline = getVisualBaseline(baselineName);
|
|
5538
|
+
if (!baseline) {
|
|
5539
|
+
return {
|
|
5540
|
+
passed: false,
|
|
5541
|
+
baseline: null,
|
|
5542
|
+
currentScreenshotPath: "",
|
|
5543
|
+
analysis: {
|
|
5544
|
+
overallStatus: "fail",
|
|
5545
|
+
summary: `Baseline "${baselineName}" not found`,
|
|
5546
|
+
changes: [],
|
|
5547
|
+
similarityScore: 0,
|
|
5548
|
+
productionReady: false,
|
|
5549
|
+
confidence: 1.0,
|
|
5550
|
+
},
|
|
5551
|
+
duration: Date.now() - startTime,
|
|
5552
|
+
};
|
|
5553
|
+
}
|
|
5554
|
+
// Capture current screenshot with same settings as baseline
|
|
5555
|
+
const browser = new CBrowser({
|
|
5556
|
+
device: baseline.device,
|
|
5557
|
+
viewportWidth: baseline.viewport.width,
|
|
5558
|
+
viewportHeight: baseline.viewport.height,
|
|
5559
|
+
});
|
|
5560
|
+
try {
|
|
5561
|
+
await browser.launch();
|
|
5562
|
+
await browser.navigate(url);
|
|
5563
|
+
// Wait if specified in options
|
|
5564
|
+
if (options.waitBeforeCapture) {
|
|
5565
|
+
await new Promise(resolve => setTimeout(resolve, options.waitBeforeCapture));
|
|
5566
|
+
}
|
|
5567
|
+
// Take screenshot
|
|
5568
|
+
const screenshotsPath = getVisualScreenshotsPath();
|
|
5569
|
+
const currentScreenshotPath = (0, path_1.join)(screenshotsPath, `current-${baseline.id}-${Date.now()}.png`);
|
|
5570
|
+
const page = await browser.getPage();
|
|
5571
|
+
if (baseline.selector) {
|
|
5572
|
+
const element = page.locator(baseline.selector).first();
|
|
5573
|
+
await element.screenshot({ path: currentScreenshotPath });
|
|
5574
|
+
}
|
|
5575
|
+
else {
|
|
5576
|
+
await page.screenshot({ path: currentScreenshotPath, fullPage: false });
|
|
5577
|
+
}
|
|
5578
|
+
// Analyze differences
|
|
5579
|
+
const analysis = await analyzeVisualDifferences(baseline.screenshotPath, currentScreenshotPath, options);
|
|
5580
|
+
// Determine pass/fail based on threshold
|
|
5581
|
+
const threshold = options.threshold ?? 0.9;
|
|
5582
|
+
const passed = analysis.similarityScore >= threshold && analysis.overallStatus !== "fail";
|
|
5583
|
+
// Generate diff image path (if we had pixel-diff capability)
|
|
5584
|
+
const diffImagePath = options.generateDiff
|
|
5585
|
+
? (0, path_1.join)(screenshotsPath, `diff-${baseline.id}-${Date.now()}.png`)
|
|
5586
|
+
: undefined;
|
|
5587
|
+
return {
|
|
5588
|
+
passed,
|
|
5589
|
+
baseline,
|
|
5590
|
+
currentScreenshotPath,
|
|
5591
|
+
diffImagePath,
|
|
5592
|
+
analysis,
|
|
5593
|
+
duration: Date.now() - startTime,
|
|
5594
|
+
};
|
|
5595
|
+
}
|
|
5596
|
+
finally {
|
|
5597
|
+
await browser.close();
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
/**
|
|
5601
|
+
* Run visual regression on multiple pages
|
|
5602
|
+
*/
|
|
5603
|
+
async function runVisualRegressionSuite(suite, options = {}) {
|
|
5604
|
+
const startTime = Date.now();
|
|
5605
|
+
const results = [];
|
|
5606
|
+
let passed = 0;
|
|
5607
|
+
let failed = 0;
|
|
5608
|
+
let warnings = 0;
|
|
5609
|
+
console.log(`\nđ Running visual regression suite: ${suite.name}`);
|
|
5610
|
+
console.log(` Testing ${suite.pages.length} page(s)...\n`);
|
|
5611
|
+
for (const page of suite.pages) {
|
|
5612
|
+
console.log(` đ¸ Testing: ${page.name}...`);
|
|
5613
|
+
const result = await runVisualRegression(page.url, page.baselineName, { ...options, ...page.options });
|
|
5614
|
+
results.push(result);
|
|
5615
|
+
if (result.passed) {
|
|
5616
|
+
if (result.analysis.overallStatus === "warning") {
|
|
5617
|
+
warnings++;
|
|
5618
|
+
console.log(` â ī¸ Warning (similarity: ${(result.analysis.similarityScore * 100).toFixed(1)}%)`);
|
|
5619
|
+
}
|
|
5620
|
+
else {
|
|
5621
|
+
passed++;
|
|
5622
|
+
console.log(` â
Passed (similarity: ${(result.analysis.similarityScore * 100).toFixed(1)}%)`);
|
|
5623
|
+
}
|
|
5624
|
+
}
|
|
5625
|
+
else {
|
|
5626
|
+
failed++;
|
|
5627
|
+
console.log(` â Failed: ${result.analysis.summary}`);
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
const duration = Date.now() - startTime;
|
|
5631
|
+
console.log(`\n${"â".repeat(60)}`);
|
|
5632
|
+
console.log(` Results: ${passed} passed, ${failed} failed, ${warnings} warnings`);
|
|
5633
|
+
console.log(` Duration: ${(duration / 1000).toFixed(1)}s`);
|
|
5634
|
+
console.log(`${"â".repeat(60)}\n`);
|
|
5635
|
+
return {
|
|
5636
|
+
suite,
|
|
5637
|
+
results,
|
|
5638
|
+
summary: {
|
|
5639
|
+
total: suite.pages.length,
|
|
5640
|
+
passed,
|
|
5641
|
+
failed,
|
|
5642
|
+
warnings,
|
|
5643
|
+
},
|
|
5644
|
+
duration,
|
|
5645
|
+
timestamp: new Date().toISOString(),
|
|
5646
|
+
};
|
|
5647
|
+
}
|
|
5648
|
+
/**
|
|
5649
|
+
* Format visual regression result as text report
|
|
5650
|
+
*/
|
|
5651
|
+
function formatVisualRegressionReport(result) {
|
|
5652
|
+
const lines = [];
|
|
5653
|
+
lines.push("ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5654
|
+
lines.push("â AI VISUAL REGRESSION REPORT â");
|
|
5655
|
+
lines.push("ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5656
|
+
lines.push("");
|
|
5657
|
+
const statusIcon = result.passed ? "â
" : "â";
|
|
5658
|
+
const statusText = result.passed ? "PASSED" : "FAILED";
|
|
5659
|
+
lines.push(`${statusIcon} Status: ${statusText}`);
|
|
5660
|
+
lines.push(`đ Similarity: ${(result.analysis.similarityScore * 100).toFixed(1)}%`);
|
|
5661
|
+
lines.push(`đ¯ Confidence: ${(result.analysis.confidence * 100).toFixed(0)}%`);
|
|
5662
|
+
lines.push(`âąī¸ Duration: ${(result.duration / 1000).toFixed(2)}s`);
|
|
5663
|
+
lines.push("");
|
|
5664
|
+
if (result.baseline) {
|
|
5665
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5666
|
+
lines.push("đ¸ BASELINE INFO");
|
|
5667
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5668
|
+
lines.push(` Name: ${result.baseline.name}`);
|
|
5669
|
+
lines.push(` URL: ${result.baseline.url}`);
|
|
5670
|
+
lines.push(` Captured: ${result.baseline.timestamp}`);
|
|
5671
|
+
lines.push(` Viewport: ${result.baseline.viewport.width}x${result.baseline.viewport.height}`);
|
|
5672
|
+
if (result.baseline.device) {
|
|
5673
|
+
lines.push(` Device: ${result.baseline.device}`);
|
|
5674
|
+
}
|
|
5675
|
+
lines.push("");
|
|
5676
|
+
}
|
|
5677
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5678
|
+
lines.push("đ ANALYSIS SUMMARY");
|
|
5679
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5680
|
+
lines.push(` ${result.analysis.summary}`);
|
|
5681
|
+
lines.push("");
|
|
5682
|
+
if (result.analysis.changes.length > 0) {
|
|
5683
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5684
|
+
lines.push("đ DETECTED CHANGES");
|
|
5685
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5686
|
+
for (const change of result.analysis.changes) {
|
|
5687
|
+
const severityIcon = {
|
|
5688
|
+
breaking: "đ¨",
|
|
5689
|
+
warning: "â ī¸",
|
|
5690
|
+
info: "âšī¸",
|
|
5691
|
+
acceptable: "â",
|
|
5692
|
+
}[change.severity];
|
|
5693
|
+
lines.push("");
|
|
5694
|
+
lines.push(` ${severityIcon} [${change.severity.toUpperCase()}] ${change.type}`);
|
|
5695
|
+
lines.push(` ${change.description}`);
|
|
5696
|
+
lines.push(` Reasoning: ${change.reasoning}`);
|
|
5697
|
+
if (change.suggestion) {
|
|
5698
|
+
lines.push(` Suggestion: ${change.suggestion}`);
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
lines.push("");
|
|
5702
|
+
}
|
|
5703
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5704
|
+
lines.push(`đ Production Ready: ${result.analysis.productionReady ? "YES" : "NO"}`);
|
|
5705
|
+
lines.push("âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ");
|
|
5706
|
+
return lines.join("\n");
|
|
5707
|
+
}
|
|
5708
|
+
/**
|
|
5709
|
+
* Generate HTML report for visual regression suite
|
|
5710
|
+
*/
|
|
5711
|
+
function generateVisualRegressionHtmlReport(suiteResult) {
|
|
5712
|
+
const { suite, results, summary, duration, timestamp } = suiteResult;
|
|
5713
|
+
return `<!DOCTYPE html>
|
|
5714
|
+
<html lang="en">
|
|
5715
|
+
<head>
|
|
5716
|
+
<meta charset="UTF-8">
|
|
5717
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5718
|
+
<title>Visual Regression Report - ${suite.name}</title>
|
|
5719
|
+
<style>
|
|
5720
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
5721
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #e2e8f0; line-height: 1.6; }
|
|
5722
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
|
5723
|
+
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
|
5724
|
+
h2 { font-size: 1.25rem; margin-bottom: 1rem; color: #94a3b8; }
|
|
5725
|
+
.header { text-align: center; margin-bottom: 2rem; }
|
|
5726
|
+
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 2rem; }
|
|
5727
|
+
.stat { background: #1e293b; padding: 1.5rem; border-radius: 0.5rem; text-align: center; }
|
|
5728
|
+
.stat-value { font-size: 2rem; font-weight: bold; }
|
|
5729
|
+
.stat-label { color: #94a3b8; font-size: 0.875rem; }
|
|
5730
|
+
.passed { color: #22c55e; }
|
|
5731
|
+
.failed { color: #ef4444; }
|
|
5732
|
+
.warning { color: #eab308; }
|
|
5733
|
+
.results { display: flex; flex-direction: column; gap: 1rem; }
|
|
5734
|
+
.result-card { background: #1e293b; border-radius: 0.5rem; overflow: hidden; }
|
|
5735
|
+
.result-header { padding: 1rem; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #334155; }
|
|
5736
|
+
.result-body { padding: 1rem; }
|
|
5737
|
+
.badge { padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
|
|
5738
|
+
.badge-pass { background: #166534; color: #22c55e; }
|
|
5739
|
+
.badge-fail { background: #7f1d1d; color: #ef4444; }
|
|
5740
|
+
.badge-warning { background: #713f12; color: #eab308; }
|
|
5741
|
+
.similarity { font-size: 1.5rem; font-weight: bold; }
|
|
5742
|
+
.changes { margin-top: 1rem; }
|
|
5743
|
+
.change { padding: 0.75rem; background: #0f172a; border-radius: 0.25rem; margin-bottom: 0.5rem; }
|
|
5744
|
+
.change-breaking { border-left: 3px solid #ef4444; }
|
|
5745
|
+
.change-warning { border-left: 3px solid #eab308; }
|
|
5746
|
+
.change-info { border-left: 3px solid #3b82f6; }
|
|
5747
|
+
.change-acceptable { border-left: 3px solid #22c55e; }
|
|
5748
|
+
footer { text-align: center; color: #64748b; margin-top: 2rem; padding-top: 2rem; border-top: 1px solid #334155; }
|
|
5749
|
+
</style>
|
|
5750
|
+
</head>
|
|
5751
|
+
<body>
|
|
5752
|
+
<div class="container">
|
|
5753
|
+
<div class="header">
|
|
5754
|
+
<h1>đ Visual Regression Report</h1>
|
|
5755
|
+
<h2>${suite.name}</h2>
|
|
5756
|
+
<p style="color: #64748b;">Generated: ${new Date(timestamp).toLocaleString()}</p>
|
|
5757
|
+
</div>
|
|
5758
|
+
|
|
5759
|
+
<div class="summary">
|
|
5760
|
+
<div class="stat">
|
|
5761
|
+
<div class="stat-value">${summary.total}</div>
|
|
5762
|
+
<div class="stat-label">Total Tests</div>
|
|
5763
|
+
</div>
|
|
5764
|
+
<div class="stat">
|
|
5765
|
+
<div class="stat-value passed">${summary.passed}</div>
|
|
5766
|
+
<div class="stat-label">Passed</div>
|
|
5767
|
+
</div>
|
|
5768
|
+
<div class="stat">
|
|
5769
|
+
<div class="stat-value failed">${summary.failed}</div>
|
|
5770
|
+
<div class="stat-label">Failed</div>
|
|
5771
|
+
</div>
|
|
5772
|
+
<div class="stat">
|
|
5773
|
+
<div class="stat-value warning">${summary.warnings}</div>
|
|
5774
|
+
<div class="stat-label">Warnings</div>
|
|
5775
|
+
</div>
|
|
5776
|
+
</div>
|
|
5777
|
+
|
|
5778
|
+
<div class="results">
|
|
5779
|
+
${results.map((result, i) => {
|
|
5780
|
+
const page = suite.pages[i];
|
|
5781
|
+
const statusClass = result.passed ? (result.analysis.overallStatus === "warning" ? "warning" : "passed") : "failed";
|
|
5782
|
+
const badgeClass = result.passed ? (result.analysis.overallStatus === "warning" ? "badge-warning" : "badge-pass") : "badge-fail";
|
|
5783
|
+
const statusText = result.passed ? (result.analysis.overallStatus === "warning" ? "WARNING" : "PASSED") : "FAILED";
|
|
5784
|
+
return `
|
|
5785
|
+
<div class="result-card">
|
|
5786
|
+
<div class="result-header">
|
|
5787
|
+
<div>
|
|
5788
|
+
<strong>${page.name}</strong>
|
|
5789
|
+
<div style="color: #64748b; font-size: 0.875rem;">${page.url}</div>
|
|
5790
|
+
</div>
|
|
5791
|
+
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
5792
|
+
<div class="similarity ${statusClass}">${(result.analysis.similarityScore * 100).toFixed(1)}%</div>
|
|
5793
|
+
<span class="badge ${badgeClass}">${statusText}</span>
|
|
5794
|
+
</div>
|
|
5795
|
+
</div>
|
|
5796
|
+
<div class="result-body">
|
|
5797
|
+
<p>${result.analysis.summary}</p>
|
|
5798
|
+
${result.analysis.changes.length > 0 ? `
|
|
5799
|
+
<div class="changes">
|
|
5800
|
+
${result.analysis.changes.map(change => `
|
|
5801
|
+
<div class="change change-${change.severity}">
|
|
5802
|
+
<strong>[${change.severity.toUpperCase()}] ${change.type}</strong>
|
|
5803
|
+
<p>${change.description}</p>
|
|
5804
|
+
${change.suggestion ? `<p style="color: #94a3b8;"><em>Suggestion: ${change.suggestion}</em></p>` : ""}
|
|
5805
|
+
</div>
|
|
5806
|
+
`).join("")}
|
|
5807
|
+
</div>
|
|
5808
|
+
` : ""}
|
|
5809
|
+
</div>
|
|
5810
|
+
</div>
|
|
5811
|
+
`;
|
|
5812
|
+
}).join("")}
|
|
5813
|
+
</div>
|
|
5814
|
+
|
|
5815
|
+
<footer>
|
|
5816
|
+
Generated by CBrowser v7.0.0 | Suite completed in ${(duration / 1000).toFixed(1)}s
|
|
5817
|
+
</footer>
|
|
5818
|
+
</div>
|
|
5819
|
+
</body>
|
|
5820
|
+
</html>`;
|
|
5821
|
+
}
|
|
4509
5822
|
//# sourceMappingURL=browser.js.map
|