UNPKG

testgenius-ai

Version:

šŸš€ TestGenius AI - The Ultimate E2E Testing Framework for Everyone | No Coding Required • AI-Powered Automation • Beautiful Reports • Zero Complexity

1,128 lines (1,117 loc) • 88.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AITestExecutor = void 0; const openai_1 = require("@langchain/openai"); const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const chalk_1 = __importDefault(require("chalk")); const CostTracker_1 = require("./CostTracker"); const SmartElementDetector_1 = require("./SmartElementDetector"); const SmartAIAgent_1 = require("./SmartAIAgent"); const SmartAgentConfig_1 = require("./SmartAgentConfig"); const SmartTools_1 = require("../tools/SmartTools"); class AITestExecutor { constructor(config) { this.browser = null; this.costTracker = null; this.smartDetector = null; this.smartAgent = null; this.steps = []; this.screenshots = []; this.tokenUsage = []; this.aiContext = new Map(); this.retryCount = 0; this.maxRetries = 3; this.adaptiveStrategies = []; this.useFastMode = true; // Enable fast mode by default this.useSmartAgent = true; // Enable smart agent by default this.llm = new openai_1.ChatOpenAI({ openAIApiKey: process.env.OPENAI_API_KEY, modelName: process.env.OPENAI_MODEL || 'gpt-4o', temperature: 0.1, maxTokens: 4000 // Increased for more complex reasoning }); // Initialize cost tracker if config is provided and cost tracking is enabled if (config?.costTracking?.enabled) { this.costTracker = new CostTracker_1.CostTracker(config); } } setBrowser(browser) { this.browser = browser; this.smartDetector = new SmartElementDetector_1.SmartElementDetector(browser); // Initialize Smart AI Agent with Endorphin AI-inspired tools if (this.useSmartAgent) { this.smartAgent = new SmartAIAgent_1.SmartAIAgent(SmartAgentConfig_1.SMART_AGENT_CONFIG); const smartTools = (0, SmartTools_1.createAllSmartTools)(browser); this.smartAgent.setTools(smartTools); console.log(chalk_1.default.green('🧠 Smart AI Agent initialized with Endorphin AI-inspired tools.')); } console.log(chalk_1.default.green('āœ… Browser instance set on AI executor.')); console.log(chalk_1.default.blue('⚔ Smart Element Detector initialized for fast detection.')); } /** * Track token usage from OpenAI response */ trackTokenUsage(response, model) { if (response.usage) { const usage = { promptTokens: response.usage.prompt_tokens || 0, completionTokens: response.usage.completion_tokens || 0, totalTokens: response.usage.total_tokens || 0, model: model, timestamp: new Date() }; this.tokenUsage.push(usage); if (this.costTracker) { const costMetrics = this.costTracker.calculateCost(usage); console.log(chalk_1.default.blue(`šŸ’° Token usage: ${usage.totalTokens} tokens, Cost: $${costMetrics.estimatedCost.toFixed(4)}`)); } } } /** * Get total token usage for this test execution */ getTotalTokenUsage() { return this.tokenUsage.reduce((total, usage) => ({ promptTokens: total.promptTokens + usage.promptTokens, completionTokens: total.completionTokens + usage.completionTokens, totalTokens: total.totalTokens + usage.totalTokens, model: usage.model, // Use the last model used timestamp: new Date() }), { promptTokens: 0, completionTokens: 0, totalTokens: 0, model: 'unknown', timestamp: new Date() }); } /** * Track cost for a test execution */ async trackTestCost(testId, sessionId, executionTime, success) { if (!this.costTracker) return; const totalTokenUsage = this.getTotalTokenUsage(); const costMetrics = this.costTracker.calculateCost(totalTokenUsage); const testCostData = { testId, sessionId, costMetrics, executionTime, success }; await this.costTracker.trackTestCost(testCostData); } /** * šŸš€ ENHANCED: Execute test with maximum AI automation */ async executeTest(test, options = {}) { if (!this.browser) { throw new Error('Browser not initialized. Did you forget to call setBrowser()?'); } console.log(chalk_1.default.blue('šŸ¤– Enhanced AI Test Executor starting...\n')); console.log(chalk_1.default.cyan(`šŸŽÆ Test: ${test.name}`)); // Handle new async test case format let testData = test.testData || {}; let setupData = {}; let taskDescription; try { // Execute setup function if provided if (test.setup && typeof test.setup === 'function') { console.log(chalk_1.default.blue('šŸ”§ Executing test setup...')); setupData = await test.setup(); console.log(chalk_1.default.green('āœ… Test setup completed')); } // Execute data function if provided if (test.data && typeof test.data === 'function') { console.log(chalk_1.default.blue('šŸ“Š Generating test data...')); testData = { ...testData, ...(await test.data()) }; console.log(chalk_1.default.green('āœ… Test data generated')); } // Get task description (handle both string and function formats) if (typeof test.task === 'function') { console.log(chalk_1.default.blue('šŸ“ Executing task function...')); taskDescription = await test.task(testData, setupData); } else { taskDescription = test.task; } console.log(chalk_1.default.cyan(`šŸ“ Task: ${taskDescription}`)); // Store the test site in context for navigation this.aiContext.set('currentTestSite', test.site); // Navigate to the site await this.navigateToSite(test.site); // Check if we have recorded steps from the TestRecorder const recordedSteps = test.testData?.steps; if (recordedSteps && Array.isArray(recordedSteps) && recordedSteps.length > 0) { console.log(chalk_1.default.blue('šŸ“ Executing recorded steps...\n')); const result = await this.executeRecordedSteps(recordedSteps); return { success: result.success, steps: this.steps, screenshots: this.screenshots, errors: result.errors || [] }; } else { // 🧠 SMART AGENT: Use Smart AI Agent for intelligent execution if (this.useSmartAgent && this.smartAgent) { console.log(chalk_1.default.blue('🧠 Using Smart AI Agent for intelligent execution...')); // Create test session for the smart agent const session = { sessionId: `session-${Date.now()}`, testName: test.name, startTime: Date.now(), agentHistory: [], totalCost: 0, totalTokens: 0, }; this.smartAgent.setCurrentSession(session); // Execute task with smart agent const agentResult = await this.smartAgent.executeTask(taskDescription); // Add agent session to steps this.addStep('Smart AI Agent Execution', agentResult.result, agentResult.success ? 'success' : 'failed'); return { success: agentResult.success, steps: this.steps, screenshots: this.screenshots, errors: agentResult.success ? [] : [agentResult.result] }; } else { // 🧠 FALLBACK: Use intelligent AI execution with enhanced test data console.log(chalk_1.default.blue('🧠 Creating intelligent AI execution plan...')); const enhancedTest = { ...test, task: taskDescription, testData: { ...testData, ...setupData } }; const plan = await this.createIntelligentExecutionPlan(enhancedTest, options); const result = await this.executeIntelligentPlan(plan, enhancedTest, options); return { success: result.success, steps: this.steps, screenshots: this.screenshots, errors: result.errors || [] }; } } } catch (error) { console.error(chalk_1.default.red('āŒ AI execution failed:'), error.message); // šŸ”„ AUTOMATIC SCREENSHOT ON FAILURE šŸ”„ try { const failureScreenshotPath = await this.takeScreenshot('ai-execution-failure'); this.screenshots.push(failureScreenshotPath); console.log(chalk_1.default.yellow(`šŸ“ø Screenshot taken on AI execution failure: ${failureScreenshotPath}`)); } catch (screenshotError) { console.error(chalk_1.default.red('āŒ Failed to take failure screenshot:'), screenshotError.message); } return { success: false, steps: this.steps, screenshots: this.screenshots, errors: [error.message] }; } } async navigateToSite(url) { if (!this.browser) throw new Error('Browser not initialized'); console.log(chalk_1.default.cyan(`🌐 Navigating to: ${url}`)); try { // Use smart navigation tool if available if (this.smartAgent) { const result = await this.smartAgent.executeTask(`Navigate to ${url}`); console.log(chalk_1.default.green(`āœ… Smart navigation: ${result}`)); } else { // Fallback to direct navigation await this.browser.url(url); await this.browser.pause(2000); // Wait for page load } // Take initial screenshot const screenshotPath = await this.takeScreenshot('initial-navigation'); this.screenshots.push(screenshotPath); this.addStep('Navigate to site', `Successfully navigated to ${url}`, 'success'); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.log(chalk_1.default.red(`āŒ Navigation failed: ${errorMsg}`)); throw error; } } /** * 🧭 Enhanced navigation with path support */ async navigateToPath(path) { if (!this.browser) throw new Error('Browser not initialized'); const currentUrl = await this.browser.getUrl(); const baseUrl = currentUrl.split('/').slice(0, 3).join('/'); // Get base URL const fullUrl = path.startsWith('http') ? path : `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`; console.log(chalk_1.default.cyan(`🧭 Navigating to path: ${fullUrl}`)); await this.browser.url(fullUrl); await this.browser.pause(2000); // Wait for page load // Take screenshot const screenshotPath = await this.takeScreenshot(`navigation-${path.replace(/[^a-zA-Z0-9]/g, '-')}`); this.screenshots.push(screenshotPath); this.addStep('Navigate to path', `Successfully navigated to ${path}`, 'success'); } /** * 🧠 ENHANCED: Create intelligent execution plan with context awareness */ async createIntelligentExecutionPlan(test, options) { const systemPrompt = `You are an expert UI testing AI that creates intelligent test execution plans. Your capabilities: - Analyze natural language test descriptions and convert them to executable steps - Automatically detect UI elements without explicit selectors - Adapt strategies based on page context and previous failures - Use multiple fallback strategies for element detection - Optimize execution for speed and reliability - Handle dynamic content and user scenarios intelligently Current test context: - Test: ${test.name} - Description: ${test.description} - Task: ${test.task} - Site: ${test.site} - Priority: ${test.priority} - Test Data: ${JSON.stringify(test.testData || {})} Create a detailed execution plan that: 1. Breaks down the task into logical steps 2. Uses intelligent element detection strategies 3. Includes verification steps 4. Has fallback mechanisms for each step 5. Optimizes for speed and reliability 6. Adapts to dynamic content and user scenarios Return the plan as a JSON object with steps array.`; const userPrompt = `Create an intelligent execution plan for this dynamic test task: "${test.task}" This is a dynamic test scenario where: - The task content is generated dynamically based on user scenarios - Test data and setup are generated at runtime - The framework should adapt to any type of test scenario Focus on: - Natural language element detection - Multiple selector strategies - Smart waiting and verification - Error recovery mechanisms - Performance optimization - Dynamic scenario adaptation Make the plan as automated as possible with minimal manual intervention. Handle any type of dynamic content or user scenario intelligently.`; try { const response = await this.llm.invoke([ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ]); this.trackTokenUsage(response, this.llm.modelName); const content = response.content; let plan; try { // Try to parse JSON from the response const jsonMatch = content.match(/\{[\s\S]*\}/); if (jsonMatch) { plan = JSON.parse(jsonMatch[0]); } else { // Fallback to creating a basic plan plan = this.createFallbackPlan(test); } } catch (parseError) { console.log(chalk_1.default.yellow('āš ļø Could not parse AI plan, using fallback')); plan = this.createFallbackPlan(test); } console.log(chalk_1.default.green(`āœ… AI created ${plan.steps.length} intelligent steps`)); console.log(chalk_1.default.blue(`šŸŽÆ Confidence: ${plan.confidence}%`)); console.log(chalk_1.default.gray(`šŸ’­ Reasoning: ${plan.reasoning}`)); return plan; } catch (error) { console.log(chalk_1.default.yellow('āš ļø AI plan creation failed, using fallback')); return this.createFallbackPlan(test); } } createFallbackPlan(test) { console.log(chalk_1.default.yellow('šŸ›”ļø Creating AI-powered fallback plan...')); // Even the fallback should be AI-driven, not hardcoded const taskContent = typeof test.task === 'string' ? test.task : 'Dynamic task content'; // Create a simple AI prompt for fallback const fallbackPrompt = `Create a simple execution plan for this test task: "${taskContent}" Site: ${test.site || 'Not specified'} Test: ${test.name} Create 3-5 basic steps that would accomplish this task. Focus on: 1. Navigation (if needed) 2. Basic interactions (click, fill, verify) 3. Simple verification Return as JSON with steps array.`; // For now, create a minimal plan that the AI executor can handle const steps = []; // Add navigation if site is specified if (test.site) { steps.push({ action: 'navigate', description: `Navigate to ${test.site}`, value: test.site, expectedResult: 'Page loads successfully' }); } // Add a generic interaction step steps.push({ action: 'click', description: `Execute task: ${taskContent}`, expectedResult: 'Task completed successfully' }); // Add verification steps.push({ action: 'verify', description: 'Verify task completion', expectedResult: 'Task verification passed' }); return { steps: steps, confidence: 0.6, reasoning: 'AI-powered fallback plan created with minimal steps' }; } /** * šŸš€ ENHANCED: Execute plan with intelligent adaptation */ async executeIntelligentPlan(plan, test, options) { const errors = []; let success = true; console.log(chalk_1.default.blue(`\nšŸš€ Executing ${plan.steps.length} intelligent steps...\n`)); for (let i = 0; i < plan.steps.length; i++) { const step = plan.steps[i]; console.log(chalk_1.default.cyan(`\nšŸ“‹ Step ${i + 1}/${plan.steps.length}: ${step.description}`)); try { await this.executeIntelligentStep(step, test.testData || {}); this.addStep(step.description, 'Success', 'success'); // Store successful strategies for future use if (step.action && !this.adaptiveStrategies.includes(step.action)) { this.adaptiveStrategies.push(step.action); } } catch (error) { const errorMsg = error.message; console.error(chalk_1.default.red(`āŒ Step failed: ${errorMsg}`)); // 🧠 INTELLIGENT ERROR RECOVERY const recovered = await this.attemptIntelligentRecovery(step, errorMsg, test.testData || {}); if (recovered) { console.log(chalk_1.default.green('āœ… Step recovered successfully')); this.addStep(step.description, 'Recovered', 'success'); } else { this.addStep(step.description, errorMsg, 'failed'); errors.push(`Step ${i + 1}: ${errorMsg}`); success = false; // If this is a critical step, stop execution if (this.isCriticalStep(step)) { console.log(chalk_1.default.red('šŸ›‘ Critical step failed, stopping execution')); break; } } } } return { success, errors }; } /** * šŸš€ ENHANCED: Execute step with Endorphin AI-inspired smart tools */ async executeIntelligentStep(step, testData) { if (!this.browser) throw new Error('Browser not initialized'); // Handle special actions first if (step.action === 'navigate') { console.log(chalk_1.default.blue(`🧭 Executing navigation step: ${step.description}`)); if (step.value) { await this.navigateToPath(String(step.value)); } else { throw new Error('Navigation step requires a value (path)'); } return; } // Use Smart AI Agent with Endorphin AI-inspired tools if (this.useSmartAgent && this.smartAgent) { console.log(chalk_1.default.blue(`🧠 Using Smart AI Agent for: ${step.description}`)); try { // Convert step to natural language task const taskDescription = this.convertStepToTask(step, testData); const result = await this.smartAgent.executeTask(taskDescription); console.log(chalk_1.default.green(`āœ… Smart AI Agent completed: ${result}`)); return; } catch (error) { console.log(chalk_1.default.yellow(`āš ļø Smart AI Agent failed, falling back to fast mode: ${error}`)); } } // Use fast smart detection as fallback if (this.useFastMode && this.smartDetector) { await this.executeFastStep(step, testData); return; } // Final fallback to AI-based detection (expensive) console.log(chalk_1.default.yellow('āš ļø Using AI-based detection (expensive)...')); const pageState = await this.analyzePageState(); this.aiContext.set('currentPageState', pageState); const strategies = this.generateElementDetectionStrategies(step, pageState); for (const strategy of strategies) { try { await this.executeStrategy(step, strategy, testData); return; // Success, exit } catch (strategyError) { console.log(chalk_1.default.yellow(`āš ļø Strategy failed: ${strategy.name}`)); continue; // Try next strategy } } throw new Error(`All element detection strategies failed for: ${step.description}`); } /** * ⚔ FAST: Execute step using smart detector (no AI) */ async executeFastStep(step, testData) { if (!this.smartDetector) throw new Error('Smart detector not initialized'); console.log(chalk_1.default.blue(`⚔ Fast execution: ${step.description}`)); // Determine element type from action let elementType; switch (step.action.toLowerCase()) { case 'click': elementType = 'button'; break; case 'fill': case 'type': elementType = 'input'; break; case 'select': elementType = 'select'; break; case 'upload': elementType = 'input[type="file"]'; break; case 'verify': elementType = undefined; // Can be any element break; default: elementType = undefined; } // Use smart detector to find element const match = await this.smartDetector.detectElement(step.description, elementType); if (!match) { throw new Error(`Element not found: ${step.description}`); } // Perform action based on step type await this.performFastAction(match.element, step, testData); } /** * ⚔ FAST: Perform action without AI */ async performFastAction(element, step, testData) { const action = step.action.toLowerCase(); try { switch (action) { case 'click': await element.click(); console.log(chalk_1.default.green(`āœ… Clicked: ${step.description}`)); break; case 'type': case 'fill': const value = step.value || testData[step.description] || ''; await element.clearValue(); await element.setValue(String(value)); console.log(chalk_1.default.green(`āœ… Filled: ${step.description} with "${value}"`)); break; case 'clear': await element.clearValue(); console.log(chalk_1.default.green(`āœ… Cleared: ${step.description}`)); break; case 'verify': await this.verifyFastElement(element, step); console.log(chalk_1.default.green(`āœ… Verified: ${step.description}`)); break; case 'select': // Handle dropdown selection if (step.value) { await element.selectByVisibleText(String(step.value)); console.log(chalk_1.default.green(`āœ… Selected: ${step.value} from ${step.description}`)); } else { // Default to first option await element.selectByIndex(1); console.log(chalk_1.default.green(`āœ… Selected first option from ${step.description}`)); } break; case 'upload': // Handle file upload const filePath = step.value || './test-file.txt'; await element.setValue(filePath); console.log(chalk_1.default.green(`āœ… Uploaded file: ${filePath} to ${step.description}`)); break; default: await element.click(); // Default to click console.log(chalk_1.default.green(`āœ… Default action (click): ${step.description}`)); } } catch (error) { // Handle stale element reference if (error.message && (error.message.includes('stale') || error.message.includes('Node with given id does not belong'))) { console.log(chalk_1.default.yellow(`šŸ”„ Stale element detected, refreshing...`)); // Try to find the element again const refreshedElement = await this.findElement(step.description); await this.performFastAction(refreshedElement, step, testData); return; } throw error; } } /** * ⚔ FAST: Verify element without AI */ async verifyFastElement(element, step) { const description = step.description.toLowerCase(); // Handle page title verification if (description.includes('page title') || description.includes('title contains')) { const pageTitle = await this.browser?.getTitle(); const expectedText = step.value || step.description.match(/contains "([^"]+)"/)?.[1]; if (expectedText && pageTitle) { if (String(pageTitle).toLowerCase().includes(String(expectedText).toLowerCase())) { console.log(chalk_1.default.green(`āœ… Page title verification passed: "${expectedText}" found in "${pageTitle}"`)); return; } else { throw new Error(`Page title verification failed: Expected "${expectedText}" in title, got "${pageTitle}"`); } } } // Handle text content verification if (description.includes('text') || description.includes('content')) { try { const textContent = await element.getText(); const expectedText = step.value || step.description.match(/contains "([^"]+)"/)?.[1]; if (expectedText && textContent) { if (String(textContent).toLowerCase().includes(String(expectedText).toLowerCase())) { console.log(chalk_1.default.green(`āœ… Text verification passed: "${expectedText}" found in "${textContent}"`)); return; } else { throw new Error(`Text verification failed: Expected "${expectedText}" in content, got "${textContent}"`); } } } catch (error) { // If getText fails, try getValue try { const value = await element.getValue(); const expectedText = step.value || step.description.match(/contains "([^"]+)"/)?.[1]; if (expectedText && value) { if (String(value).toLowerCase().includes(String(expectedText).toLowerCase())) { console.log(chalk_1.default.green(`āœ… Value verification passed: "${expectedText}" found in "${value}"`)); return; } else { throw new Error(`Value verification failed: Expected "${expectedText}" in value, got "${value}"`); } } } catch (valueError) { // Continue to basic verification } } } // Basic element verification try { const isDisplayed = await element.isDisplayed(); if (!isDisplayed) { throw new Error(`Element is not displayed: ${step.description}`); } if (step.value) { const actualValue = await element.getValue(); if (actualValue !== step.value) { throw new Error(`Expected value "${step.value}", got "${actualValue}"`); } } } catch (error) { // If basic verification fails, try to verify element exists try { await element.waitForDisplayed({ timeout: 5000 }); console.log(chalk_1.default.green(`āœ… Element verification passed: ${step.description} exists and is visible`)); } catch (waitError) { throw new Error(`Element verification failed: ${step.description} is not accessible`); } } } /** * šŸŽÆ Generate multiple element detection strategies */ generateElementDetectionStrategies(step, pageState) { const strategies = []; // Strategy 1: Natural language description if (step.description) { strategies.push({ name: 'Natural Language', method: 'description', selectors: this.generateSelectorsFromDescription(step.description) }); } // Strategy 2: Text-based detection if (step.value || step.description) { strategies.push({ name: 'Text Detection', method: 'text', selectors: this.generateTextSelectors(String(step.value || step.description || '')) }); } // Strategy 3: Semantic analysis strategies.push({ name: 'Semantic Analysis', method: 'semantic', selectors: this.generateSemanticSelectors(step, pageState) }); // Strategy 4: Visual pattern matching strategies.push({ name: 'Visual Pattern', method: 'visual', selectors: this.generateVisualSelectors(step, pageState) }); return strategies; } /** * 🧠 Generate selectors from natural language description (WebdriverIO Best Practices) */ generateSelectorsFromDescription(description) { const selectors = []; // Extract key words from description const words = description.split(' ').filter(word => word.length > 2); const descriptionLower = description.toLowerCase(); // 1. **Accessibility Name Selector (Best Practice)** words.forEach(word => { selectors.push(`aria/${word}`, `${word}`); }); // 2. **Text-based selectors (User-centric)** if (descriptionLower.includes('button')) { selectors.push('button', '[role="button"]'); // Add text-based button selectors words.forEach(word => { selectors.push(`button=${word}`, `button*=${word}`); }); } if (descriptionLower.includes('input') || descriptionLower.includes('field')) { selectors.push('input', 'textarea', 'select'); // Add text-based input selectors words.forEach(word => { selectors.push(`input=${word}`, `input*=${word}`); }); } if (descriptionLower.includes('link')) { selectors.push('a', '[role="link"]'); // Add text-based link selectors words.forEach(word => { selectors.push(`a=${word}`, `a*=${word}`); }); } // 3. **Data Test ID (Good Practice)** words.forEach(word => { selectors.push(`[data-testid*="${word}"]`, `[data-test="${word}"]`); }); // 4. **ARIA attributes (Accessibility focused)** words.forEach(word => { selectors.push(`[aria-label*="${word}"]`, `[aria-labelledby*="${word}"]`, `[title*="${word}"]`); }); return selectors; } /** * šŸ“ Generate text-based selectors (WebdriverIO Best Practices) */ generateTextSelectors(text) { const selectors = []; const words = text.split(' ').filter(word => word.length > 2); words.forEach(word => { // 1. **Exact text matching (Best Practice)** selectors.push(`${word}`, `aria/${word}`); // 2. **Partial text matching** selectors.push(`*=${word}`); // 3. **Case-insensitive matching** selectors.push(`.*=${word}`); // 4. **Element-specific text matching** selectors.push(`button=${word}`, `button*=${word}`); selectors.push(`input=${word}`, `input*=${word}`); selectors.push(`a=${word}`, `a*=${word}`); // 5. **ARIA and accessibility attributes** selectors.push(`[aria-label*="${word}"]`, `[aria-labelledby*="${word}"]`); selectors.push(`[title*="${word}"]`, `[placeholder*="${word}"]`); // 6. **Data attributes (Good Practice)** selectors.push(`[data-testid*="${word}"]`, `[data-test="${word}"]`); }); return selectors; } /** * 🧠 Generate semantic selectors based on context */ generateSemanticSelectors(step, pageState) { const selectors = []; // Based on page type if (pageState.pageType === 'login') { selectors.push('[type="email"]', '[type="password"]', '[name="username"]', '[name="password"]'); } if (pageState.pageType === 'form') { selectors.push('input', 'textarea', 'select', 'button[type="submit"]'); } if (pageState.pageType === 'navigation') { selectors.push('nav', '.nav', '.menu', 'a'); } return selectors; } /** * šŸ‘ļø Generate visual pattern selectors */ generateVisualSelectors(step, pageState) { const selectors = []; // Common visual patterns selectors.push('.primary-button', '.secondary-button', '.cta-button', '.submit-button', '.form-input', '.search-input', '.menu-item', '.nav-link'); return selectors; } /** * šŸš€ Execute a specific strategy */ async executeStrategy(step, strategy, testData) { console.log(chalk_1.default.blue(`šŸŽÆ Trying strategy: ${strategy.name}`)); for (const selector of strategy.selectors) { try { const element = await this.findElementWithSelector(selector, step.description); if (element) { await this.performAction(element, step, testData); console.log(chalk_1.default.green(`āœ… Strategy ${strategy.name} succeeded with selector: ${selector}`)); return; } } catch (selectorError) { continue; // Try next selector } } throw new Error(`Strategy ${strategy.name} failed with all selectors`); } /** * šŸ” Enhanced element finding with multiple approaches */ async findElementWithSelector(selector, description) { if (!this.browser) throw new Error('Browser not initialized'); try { // Wait for element with smart timeout const element = await this.smartWaitForElement(selector, 5000); return element; } catch (error) { // Try alternative approaches return await this.findElementWithAlternativeApproaches(selector, description); } } /** * šŸ”„ Alternative element finding approaches (WebdriverIO Best Practices) */ async findElementWithAlternativeApproaches(selector, description) { const words = description.split(' ').filter(word => word.length > 2); // Approach 1: Accessibility Name Selector (Best Practice) for (const word of words) { try { const element = await this.browser.$(`aria/${word}`); if (element && await element.isExisting()) return element; } catch (error) { // Continue to next approach } } // Approach 2: Text-based selectors (User-centric) for (const word of words) { try { // Try exact text match const element = await this.browser.$(`${word}`); if (element && await element.isExisting()) return element; // Try partial text match const partialElement = await this.browser.$(`*=${word}`); if (partialElement && await partialElement.isExisting()) return partialElement; } catch (error) { // Continue to next approach } } // Approach 3: Element-specific text matching for (const word of words) { try { // Try button with text const buttonElement = await this.browser.$(`button=${word}`); if (buttonElement && await buttonElement.isExisting()) return buttonElement; // Try input with text const inputElement = await this.browser.$(`input=${word}`); if (inputElement && await inputElement.isExisting()) return inputElement; // Try link with text const linkElement = await this.browser.$(`a=${word}`); if (linkElement && await linkElement.isExisting()) return linkElement; } catch (error) { // Continue to next approach } } // Approach 4: Data Test ID (Good Practice) for (const word of words) { try { const element = await this.browser.$(`[data-testid*="${word}"]`); if (element && await element.isExisting()) return element; } catch (error) { // Continue to next approach } } // Approach 5: ARIA attributes (Accessibility focused) for (const word of words) { try { const element = await this.browser.$(`[aria-label*="${word}"]`); if (element && await element.isExisting()) return element; } catch (error) { // Continue to next approach } } // Approach 6: Role-based selectors (Fallback) try { const element = await this.browser.$('[role="button"]'); if (element && await element.isExisting()) return element; } catch (error) { // Continue to next approach } // Approach 7: Generic selectors (Last resort) try { const element = await this.browser.$('button, input, a, [role="button"]'); if (element && await element.isExisting()) return element; } catch (error) { // No element found } return null; } /** * šŸŽÆ Perform action on element */ async performAction(element, step, testData) { const action = step.action.toLowerCase(); switch (action) { case 'click': await element.click(); break; case 'type': case 'fill': await element.setValue(step.value || ''); break; case 'clear': await element.clearValue(); break; case 'verify': await this.verifyElement(element, step); break; case 'navigate': // Handle navigation to specific paths if (step.value) { await this.navigateToPath(String(step.value)); } break; default: await element.click(); // Default to click } } /** * āœ… Verify element state */ async verifyElement(element, step) { const isDisplayed = await element.isDisplayed(); if (!isDisplayed) { throw new Error(`Element is not displayed: ${step.description}`); } if (step.value) { const actualValue = await element.getValue(); if (actualValue !== step.value) { throw new Error(`Expected value "${step.value}", got "${actualValue}"`); } } } /** * 🧠 Intelligent error recovery */ async attemptIntelligentRecovery(step, errorMsg, testData) { this.retryCount++; if (this.retryCount > this.maxRetries) { return false; } console.log(chalk_1.default.yellow(`šŸ”„ Attempting intelligent recovery (${this.retryCount}/${this.maxRetries})...`)); try { // Strategy 1: Wait and retry await this.browser.pause(2000); // Strategy 2: Refresh page if navigation-related error if (errorMsg.includes('navigation') || errorMsg.includes('timeout')) { await this.browser.refresh(); await this.browser.pause(3000); } // Strategy 3: Try alternative approach await this.executeIntelligentStep(step, testData); return true; } catch (recoveryError) { console.log(chalk_1.default.red(`āŒ Recovery attempt ${this.retryCount} failed`)); return false; } } /** * šŸŽÆ Check if step is critical */ isCriticalStep(step) { const criticalKeywords = ['login', 'submit', 'save', 'confirm', 'delete']; return criticalKeywords.some(keyword => step.description.toLowerCase().includes(keyword) || step.action.toLowerCase().includes(keyword)); } async takeScreenshot(name) { if (!this.browser) throw new Error('Browser not initialized'); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `${name}_${timestamp}.png`; const screenshotPath = path_1.default.join(process.cwd(), 'screenshots', filename); await fs_extra_1.default.ensureDir(path_1.default.dirname(screenshotPath)); await this.browser.saveScreenshot(screenshotPath); return screenshotPath; } addStep(description, result, status) { this.steps.push({ description, result, status, timestamp: new Date() }); } async executeRecordedSteps(recordedSteps) { console.log(chalk_1.default.blue('⚔ Executing recorded steps...\n')); const errors = []; let success = true; for (let i = 0; i < recordedSteps.length; i++) { const step = recordedSteps[i]; const stepNumber = i + 1; try { console.log(chalk_1.default.cyan(` šŸ”„ Step ${stepNumber}/${recordedSteps.length}: ${step.description}`)); console.log(chalk_1.default.gray(` Action: ${step.action} | Target: ${step.target}${step.value ? ` | Value: ${step.value}` : ''}`)); await this.executeRecordedStep(step); // Add a small pause between steps for better reliability if (i < recordedSteps.length - 1) { await this.browser.pause(500); } this.addStep(step.description, 'Step executed successfully', 'success'); console.log(chalk_1.default.green(` āœ… Step ${stepNumber}/${recordedSteps.length}: ${step.description}`)); } catch (error) { console.error(chalk_1.default.red(` āŒ Step ${stepNumber}/${recordedSteps.length}: ${step.description}:`), error.message); // šŸ”„ AUTOMATIC SCREENSHOT ON FAILURE šŸ”„ try { const failureScreenshotPath = await this.takeScreenshot(`failure-step-${stepNumber}-${step.action}`); this.screenshots.push(failureScreenshotPath); console.log(chalk_1.default.yellow(` šŸ“ø Screenshot taken on failure: ${failureScreenshotPath}`)); } catch (screenshotError) { console.error(chalk_1.default.red(` āŒ Failed to take failure screenshot:`), screenshotError.message); } this.addStep(step.description, error.message, 'failed'); errors.push(`Step ${stepNumber}: ${step.description}: ${error.message}`); success = false; // Continue with next step instead of stopping console.log(chalk_1.default.yellow(` āš ļø Continuing with next step...`)); } } return { success, errors }; } async executeRecordedStep(step) { if (!this.browser) throw new Error('Browser not initialized'); // Validate step has required properties if (!step.action) { throw new Error('Step action is required'); } // Handle missing target property by using description as fallback const target = step.target || step.description || 'unknown element'; const value = step.value || ''; switch (step.action) { case 'navigate': // Handle relative URLs by combining with the test's site URL let navigateUrl = target; if (target.startsWith('/')) { // Use the test's site URL as base for relative paths const testSite = this.getCurrentTestSite(); if (testSite) { navigateUrl = `${testSite}${target}`; } else { // Fallback to current URL if test site not available const currentUrl = await this.browser.getUrl(); const urlObj = new URL(currentUrl); navigateUrl = `${urlObj.protocol}//${urlObj.host}${target}`; } } else if (!target.startsWith('http')) { // If it's not a full URL and not relative, assume it's a path const testSite = this.getCurrentTestSite(); if (testSite) { navigateUrl = `${testSite}/${target}`; } else { // Fallback to current URL if test site not available const currentUrl = await this.browser.getUrl(); const urlObj = new URL(currentUrl); navigateUrl = `${urlObj.protocol}//${urlObj.host}/${target}`; } } console.log(chalk_1.default.cyan(` 🌐 Navigating to: ${navigateUrl}`)); await this.browser.url(navigateUrl); await this.browser.pause(2000); // Wait for page load break; case 'click': console.log(chalk_1.default.yellow(` šŸ” Looking for button: "${target}"`)); // Try to find element by various selectors const clickElement = await this.findButton(target); // Check if element is clickable const isClickable = await clickElement.isClickable(); if (!isClickable) { console.log(chalk_1.default.yellow(` āš ļø Element found but not clickable, trying to scroll into view...`)); await clickElement.scrollIntoView(); await this.browser.pause(500); } console.log(chalk_1.default.green(` āœ… Clicking button: "${target}"`)); await clickElement.click(); // Special handling for login button - wait for page navigation if (target.toLowerCase().includes('login') || target.toLowerCase().includes('submit') || target.toLowerCase().includes('enter')) { console.log(chalk_1.default.yellow(` ā³ Waiting for page navigation after login...`)); await this.browser.pause(3000); // Wait longer for login redirect // Wait for page to load completely await this.browser.waitUntil(async () => { if (!this.browser) return false; const readyState = await this.browser.execute(() => document.readyState); return readyState === 'complete'; }, { timeout: 10000, timeoutMsg: 'Page did not load completely after login' }); console.log(chalk_1.default.green(` āœ… Page loaded successfully after login`)); } else { await this.browser.pause(1000); // Normal wait after click