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
JavaScript
"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