cbrowser 6.3.2 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.ts +75 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1071 -0
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +435 -2
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +61 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +358 -0
- package/dist/daemon.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +209 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -27,6 +27,20 @@ exports.formatRepairReport = formatRepairReport;
|
|
|
27
27
|
exports.exportRepairedTest = exportRepairedTest;
|
|
28
28
|
exports.detectFlakyTests = detectFlakyTests;
|
|
29
29
|
exports.formatFlakyTestReport = formatFlakyTestReport;
|
|
30
|
+
exports.capturePerformanceBaseline = capturePerformanceBaseline;
|
|
31
|
+
exports.listPerformanceBaselines = listPerformanceBaselines;
|
|
32
|
+
exports.loadPerformanceBaseline = loadPerformanceBaseline;
|
|
33
|
+
exports.deletePerformanceBaseline = deletePerformanceBaseline;
|
|
34
|
+
exports.detectPerformanceRegression = detectPerformanceRegression;
|
|
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;
|
|
30
44
|
const playwright_1 = require("playwright");
|
|
31
45
|
const fs_1 = require("fs");
|
|
32
46
|
const path_1 = require("path");
|
|
@@ -4141,4 +4155,1061 @@ function formatFlakyTestReport(result) {
|
|
|
4141
4155
|
lines.push("");
|
|
4142
4156
|
return lines.join("\n");
|
|
4143
4157
|
}
|
|
4158
|
+
// ============================================================================
|
|
4159
|
+
// Performance Regression Detection (v6.4.0)
|
|
4160
|
+
// ============================================================================
|
|
4161
|
+
const DEFAULT_REGRESSION_THRESHOLDS = {
|
|
4162
|
+
lcp: 20, // 20% increase
|
|
4163
|
+
fid: 50, // 50% increase
|
|
4164
|
+
cls: 0.1, // Absolute increase of 0.1
|
|
4165
|
+
fcp: 20, // 20% increase
|
|
4166
|
+
ttfb: 30, // 30% increase
|
|
4167
|
+
tti: 25, // 25% increase
|
|
4168
|
+
tbt: 50, // 50% increase
|
|
4169
|
+
transferSize: 25, // 25% increase
|
|
4170
|
+
};
|
|
4171
|
+
/**
|
|
4172
|
+
* Capture a performance baseline for a URL
|
|
4173
|
+
*/
|
|
4174
|
+
async function capturePerformanceBaseline(url, options = {}) {
|
|
4175
|
+
const { runs = 3, name, headless = true, device, throttle } = options;
|
|
4176
|
+
const paths = (0, config_js_1.getPaths)();
|
|
4177
|
+
(0, config_js_1.ensureDirectories)();
|
|
4178
|
+
const browser = new CBrowser({ headless });
|
|
4179
|
+
const allMetrics = [];
|
|
4180
|
+
try {
|
|
4181
|
+
for (let i = 0; i < runs; i++) {
|
|
4182
|
+
await browser.navigate(url);
|
|
4183
|
+
// Wait for page to stabilize
|
|
4184
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
4185
|
+
const metrics = await browser.getPerformanceMetrics();
|
|
4186
|
+
allMetrics.push(metrics);
|
|
4187
|
+
// Brief pause between runs
|
|
4188
|
+
if (i < runs - 1) {
|
|
4189
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
finally {
|
|
4194
|
+
await browser.close();
|
|
4195
|
+
}
|
|
4196
|
+
// Average the metrics
|
|
4197
|
+
const avgMetrics = {};
|
|
4198
|
+
const numericMetricKeys = [
|
|
4199
|
+
"lcp", "fid", "cls", "fcp", "ttfb", "tti", "tbt",
|
|
4200
|
+
"domContentLoaded", "load", "resourceCount", "transferSize"
|
|
4201
|
+
];
|
|
4202
|
+
for (const key of numericMetricKeys) {
|
|
4203
|
+
const values = allMetrics
|
|
4204
|
+
.map((m) => m[key])
|
|
4205
|
+
.filter((v) => v !== undefined && v !== null);
|
|
4206
|
+
if (values.length > 0) {
|
|
4207
|
+
avgMetrics[key] = values.reduce((a, b) => a + b, 0) / values.length;
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
// Determine ratings
|
|
4211
|
+
if (avgMetrics.lcp !== undefined) {
|
|
4212
|
+
avgMetrics.lcpRating = avgMetrics.lcp <= 2500 ? "good" : avgMetrics.lcp <= 4000 ? "needs-improvement" : "poor";
|
|
4213
|
+
}
|
|
4214
|
+
if (avgMetrics.cls !== undefined) {
|
|
4215
|
+
avgMetrics.clsRating = avgMetrics.cls <= 0.1 ? "good" : avgMetrics.cls <= 0.25 ? "needs-improvement" : "poor";
|
|
4216
|
+
}
|
|
4217
|
+
const baseline = {
|
|
4218
|
+
id: `baseline-${Date.now()}`,
|
|
4219
|
+
url,
|
|
4220
|
+
name: name || new URL(url).hostname,
|
|
4221
|
+
timestamp: new Date().toISOString(),
|
|
4222
|
+
metrics: avgMetrics,
|
|
4223
|
+
runsAveraged: runs,
|
|
4224
|
+
environment: {
|
|
4225
|
+
browser: "chromium",
|
|
4226
|
+
viewport: { width: 1280, height: 720 },
|
|
4227
|
+
device,
|
|
4228
|
+
connection: throttle,
|
|
4229
|
+
},
|
|
4230
|
+
};
|
|
4231
|
+
// Save baseline
|
|
4232
|
+
const baselinesDir = (0, path_1.join)(paths.dataDir, "baselines");
|
|
4233
|
+
if (!(0, fs_1.existsSync)(baselinesDir)) {
|
|
4234
|
+
(0, fs_1.mkdirSync)(baselinesDir, { recursive: true });
|
|
4235
|
+
}
|
|
4236
|
+
const baselineFile = (0, path_1.join)(baselinesDir, `${baseline.id}.json`);
|
|
4237
|
+
(0, fs_1.writeFileSync)(baselineFile, JSON.stringify(baseline, null, 2));
|
|
4238
|
+
return baseline;
|
|
4239
|
+
}
|
|
4240
|
+
/**
|
|
4241
|
+
* List all saved performance baselines
|
|
4242
|
+
*/
|
|
4243
|
+
function listPerformanceBaselines() {
|
|
4244
|
+
const paths = (0, config_js_1.getPaths)();
|
|
4245
|
+
const baselinesDir = (0, path_1.join)(paths.dataDir, "baselines");
|
|
4246
|
+
if (!(0, fs_1.existsSync)(baselinesDir)) {
|
|
4247
|
+
return [];
|
|
4248
|
+
}
|
|
4249
|
+
const files = (0, fs_1.readdirSync)(baselinesDir).filter((f) => f.endsWith(".json"));
|
|
4250
|
+
const baselines = [];
|
|
4251
|
+
for (const file of files) {
|
|
4252
|
+
try {
|
|
4253
|
+
const content = (0, fs_1.readFileSync)((0, path_1.join)(baselinesDir, file), "utf-8");
|
|
4254
|
+
baselines.push(JSON.parse(content));
|
|
4255
|
+
}
|
|
4256
|
+
catch {
|
|
4257
|
+
// Skip invalid files
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
return baselines.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
4261
|
+
}
|
|
4262
|
+
/**
|
|
4263
|
+
* Load a specific baseline by ID or name
|
|
4264
|
+
*/
|
|
4265
|
+
function loadPerformanceBaseline(idOrName) {
|
|
4266
|
+
const baselines = listPerformanceBaselines();
|
|
4267
|
+
// Try exact ID match first
|
|
4268
|
+
let baseline = baselines.find((b) => b.id === idOrName);
|
|
4269
|
+
if (baseline)
|
|
4270
|
+
return baseline;
|
|
4271
|
+
// Try name match
|
|
4272
|
+
baseline = baselines.find((b) => b.name === idOrName);
|
|
4273
|
+
if (baseline)
|
|
4274
|
+
return baseline;
|
|
4275
|
+
// Try URL match
|
|
4276
|
+
baseline = baselines.find((b) => b.url.includes(idOrName));
|
|
4277
|
+
return baseline || null;
|
|
4278
|
+
}
|
|
4279
|
+
/**
|
|
4280
|
+
* Delete a performance baseline
|
|
4281
|
+
*/
|
|
4282
|
+
function deletePerformanceBaseline(idOrName) {
|
|
4283
|
+
const baseline = loadPerformanceBaseline(idOrName);
|
|
4284
|
+
if (!baseline)
|
|
4285
|
+
return false;
|
|
4286
|
+
const paths = (0, config_js_1.getPaths)();
|
|
4287
|
+
const baselineFile = (0, path_1.join)(paths.dataDir, "baselines", `${baseline.id}.json`);
|
|
4288
|
+
if ((0, fs_1.existsSync)(baselineFile)) {
|
|
4289
|
+
(0, fs_1.unlinkSync)(baselineFile);
|
|
4290
|
+
return true;
|
|
4291
|
+
}
|
|
4292
|
+
return false;
|
|
4293
|
+
}
|
|
4294
|
+
/**
|
|
4295
|
+
* Compare current performance against a baseline
|
|
4296
|
+
*/
|
|
4297
|
+
async function detectPerformanceRegression(url, baselineIdOrName, options = {}) {
|
|
4298
|
+
const { thresholds = DEFAULT_REGRESSION_THRESHOLDS, headless = true } = options;
|
|
4299
|
+
const startTime = Date.now();
|
|
4300
|
+
// Load baseline
|
|
4301
|
+
const baseline = loadPerformanceBaseline(baselineIdOrName);
|
|
4302
|
+
if (!baseline) {
|
|
4303
|
+
throw new Error(`Baseline not found: ${baselineIdOrName}`);
|
|
4304
|
+
}
|
|
4305
|
+
// Capture current metrics
|
|
4306
|
+
const browser = new CBrowser({ headless });
|
|
4307
|
+
let currentMetrics;
|
|
4308
|
+
try {
|
|
4309
|
+
await browser.navigate(url);
|
|
4310
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
4311
|
+
currentMetrics = await browser.getPerformanceMetrics();
|
|
4312
|
+
}
|
|
4313
|
+
finally {
|
|
4314
|
+
await browser.close();
|
|
4315
|
+
}
|
|
4316
|
+
// Compare metrics
|
|
4317
|
+
const comparisons = [];
|
|
4318
|
+
const regressions = [];
|
|
4319
|
+
const metricsToCompare = [
|
|
4320
|
+
"lcp", "fid", "cls", "fcp", "ttfb", "tti", "tbt", "transferSize"
|
|
4321
|
+
];
|
|
4322
|
+
for (const metric of metricsToCompare) {
|
|
4323
|
+
const baselineValue = baseline.metrics[metric];
|
|
4324
|
+
const currentValue = currentMetrics[metric];
|
|
4325
|
+
if (baselineValue === undefined || currentValue === undefined)
|
|
4326
|
+
continue;
|
|
4327
|
+
const change = currentValue - baselineValue;
|
|
4328
|
+
const changePercent = baselineValue > 0 ? (change / baselineValue) * 100 : 0;
|
|
4329
|
+
// Determine threshold and if it's a regression
|
|
4330
|
+
const threshold = thresholds[metric] || 20;
|
|
4331
|
+
const isClsMetric = metric === "cls";
|
|
4332
|
+
// For CLS, threshold is absolute; for others, it's percentage
|
|
4333
|
+
const exceedsThreshold = isClsMetric
|
|
4334
|
+
? change > threshold
|
|
4335
|
+
: changePercent > threshold;
|
|
4336
|
+
let status = "stable";
|
|
4337
|
+
let severity = "warning";
|
|
4338
|
+
if (changePercent < -10 || (isClsMetric && change < -0.05)) {
|
|
4339
|
+
status = "improved";
|
|
4340
|
+
}
|
|
4341
|
+
else if (exceedsThreshold) {
|
|
4342
|
+
if (isClsMetric ? change > threshold * 2 : changePercent > threshold * 2) {
|
|
4343
|
+
status = "critical";
|
|
4344
|
+
severity = "critical";
|
|
4345
|
+
}
|
|
4346
|
+
else {
|
|
4347
|
+
status = "regression";
|
|
4348
|
+
severity = "regression";
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
else if (changePercent > 5 || (isClsMetric && change > 0.02)) {
|
|
4352
|
+
status = "warning";
|
|
4353
|
+
}
|
|
4354
|
+
const comparison = {
|
|
4355
|
+
metric,
|
|
4356
|
+
baseline: baselineValue,
|
|
4357
|
+
current: currentValue,
|
|
4358
|
+
change,
|
|
4359
|
+
changePercent,
|
|
4360
|
+
isRegression: status === "regression" || status === "critical",
|
|
4361
|
+
isImprovement: status === "improved",
|
|
4362
|
+
status,
|
|
4363
|
+
};
|
|
4364
|
+
comparisons.push(comparison);
|
|
4365
|
+
if (comparison.isRegression) {
|
|
4366
|
+
regressions.push({
|
|
4367
|
+
metric,
|
|
4368
|
+
baselineValue,
|
|
4369
|
+
currentValue,
|
|
4370
|
+
change,
|
|
4371
|
+
changePercent,
|
|
4372
|
+
threshold,
|
|
4373
|
+
severity,
|
|
4374
|
+
});
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
// Calculate summary
|
|
4378
|
+
const improved = comparisons.filter((c) => c.isImprovement).length;
|
|
4379
|
+
const regressed = comparisons.filter((c) => c.status === "regression").length;
|
|
4380
|
+
const critical = comparisons.filter((c) => c.status === "critical").length;
|
|
4381
|
+
const stable = comparisons.filter((c) => c.status === "stable" || c.status === "warning").length;
|
|
4382
|
+
const overallChange = comparisons.length > 0
|
|
4383
|
+
? comparisons.reduce((sum, c) => sum + c.changePercent, 0) / comparisons.length
|
|
4384
|
+
: 0;
|
|
4385
|
+
return {
|
|
4386
|
+
url,
|
|
4387
|
+
baseline,
|
|
4388
|
+
currentMetrics,
|
|
4389
|
+
timestamp: new Date().toISOString(),
|
|
4390
|
+
duration: Date.now() - startTime,
|
|
4391
|
+
comparisons,
|
|
4392
|
+
regressions,
|
|
4393
|
+
passed: regressions.length === 0,
|
|
4394
|
+
summary: {
|
|
4395
|
+
totalMetrics: comparisons.length,
|
|
4396
|
+
improved,
|
|
4397
|
+
stable,
|
|
4398
|
+
regressed,
|
|
4399
|
+
critical,
|
|
4400
|
+
overallChange,
|
|
4401
|
+
},
|
|
4402
|
+
};
|
|
4403
|
+
}
|
|
4404
|
+
/**
|
|
4405
|
+
* Format a performance regression report
|
|
4406
|
+
*/
|
|
4407
|
+
function formatPerformanceRegressionReport(result) {
|
|
4408
|
+
const lines = [];
|
|
4409
|
+
lines.push("🔍 PERFORMANCE REGRESSION REPORT");
|
|
4410
|
+
lines.push("═".repeat(60));
|
|
4411
|
+
lines.push("");
|
|
4412
|
+
lines.push(`📍 URL: ${result.url}`);
|
|
4413
|
+
lines.push(`📊 Baseline: ${result.baseline.name} (${new Date(result.baseline.timestamp).toLocaleDateString()})`);
|
|
4414
|
+
lines.push(`⏱️ Duration: ${(result.duration / 1000).toFixed(1)}s`);
|
|
4415
|
+
lines.push("");
|
|
4416
|
+
// Overall result
|
|
4417
|
+
if (result.passed) {
|
|
4418
|
+
lines.push("✅ PASSED - No performance regressions detected");
|
|
4419
|
+
}
|
|
4420
|
+
else {
|
|
4421
|
+
lines.push(`❌ FAILED - ${result.regressions.length} regression(s) detected`);
|
|
4422
|
+
}
|
|
4423
|
+
lines.push("");
|
|
4424
|
+
// Detailed comparisons
|
|
4425
|
+
lines.push("─".repeat(60));
|
|
4426
|
+
lines.push("METRIC COMPARISON");
|
|
4427
|
+
lines.push("─".repeat(60));
|
|
4428
|
+
lines.push("");
|
|
4429
|
+
const metricNames = {
|
|
4430
|
+
lcp: "LCP (Largest Contentful Paint)",
|
|
4431
|
+
fid: "FID (First Input Delay)",
|
|
4432
|
+
cls: "CLS (Cumulative Layout Shift)",
|
|
4433
|
+
fcp: "FCP (First Contentful Paint)",
|
|
4434
|
+
ttfb: "TTFB (Time to First Byte)",
|
|
4435
|
+
tti: "TTI (Time to Interactive)",
|
|
4436
|
+
tbt: "TBT (Total Blocking Time)",
|
|
4437
|
+
transferSize: "Transfer Size",
|
|
4438
|
+
};
|
|
4439
|
+
for (const comp of result.comparisons) {
|
|
4440
|
+
const name = metricNames[comp.metric] || comp.metric;
|
|
4441
|
+
const icon = comp.isImprovement ? "✅" :
|
|
4442
|
+
comp.status === "critical" ? "🔴" :
|
|
4443
|
+
comp.status === "regression" ? "❌" :
|
|
4444
|
+
comp.status === "warning" ? "⚠️" : "✓";
|
|
4445
|
+
const unit = comp.metric === "cls" ? "" :
|
|
4446
|
+
comp.metric === "transferSize" ? " KB" : " ms";
|
|
4447
|
+
const baseVal = comp.metric === "transferSize"
|
|
4448
|
+
? (comp.baseline / 1024).toFixed(1)
|
|
4449
|
+
: comp.baseline.toFixed(1);
|
|
4450
|
+
const currVal = comp.metric === "transferSize"
|
|
4451
|
+
? (comp.current / 1024).toFixed(1)
|
|
4452
|
+
: comp.current.toFixed(1);
|
|
4453
|
+
const changeStr = comp.changePercent >= 0
|
|
4454
|
+
? `+${comp.changePercent.toFixed(1)}%`
|
|
4455
|
+
: `${comp.changePercent.toFixed(1)}%`;
|
|
4456
|
+
lines.push(`${icon} ${name}`);
|
|
4457
|
+
lines.push(` Baseline: ${baseVal}${unit} → Current: ${currVal}${unit} (${changeStr})`);
|
|
4458
|
+
lines.push("");
|
|
4459
|
+
}
|
|
4460
|
+
// Summary
|
|
4461
|
+
lines.push("─".repeat(60));
|
|
4462
|
+
lines.push("SUMMARY");
|
|
4463
|
+
lines.push("─".repeat(60));
|
|
4464
|
+
lines.push("");
|
|
4465
|
+
lines.push(` Total Metrics: ${result.summary.totalMetrics}`);
|
|
4466
|
+
lines.push(` ✅ Improved: ${result.summary.improved}`);
|
|
4467
|
+
lines.push(` ✓ Stable: ${result.summary.stable}`);
|
|
4468
|
+
lines.push(` ❌ Regressed: ${result.summary.regressed}`);
|
|
4469
|
+
lines.push(` 🔴 Critical: ${result.summary.critical}`);
|
|
4470
|
+
lines.push(` 📊 Overall Change: ${result.summary.overallChange >= 0 ? "+" : ""}${result.summary.overallChange.toFixed(1)}%`);
|
|
4471
|
+
lines.push("");
|
|
4472
|
+
// Recommendations if regressions found
|
|
4473
|
+
if (result.regressions.length > 0) {
|
|
4474
|
+
lines.push("─".repeat(60));
|
|
4475
|
+
lines.push("💡 RECOMMENDATIONS");
|
|
4476
|
+
lines.push("─".repeat(60));
|
|
4477
|
+
lines.push("");
|
|
4478
|
+
for (const reg of result.regressions) {
|
|
4479
|
+
const name = metricNames[reg.metric] || reg.metric;
|
|
4480
|
+
lines.push(`⚠️ ${name}:`);
|
|
4481
|
+
switch (reg.metric) {
|
|
4482
|
+
case "lcp":
|
|
4483
|
+
lines.push(" - Optimize largest content element (images, videos)");
|
|
4484
|
+
lines.push(" - Consider lazy loading below-fold content");
|
|
4485
|
+
lines.push(" - Improve server response times");
|
|
4486
|
+
break;
|
|
4487
|
+
case "cls":
|
|
4488
|
+
lines.push(" - Set explicit dimensions on images/embeds");
|
|
4489
|
+
lines.push(" - Avoid inserting content above existing content");
|
|
4490
|
+
lines.push(" - Reserve space for dynamic content");
|
|
4491
|
+
break;
|
|
4492
|
+
case "fcp":
|
|
4493
|
+
case "ttfb":
|
|
4494
|
+
lines.push(" - Optimize server response time");
|
|
4495
|
+
lines.push(" - Use CDN for static assets");
|
|
4496
|
+
lines.push(" - Enable compression (gzip/brotli)");
|
|
4497
|
+
break;
|
|
4498
|
+
case "tbt":
|
|
4499
|
+
case "tti":
|
|
4500
|
+
lines.push(" - Split long JavaScript tasks");
|
|
4501
|
+
lines.push(" - Defer non-critical JavaScript");
|
|
4502
|
+
lines.push(" - Remove unused code");
|
|
4503
|
+
break;
|
|
4504
|
+
case "transferSize":
|
|
4505
|
+
lines.push(" - Compress and optimize assets");
|
|
4506
|
+
lines.push(" - Remove unused CSS/JavaScript");
|
|
4507
|
+
lines.push(" - Optimize images (WebP, proper sizing)");
|
|
4508
|
+
break;
|
|
4509
|
+
default:
|
|
4510
|
+
lines.push(" - Review recent changes for performance impact");
|
|
4511
|
+
}
|
|
4512
|
+
lines.push("");
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
return lines.join("\n");
|
|
4516
|
+
}
|
|
4517
|
+
// ============================================================================
|
|
4518
|
+
// Test Coverage Map (v6.5.0)
|
|
4519
|
+
// ============================================================================
|
|
4520
|
+
/**
|
|
4521
|
+
* Parse test files to extract tested URLs and actions
|
|
4522
|
+
*/
|
|
4523
|
+
function parseTestFilesForCoverage(testFiles) {
|
|
4524
|
+
const pageMap = new Map();
|
|
4525
|
+
for (const testFile of testFiles) {
|
|
4526
|
+
if (!(0, fs_1.existsSync)(testFile))
|
|
4527
|
+
continue;
|
|
4528
|
+
const content = (0, fs_1.readFileSync)(testFile, "utf-8");
|
|
4529
|
+
const lines = content.split("\n");
|
|
4530
|
+
let currentUrl = null;
|
|
4531
|
+
let lineNumber = 0;
|
|
4532
|
+
for (const line of lines) {
|
|
4533
|
+
lineNumber++;
|
|
4534
|
+
const trimmed = line.trim().toLowerCase();
|
|
4535
|
+
// Skip comments and empty lines
|
|
4536
|
+
if (trimmed.startsWith("#") || !trimmed)
|
|
4537
|
+
continue;
|
|
4538
|
+
// Detect navigation
|
|
4539
|
+
const navMatch = line.match(/(?:go to|navigate to|open|visit)\s+["']?([^"'\s]+)["']?/i);
|
|
4540
|
+
if (navMatch) {
|
|
4541
|
+
currentUrl = navMatch[1];
|
|
4542
|
+
const path = normalizeUrlToPath(currentUrl);
|
|
4543
|
+
if (!pageMap.has(path)) {
|
|
4544
|
+
pageMap.set(path, {
|
|
4545
|
+
url: currentUrl,
|
|
4546
|
+
path,
|
|
4547
|
+
testFiles: [],
|
|
4548
|
+
actions: [],
|
|
4549
|
+
testCount: 0,
|
|
4550
|
+
coverageScore: 0,
|
|
4551
|
+
});
|
|
4552
|
+
}
|
|
4553
|
+
const page = pageMap.get(path);
|
|
4554
|
+
if (!page.testFiles.includes(testFile)) {
|
|
4555
|
+
page.testFiles.push(testFile);
|
|
4556
|
+
page.testCount++;
|
|
4557
|
+
}
|
|
4558
|
+
page.actions.push({
|
|
4559
|
+
type: "navigate",
|
|
4560
|
+
target: currentUrl,
|
|
4561
|
+
testFile,
|
|
4562
|
+
lineNumber,
|
|
4563
|
+
});
|
|
4564
|
+
}
|
|
4565
|
+
// Detect click actions
|
|
4566
|
+
const clickMatch = line.match(/click\s+(?:on\s+)?(?:the\s+)?["']?([^"'\n]+)["']?/i);
|
|
4567
|
+
if (clickMatch && currentUrl) {
|
|
4568
|
+
const path = normalizeUrlToPath(currentUrl);
|
|
4569
|
+
const page = pageMap.get(path);
|
|
4570
|
+
if (page) {
|
|
4571
|
+
page.actions.push({
|
|
4572
|
+
type: "click",
|
|
4573
|
+
target: clickMatch[1].trim(),
|
|
4574
|
+
testFile,
|
|
4575
|
+
lineNumber,
|
|
4576
|
+
});
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
// Detect fill/type actions
|
|
4580
|
+
const fillMatch = line.match(/(?:type|fill|enter)\s+["']([^"']+)["']\s+(?:in|into)\s+(?:the\s+)?["']?([^"'\n]+)["']?/i);
|
|
4581
|
+
if (fillMatch && currentUrl) {
|
|
4582
|
+
const path = normalizeUrlToPath(currentUrl);
|
|
4583
|
+
const page = pageMap.get(path);
|
|
4584
|
+
if (page) {
|
|
4585
|
+
page.actions.push({
|
|
4586
|
+
type: "fill",
|
|
4587
|
+
target: fillMatch[2].trim(),
|
|
4588
|
+
value: fillMatch[1],
|
|
4589
|
+
testFile,
|
|
4590
|
+
lineNumber,
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
// Detect verify actions
|
|
4595
|
+
const verifyMatch = line.match(/(?:verify|assert|check|expect|should)\s+(.+)/i);
|
|
4596
|
+
if (verifyMatch && currentUrl) {
|
|
4597
|
+
const path = normalizeUrlToPath(currentUrl);
|
|
4598
|
+
const page = pageMap.get(path);
|
|
4599
|
+
if (page) {
|
|
4600
|
+
page.actions.push({
|
|
4601
|
+
type: "verify",
|
|
4602
|
+
target: verifyMatch[1].trim(),
|
|
4603
|
+
testFile,
|
|
4604
|
+
lineNumber,
|
|
4605
|
+
});
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
// Detect wait actions
|
|
4609
|
+
const waitMatch = line.match(/wait\s+(?:for\s+)?(.+)/i);
|
|
4610
|
+
if (waitMatch && currentUrl) {
|
|
4611
|
+
const path = normalizeUrlToPath(currentUrl);
|
|
4612
|
+
const page = pageMap.get(path);
|
|
4613
|
+
if (page) {
|
|
4614
|
+
page.actions.push({
|
|
4615
|
+
type: "wait",
|
|
4616
|
+
target: waitMatch[1].trim(),
|
|
4617
|
+
testFile,
|
|
4618
|
+
lineNumber,
|
|
4619
|
+
});
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
// Calculate coverage scores
|
|
4625
|
+
for (const page of pageMap.values()) {
|
|
4626
|
+
const hasClicks = page.actions.some(a => a.type === "click");
|
|
4627
|
+
const hasFills = page.actions.some(a => a.type === "fill");
|
|
4628
|
+
const hasVerifies = page.actions.some(a => a.type === "verify");
|
|
4629
|
+
let score = 20; // Base score for visiting
|
|
4630
|
+
if (hasClicks)
|
|
4631
|
+
score += 25;
|
|
4632
|
+
if (hasFills)
|
|
4633
|
+
score += 25;
|
|
4634
|
+
if (hasVerifies)
|
|
4635
|
+
score += 30;
|
|
4636
|
+
page.coverageScore = Math.min(100, score);
|
|
4637
|
+
}
|
|
4638
|
+
return Array.from(pageMap.values());
|
|
4639
|
+
}
|
|
4640
|
+
/**
|
|
4641
|
+
* Normalize URL to a path for comparison
|
|
4642
|
+
*/
|
|
4643
|
+
function normalizeUrlToPath(url) {
|
|
4644
|
+
try {
|
|
4645
|
+
const parsed = new URL(url);
|
|
4646
|
+
return parsed.pathname.replace(/\/$/, "") || "/";
|
|
4647
|
+
}
|
|
4648
|
+
catch {
|
|
4649
|
+
// Not a full URL, treat as path
|
|
4650
|
+
return url.replace(/\/$/, "") || "/";
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
/**
|
|
4654
|
+
* Fetch and parse sitemap.xml
|
|
4655
|
+
*/
|
|
4656
|
+
async function parseSitemap(sitemapUrl) {
|
|
4657
|
+
const pages = [];
|
|
4658
|
+
try {
|
|
4659
|
+
const response = await fetch(sitemapUrl);
|
|
4660
|
+
const xml = await response.text();
|
|
4661
|
+
// Simple XML parsing for sitemap
|
|
4662
|
+
const locMatches = xml.matchAll(/<loc>([^<]+)<\/loc>/g);
|
|
4663
|
+
for (const match of locMatches) {
|
|
4664
|
+
const url = match[1].trim();
|
|
4665
|
+
pages.push({
|
|
4666
|
+
url,
|
|
4667
|
+
path: normalizeUrlToPath(url),
|
|
4668
|
+
source: "sitemap",
|
|
4669
|
+
});
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
catch (err) {
|
|
4673
|
+
console.error(`Failed to fetch sitemap: ${err}`);
|
|
4674
|
+
}
|
|
4675
|
+
return pages;
|
|
4676
|
+
}
|
|
4677
|
+
/**
|
|
4678
|
+
* Crawl a site to discover pages
|
|
4679
|
+
*/
|
|
4680
|
+
async function crawlSiteForCoverage(startUrl, maxPages = 100, includePattern, excludePattern) {
|
|
4681
|
+
const pages = [];
|
|
4682
|
+
const visited = new Set();
|
|
4683
|
+
const queue = [startUrl];
|
|
4684
|
+
const browser = new CBrowser({
|
|
4685
|
+
headless: true,
|
|
4686
|
+
browser: "chromium",
|
|
4687
|
+
});
|
|
4688
|
+
const baseUrl = new URL(startUrl);
|
|
4689
|
+
const includeRegex = includePattern ? new RegExp(includePattern) : null;
|
|
4690
|
+
const excludeRegex = excludePattern ? new RegExp(excludePattern) : null;
|
|
4691
|
+
try {
|
|
4692
|
+
while (queue.length > 0 && pages.length < maxPages) {
|
|
4693
|
+
const url = queue.shift();
|
|
4694
|
+
const path = normalizeUrlToPath(url);
|
|
4695
|
+
if (visited.has(path))
|
|
4696
|
+
continue;
|
|
4697
|
+
visited.add(path);
|
|
4698
|
+
// Check patterns
|
|
4699
|
+
if (includeRegex && !includeRegex.test(path))
|
|
4700
|
+
continue;
|
|
4701
|
+
if (excludeRegex && excludeRegex.test(path))
|
|
4702
|
+
continue;
|
|
4703
|
+
try {
|
|
4704
|
+
const result = await browser.navigate(url);
|
|
4705
|
+
// Count interactive elements
|
|
4706
|
+
const page = await browser.getPage();
|
|
4707
|
+
const interactiveElements = await page.locator("button, a, input, select, textarea, [onclick], [role='button']").count();
|
|
4708
|
+
const formCount = await page.locator("form").count();
|
|
4709
|
+
// Get outbound links
|
|
4710
|
+
const links = await page.locator("a[href]").evaluateAll((els) => els.map(el => el.href).filter(href => href && !href.startsWith("javascript:")));
|
|
4711
|
+
const sitePage = {
|
|
4712
|
+
url,
|
|
4713
|
+
path,
|
|
4714
|
+
title: result.title,
|
|
4715
|
+
source: pages.length === 0 ? "crawl" : "link",
|
|
4716
|
+
status: 200,
|
|
4717
|
+
outboundLinks: links,
|
|
4718
|
+
interactiveElements,
|
|
4719
|
+
formCount,
|
|
4720
|
+
};
|
|
4721
|
+
pages.push(sitePage);
|
|
4722
|
+
// Add internal links to queue
|
|
4723
|
+
for (const link of links) {
|
|
4724
|
+
try {
|
|
4725
|
+
const linkUrl = new URL(link);
|
|
4726
|
+
if (linkUrl.hostname === baseUrl.hostname && !visited.has(normalizeUrlToPath(link))) {
|
|
4727
|
+
queue.push(link);
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
catch {
|
|
4731
|
+
// Invalid URL, skip
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
catch (err) {
|
|
4736
|
+
// Page failed to load
|
|
4737
|
+
pages.push({
|
|
4738
|
+
url,
|
|
4739
|
+
path,
|
|
4740
|
+
source: "link",
|
|
4741
|
+
status: 0,
|
|
4742
|
+
});
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
finally {
|
|
4747
|
+
await browser.close();
|
|
4748
|
+
}
|
|
4749
|
+
return pages;
|
|
4750
|
+
}
|
|
4751
|
+
/**
|
|
4752
|
+
* Identify coverage gaps
|
|
4753
|
+
*/
|
|
4754
|
+
function identifyCoverageGaps(sitePages, testedPages, minCoverage = 50) {
|
|
4755
|
+
const gaps = [];
|
|
4756
|
+
const testedPaths = new Set(testedPages.map(p => p.path));
|
|
4757
|
+
for (const sitePage of sitePages) {
|
|
4758
|
+
const testedPage = testedPages.find(p => p.path === sitePage.path);
|
|
4759
|
+
// Completely untested
|
|
4760
|
+
if (!testedPage) {
|
|
4761
|
+
const priority = determinePriority(sitePage);
|
|
4762
|
+
gaps.push({
|
|
4763
|
+
page: sitePage,
|
|
4764
|
+
reason: "untested",
|
|
4765
|
+
priority,
|
|
4766
|
+
suggestedTests: generateSuggestedTests(sitePage),
|
|
4767
|
+
similarTestedPages: findSimilarTestedPages(sitePage.path, testedPages),
|
|
4768
|
+
});
|
|
4769
|
+
continue;
|
|
4770
|
+
}
|
|
4771
|
+
// Low coverage
|
|
4772
|
+
if (testedPage.coverageScore < minCoverage) {
|
|
4773
|
+
gaps.push({
|
|
4774
|
+
page: sitePage,
|
|
4775
|
+
reason: "low-coverage",
|
|
4776
|
+
priority: "medium",
|
|
4777
|
+
suggestedTests: generateSuggestedTests(sitePage, testedPage),
|
|
4778
|
+
});
|
|
4779
|
+
continue;
|
|
4780
|
+
}
|
|
4781
|
+
// No interactions tested
|
|
4782
|
+
const hasInteractions = testedPage.actions.some(a => a.type === "click" || a.type === "fill");
|
|
4783
|
+
if (!hasInteractions && sitePage.interactiveElements && sitePage.interactiveElements > 5) {
|
|
4784
|
+
gaps.push({
|
|
4785
|
+
page: sitePage,
|
|
4786
|
+
reason: "no-interactions",
|
|
4787
|
+
priority: "low",
|
|
4788
|
+
suggestedTests: [`Test interactive elements on ${sitePage.path}`],
|
|
4789
|
+
});
|
|
4790
|
+
}
|
|
4791
|
+
// No verifications
|
|
4792
|
+
const hasVerifications = testedPage.actions.some(a => a.type === "verify");
|
|
4793
|
+
if (!hasVerifications) {
|
|
4794
|
+
gaps.push({
|
|
4795
|
+
page: sitePage,
|
|
4796
|
+
reason: "no-verifications",
|
|
4797
|
+
priority: "low",
|
|
4798
|
+
suggestedTests: [`Add assertions to verify ${sitePage.path} content`],
|
|
4799
|
+
});
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
// Sort by priority
|
|
4803
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
4804
|
+
gaps.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
4805
|
+
return gaps;
|
|
4806
|
+
}
|
|
4807
|
+
/**
|
|
4808
|
+
* Determine priority of an untested page
|
|
4809
|
+
*/
|
|
4810
|
+
function determinePriority(page) {
|
|
4811
|
+
const path = page.path.toLowerCase();
|
|
4812
|
+
// Critical paths
|
|
4813
|
+
if (path.includes("checkout") || path.includes("payment") || path.includes("login") ||
|
|
4814
|
+
path.includes("register") || path.includes("signup") || path.includes("auth")) {
|
|
4815
|
+
return "critical";
|
|
4816
|
+
}
|
|
4817
|
+
// High priority - user account, settings
|
|
4818
|
+
if (path.includes("account") || path.includes("profile") || path.includes("settings") ||
|
|
4819
|
+
path.includes("dashboard") || path.includes("admin")) {
|
|
4820
|
+
return "high";
|
|
4821
|
+
}
|
|
4822
|
+
// Medium - has forms or many interactive elements
|
|
4823
|
+
if (page.formCount && page.formCount > 0)
|
|
4824
|
+
return "medium";
|
|
4825
|
+
if (page.interactiveElements && page.interactiveElements > 10)
|
|
4826
|
+
return "medium";
|
|
4827
|
+
return "low";
|
|
4828
|
+
}
|
|
4829
|
+
/**
|
|
4830
|
+
* Generate suggested test steps for a page
|
|
4831
|
+
*/
|
|
4832
|
+
function generateSuggestedTests(sitePage, existingTests) {
|
|
4833
|
+
const suggestions = [];
|
|
4834
|
+
suggestions.push(`go to ${sitePage.url}`);
|
|
4835
|
+
if (sitePage.formCount && sitePage.formCount > 0) {
|
|
4836
|
+
suggestions.push(`fill form fields with test data`);
|
|
4837
|
+
suggestions.push(`submit form and verify success`);
|
|
4838
|
+
}
|
|
4839
|
+
if (sitePage.interactiveElements && sitePage.interactiveElements > 0) {
|
|
4840
|
+
suggestions.push(`click primary call-to-action`);
|
|
4841
|
+
}
|
|
4842
|
+
suggestions.push(`verify page contains expected content`);
|
|
4843
|
+
suggestions.push(`verify no console errors`);
|
|
4844
|
+
if (existingTests) {
|
|
4845
|
+
// Add specific suggestions based on what's missing
|
|
4846
|
+
const hasClicks = existingTests.actions.some(a => a.type === "click");
|
|
4847
|
+
const hasFills = existingTests.actions.some(a => a.type === "fill");
|
|
4848
|
+
const hasVerifies = existingTests.actions.some(a => a.type === "verify");
|
|
4849
|
+
if (!hasClicks)
|
|
4850
|
+
suggestions.unshift(`# Add click interactions`);
|
|
4851
|
+
if (!hasFills && sitePage.formCount)
|
|
4852
|
+
suggestions.unshift(`# Add form fill tests`);
|
|
4853
|
+
if (!hasVerifies)
|
|
4854
|
+
suggestions.unshift(`# Add verification assertions`);
|
|
4855
|
+
}
|
|
4856
|
+
return suggestions;
|
|
4857
|
+
}
|
|
4858
|
+
/**
|
|
4859
|
+
* Find similar tested pages for reference
|
|
4860
|
+
*/
|
|
4861
|
+
function findSimilarTestedPages(path, testedPages) {
|
|
4862
|
+
const segments = path.split("/").filter(Boolean);
|
|
4863
|
+
if (segments.length === 0)
|
|
4864
|
+
return [];
|
|
4865
|
+
const similar = [];
|
|
4866
|
+
const prefix = "/" + segments[0];
|
|
4867
|
+
for (const tested of testedPages) {
|
|
4868
|
+
if (tested.path.startsWith(prefix) && tested.path !== path) {
|
|
4869
|
+
similar.push(tested.path);
|
|
4870
|
+
if (similar.length >= 3)
|
|
4871
|
+
break;
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
return similar;
|
|
4875
|
+
}
|
|
4876
|
+
/**
|
|
4877
|
+
* Calculate overall coverage analysis
|
|
4878
|
+
*/
|
|
4879
|
+
function calculateCoverageAnalysis(sitePages, testedPages) {
|
|
4880
|
+
const testedPaths = new Set(testedPages.map(p => p.path));
|
|
4881
|
+
// Section coverage
|
|
4882
|
+
const sections = {};
|
|
4883
|
+
for (const page of sitePages) {
|
|
4884
|
+
const segments = page.path.split("/").filter(Boolean);
|
|
4885
|
+
const section = segments.length > 0 ? "/" + segments[0] : "/";
|
|
4886
|
+
if (!sections[section]) {
|
|
4887
|
+
sections[section] = { total: 0, tested: 0 };
|
|
4888
|
+
}
|
|
4889
|
+
sections[section].total++;
|
|
4890
|
+
if (testedPaths.has(page.path)) {
|
|
4891
|
+
sections[section].tested++;
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
const sectionCoverage = {};
|
|
4895
|
+
for (const [section, data] of Object.entries(sections)) {
|
|
4896
|
+
sectionCoverage[section] = {
|
|
4897
|
+
...data,
|
|
4898
|
+
percent: data.total > 0 ? Math.round((data.tested / data.total) * 100) : 0,
|
|
4899
|
+
};
|
|
4900
|
+
}
|
|
4901
|
+
const totalPages = sitePages.length;
|
|
4902
|
+
const testedCount = sitePages.filter(p => testedPaths.has(p.path)).length;
|
|
4903
|
+
return {
|
|
4904
|
+
totalPages,
|
|
4905
|
+
testedPages: testedCount,
|
|
4906
|
+
untestedPages: totalPages - testedCount,
|
|
4907
|
+
coveragePercent: totalPages > 0 ? Math.round((testedCount / totalPages) * 100) : 0,
|
|
4908
|
+
sectionCoverage,
|
|
4909
|
+
};
|
|
4910
|
+
}
|
|
4911
|
+
/**
|
|
4912
|
+
* Generate complete coverage map
|
|
4913
|
+
*/
|
|
4914
|
+
async function generateCoverageMap(baseUrl, testFiles, options = {}) {
|
|
4915
|
+
const startTime = Date.now();
|
|
4916
|
+
// Parse test files
|
|
4917
|
+
const testedPages = parseTestFilesForCoverage(testFiles);
|
|
4918
|
+
// Get site pages
|
|
4919
|
+
let sitePages;
|
|
4920
|
+
if (options.sitemapUrl) {
|
|
4921
|
+
sitePages = await parseSitemap(options.sitemapUrl);
|
|
4922
|
+
}
|
|
4923
|
+
else {
|
|
4924
|
+
sitePages = await crawlSiteForCoverage(baseUrl, options.maxPages || 100, options.includePattern, options.excludePattern);
|
|
4925
|
+
}
|
|
4926
|
+
// Identify gaps
|
|
4927
|
+
const gaps = identifyCoverageGaps(sitePages, testedPages, options.minCoverage || 50);
|
|
4928
|
+
// Calculate analysis
|
|
4929
|
+
const analysis = calculateCoverageAnalysis(sitePages, testedPages);
|
|
4930
|
+
// Generate recommendations
|
|
4931
|
+
const recommendations = [];
|
|
4932
|
+
if (analysis.coveragePercent < 50) {
|
|
4933
|
+
recommendations.push("Coverage is below 50% - prioritize testing critical paths");
|
|
4934
|
+
}
|
|
4935
|
+
const criticalGaps = gaps.filter(g => g.priority === "critical");
|
|
4936
|
+
if (criticalGaps.length > 0) {
|
|
4937
|
+
recommendations.push(`${criticalGaps.length} critical pages have no tests (checkout, auth, etc.)`);
|
|
4938
|
+
}
|
|
4939
|
+
const lowCoverageSections = Object.entries(analysis.sectionCoverage)
|
|
4940
|
+
.filter(([_, data]) => data.percent < 30 && data.total > 2)
|
|
4941
|
+
.map(([section]) => section);
|
|
4942
|
+
if (lowCoverageSections.length > 0) {
|
|
4943
|
+
recommendations.push(`Sections with low coverage: ${lowCoverageSections.join(", ")}`);
|
|
4944
|
+
}
|
|
4945
|
+
if (gaps.filter(g => g.reason === "no-verifications").length > 3) {
|
|
4946
|
+
recommendations.push("Many tests lack assertions - add verification steps");
|
|
4947
|
+
}
|
|
4948
|
+
return {
|
|
4949
|
+
baseUrl,
|
|
4950
|
+
timestamp: new Date().toISOString(),
|
|
4951
|
+
duration: Date.now() - startTime,
|
|
4952
|
+
testFiles,
|
|
4953
|
+
sitePages,
|
|
4954
|
+
testedPages,
|
|
4955
|
+
gaps,
|
|
4956
|
+
analysis,
|
|
4957
|
+
recommendations,
|
|
4958
|
+
};
|
|
4959
|
+
}
|
|
4960
|
+
/**
|
|
4961
|
+
* Format coverage map as text report
|
|
4962
|
+
*/
|
|
4963
|
+
function formatCoverageReport(result) {
|
|
4964
|
+
const lines = [];
|
|
4965
|
+
lines.push("╔══════════════════════════════════════════════════════════════════════════════╗");
|
|
4966
|
+
lines.push("║ TEST COVERAGE MAP REPORT ║");
|
|
4967
|
+
lines.push("╚══════════════════════════════════════════════════════════════════════════════╝");
|
|
4968
|
+
lines.push("");
|
|
4969
|
+
lines.push(`📊 Site: ${result.baseUrl}`);
|
|
4970
|
+
lines.push(`📅 Generated: ${result.timestamp}`);
|
|
4971
|
+
lines.push(`⏱️ Analysis time: ${(result.duration / 1000).toFixed(1)}s`);
|
|
4972
|
+
lines.push(`📝 Test files analyzed: ${result.testFiles.length}`);
|
|
4973
|
+
lines.push("");
|
|
4974
|
+
// Overall coverage
|
|
4975
|
+
lines.push("═══════════════════════════════════════════════════════════════════════════════");
|
|
4976
|
+
lines.push("📈 OVERALL COVERAGE");
|
|
4977
|
+
lines.push("═══════════════════════════════════════════════════════════════════════════════");
|
|
4978
|
+
lines.push("");
|
|
4979
|
+
const { analysis } = result;
|
|
4980
|
+
const coverageBar = generateCoverageProgressBar(analysis.coveragePercent);
|
|
4981
|
+
lines.push(` Coverage: ${coverageBar} ${analysis.coveragePercent}%`);
|
|
4982
|
+
lines.push("");
|
|
4983
|
+
lines.push(` Total pages: ${analysis.totalPages}`);
|
|
4984
|
+
lines.push(` Tested pages: ${analysis.testedPages}`);
|
|
4985
|
+
lines.push(` Untested pages: ${analysis.untestedPages}`);
|
|
4986
|
+
lines.push("");
|
|
4987
|
+
// Section coverage
|
|
4988
|
+
lines.push("───────────────────────────────────────────────────────────────────────────────");
|
|
4989
|
+
lines.push("📁 COVERAGE BY SECTION");
|
|
4990
|
+
lines.push("───────────────────────────────────────────────────────────────────────────────");
|
|
4991
|
+
lines.push("");
|
|
4992
|
+
const sections = Object.entries(analysis.sectionCoverage)
|
|
4993
|
+
.sort((a, b) => b[1].total - a[1].total);
|
|
4994
|
+
for (const [section, data] of sections) {
|
|
4995
|
+
const bar = generateCoverageProgressBar(data.percent, 20);
|
|
4996
|
+
const status = data.percent >= 70 ? "✅" : data.percent >= 40 ? "⚠️" : "❌";
|
|
4997
|
+
lines.push(` ${status} ${section.padEnd(20)} ${bar} ${data.tested}/${data.total} (${data.percent}%)`);
|
|
4998
|
+
}
|
|
4999
|
+
lines.push("");
|
|
5000
|
+
// Coverage gaps
|
|
5001
|
+
if (result.gaps.length > 0) {
|
|
5002
|
+
lines.push("═══════════════════════════════════════════════════════════════════════════════");
|
|
5003
|
+
lines.push("🕳️ COVERAGE GAPS");
|
|
5004
|
+
lines.push("═══════════════════════════════════════════════════════════════════════════════");
|
|
5005
|
+
lines.push("");
|
|
5006
|
+
const priorityEmoji = { critical: "🚨", high: "🔴", medium: "🟡", low: "🟢" };
|
|
5007
|
+
for (const gap of result.gaps.slice(0, 15)) {
|
|
5008
|
+
const emoji = priorityEmoji[gap.priority];
|
|
5009
|
+
lines.push(` ${emoji} ${gap.page.path}`);
|
|
5010
|
+
lines.push(` Reason: ${gap.reason} | Priority: ${gap.priority}`);
|
|
5011
|
+
if (gap.suggestedTests.length > 0) {
|
|
5012
|
+
lines.push(` Suggested: ${gap.suggestedTests[0]}`);
|
|
5013
|
+
}
|
|
5014
|
+
lines.push("");
|
|
5015
|
+
}
|
|
5016
|
+
if (result.gaps.length > 15) {
|
|
5017
|
+
lines.push(` ... and ${result.gaps.length - 15} more gaps`);
|
|
5018
|
+
lines.push("");
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
// Recommendations
|
|
5022
|
+
if (result.recommendations.length > 0) {
|
|
5023
|
+
lines.push("═══════════════════════════════════════════════════════════════════════════════");
|
|
5024
|
+
lines.push("💡 RECOMMENDATIONS");
|
|
5025
|
+
lines.push("═══════════════════════════════════════════════════════════════════════════════");
|
|
5026
|
+
lines.push("");
|
|
5027
|
+
for (const rec of result.recommendations) {
|
|
5028
|
+
lines.push(` ${rec}`);
|
|
5029
|
+
}
|
|
5030
|
+
lines.push("");
|
|
5031
|
+
}
|
|
5032
|
+
// Tested pages summary
|
|
5033
|
+
lines.push("───────────────────────────────────────────────────────────────────────────────");
|
|
5034
|
+
lines.push("✅ TESTED PAGES (Top 10 by coverage)");
|
|
5035
|
+
lines.push("───────────────────────────────────────────────────────────────────────────────");
|
|
5036
|
+
lines.push("");
|
|
5037
|
+
const topTested = [...result.testedPages]
|
|
5038
|
+
.sort((a, b) => b.coverageScore - a.coverageScore)
|
|
5039
|
+
.slice(0, 10);
|
|
5040
|
+
for (const page of topTested) {
|
|
5041
|
+
const bar = generateCoverageProgressBar(page.coverageScore, 15);
|
|
5042
|
+
lines.push(` ${bar} ${page.coverageScore}% ${page.path}`);
|
|
5043
|
+
lines.push(` Actions: ${page.actions.length} | Tests: ${page.testCount}`);
|
|
5044
|
+
}
|
|
5045
|
+
return lines.join("\n");
|
|
5046
|
+
}
|
|
5047
|
+
/**
|
|
5048
|
+
* Generate HTML coverage report
|
|
5049
|
+
*/
|
|
5050
|
+
function generateCoverageHtmlReport(result) {
|
|
5051
|
+
const { analysis, gaps, testedPages } = result;
|
|
5052
|
+
const coverageColor = analysis.coveragePercent >= 70 ? "#22c55e" :
|
|
5053
|
+
analysis.coveragePercent >= 40 ? "#eab308" : "#ef4444";
|
|
5054
|
+
return `<!DOCTYPE html>
|
|
5055
|
+
<html lang="en">
|
|
5056
|
+
<head>
|
|
5057
|
+
<meta charset="UTF-8">
|
|
5058
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5059
|
+
<title>Test Coverage Map - ${result.baseUrl}</title>
|
|
5060
|
+
<style>
|
|
5061
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
5062
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; padding: 2rem; }
|
|
5063
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
5064
|
+
h1 { color: #fff; margin-bottom: 0.5rem; }
|
|
5065
|
+
.subtitle { color: #888; margin-bottom: 2rem; }
|
|
5066
|
+
.card { background: #252540; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
|
|
5067
|
+
.card h2 { color: #fff; font-size: 1.1rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
|
|
5068
|
+
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; }
|
|
5069
|
+
.stat { text-align: center; }
|
|
5070
|
+
.stat-value { font-size: 2rem; font-weight: bold; color: ${coverageColor}; }
|
|
5071
|
+
.stat-label { color: #888; font-size: 0.875rem; }
|
|
5072
|
+
.progress-bar { height: 8px; background: #333; border-radius: 4px; overflow: hidden; margin: 1rem 0; }
|
|
5073
|
+
.progress-fill { height: 100%; background: ${coverageColor}; transition: width 0.5s; }
|
|
5074
|
+
.section-list { list-style: none; }
|
|
5075
|
+
.section-item { display: flex; align-items: center; gap: 1rem; padding: 0.75rem 0; border-bottom: 1px solid #333; }
|
|
5076
|
+
.section-name { flex: 1; }
|
|
5077
|
+
.section-bar { width: 150px; height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
|
|
5078
|
+
.section-bar-fill { height: 100%; border-radius: 3px; }
|
|
5079
|
+
.section-percent { width: 60px; text-align: right; font-weight: 500; }
|
|
5080
|
+
.gap-list { list-style: none; }
|
|
5081
|
+
.gap-item { padding: 1rem; margin-bottom: 0.75rem; background: #1a1a2e; border-radius: 8px; border-left: 4px solid; }
|
|
5082
|
+
.gap-critical { border-color: #ef4444; }
|
|
5083
|
+
.gap-high { border-color: #f97316; }
|
|
5084
|
+
.gap-medium { border-color: #eab308; }
|
|
5085
|
+
.gap-low { border-color: #22c55e; }
|
|
5086
|
+
.gap-path { font-weight: 600; color: #fff; }
|
|
5087
|
+
.gap-reason { color: #888; font-size: 0.875rem; margin-top: 0.25rem; }
|
|
5088
|
+
.badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
|
|
5089
|
+
.badge-critical { background: #ef4444; color: #fff; }
|
|
5090
|
+
.badge-high { background: #f97316; color: #fff; }
|
|
5091
|
+
.badge-medium { background: #eab308; color: #000; }
|
|
5092
|
+
.badge-low { background: #22c55e; color: #fff; }
|
|
5093
|
+
.recommendations { list-style: none; }
|
|
5094
|
+
.recommendations li { padding: 0.75rem; background: #1a1a2e; border-radius: 6px; margin-bottom: 0.5rem; }
|
|
5095
|
+
.page-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; }
|
|
5096
|
+
.page-card { background: #1a1a2e; border-radius: 8px; padding: 1rem; }
|
|
5097
|
+
.page-score { font-size: 1.5rem; font-weight: bold; }
|
|
5098
|
+
.page-path { color: #888; font-size: 0.875rem; word-break: break-all; }
|
|
5099
|
+
</style>
|
|
5100
|
+
</head>
|
|
5101
|
+
<body>
|
|
5102
|
+
<div class="container">
|
|
5103
|
+
<h1>Test Coverage Map</h1>
|
|
5104
|
+
<p class="subtitle">${result.baseUrl} | Generated ${new Date(result.timestamp).toLocaleString()}</p>
|
|
5105
|
+
|
|
5106
|
+
<div class="card">
|
|
5107
|
+
<h2>📊 Overall Coverage</h2>
|
|
5108
|
+
<div class="stats">
|
|
5109
|
+
<div class="stat">
|
|
5110
|
+
<div class="stat-value">${analysis.coveragePercent}%</div>
|
|
5111
|
+
<div class="stat-label">Coverage</div>
|
|
5112
|
+
</div>
|
|
5113
|
+
<div class="stat">
|
|
5114
|
+
<div class="stat-value">${analysis.totalPages}</div>
|
|
5115
|
+
<div class="stat-label">Total Pages</div>
|
|
5116
|
+
</div>
|
|
5117
|
+
<div class="stat">
|
|
5118
|
+
<div class="stat-value">${analysis.testedPages}</div>
|
|
5119
|
+
<div class="stat-label">Tested</div>
|
|
5120
|
+
</div>
|
|
5121
|
+
<div class="stat">
|
|
5122
|
+
<div class="stat-value">${analysis.untestedPages}</div>
|
|
5123
|
+
<div class="stat-label">Untested</div>
|
|
5124
|
+
</div>
|
|
5125
|
+
</div>
|
|
5126
|
+
<div class="progress-bar">
|
|
5127
|
+
<div class="progress-fill" style="width: ${analysis.coveragePercent}%"></div>
|
|
5128
|
+
</div>
|
|
5129
|
+
</div>
|
|
5130
|
+
|
|
5131
|
+
<div class="card">
|
|
5132
|
+
<h2>📁 Coverage by Section</h2>
|
|
5133
|
+
<ul class="section-list">
|
|
5134
|
+
${Object.entries(analysis.sectionCoverage)
|
|
5135
|
+
.sort((a, b) => b[1].total - a[1].total)
|
|
5136
|
+
.map(([section, data]) => {
|
|
5137
|
+
const color = data.percent >= 70 ? "#22c55e" : data.percent >= 40 ? "#eab308" : "#ef4444";
|
|
5138
|
+
return `
|
|
5139
|
+
<li class="section-item">
|
|
5140
|
+
<span class="section-name">${section}</span>
|
|
5141
|
+
<div class="section-bar">
|
|
5142
|
+
<div class="section-bar-fill" style="width: ${data.percent}%; background: ${color}"></div>
|
|
5143
|
+
</div>
|
|
5144
|
+
<span class="section-percent" style="color: ${color}">${data.percent}%</span>
|
|
5145
|
+
<span style="color: #666; font-size: 0.875rem">${data.tested}/${data.total}</span>
|
|
5146
|
+
</li>
|
|
5147
|
+
`;
|
|
5148
|
+
}).join("")}
|
|
5149
|
+
</ul>
|
|
5150
|
+
</div>
|
|
5151
|
+
|
|
5152
|
+
${gaps.length > 0 ? `
|
|
5153
|
+
<div class="card">
|
|
5154
|
+
<h2>🕳️ Coverage Gaps (${gaps.length})</h2>
|
|
5155
|
+
<ul class="gap-list">
|
|
5156
|
+
${gaps.slice(0, 20).map(gap => `
|
|
5157
|
+
<li class="gap-item gap-${gap.priority}">
|
|
5158
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
5159
|
+
<span class="gap-path">${gap.page.path}</span>
|
|
5160
|
+
<span class="badge badge-${gap.priority}">${gap.priority}</span>
|
|
5161
|
+
</div>
|
|
5162
|
+
<div class="gap-reason">Reason: ${gap.reason}</div>
|
|
5163
|
+
</li>
|
|
5164
|
+
`).join("")}
|
|
5165
|
+
</ul>
|
|
5166
|
+
${gaps.length > 20 ? `<p style="color: #666; text-align: center;">...and ${gaps.length - 20} more gaps</p>` : ""}
|
|
5167
|
+
</div>
|
|
5168
|
+
` : ""}
|
|
5169
|
+
|
|
5170
|
+
${result.recommendations.length > 0 ? `
|
|
5171
|
+
<div class="card">
|
|
5172
|
+
<h2>💡 Recommendations</h2>
|
|
5173
|
+
<ul class="recommendations">
|
|
5174
|
+
${result.recommendations.map(rec => `<li>${rec}</li>`).join("")}
|
|
5175
|
+
</ul>
|
|
5176
|
+
</div>
|
|
5177
|
+
` : ""}
|
|
5178
|
+
|
|
5179
|
+
<div class="card">
|
|
5180
|
+
<h2>✅ Tested Pages (Top 12)</h2>
|
|
5181
|
+
<div class="page-grid">
|
|
5182
|
+
${testedPages
|
|
5183
|
+
.sort((a, b) => b.coverageScore - a.coverageScore)
|
|
5184
|
+
.slice(0, 12)
|
|
5185
|
+
.map(page => {
|
|
5186
|
+
const color = page.coverageScore >= 70 ? "#22c55e" : page.coverageScore >= 40 ? "#eab308" : "#ef4444";
|
|
5187
|
+
return `
|
|
5188
|
+
<div class="page-card">
|
|
5189
|
+
<div class="page-score" style="color: ${color}">${page.coverageScore}%</div>
|
|
5190
|
+
<div class="page-path">${page.path}</div>
|
|
5191
|
+
<div style="color: #666; font-size: 0.75rem; margin-top: 0.5rem;">
|
|
5192
|
+
${page.actions.length} actions | ${page.testCount} test(s)
|
|
5193
|
+
</div>
|
|
5194
|
+
</div>
|
|
5195
|
+
`;
|
|
5196
|
+
}).join("")}
|
|
5197
|
+
</div>
|
|
5198
|
+
</div>
|
|
5199
|
+
|
|
5200
|
+
<footer style="text-align: center; color: #666; margin-top: 2rem; font-size: 0.875rem;">
|
|
5201
|
+
Generated by CBrowser v6.5.0 | Analysis took ${(result.duration / 1000).toFixed(1)}s
|
|
5202
|
+
</footer>
|
|
5203
|
+
</div>
|
|
5204
|
+
</body>
|
|
5205
|
+
</html>`;
|
|
5206
|
+
}
|
|
5207
|
+
/**
|
|
5208
|
+
* Generate a text progress bar for coverage
|
|
5209
|
+
*/
|
|
5210
|
+
function generateCoverageProgressBar(percent, width = 30) {
|
|
5211
|
+
const filled = Math.round((percent / 100) * width);
|
|
5212
|
+
const empty = width - filled;
|
|
5213
|
+
return "█".repeat(filled) + "░".repeat(empty);
|
|
5214
|
+
}
|
|
4144
5215
|
//# sourceMappingURL=browser.js.map
|