cbrowser 6.1.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.js CHANGED
@@ -21,6 +21,12 @@ exports.parseNLTestSuite = parseNLTestSuite;
21
21
  exports.runNLTestSuite = runNLTestSuite;
22
22
  exports.formatNLTestReport = formatNLTestReport;
23
23
  exports.runNLTestFile = runNLTestFile;
24
+ exports.repairTest = repairTest;
25
+ exports.repairTestSuite = repairTestSuite;
26
+ exports.formatRepairReport = formatRepairReport;
27
+ exports.exportRepairedTest = exportRepairedTest;
28
+ exports.detectFlakyTests = detectFlakyTests;
29
+ exports.formatFlakyTestReport = formatFlakyTestReport;
24
30
  const playwright_1 = require("playwright");
25
31
  const fs_1 = require("fs");
26
32
  const path_1 = require("path");
@@ -3297,4 +3303,842 @@ async function runNLTestFile(filepath, options = {}) {
3297
3303
  const suite = parseNLTestSuite(content, suiteName);
3298
3304
  return runNLTestSuite(suite, options);
3299
3305
  }
3306
+ // =========================================================================
3307
+ // AI Test Repair (v6.2.0)
3308
+ // =========================================================================
3309
+ /**
3310
+ * Classify the type of failure from an error message.
3311
+ */
3312
+ function classifyFailure(error, step) {
3313
+ const lowerError = error.toLowerCase();
3314
+ if (lowerError.includes("not found") || lowerError.includes("no element") || lowerError.includes("failed to click")) {
3315
+ return "selector_not_found";
3316
+ }
3317
+ if (lowerError.includes("assertion") || lowerError.includes("verify") || lowerError.includes("expected")) {
3318
+ return "assertion_failed";
3319
+ }
3320
+ if (lowerError.includes("timeout") || lowerError.includes("timed out")) {
3321
+ return "timeout";
3322
+ }
3323
+ if (lowerError.includes("navigation") || lowerError.includes("navigate") || lowerError.includes("url")) {
3324
+ return "navigation_failed";
3325
+ }
3326
+ if (lowerError.includes("not interactable") || lowerError.includes("disabled") || lowerError.includes("hidden")) {
3327
+ return "element_not_interactable";
3328
+ }
3329
+ return "unknown";
3330
+ }
3331
+ /**
3332
+ * Find alternative selectors for a target on the current page.
3333
+ */
3334
+ async function findAlternatives(browser, originalTarget) {
3335
+ const page = browser.page;
3336
+ if (!page)
3337
+ return [];
3338
+ try {
3339
+ const alternatives = await page.evaluate((target) => {
3340
+ const results = [];
3341
+ const lowerTarget = target.toLowerCase();
3342
+ // Find buttons with similar text
3343
+ document.querySelectorAll("button, [role='button'], input[type='submit'], input[type='button']").forEach((el) => {
3344
+ const text = el.textContent?.trim() || el.value || "";
3345
+ if (text && (text.toLowerCase().includes(lowerTarget) || lowerTarget.includes(text.toLowerCase()))) {
3346
+ results.push(`button: "${text}"`);
3347
+ }
3348
+ });
3349
+ // Find links with similar text
3350
+ document.querySelectorAll("a").forEach((el) => {
3351
+ const text = el.textContent?.trim() || "";
3352
+ if (text && (text.toLowerCase().includes(lowerTarget) || lowerTarget.includes(text.toLowerCase()))) {
3353
+ results.push(`link: "${text}"`);
3354
+ }
3355
+ });
3356
+ // Find inputs with similar labels/placeholders
3357
+ document.querySelectorAll("input, textarea, select").forEach((el) => {
3358
+ const input = el;
3359
+ const placeholder = input.placeholder || "";
3360
+ const label = document.querySelector(`label[for="${input.id}"]`)?.textContent?.trim() || "";
3361
+ const name = input.name || "";
3362
+ if (placeholder.toLowerCase().includes(lowerTarget) || lowerTarget.includes(placeholder.toLowerCase())) {
3363
+ results.push(`input with placeholder "${placeholder}"`);
3364
+ }
3365
+ if (label.toLowerCase().includes(lowerTarget) || lowerTarget.includes(label.toLowerCase())) {
3366
+ results.push(`input labeled "${label}"`);
3367
+ }
3368
+ if (name.toLowerCase().includes(lowerTarget)) {
3369
+ results.push(`input named "${name}"`);
3370
+ }
3371
+ });
3372
+ // Find elements by aria-label
3373
+ document.querySelectorAll("[aria-label]").forEach((el) => {
3374
+ const label = el.getAttribute("aria-label") || "";
3375
+ if (label.toLowerCase().includes(lowerTarget) || lowerTarget.includes(label.toLowerCase())) {
3376
+ results.push(`aria:${el.tagName.toLowerCase()}/"${label}"`);
3377
+ }
3378
+ });
3379
+ return [...new Set(results)].slice(0, 10);
3380
+ }, originalTarget);
3381
+ return alternatives;
3382
+ }
3383
+ catch {
3384
+ return [];
3385
+ }
3386
+ }
3387
+ /**
3388
+ * Get page context for failure analysis.
3389
+ */
3390
+ async function getPageContext(browser) {
3391
+ const page = browser.page;
3392
+ if (!page)
3393
+ return { url: "", title: "", visibleText: [] };
3394
+ try {
3395
+ const context = await page.evaluate(() => {
3396
+ const visibleText = [];
3397
+ // Get visible button/link text
3398
+ document.querySelectorAll("button, a, [role='button']").forEach((el) => {
3399
+ const text = el.textContent?.trim();
3400
+ if (text && text.length < 50) {
3401
+ visibleText.push(text);
3402
+ }
3403
+ });
3404
+ return {
3405
+ url: window.location.href,
3406
+ title: document.title,
3407
+ visibleText: [...new Set(visibleText)].slice(0, 20),
3408
+ };
3409
+ });
3410
+ return context;
3411
+ }
3412
+ catch {
3413
+ return { url: "", title: "", visibleText: [] };
3414
+ }
3415
+ }
3416
+ /**
3417
+ * Generate repair suggestions for a failed step.
3418
+ */
3419
+ async function generateRepairSuggestions(browser, step, error, failureType, alternatives, pageContext) {
3420
+ const suggestions = [];
3421
+ switch (failureType) {
3422
+ case "selector_not_found": {
3423
+ // Suggest alternative selectors
3424
+ for (const alt of alternatives.slice(0, 3)) {
3425
+ const newInstruction = step.instruction.replace(step.target || "", alt.replace(/^(button|link|input|aria):\s*/, "").replace(/"/g, "'"));
3426
+ suggestions.push({
3427
+ type: "selector_update",
3428
+ confidence: 0.7,
3429
+ description: `Update selector to "${alt}"`,
3430
+ originalInstruction: step.instruction,
3431
+ suggestedInstruction: `click ${alt.replace(/^(button|link|input|aria):\s*/, "")}`,
3432
+ reasoning: `Found similar element on page: ${alt}`,
3433
+ });
3434
+ }
3435
+ // Suggest adding a wait
3436
+ if (suggestions.length === 0) {
3437
+ suggestions.push({
3438
+ type: "add_wait",
3439
+ confidence: 0.5,
3440
+ description: "Add wait before this step",
3441
+ originalInstruction: step.instruction,
3442
+ suggestedInstruction: `wait 2 seconds\n${step.instruction}`,
3443
+ reasoning: "Element might not be loaded yet - adding wait may help",
3444
+ });
3445
+ }
3446
+ break;
3447
+ }
3448
+ case "assertion_failed": {
3449
+ // Suggest updating the assertion based on page content
3450
+ if (step.action === "assert" && pageContext.visibleText.length > 0) {
3451
+ const possibleText = pageContext.visibleText.find((t) => t.length > 3 && t.length < 30);
3452
+ if (possibleText) {
3453
+ suggestions.push({
3454
+ type: "assertion_update",
3455
+ confidence: 0.6,
3456
+ description: `Update assertion to check for visible text`,
3457
+ originalInstruction: step.instruction,
3458
+ suggestedInstruction: `verify page contains "${possibleText}"`,
3459
+ reasoning: `Page contains "${possibleText}" which might be the intended check`,
3460
+ });
3461
+ }
3462
+ }
3463
+ // Suggest checking URL instead
3464
+ if (pageContext.url) {
3465
+ const urlPath = new URL(pageContext.url).pathname;
3466
+ suggestions.push({
3467
+ type: "assertion_update",
3468
+ confidence: 0.5,
3469
+ description: "Assert URL instead of content",
3470
+ originalInstruction: step.instruction,
3471
+ suggestedInstruction: `verify url contains "${urlPath}"`,
3472
+ reasoning: `Current URL is ${pageContext.url}`,
3473
+ });
3474
+ }
3475
+ break;
3476
+ }
3477
+ case "timeout": {
3478
+ suggestions.push({
3479
+ type: "add_wait",
3480
+ confidence: 0.7,
3481
+ description: "Increase wait time",
3482
+ originalInstruction: step.instruction,
3483
+ suggestedInstruction: `wait 5 seconds\n${step.instruction}`,
3484
+ reasoning: "Operation timed out - page may need more time to load",
3485
+ });
3486
+ break;
3487
+ }
3488
+ case "element_not_interactable": {
3489
+ suggestions.push({
3490
+ type: "add_wait",
3491
+ confidence: 0.6,
3492
+ description: "Wait for element to become interactive",
3493
+ originalInstruction: step.instruction,
3494
+ suggestedInstruction: `wait 2 seconds\n${step.instruction}`,
3495
+ reasoning: "Element exists but is not interactable - may need to wait",
3496
+ });
3497
+ // Suggest scrolling
3498
+ suggestions.push({
3499
+ type: "change_action",
3500
+ confidence: 0.5,
3501
+ description: "Scroll element into view first",
3502
+ originalInstruction: step.instruction,
3503
+ suggestedInstruction: `scroll down\n${step.instruction}`,
3504
+ reasoning: "Element might be outside viewport",
3505
+ });
3506
+ break;
3507
+ }
3508
+ default: {
3509
+ // Generic suggestion to skip
3510
+ suggestions.push({
3511
+ type: "skip_step",
3512
+ confidence: 0.3,
3513
+ description: "Skip this step",
3514
+ originalInstruction: step.instruction,
3515
+ suggestedInstruction: `// SKIPPED: ${step.instruction}`,
3516
+ reasoning: "Unable to determine a fix - consider removing this step",
3517
+ });
3518
+ }
3519
+ }
3520
+ // Sort by confidence
3521
+ return suggestions.sort((a, b) => b.confidence - a.confidence);
3522
+ }
3523
+ /**
3524
+ * Analyze a failed test step and suggest repairs.
3525
+ */
3526
+ async function analyzeFailure(browser, step, error) {
3527
+ const failureType = classifyFailure(error, step);
3528
+ const alternatives = step.target ? await findAlternatives(browser, step.target) : [];
3529
+ const pageContext = await getPageContext(browser);
3530
+ const suggestions = await generateRepairSuggestions(browser, step, error, failureType, alternatives, pageContext);
3531
+ return {
3532
+ step,
3533
+ error,
3534
+ failureType,
3535
+ targetSelector: step.target,
3536
+ alternativeSelectors: alternatives,
3537
+ pageContext,
3538
+ suggestions,
3539
+ };
3540
+ }
3541
+ /**
3542
+ * Run a test, analyze failures, and suggest/apply repairs.
3543
+ */
3544
+ async function repairTest(test, options = {}) {
3545
+ const { headless = true, autoApply = false, verifyRepairs = true, maxRetries = 3, } = options;
3546
+ const browser = new CBrowser({ headless });
3547
+ const failureAnalyses = [];
3548
+ const repairedSteps = [];
3549
+ let failedSteps = 0;
3550
+ console.log(`\n🔧 Analyzing test: ${test.name}`);
3551
+ console.log(` Steps: ${test.steps.length}`);
3552
+ console.log(` Auto-apply: ${autoApply}`);
3553
+ console.log("");
3554
+ try {
3555
+ await browser.launch();
3556
+ for (const step of test.steps) {
3557
+ console.log(` → ${step.instruction}`);
3558
+ let stepPassed = false;
3559
+ let lastError = "";
3560
+ let attempts = 0;
3561
+ while (!stepPassed && attempts < maxRetries) {
3562
+ attempts++;
3563
+ try {
3564
+ // Execute the step
3565
+ switch (step.action) {
3566
+ case "navigate":
3567
+ await browser.navigate(step.target || "");
3568
+ stepPassed = true;
3569
+ break;
3570
+ case "click":
3571
+ const clickResult = await browser.smartClick(step.target || "");
3572
+ stepPassed = clickResult.success;
3573
+ if (!stepPassed)
3574
+ lastError = `Failed to click: ${step.target}`;
3575
+ break;
3576
+ case "fill":
3577
+ await browser.fill(step.target || "", step.value || "");
3578
+ stepPassed = true;
3579
+ break;
3580
+ case "assert":
3581
+ const assertResult = await browser.assert(step.instruction);
3582
+ stepPassed = assertResult.passed;
3583
+ if (!stepPassed)
3584
+ lastError = assertResult.message;
3585
+ break;
3586
+ case "wait":
3587
+ if (step.target) {
3588
+ const page = browser.page;
3589
+ if (page) {
3590
+ await page.waitForSelector(`text=${step.target}`, { timeout: 10000 });
3591
+ }
3592
+ }
3593
+ else {
3594
+ const ms = parseFloat(step.value || "1") * 1000;
3595
+ await new Promise((r) => setTimeout(r, ms));
3596
+ }
3597
+ stepPassed = true;
3598
+ break;
3599
+ case "scroll":
3600
+ const direction = step.target?.toLowerCase() === "up" ? -500 : 500;
3601
+ const page = browser.page;
3602
+ if (page) {
3603
+ await page.evaluate((d) => window.scrollBy(0, d), direction);
3604
+ }
3605
+ stepPassed = true;
3606
+ break;
3607
+ case "screenshot":
3608
+ await browser.screenshot();
3609
+ stepPassed = true;
3610
+ break;
3611
+ default:
3612
+ // Try as click
3613
+ const unknownResult = await browser.smartClick(step.target || step.instruction);
3614
+ stepPassed = unknownResult.success;
3615
+ if (!stepPassed)
3616
+ lastError = `Could not interpret: ${step.instruction}`;
3617
+ }
3618
+ }
3619
+ catch (e) {
3620
+ lastError = e.message;
3621
+ stepPassed = false;
3622
+ }
3623
+ if (!stepPassed && attempts < maxRetries) {
3624
+ console.log(` ⚠️ Attempt ${attempts} failed, retrying...`);
3625
+ await new Promise((r) => setTimeout(r, 500));
3626
+ }
3627
+ }
3628
+ if (stepPassed) {
3629
+ console.log(` ✓ Passed`);
3630
+ repairedSteps.push(step);
3631
+ }
3632
+ else {
3633
+ console.log(` ✗ Failed: ${lastError}`);
3634
+ failedSteps++;
3635
+ // Analyze the failure
3636
+ const analysis = await analyzeFailure(browser, step, lastError);
3637
+ failureAnalyses.push(analysis);
3638
+ if (autoApply && analysis.suggestions.length > 0) {
3639
+ const bestSuggestion = analysis.suggestions[0];
3640
+ console.log(` 🔧 Auto-applying: ${bestSuggestion.description}`);
3641
+ // Parse the suggested instruction into a step
3642
+ const repairedStep = parseNLInstruction(bestSuggestion.suggestedInstruction.split("\n").pop() || "");
3643
+ repairedSteps.push(repairedStep);
3644
+ }
3645
+ else {
3646
+ // Keep original step
3647
+ repairedSteps.push(step);
3648
+ if (analysis.suggestions.length > 0) {
3649
+ console.log(` 💡 Suggestions:`);
3650
+ for (const suggestion of analysis.suggestions.slice(0, 2)) {
3651
+ console.log(` - ${suggestion.description} (${Math.round(suggestion.confidence * 100)}%)`);
3652
+ console.log(` → ${suggestion.suggestedInstruction}`);
3653
+ }
3654
+ }
3655
+ }
3656
+ }
3657
+ }
3658
+ // Create repaired test
3659
+ const repairedTest = {
3660
+ name: test.name,
3661
+ description: test.description,
3662
+ steps: repairedSteps,
3663
+ };
3664
+ // Optionally verify the repaired test
3665
+ let repairedTestPasses;
3666
+ if (verifyRepairs && autoApply && failedSteps > 0) {
3667
+ console.log(`\n 🔄 Verifying repaired test...`);
3668
+ await browser.close();
3669
+ const verifyBrowser = new CBrowser({ headless });
3670
+ try {
3671
+ await verifyBrowser.launch();
3672
+ let allPassed = true;
3673
+ for (const step of repairedSteps) {
3674
+ try {
3675
+ switch (step.action) {
3676
+ case "navigate":
3677
+ await verifyBrowser.navigate(step.target || "");
3678
+ break;
3679
+ case "click":
3680
+ const result = await verifyBrowser.smartClick(step.target || "");
3681
+ if (!result.success)
3682
+ allPassed = false;
3683
+ break;
3684
+ case "fill":
3685
+ await verifyBrowser.fill(step.target || "", step.value || "");
3686
+ break;
3687
+ case "assert":
3688
+ const assertResult = await verifyBrowser.assert(step.instruction);
3689
+ if (!assertResult.passed)
3690
+ allPassed = false;
3691
+ break;
3692
+ case "wait":
3693
+ if (!step.target) {
3694
+ const ms = parseFloat(step.value || "1") * 1000;
3695
+ await new Promise((r) => setTimeout(r, ms));
3696
+ }
3697
+ break;
3698
+ }
3699
+ }
3700
+ catch {
3701
+ allPassed = false;
3702
+ }
3703
+ }
3704
+ repairedTestPasses = allPassed;
3705
+ console.log(` ${allPassed ? "✅" : "❌"} Repaired test ${allPassed ? "PASSES" : "still FAILS"}`);
3706
+ }
3707
+ finally {
3708
+ await verifyBrowser.close();
3709
+ }
3710
+ }
3711
+ return {
3712
+ originalTest: test,
3713
+ repairedTest: failedSteps > 0 ? repairedTest : undefined,
3714
+ failedSteps,
3715
+ repairedSteps: autoApply ? failedSteps : 0,
3716
+ failureAnalyses,
3717
+ repairedTestPasses,
3718
+ };
3719
+ }
3720
+ finally {
3721
+ await browser.close();
3722
+ }
3723
+ }
3724
+ /**
3725
+ * Run test repair on a full suite.
3726
+ */
3727
+ async function repairTestSuite(suite, options = {}) {
3728
+ const startTime = Date.now();
3729
+ const testResults = [];
3730
+ console.log(`\n🔧 Repairing Test Suite: ${suite.name}`);
3731
+ console.log(` Tests: ${suite.tests.length}`);
3732
+ console.log("");
3733
+ for (const test of suite.tests) {
3734
+ const result = await repairTest(test, options);
3735
+ testResults.push(result);
3736
+ }
3737
+ const testsWithFailures = testResults.filter((r) => r.failedSteps > 0).length;
3738
+ const testsRepaired = testResults.filter((r) => r.repairedSteps > 0).length;
3739
+ const totalFailedSteps = testResults.reduce((sum, r) => sum + r.failedSteps, 0);
3740
+ const totalRepairedSteps = testResults.reduce((sum, r) => sum + r.repairedSteps, 0);
3741
+ return {
3742
+ suiteName: suite.name,
3743
+ timestamp: new Date().toISOString(),
3744
+ duration: Date.now() - startTime,
3745
+ testResults,
3746
+ summary: {
3747
+ totalTests: suite.tests.length,
3748
+ testsWithFailures,
3749
+ testsRepaired,
3750
+ totalFailedSteps,
3751
+ totalRepairedSteps,
3752
+ repairSuccessRate: totalFailedSteps > 0 ? (totalRepairedSteps / totalFailedSteps) * 100 : 100,
3753
+ },
3754
+ };
3755
+ }
3756
+ /**
3757
+ * Format a repair result as a report.
3758
+ */
3759
+ function formatRepairReport(result) {
3760
+ const lines = [];
3761
+ lines.push("");
3762
+ lines.push("╔══════════════════════════════════════════════════════════════════════════════╗");
3763
+ lines.push("║ AI TEST REPAIR REPORT ║");
3764
+ lines.push("╚══════════════════════════════════════════════════════════════════════════════╝");
3765
+ lines.push("");
3766
+ lines.push(`📋 Suite: ${result.suiteName}`);
3767
+ lines.push(`⏱️ Duration: ${(result.duration / 1000).toFixed(1)}s`);
3768
+ lines.push(`📅 Timestamp: ${result.timestamp}`);
3769
+ lines.push("");
3770
+ // Summary
3771
+ lines.push("📊 SUMMARY");
3772
+ lines.push("─".repeat(60));
3773
+ lines.push(` Total Tests: ${result.summary.totalTests}`);
3774
+ lines.push(` Tests with Failures: ${result.summary.testsWithFailures}`);
3775
+ lines.push(` Tests Repaired: ${result.summary.testsRepaired}`);
3776
+ lines.push(` Total Failed Steps: ${result.summary.totalFailedSteps}`);
3777
+ lines.push(` Total Repaired Steps: ${result.summary.totalRepairedSteps}`);
3778
+ lines.push(` Repair Success Rate: ${result.summary.repairSuccessRate.toFixed(0)}%`);
3779
+ lines.push("");
3780
+ // Per-test details
3781
+ for (const testResult of result.testResults) {
3782
+ if (testResult.failedSteps === 0)
3783
+ continue;
3784
+ lines.push(`\n🔧 ${testResult.originalTest.name}`);
3785
+ lines.push(` Failed Steps: ${testResult.failedSteps}`);
3786
+ lines.push(` Repaired: ${testResult.repairedSteps}`);
3787
+ for (const analysis of testResult.failureAnalyses) {
3788
+ lines.push(`\n ❌ ${analysis.step.instruction}`);
3789
+ lines.push(` Error: ${analysis.error}`);
3790
+ lines.push(` Type: ${analysis.failureType}`);
3791
+ if (analysis.suggestions.length > 0) {
3792
+ lines.push(` 💡 Suggestions:`);
3793
+ for (const s of analysis.suggestions.slice(0, 2)) {
3794
+ lines.push(` - ${s.description} (${Math.round(s.confidence * 100)}%)`);
3795
+ lines.push(` → ${s.suggestedInstruction}`);
3796
+ }
3797
+ }
3798
+ }
3799
+ if (testResult.repairedTestPasses !== undefined) {
3800
+ lines.push(`\n ${testResult.repairedTestPasses ? "✅" : "❌"} Repaired test ${testResult.repairedTestPasses ? "PASSES" : "still FAILS"}`);
3801
+ }
3802
+ }
3803
+ lines.push("");
3804
+ return lines.join("\n");
3805
+ }
3806
+ /**
3807
+ * Export repaired test to file.
3808
+ */
3809
+ function exportRepairedTest(result) {
3810
+ if (!result.repairedTest) {
3811
+ return `# Test: ${result.originalTest.name}\n# No repairs needed\n${result.originalTest.steps.map((s) => s.instruction).join("\n")}`;
3812
+ }
3813
+ const lines = [];
3814
+ lines.push(`# Test: ${result.repairedTest.name} (Repaired)`);
3815
+ lines.push(`# Original failures: ${result.failedSteps}`);
3816
+ lines.push(`# Repairs applied: ${result.repairedSteps}`);
3817
+ lines.push("");
3818
+ for (const step of result.repairedTest.steps) {
3819
+ lines.push(step.instruction);
3820
+ }
3821
+ return lines.join("\n");
3822
+ }
3823
+ /**
3824
+ * Calculate flakiness score.
3825
+ * 0 = completely stable (all same result)
3826
+ * 100 = maximally flaky (50% pass, 50% fail)
3827
+ */
3828
+ function calculateFlakinessScore(passCount, failCount) {
3829
+ const total = passCount + failCount;
3830
+ if (total === 0)
3831
+ return 0;
3832
+ const passRate = passCount / total;
3833
+ // Flakiness is maximized at 50% pass rate
3834
+ // Score = 1 - |passRate - 0.5| * 2, scaled to 0-100
3835
+ const flakiness = (1 - Math.abs(passRate - 0.5) * 2) * 100;
3836
+ return Math.round(flakiness);
3837
+ }
3838
+ /**
3839
+ * Classify a test based on its results.
3840
+ */
3841
+ function classifyTest(passCount, failCount) {
3842
+ const total = passCount + failCount;
3843
+ if (total === 0)
3844
+ return "stable_pass";
3845
+ const passRate = passCount / total;
3846
+ if (passRate === 1)
3847
+ return "stable_pass";
3848
+ if (passRate === 0)
3849
+ return "stable_fail";
3850
+ if (passRate >= 0.8)
3851
+ return "mostly_pass";
3852
+ if (passRate <= 0.2)
3853
+ return "mostly_fail";
3854
+ return "flaky";
3855
+ }
3856
+ /**
3857
+ * Run a single test once and return the result.
3858
+ */
3859
+ async function runTestOnce(test, runNumber, headless) {
3860
+ const browser = new CBrowser({ headless });
3861
+ const stepResults = [];
3862
+ let testPassed = true;
3863
+ let testError;
3864
+ const startTime = Date.now();
3865
+ try {
3866
+ await browser.launch();
3867
+ for (const step of test.steps) {
3868
+ let stepPassed = true;
3869
+ let stepError;
3870
+ try {
3871
+ switch (step.action) {
3872
+ case "navigate":
3873
+ await browser.navigate(step.target || "");
3874
+ break;
3875
+ case "click":
3876
+ const clickResult = await browser.smartClick(step.target || "");
3877
+ if (!clickResult.success) {
3878
+ stepPassed = false;
3879
+ stepError = `Failed to click: ${step.target}`;
3880
+ }
3881
+ break;
3882
+ case "fill":
3883
+ await browser.fill(step.target || "", step.value || "");
3884
+ break;
3885
+ case "assert":
3886
+ const assertResult = await browser.assert(step.instruction);
3887
+ stepPassed = assertResult.passed;
3888
+ if (!assertResult.passed) {
3889
+ stepError = assertResult.message;
3890
+ }
3891
+ break;
3892
+ case "wait":
3893
+ if (!step.target) {
3894
+ const ms = parseFloat(step.value || "1") * 1000;
3895
+ await new Promise((r) => setTimeout(r, ms));
3896
+ }
3897
+ else {
3898
+ const page = browser.page;
3899
+ if (page) {
3900
+ await page.waitForSelector(`text=${step.target}`, { timeout: 10000 });
3901
+ }
3902
+ }
3903
+ break;
3904
+ case "scroll":
3905
+ const direction = step.target?.toLowerCase() === "up" ? -500 : 500;
3906
+ const page = browser.page;
3907
+ if (page) {
3908
+ await page.evaluate((d) => window.scrollBy(0, d), direction);
3909
+ }
3910
+ break;
3911
+ case "screenshot":
3912
+ await browser.screenshot();
3913
+ break;
3914
+ default:
3915
+ const unknownResult = await browser.smartClick(step.target || step.instruction);
3916
+ if (!unknownResult.success) {
3917
+ stepPassed = false;
3918
+ stepError = `Could not interpret: ${step.instruction}`;
3919
+ }
3920
+ }
3921
+ }
3922
+ catch (e) {
3923
+ stepPassed = false;
3924
+ stepError = e.message;
3925
+ }
3926
+ stepResults.push({
3927
+ instruction: step.instruction,
3928
+ passed: stepPassed,
3929
+ error: stepError,
3930
+ });
3931
+ if (!stepPassed) {
3932
+ testPassed = false;
3933
+ testError = testError || stepError;
3934
+ }
3935
+ }
3936
+ }
3937
+ catch (e) {
3938
+ testPassed = false;
3939
+ testError = e.message;
3940
+ }
3941
+ finally {
3942
+ await browser.close();
3943
+ }
3944
+ return {
3945
+ runNumber,
3946
+ passed: testPassed,
3947
+ duration: Date.now() - startTime,
3948
+ error: testError,
3949
+ stepResults,
3950
+ };
3951
+ }
3952
+ /**
3953
+ * Analyze a test for flakiness by running it multiple times.
3954
+ */
3955
+ async function analyzeTestFlakiness(test, options) {
3956
+ const { runs = 5, headless = true, flakinessThreshold = 20, delayBetweenRuns = 500, } = options;
3957
+ const testRuns = [];
3958
+ console.log(`\n 🔄 Running ${runs} times...`);
3959
+ for (let i = 1; i <= runs; i++) {
3960
+ const result = await runTestOnce(test, i, headless);
3961
+ testRuns.push(result);
3962
+ const icon = result.passed ? "✓" : "✗";
3963
+ console.log(` Run ${i}: ${icon} (${result.duration}ms)`);
3964
+ if (i < runs && delayBetweenRuns > 0) {
3965
+ await new Promise((r) => setTimeout(r, delayBetweenRuns));
3966
+ }
3967
+ }
3968
+ // Calculate overall stats
3969
+ const passCount = testRuns.filter((r) => r.passed).length;
3970
+ const failCount = testRuns.filter((r) => !r.passed).length;
3971
+ const flakinessScore = calculateFlakinessScore(passCount, failCount);
3972
+ const classification = classifyTest(passCount, failCount);
3973
+ const isFlaky = flakinessScore >= flakinessThreshold;
3974
+ // Calculate duration stats
3975
+ const durations = testRuns.map((r) => r.duration);
3976
+ const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
3977
+ const variance = Math.sqrt(durations.reduce((sum, d) => sum + Math.pow(d - avgDuration, 2), 0) / durations.length);
3978
+ // Analyze per-step flakiness
3979
+ const stepAnalysis = [];
3980
+ for (let stepIdx = 0; stepIdx < test.steps.length; stepIdx++) {
3981
+ const step = test.steps[stepIdx];
3982
+ const stepResults = testRuns.map((r) => r.stepResults[stepIdx]).filter(Boolean);
3983
+ const stepPassCount = stepResults.filter((s) => s?.passed).length;
3984
+ const stepFailCount = stepResults.filter((s) => !s?.passed).length;
3985
+ const stepFlakinessScore = calculateFlakinessScore(stepPassCount, stepFailCount);
3986
+ const stepErrors = [...new Set(stepResults.filter((s) => s?.error).map((s) => s.error))];
3987
+ stepAnalysis.push({
3988
+ instruction: step.instruction,
3989
+ passCount: stepPassCount,
3990
+ failCount: stepFailCount,
3991
+ flakinessScore: stepFlakinessScore,
3992
+ isFlaky: stepFlakinessScore >= flakinessThreshold,
3993
+ errors: stepErrors,
3994
+ });
3995
+ }
3996
+ return {
3997
+ testName: test.name,
3998
+ totalRuns: runs,
3999
+ passCount,
4000
+ failCount,
4001
+ flakinessScore,
4002
+ isFlaky,
4003
+ classification,
4004
+ runs: testRuns,
4005
+ stepAnalysis,
4006
+ avgDuration: Math.round(avgDuration),
4007
+ durationVariance: Math.round(variance),
4008
+ };
4009
+ }
4010
+ /**
4011
+ * Run flaky test detection on a test suite.
4012
+ */
4013
+ async function detectFlakyTests(suite, options = {}) {
4014
+ const { runs = 5, flakinessThreshold = 20 } = options;
4015
+ const startTime = Date.now();
4016
+ const testAnalyses = [];
4017
+ console.log(`\n🔍 Flaky Test Detection: ${suite.name}`);
4018
+ console.log(` Tests: ${suite.tests.length}`);
4019
+ console.log(` Runs per test: ${runs}`);
4020
+ console.log(` Flakiness threshold: ${flakinessThreshold}%`);
4021
+ for (const test of suite.tests) {
4022
+ console.log(`\n📋 Test: ${test.name}`);
4023
+ const analysis = await analyzeTestFlakiness(test, options);
4024
+ testAnalyses.push(analysis);
4025
+ const statusIcon = analysis.classification === "stable_pass" ? "✅" :
4026
+ analysis.classification === "stable_fail" ? "❌" :
4027
+ analysis.classification === "flaky" ? "⚠️" :
4028
+ analysis.classification === "mostly_pass" ? "🟡" : "🟠";
4029
+ console.log(` ${statusIcon} ${analysis.classification.toUpperCase()} (${analysis.passCount}/${analysis.totalRuns} passed, flakiness: ${analysis.flakinessScore}%)`);
4030
+ }
4031
+ // Calculate summary
4032
+ const stablePassTests = testAnalyses.filter((t) => t.classification === "stable_pass").length;
4033
+ const stableFailTests = testAnalyses.filter((t) => t.classification === "stable_fail").length;
4034
+ const flakyTests = testAnalyses.filter((t) => t.isFlaky).length;
4035
+ const mostFlakyTest = testAnalyses.reduce((max, t) => t.flakinessScore > (max?.flakinessScore || 0) ? t : max, testAnalyses[0])?.testName;
4036
+ const allSteps = testAnalyses.flatMap((t) => t.stepAnalysis);
4037
+ const mostFlakyStep = allSteps.reduce((max, s) => s.flakinessScore > (max?.flakinessScore || 0) ? s : max, allSteps[0])?.instruction;
4038
+ const overallFlakinessScore = testAnalyses.length > 0
4039
+ ? Math.round(testAnalyses.reduce((sum, t) => sum + t.flakinessScore, 0) / testAnalyses.length)
4040
+ : 0;
4041
+ return {
4042
+ suiteName: suite.name,
4043
+ timestamp: new Date().toISOString(),
4044
+ duration: Date.now() - startTime,
4045
+ runsPerTest: runs,
4046
+ testAnalyses,
4047
+ summary: {
4048
+ totalTests: suite.tests.length,
4049
+ stablePassTests,
4050
+ stableFailTests,
4051
+ flakyTests,
4052
+ mostFlakyTest: flakyTests > 0 ? mostFlakyTest : undefined,
4053
+ mostFlakyStep: allSteps.some((s) => s.isFlaky) ? mostFlakyStep : undefined,
4054
+ overallFlakinessScore,
4055
+ },
4056
+ };
4057
+ }
4058
+ /**
4059
+ * Format a flaky test report.
4060
+ */
4061
+ function formatFlakyTestReport(result) {
4062
+ const lines = [];
4063
+ lines.push("");
4064
+ lines.push("╔══════════════════════════════════════════════════════════════════════════════╗");
4065
+ lines.push("║ FLAKY TEST DETECTION REPORT ║");
4066
+ lines.push("╚══════════════════════════════════════════════════════════════════════════════╝");
4067
+ lines.push("");
4068
+ lines.push(`📋 Suite: ${result.suiteName}`);
4069
+ lines.push(`⏱️ Duration: ${(result.duration / 1000).toFixed(1)}s`);
4070
+ lines.push(`🔄 Runs per test: ${result.runsPerTest}`);
4071
+ lines.push(`📅 Timestamp: ${result.timestamp}`);
4072
+ lines.push("");
4073
+ // Summary
4074
+ const flakyEmoji = result.summary.flakyTests === 0 ? "✅" : "⚠️";
4075
+ lines.push(`${flakyEmoji} Overall Flakiness: ${result.summary.overallFlakinessScore}%`);
4076
+ lines.push("");
4077
+ lines.push("📊 SUMMARY");
4078
+ lines.push("─".repeat(60));
4079
+ lines.push(` Total Tests: ${result.summary.totalTests}`);
4080
+ lines.push(` Stable (Pass): ${result.summary.stablePassTests}`);
4081
+ lines.push(` Stable (Fail): ${result.summary.stableFailTests}`);
4082
+ lines.push(` Flaky: ${result.summary.flakyTests}`);
4083
+ if (result.summary.mostFlakyTest) {
4084
+ lines.push(` Most Flaky Test: ${result.summary.mostFlakyTest}`);
4085
+ }
4086
+ if (result.summary.mostFlakyStep) {
4087
+ lines.push(` Most Flaky Step: ${result.summary.mostFlakyStep.slice(0, 50)}...`);
4088
+ }
4089
+ lines.push("");
4090
+ // Results table
4091
+ lines.push("┌────────────────────────────────┬────────────┬──────────┬───────────┬────────────┐");
4092
+ lines.push("│ Test │ Status │ Pass/Fail│ Flakiness │ Avg Time │");
4093
+ lines.push("├────────────────────────────────┼────────────┼──────────┼───────────┼────────────┤");
4094
+ for (const test of result.testAnalyses) {
4095
+ const name = test.testName.padEnd(30).slice(0, 30);
4096
+ const status = test.classification.replace("_", " ").toUpperCase().padEnd(10).slice(0, 10);
4097
+ const passFailStr = `${test.passCount}/${test.totalRuns}`.padEnd(8);
4098
+ const flakiness = `${test.flakinessScore}%`.padEnd(9);
4099
+ const avgTime = `${(test.avgDuration / 1000).toFixed(1)}s`.padEnd(10);
4100
+ lines.push(`│ ${name} │ ${status} │ ${passFailStr} │ ${flakiness} │ ${avgTime} │`);
4101
+ }
4102
+ lines.push("└────────────────────────────────┴────────────┴──────────┴───────────┴────────────┘");
4103
+ lines.push("");
4104
+ // Flaky tests details
4105
+ const flakyTests = result.testAnalyses.filter((t) => t.isFlaky);
4106
+ if (flakyTests.length > 0) {
4107
+ lines.push("⚠️ FLAKY TESTS DETAILS");
4108
+ lines.push("─".repeat(60));
4109
+ for (const test of flakyTests) {
4110
+ lines.push(`\n 📋 ${test.testName}`);
4111
+ lines.push(` Flakiness: ${test.flakinessScore}%`);
4112
+ lines.push(` Pass Rate: ${test.passCount}/${test.totalRuns} (${Math.round((test.passCount / test.totalRuns) * 100)}%)`);
4113
+ lines.push(` Duration: ${test.avgDuration}ms ± ${test.durationVariance}ms`);
4114
+ const flakySteps = test.stepAnalysis.filter((s) => s.isFlaky);
4115
+ if (flakySteps.length > 0) {
4116
+ lines.push(` Flaky Steps:`);
4117
+ for (const step of flakySteps) {
4118
+ lines.push(` - "${step.instruction.slice(0, 40)}..." (${step.flakinessScore}% flaky)`);
4119
+ if (step.errors.length > 0) {
4120
+ lines.push(` Errors: ${step.errors[0].slice(0, 50)}`);
4121
+ }
4122
+ }
4123
+ }
4124
+ }
4125
+ lines.push("");
4126
+ }
4127
+ // Recommendations
4128
+ lines.push("💡 RECOMMENDATIONS");
4129
+ lines.push("─".repeat(60));
4130
+ if (result.summary.flakyTests === 0) {
4131
+ lines.push(" ✅ All tests are stable - no action needed");
4132
+ }
4133
+ else {
4134
+ lines.push(` ⚠️ ${result.summary.flakyTests} flaky test(s) detected`);
4135
+ lines.push(" Consider:");
4136
+ lines.push(" - Adding explicit waits for timing-sensitive operations");
4137
+ lines.push(" - Using more specific selectors");
4138
+ lines.push(" - Checking for race conditions in the application");
4139
+ lines.push(" - Isolating tests to avoid shared state issues");
4140
+ }
4141
+ lines.push("");
4142
+ return lines.join("\n");
4143
+ }
3300
4144
  //# sourceMappingURL=browser.js.map