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.
- package/dist/analysis/bug-hunter.d.ts +32 -0
- package/dist/analysis/bug-hunter.d.ts.map +1 -0
- package/dist/analysis/bug-hunter.js +106 -0
- package/dist/analysis/bug-hunter.js.map +1 -0
- package/dist/analysis/chaos-testing.d.ts +41 -0
- package/dist/analysis/chaos-testing.d.ts.map +1 -0
- package/dist/analysis/chaos-testing.js +87 -0
- package/dist/analysis/chaos-testing.js.map +1 -0
- package/dist/analysis/index.d.ts +10 -0
- package/dist/analysis/index.d.ts.map +1 -0
- package/dist/analysis/index.js +26 -0
- package/dist/analysis/index.js.map +1 -0
- package/dist/analysis/natural-language.d.ts +43 -0
- package/dist/analysis/natural-language.d.ts.map +1 -0
- package/dist/analysis/natural-language.js +205 -0
- package/dist/analysis/natural-language.js.map +1 -0
- package/dist/analysis/persona-comparison.d.ts +31 -0
- package/dist/analysis/persona-comparison.d.ts.map +1 -0
- package/dist/analysis/persona-comparison.js +217 -0
- package/dist/analysis/persona-comparison.js.map +1 -0
- package/dist/browser.d.ts +1 -411
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +0 -4745
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +64 -56
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +2 -1
- package/dist/daemon.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +406 -1
- package/dist/mcp-server.js.map +1 -1
- package/dist/performance/index.d.ts +7 -0
- package/dist/performance/index.d.ts.map +1 -0
- package/dist/performance/index.js +23 -0
- package/dist/performance/index.js.map +1 -0
- package/dist/performance/metrics.d.ts +49 -0
- package/dist/performance/metrics.d.ts.map +1 -0
- package/dist/performance/metrics.js +386 -0
- package/dist/performance/metrics.js.map +1 -0
- package/dist/testing/coverage.d.ts +39 -0
- package/dist/testing/coverage.d.ts.map +1 -0
- package/dist/testing/coverage.js +713 -0
- package/dist/testing/coverage.js.map +1 -0
- package/dist/testing/flaky-detection.d.ts +28 -0
- package/dist/testing/flaky-detection.d.ts.map +1 -0
- package/dist/testing/flaky-detection.js +332 -0
- package/dist/testing/flaky-detection.js.map +1 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +26 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/nl-test-suite.d.ts +70 -0
- package/dist/testing/nl-test-suite.d.ts.map +1 -0
- package/dist/testing/nl-test-suite.js +427 -0
- package/dist/testing/nl-test-suite.js.map +1 -0
- package/dist/testing/test-repair.d.ts +36 -0
- package/dist/testing/test-repair.d.ts.map +1 -0
- package/dist/testing/test-repair.js +528 -0
- package/dist/testing/test-repair.js.map +1 -0
- package/dist/visual/ab-comparison.d.ts +23 -0
- package/dist/visual/ab-comparison.d.ts.map +1 -0
- package/dist/visual/ab-comparison.js +366 -0
- package/dist/visual/ab-comparison.js.map +1 -0
- package/dist/visual/cross-browser.d.ts +41 -0
- package/dist/visual/cross-browser.d.ts.map +1 -0
- package/dist/visual/cross-browser.js +442 -0
- package/dist/visual/cross-browser.js.map +1 -0
- package/dist/visual/index.d.ts +10 -0
- package/dist/visual/index.d.ts.map +1 -0
- package/dist/visual/index.js +26 -0
- package/dist/visual/index.js.map +1 -0
- package/dist/visual/regression.d.ts +55 -0
- package/dist/visual/regression.d.ts.map +1 -0
- package/dist/visual/regression.js +616 -0
- package/dist/visual/regression.js.map +1 -0
- package/dist/visual/responsive.d.ts +27 -0
- package/dist/visual/responsive.d.ts.map +1 -0
- package/dist/visual/responsive.js +450 -0
- package/dist/visual/responsive.js.map +1 -0
- 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
|