cbrowser 5.3.0 → 6.1.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
@@ -14,6 +14,13 @@ exports.huntBugs = huntBugs;
14
14
  exports.crossBrowserDiff = crossBrowserDiff;
15
15
  exports.applyChaos = applyChaos;
16
16
  exports.runChaosTest = runChaosTest;
17
+ exports.comparePersonas = comparePersonas;
18
+ exports.formatComparisonReport = formatComparisonReport;
19
+ exports.parseNLInstruction = parseNLInstruction;
20
+ exports.parseNLTestSuite = parseNLTestSuite;
21
+ exports.runNLTestSuite = runNLTestSuite;
22
+ exports.formatNLTestReport = formatNLTestReport;
23
+ exports.runNLTestFile = runNLTestFile;
17
24
  const playwright_1 = require("playwright");
18
25
  const fs_1 = require("fs");
19
26
  const path_1 = require("path");
@@ -2671,4 +2678,623 @@ async function runChaosTest(browser, url, chaos, actions = []) {
2671
2678
  };
2672
2679
  }
2673
2680
  }
2681
+ /**
2682
+ * Run the same journey with multiple personas and compare results.
2683
+ * This runs personas in parallel (up to maxConcurrency) for efficiency.
2684
+ */
2685
+ async function comparePersonas(options) {
2686
+ const { startUrl, goal, personas, maxSteps = 20, maxConcurrency = 3, headless = true, } = options;
2687
+ const startTime = Date.now();
2688
+ const results = [];
2689
+ console.log(`\nšŸ”„ Comparing ${personas.length} personas...`);
2690
+ console.log(` URL: ${startUrl}`);
2691
+ console.log(` Goal: ${goal}`);
2692
+ console.log(` Concurrency: ${maxConcurrency}\n`);
2693
+ // Process personas in batches
2694
+ for (let i = 0; i < personas.length; i += maxConcurrency) {
2695
+ const batch = personas.slice(i, i + maxConcurrency);
2696
+ console.log(`šŸ“¦ Batch ${Math.floor(i / maxConcurrency) + 1}: ${batch.join(", ")}`);
2697
+ const batchPromises = batch.map(async (personaName) => {
2698
+ const browser = new CBrowser({ headless });
2699
+ try {
2700
+ const persona = (0, personas_js_1.getPersona)(personaName) || personas_js_1.BUILTIN_PERSONAS["first-timer"];
2701
+ const journeyStart = Date.now();
2702
+ // Run the journey
2703
+ const journey = await browser.journey({
2704
+ persona: personaName,
2705
+ startUrl,
2706
+ goal,
2707
+ maxSteps,
2708
+ });
2709
+ // Calculate average reaction time from persona config
2710
+ const timing = persona.humanBehavior?.timing;
2711
+ const avgReactionTime = timing
2712
+ ? (timing.reactionTime.min + timing.reactionTime.max) / 2
2713
+ : 500;
2714
+ // Calculate error rate from persona config
2715
+ const errors = persona.humanBehavior?.errors;
2716
+ const errorRate = errors
2717
+ ? (errors.misClickRate + errors.typoRate) / 2
2718
+ : 0.05;
2719
+ const result = {
2720
+ persona: personaName,
2721
+ description: persona.description,
2722
+ techLevel: persona.demographics.tech_level || "intermediate",
2723
+ device: persona.demographics.device || "desktop",
2724
+ success: journey.success,
2725
+ totalTime: journey.totalTime,
2726
+ stepCount: journey.steps.length,
2727
+ frictionCount: journey.frictionPoints.length,
2728
+ frictionPoints: journey.frictionPoints,
2729
+ avgReactionTime,
2730
+ errorRate,
2731
+ screenshots: {
2732
+ start: journey.steps[0]?.screenshot || "",
2733
+ end: journey.steps[journey.steps.length - 1]?.screenshot || "",
2734
+ },
2735
+ };
2736
+ console.log(` āœ“ ${personaName}: ${journey.success ? "SUCCESS" : "FAILED"} (${journey.totalTime}ms, ${journey.frictionPoints.length} friction)`);
2737
+ return result;
2738
+ }
2739
+ catch (e) {
2740
+ console.log(` āœ— ${personaName}: ERROR - ${e.message}`);
2741
+ return {
2742
+ persona: personaName,
2743
+ description: "Unknown",
2744
+ techLevel: "unknown",
2745
+ device: "unknown",
2746
+ success: false,
2747
+ totalTime: 0,
2748
+ stepCount: 0,
2749
+ frictionCount: 1,
2750
+ frictionPoints: [`Error: ${e.message}`],
2751
+ avgReactionTime: 0,
2752
+ errorRate: 0,
2753
+ screenshots: { start: "", end: "" },
2754
+ };
2755
+ }
2756
+ finally {
2757
+ await browser.close();
2758
+ }
2759
+ });
2760
+ const batchResults = await Promise.all(batchPromises);
2761
+ results.push(...batchResults);
2762
+ }
2763
+ // Generate summary
2764
+ const successfulResults = results.filter((r) => r.success);
2765
+ const failedResults = results.filter((r) => !r.success);
2766
+ const sortedByTime = [...successfulResults].sort((a, b) => a.totalTime - b.totalTime);
2767
+ const sortedByFriction = [...results].sort((a, b) => b.frictionCount - a.frictionCount);
2768
+ // Collect all friction points
2769
+ const allFrictionPoints = results.flatMap((r) => r.frictionPoints);
2770
+ const frictionCounts = allFrictionPoints.reduce((acc, fp) => {
2771
+ acc[fp] = (acc[fp] || 0) + 1;
2772
+ return acc;
2773
+ }, {});
2774
+ const commonFriction = Object.entries(frictionCounts)
2775
+ .filter(([_, count]) => count > 1)
2776
+ .sort((a, b) => b[1] - a[1])
2777
+ .slice(0, 5)
2778
+ .map(([fp]) => fp);
2779
+ // Generate recommendations
2780
+ const recommendations = [];
2781
+ if (failedResults.length > 0) {
2782
+ recommendations.push(`āš ļø ${failedResults.length} persona(s) failed to complete the journey: ${failedResults.map((r) => r.persona).join(", ")}`);
2783
+ }
2784
+ if (sortedByFriction[0]?.frictionCount > 0) {
2785
+ recommendations.push(`šŸ”§ "${sortedByFriction[0].persona}" experienced the most friction (${sortedByFriction[0].frictionCount} points) - review for accessibility improvements`);
2786
+ }
2787
+ const beginnerPersonas = results.filter((r) => r.techLevel === "beginner");
2788
+ const expertPersonas = results.filter((r) => r.techLevel === "expert");
2789
+ if (beginnerPersonas.length > 0 && expertPersonas.length > 0) {
2790
+ const avgBeginnerTime = beginnerPersonas.reduce((sum, r) => sum + r.totalTime, 0) / beginnerPersonas.length;
2791
+ const avgExpertTime = expertPersonas.reduce((sum, r) => sum + r.totalTime, 0) / expertPersonas.length;
2792
+ if (avgBeginnerTime > avgExpertTime * 3) {
2793
+ recommendations.push(`šŸ“š Beginners take ${(avgBeginnerTime / avgExpertTime).toFixed(1)}x longer than experts - consider adding more guidance`);
2794
+ }
2795
+ }
2796
+ const mobilePersonas = results.filter((r) => r.device === "mobile");
2797
+ const desktopPersonas = results.filter((r) => r.device === "desktop");
2798
+ if (mobilePersonas.length > 0 && desktopPersonas.length > 0) {
2799
+ const mobileFriction = mobilePersonas.reduce((sum, r) => sum + r.frictionCount, 0) / mobilePersonas.length;
2800
+ const desktopFriction = desktopPersonas.reduce((sum, r) => sum + r.frictionCount, 0) / desktopPersonas.length;
2801
+ if (mobileFriction > desktopFriction * 2) {
2802
+ recommendations.push(`šŸ“± Mobile users experience ${(mobileFriction / desktopFriction).toFixed(1)}x more friction - review mobile UX`);
2803
+ }
2804
+ }
2805
+ if (commonFriction.length > 0) {
2806
+ recommendations.push(`šŸŽÆ Common friction points across personas: ${commonFriction.slice(0, 3).join("; ")}`);
2807
+ }
2808
+ if (recommendations.length === 0) {
2809
+ recommendations.push("āœ… All personas completed the journey without significant issues");
2810
+ }
2811
+ const avgTime = successfulResults.length > 0
2812
+ ? successfulResults.reduce((sum, r) => sum + r.totalTime, 0) / successfulResults.length
2813
+ : 0;
2814
+ const comparison = {
2815
+ url: startUrl,
2816
+ goal,
2817
+ timestamp: new Date().toISOString(),
2818
+ duration: Date.now() - startTime,
2819
+ personas: results,
2820
+ summary: {
2821
+ totalPersonas: personas.length,
2822
+ successCount: successfulResults.length,
2823
+ failureCount: failedResults.length,
2824
+ fastestPersona: sortedByTime[0]?.persona || "N/A",
2825
+ slowestPersona: sortedByTime[sortedByTime.length - 1]?.persona || "N/A",
2826
+ mostFriction: sortedByFriction[0]?.persona || "N/A",
2827
+ leastFriction: sortedByFriction[sortedByFriction.length - 1]?.persona || "N/A",
2828
+ avgCompletionTime: Math.round(avgTime),
2829
+ commonFrictionPoints: commonFriction,
2830
+ },
2831
+ recommendations,
2832
+ };
2833
+ return comparison;
2834
+ }
2835
+ /**
2836
+ * Generate a formatted comparison report.
2837
+ */
2838
+ function formatComparisonReport(comparison) {
2839
+ const lines = [];
2840
+ lines.push("");
2841
+ lines.push("╔══════════════════════════════════════════════════════════════════════════════╗");
2842
+ lines.push("ā•‘ MULTI-PERSONA COMPARISON REPORT ā•‘");
2843
+ lines.push("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•");
2844
+ lines.push("");
2845
+ lines.push(`šŸ“ URL: ${comparison.url}`);
2846
+ lines.push(`šŸŽÆ Goal: ${comparison.goal}`);
2847
+ lines.push(`ā±ļø Total Duration: ${(comparison.duration / 1000).toFixed(1)}s`);
2848
+ lines.push(`šŸ“… Timestamp: ${comparison.timestamp}`);
2849
+ lines.push("");
2850
+ // Results table
2851
+ lines.push("ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”");
2852
+ lines.push("│ Persona │ Success │ Time │ Steps │ Friction │ Key Issues │");
2853
+ lines.push("ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤");
2854
+ for (const result of comparison.personas) {
2855
+ const name = result.persona.padEnd(19).slice(0, 19);
2856
+ const success = result.success ? "āœ“".padEnd(8) : "āœ—".padEnd(8);
2857
+ const time = `${(result.totalTime / 1000).toFixed(1)}s`.padEnd(8);
2858
+ const steps = `${result.stepCount}`.padEnd(8);
2859
+ const friction = `${result.frictionCount}`.padEnd(8);
2860
+ const issues = (result.frictionPoints[0] || "-").slice(0, 27).padEnd(27);
2861
+ lines.push(`│ ${name} │ ${success} │ ${time} │ ${steps} │ ${friction} │ ${issues} │`);
2862
+ }
2863
+ lines.push("ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜");
2864
+ lines.push("");
2865
+ // Summary
2866
+ lines.push("šŸ“Š SUMMARY");
2867
+ lines.push("─".repeat(60));
2868
+ lines.push(` Total Personas: ${comparison.summary.totalPersonas}`);
2869
+ lines.push(` Success Rate: ${comparison.summary.successCount}/${comparison.summary.totalPersonas} (${Math.round((comparison.summary.successCount / comparison.summary.totalPersonas) * 100)}%)`);
2870
+ lines.push(` Avg Completion Time: ${(comparison.summary.avgCompletionTime / 1000).toFixed(1)}s`);
2871
+ lines.push(` Fastest: ${comparison.summary.fastestPersona}`);
2872
+ lines.push(` Slowest: ${comparison.summary.slowestPersona}`);
2873
+ lines.push(` Most Friction: ${comparison.summary.mostFriction}`);
2874
+ lines.push(` Least Friction: ${comparison.summary.leastFriction}`);
2875
+ lines.push("");
2876
+ // Recommendations
2877
+ lines.push("šŸ’” RECOMMENDATIONS");
2878
+ lines.push("─".repeat(60));
2879
+ for (const rec of comparison.recommendations) {
2880
+ lines.push(` ${rec}`);
2881
+ }
2882
+ lines.push("");
2883
+ return lines.join("\n");
2884
+ }
2885
+ // =========================================================================
2886
+ // Natural Language Test Suites
2887
+ // =========================================================================
2888
+ /**
2889
+ * Parse a single natural language instruction into an NLTestStep.
2890
+ *
2891
+ * Supported patterns:
2892
+ * - "go to https://..." / "navigate to https://..." / "open https://..."
2893
+ * - "click [the] <target>" / "press <target>"
2894
+ * - "type '<value>' in[to] <target>" / "fill <target> with '<value>'"
2895
+ * - "select '<option>' from <dropdown>"
2896
+ * - "scroll down/up"
2897
+ * - "wait [for] <seconds> seconds"
2898
+ * - "verify <assertion>" / "assert <assertion>" / "check <assertion>"
2899
+ * - "take screenshot"
2900
+ */
2901
+ function parseNLInstruction(instruction) {
2902
+ const lower = instruction.toLowerCase().trim();
2903
+ // Navigate patterns
2904
+ const navigateMatch = lower.match(/^(?:go to|navigate to|open|visit)\s+(.+)$/i);
2905
+ if (navigateMatch) {
2906
+ return {
2907
+ instruction,
2908
+ action: "navigate",
2909
+ target: navigateMatch[1].trim(),
2910
+ };
2911
+ }
2912
+ // Click patterns
2913
+ const clickMatch = lower.match(/^(?:click|tap|press)\s+(?:on\s+)?(?:the\s+)?(.+)$/i);
2914
+ if (clickMatch) {
2915
+ return {
2916
+ instruction,
2917
+ action: "click",
2918
+ target: clickMatch[1].trim(),
2919
+ };
2920
+ }
2921
+ // Fill patterns: "type 'value' in target" or "fill target with 'value'"
2922
+ const typeMatch = lower.match(/^(?:type|enter)\s+['"](.+?)['"]\s+(?:in|into)\s+(?:the\s+)?(.+)$/i);
2923
+ if (typeMatch) {
2924
+ return {
2925
+ instruction,
2926
+ action: "fill",
2927
+ value: typeMatch[1],
2928
+ target: typeMatch[2].trim(),
2929
+ };
2930
+ }
2931
+ const fillMatch = lower.match(/^fill\s+(?:the\s+)?(.+?)\s+with\s+['"](.+?)['"]$/i);
2932
+ if (fillMatch) {
2933
+ return {
2934
+ instruction,
2935
+ action: "fill",
2936
+ target: fillMatch[1].trim(),
2937
+ value: fillMatch[2],
2938
+ };
2939
+ }
2940
+ // Select patterns
2941
+ const selectMatch = lower.match(/^select\s+['"](.+?)['"]\s+(?:from|in)\s+(?:the\s+)?(.+)$/i);
2942
+ if (selectMatch) {
2943
+ return {
2944
+ instruction,
2945
+ action: "select",
2946
+ value: selectMatch[1],
2947
+ target: selectMatch[2].trim(),
2948
+ };
2949
+ }
2950
+ // Scroll patterns
2951
+ const scrollMatch = lower.match(/^scroll\s+(up|down|left|right)(?:\s+(\d+)\s+(?:times|pixels))?$/i);
2952
+ if (scrollMatch) {
2953
+ return {
2954
+ instruction,
2955
+ action: "scroll",
2956
+ target: scrollMatch[1],
2957
+ value: scrollMatch[2] || "3",
2958
+ };
2959
+ }
2960
+ // Wait patterns
2961
+ const waitMatch = lower.match(/^wait\s+(?:for\s+)?(\d+(?:\.\d+)?)\s*(?:seconds?|s)$/i);
2962
+ if (waitMatch) {
2963
+ return {
2964
+ instruction,
2965
+ action: "wait",
2966
+ value: waitMatch[1],
2967
+ };
2968
+ }
2969
+ // Wait for text pattern
2970
+ const waitForMatch = lower.match(/^wait\s+(?:for|until)\s+['"](.+?)['"]\s+(?:appears?|is visible|shows?)$/i);
2971
+ if (waitForMatch) {
2972
+ return {
2973
+ instruction,
2974
+ action: "wait",
2975
+ target: waitForMatch[1],
2976
+ };
2977
+ }
2978
+ // Assert/verify patterns
2979
+ const assertPatterns = [
2980
+ // Title assertions
2981
+ { pattern: /^(?:verify|assert|check)\s+(?:that\s+)?(?:page\s+)?title\s+(?:contains?|has)\s+['"](.+?)['"]$/i, type: "title", assertType: "contains" },
2982
+ { pattern: /^(?:verify|assert|check)\s+(?:that\s+)?(?:page\s+)?title\s+(?:is|equals?)\s+['"](.+?)['"]$/i, type: "title", assertType: "equals" },
2983
+ // URL assertions
2984
+ { pattern: /^(?:verify|assert|check)\s+(?:that\s+)?url\s+(?:contains?|has)\s+['"](.+?)['"]$/i, type: "url", assertType: "contains" },
2985
+ { pattern: /^(?:verify|assert|check)\s+(?:that\s+)?url\s+(?:is|equals?)\s+['"](.+?)['"]$/i, type: "url", assertType: "equals" },
2986
+ // Content assertions
2987
+ { pattern: /^(?:verify|assert|check)\s+(?:that\s+)?(?:page\s+)?(?:contains?|has|shows?)\s+['"](.+?)['"]$/i, type: "content", assertType: "contains" },
2988
+ { pattern: /^(?:verify|assert|check)\s+(?:that\s+)?['"](.+?)['"]\s+(?:is\s+)?(?:visible|displayed|shown)$/i, type: "content", assertType: "contains" },
2989
+ // Element exists
2990
+ { pattern: /^(?:verify|assert|check)\s+(?:that\s+)?['"](.+?)['"]\s+exists?$/i, type: "element", assertType: "exists" },
2991
+ { pattern: /^(?:verify|assert|check)\s+(?:that\s+)?(?:there\s+is\s+)?(?:a|an)\s+['"](.+?)['"]$/i, type: "element", assertType: "exists" },
2992
+ // Count assertions
2993
+ { pattern: /^(?:verify|assert|check)\s+(?:that\s+)?(?:there\s+are\s+)?(\d+)\s+(.+?)$/i, type: "count", assertType: "count" },
2994
+ ];
2995
+ for (const { pattern, type, assertType } of assertPatterns) {
2996
+ const match = lower.match(pattern);
2997
+ if (match) {
2998
+ return {
2999
+ instruction,
3000
+ action: "assert",
3001
+ target: type === "count" ? match[2] : match[1],
3002
+ value: type === "count" ? match[1] : undefined,
3003
+ assertionType: assertType,
3004
+ };
3005
+ }
3006
+ }
3007
+ // Screenshot pattern
3008
+ if (/^(?:take\s+(?:a\s+)?screenshot|screenshot|capture\s+(?:the\s+)?(?:page|screen))$/i.test(lower)) {
3009
+ return {
3010
+ instruction,
3011
+ action: "screenshot",
3012
+ };
3013
+ }
3014
+ // Unknown - return as-is for AI-powered interpretation later
3015
+ return {
3016
+ instruction,
3017
+ action: "unknown",
3018
+ target: instruction,
3019
+ };
3020
+ }
3021
+ /**
3022
+ * Parse a natural language test suite from text.
3023
+ *
3024
+ * Format:
3025
+ * ```
3026
+ * # Test: Login Flow
3027
+ * go to https://example.com
3028
+ * click the login button
3029
+ * type "user@example.com" in email field
3030
+ * type "password123" in password field
3031
+ * click submit
3032
+ * verify url contains "/dashboard"
3033
+ *
3034
+ * # Test: Search Functionality
3035
+ * go to https://example.com
3036
+ * type "test query" in search box
3037
+ * click search button
3038
+ * verify page contains "results"
3039
+ * ```
3040
+ */
3041
+ function parseNLTestSuite(text, suiteName = "Unnamed Suite") {
3042
+ const lines = text.split("\n").map(l => l.trim()).filter(l => l && !l.startsWith("//"));
3043
+ const tests = [];
3044
+ let currentTest = null;
3045
+ for (const line of lines) {
3046
+ // Check for test header: "# Test: Name" or "## Name" or "Test: Name"
3047
+ const testHeaderMatch = line.match(/^(?:#\s*)?(?:test:\s*)?(.+)$/i);
3048
+ if (line.startsWith("#") || line.toLowerCase().startsWith("test:")) {
3049
+ // Save previous test if exists
3050
+ if (currentTest && currentTest.steps.length > 0) {
3051
+ tests.push(currentTest);
3052
+ }
3053
+ const name = testHeaderMatch?.[1]?.replace(/^#+\s*/, "").replace(/^test:\s*/i, "").trim() || "Unnamed Test";
3054
+ currentTest = {
3055
+ name,
3056
+ steps: [],
3057
+ };
3058
+ }
3059
+ else if (line.length > 0) {
3060
+ // Parse as instruction
3061
+ if (!currentTest) {
3062
+ // Create default test if no header found
3063
+ currentTest = {
3064
+ name: "Default Test",
3065
+ steps: [],
3066
+ };
3067
+ }
3068
+ const step = parseNLInstruction(line);
3069
+ currentTest.steps.push(step);
3070
+ }
3071
+ }
3072
+ // Save final test
3073
+ if (currentTest && currentTest.steps.length > 0) {
3074
+ tests.push(currentTest);
3075
+ }
3076
+ return { name: suiteName, tests };
3077
+ }
3078
+ /**
3079
+ * Run a natural language test suite.
3080
+ */
3081
+ async function runNLTestSuite(suite, options = {}) {
3082
+ const { stepTimeout = 30000, continueOnFailure = true, screenshotOnFailure = true, headless = true, } = options;
3083
+ const startTime = Date.now();
3084
+ const testResults = [];
3085
+ console.log(`\n🧪 Running Test Suite: ${suite.name}`);
3086
+ console.log(` Tests: ${suite.tests.length}`);
3087
+ console.log(` Continue on failure: ${continueOnFailure}`);
3088
+ console.log("");
3089
+ const browser = new CBrowser({
3090
+ headless,
3091
+ });
3092
+ try {
3093
+ await browser.launch();
3094
+ for (const test of suite.tests) {
3095
+ console.log(`\nšŸ“‹ Test: ${test.name}`);
3096
+ const testStartTime = Date.now();
3097
+ const stepResults = [];
3098
+ let testPassed = true;
3099
+ let testError;
3100
+ for (const step of test.steps) {
3101
+ console.log(` → ${step.instruction}`);
3102
+ const stepStartTime = Date.now();
3103
+ let stepPassed = true;
3104
+ let stepError;
3105
+ let screenshot;
3106
+ let actualValue;
3107
+ try {
3108
+ // Execute the step based on action type
3109
+ switch (step.action) {
3110
+ case "navigate": {
3111
+ await browser.navigate(step.target || "");
3112
+ break;
3113
+ }
3114
+ case "click": {
3115
+ const result = await browser.smartClick(step.target || "");
3116
+ if (!result.success) {
3117
+ throw new Error(`Failed to click: ${step.target}`);
3118
+ }
3119
+ break;
3120
+ }
3121
+ case "fill": {
3122
+ await browser.fill(step.target || "", step.value || "");
3123
+ break;
3124
+ }
3125
+ case "select": {
3126
+ await browser.fill(step.target || "", step.value || "");
3127
+ break;
3128
+ }
3129
+ case "scroll": {
3130
+ const direction = step.target?.toLowerCase() === "up" ? -500 : 500;
3131
+ // Use private page access through cast
3132
+ const page = browser.page;
3133
+ if (page) {
3134
+ await page.evaluate((d) => window.scrollBy(0, d), direction);
3135
+ }
3136
+ break;
3137
+ }
3138
+ case "wait": {
3139
+ if (step.target) {
3140
+ // Wait for text to appear - use private page access
3141
+ const page = browser.page;
3142
+ if (page) {
3143
+ await page.waitForSelector(`text=${step.target}`, { timeout: stepTimeout });
3144
+ }
3145
+ }
3146
+ else {
3147
+ // Wait for duration
3148
+ const ms = parseFloat(step.value || "1") * 1000;
3149
+ await new Promise(r => setTimeout(r, ms));
3150
+ }
3151
+ break;
3152
+ }
3153
+ case "assert": {
3154
+ const assertResult = await browser.assert(step.instruction);
3155
+ stepPassed = assertResult.passed;
3156
+ actualValue = String(assertResult.actual);
3157
+ if (!assertResult.passed) {
3158
+ stepError = assertResult.message;
3159
+ }
3160
+ break;
3161
+ }
3162
+ case "screenshot": {
3163
+ screenshot = await browser.screenshot();
3164
+ break;
3165
+ }
3166
+ case "unknown": {
3167
+ // Try to interpret as a click or fill
3168
+ console.log(` āš ļø Unknown instruction, attempting smart interpretation...`);
3169
+ const result = await browser.smartClick(step.target || step.instruction);
3170
+ if (!result.success) {
3171
+ throw new Error(`Could not interpret: ${step.instruction}`);
3172
+ }
3173
+ break;
3174
+ }
3175
+ }
3176
+ console.log(` āœ“ Passed (${Date.now() - stepStartTime}ms)`);
3177
+ }
3178
+ catch (e) {
3179
+ stepPassed = false;
3180
+ stepError = e.message;
3181
+ testPassed = false;
3182
+ testError = testError || e.message;
3183
+ console.log(` āœ— Failed: ${e.message}`);
3184
+ if (screenshotOnFailure) {
3185
+ try {
3186
+ screenshot = await browser.screenshot();
3187
+ }
3188
+ catch { }
3189
+ }
3190
+ }
3191
+ stepResults.push({
3192
+ instruction: step.instruction,
3193
+ action: step.action,
3194
+ passed: stepPassed,
3195
+ duration: Date.now() - stepStartTime,
3196
+ error: stepError,
3197
+ screenshot,
3198
+ actualValue,
3199
+ });
3200
+ // Stop test if step failed and not continuing on failure
3201
+ if (!stepPassed && !continueOnFailure) {
3202
+ break;
3203
+ }
3204
+ }
3205
+ testResults.push({
3206
+ name: test.name,
3207
+ passed: testPassed,
3208
+ duration: Date.now() - testStartTime,
3209
+ stepResults,
3210
+ error: testError,
3211
+ });
3212
+ console.log(` ${testPassed ? "āœ…" : "āŒ"} ${test.name}: ${testPassed ? "PASSED" : "FAILED"}`);
3213
+ }
3214
+ }
3215
+ finally {
3216
+ await browser.close();
3217
+ }
3218
+ const passed = testResults.filter(t => t.passed).length;
3219
+ const failed = testResults.filter(t => !t.passed).length;
3220
+ const result = {
3221
+ name: suite.name,
3222
+ timestamp: new Date().toISOString(),
3223
+ duration: Date.now() - startTime,
3224
+ testResults,
3225
+ summary: {
3226
+ total: suite.tests.length,
3227
+ passed,
3228
+ failed,
3229
+ skipped: 0,
3230
+ passRate: suite.tests.length > 0 ? (passed / suite.tests.length) * 100 : 0,
3231
+ },
3232
+ };
3233
+ return result;
3234
+ }
3235
+ /**
3236
+ * Format a test suite result as a report.
3237
+ */
3238
+ function formatNLTestReport(result) {
3239
+ const lines = [];
3240
+ lines.push("");
3241
+ lines.push("╔══════════════════════════════════════════════════════════════════════════════╗");
3242
+ lines.push("ā•‘ NATURAL LANGUAGE TEST REPORT ā•‘");
3243
+ lines.push("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•");
3244
+ lines.push("");
3245
+ lines.push(`šŸ“‹ Suite: ${result.name}`);
3246
+ lines.push(`ā±ļø Duration: ${(result.duration / 1000).toFixed(1)}s`);
3247
+ lines.push(`šŸ“… Timestamp: ${result.timestamp}`);
3248
+ lines.push("");
3249
+ // Summary stats
3250
+ const passEmoji = result.summary.passRate === 100 ? "šŸŽ‰" : result.summary.passRate >= 80 ? "āœ…" : "āš ļø";
3251
+ lines.push(`${passEmoji} Pass Rate: ${result.summary.passed}/${result.summary.total} (${result.summary.passRate.toFixed(0)}%)`);
3252
+ lines.push("");
3253
+ // Results table
3254
+ lines.push("ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”");
3255
+ lines.push("│ Test │ Status │ Duration │ Error │");
3256
+ lines.push("ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤");
3257
+ for (const test of result.testResults) {
3258
+ const name = test.name.padEnd(37).slice(0, 37);
3259
+ const status = test.passed ? "āœ“ PASS".padEnd(8) : "āœ— FAIL".padEnd(8);
3260
+ const duration = `${(test.duration / 1000).toFixed(1)}s`.padEnd(8);
3261
+ const error = (test.error || "-").slice(0, 18).padEnd(18);
3262
+ lines.push(`│ ${name} │ ${status} │ ${duration} │ ${error} │`);
3263
+ }
3264
+ lines.push("ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜");
3265
+ lines.push("");
3266
+ // Failed test details
3267
+ const failedTests = result.testResults.filter(t => !t.passed);
3268
+ if (failedTests.length > 0) {
3269
+ lines.push("āŒ FAILED TESTS");
3270
+ lines.push("─".repeat(60));
3271
+ for (const test of failedTests) {
3272
+ lines.push(`\n šŸ“‹ ${test.name}`);
3273
+ const failedSteps = test.stepResults.filter(s => !s.passed);
3274
+ for (const step of failedSteps) {
3275
+ lines.push(` āœ— ${step.instruction}`);
3276
+ if (step.error) {
3277
+ lines.push(` Error: ${step.error}`);
3278
+ }
3279
+ if (step.screenshot) {
3280
+ lines.push(` Screenshot: ${step.screenshot}`);
3281
+ }
3282
+ }
3283
+ }
3284
+ lines.push("");
3285
+ }
3286
+ return lines.join("\n");
3287
+ }
3288
+ /**
3289
+ * Run a natural language test suite from a file.
3290
+ */
3291
+ async function runNLTestFile(filepath, options = {}) {
3292
+ if (!(0, fs_1.existsSync)(filepath)) {
3293
+ throw new Error(`Test file not found: ${filepath}`);
3294
+ }
3295
+ const content = (0, fs_1.readFileSync)(filepath, "utf-8");
3296
+ const suiteName = filepath.split("/").pop()?.replace(/\.[^.]+$/, "") || "Test Suite";
3297
+ const suite = parseNLTestSuite(content, suiteName);
3298
+ return runNLTestSuite(suite, options);
3299
+ }
2674
3300
  //# sourceMappingURL=browser.js.map