smart-analyzer.jsā¢29.7 kB
/**
* Smart Analysis Module
* Provides intelligent pattern recognition and suggestion capabilities
*/
import fs from 'fs-extra';
import path from 'path';
export class SmartAnalyzer {
constructor(config, fileAnalyzer) {
this.config = config;
this.fileAnalyzer = fileAnalyzer;
this.cache = new Map();
this.analysisHistory = [];
}
// Main smart analysis entry point
async performSmartAnalysis(repositoryPath, scenario) {
try {
const cacheKey = this.generateCacheKey(repositoryPath, scenario);
if (this.cache.has(cacheKey)) {
console.log('Using cached smart analysis result');
return this.cache.get(cacheKey);
}
const analysis = await this.executeAnalysis(repositoryPath, scenario);
// Cache result if configured
if (this.config.get('performance.enableCaching')) {
this.cache.set(cacheKey, analysis);
}
// Store analysis history
this.analysisHistory.push({
timestamp: new Date().toISOString(),
repositoryPath,
scenario: scenario.scenario_title,
cacheKey,
patternCount: analysis.patterns?.length || 0
});
return analysis;
} catch (error) {
console.error('Smart analysis failed:', error);
return this.getFallbackAnalysis(scenario);
}
}
async executeAnalysis(repositoryPath, scenario) {
const analysis = {
timestamp: new Date().toISOString(),
scenario_analysis: {
title: scenario.scenario_title,
complexity: this.calculateComplexity(scenario),
domain: this.identifyDomain(scenario),
test_type: this.classifyTestType(scenario)
},
patterns: [],
recommendations: [],
file_suggestions: {},
reusability_score: 0,
performance_insights: {}
};
// Analyze existing patterns
const existingPatterns = await this.analyzeExistingPatterns(repositoryPath);
analysis.patterns = existingPatterns;
// Generate recommendations
analysis.recommendations = await this.generateRecommendations(scenario, existingPatterns);
// Calculate reusability score
analysis.reusability_score = this.calculateReusabilityScore(scenario, existingPatterns);
// Generate file suggestions
analysis.file_suggestions = await this.generateFileSuggestions(scenario, existingPatterns);
// Performance insights
analysis.performance_insights = this.generatePerformanceInsights(scenario, existingPatterns);
return analysis;
}
async analyzeExistingPatterns(repositoryPath) {
const patterns = [];
try {
// Analyze step patterns
const stepPatterns = await this.analyzeStepPatterns(repositoryPath);
patterns.push(...stepPatterns);
// Analyze page object patterns
const pagePatterns = await this.analyzePageObjectPatterns(repositoryPath);
patterns.push(...pagePatterns);
// Analyze selector patterns
const selectorPatterns = await this.analyzeSelectorPatterns(repositoryPath);
patterns.push(...selectorPatterns);
// Analyze data patterns
const dataPatterns = await this.analyzeDataPatterns(repositoryPath);
patterns.push(...dataPatterns);
} catch (error) {
console.warn('Pattern analysis failed:', error.message);
}
return patterns;
}
async analyzeStepPatterns(repositoryPath) {
const stepFiles = await this.fileAnalyzer.findFilesByPattern(repositoryPath, '**/*.steps.js');
const patterns = [];
for (const file of stepFiles) {
try {
const content = await fs.readFile(file, 'utf8');
const stepDefinitions = this.extractStepDefinitions(content);
patterns.push({
type: 'step_definition',
file: path.relative(repositoryPath, file),
patterns: stepDefinitions,
reusability: this.calculateStepReusability(stepDefinitions),
complexity: this.calculateStepComplexity(stepDefinitions)
});
} catch (error) {
console.warn(`Failed to analyze step file ${file}:`, error.message);
}
}
return patterns;
}
async analyzePageObjectPatterns(repositoryPath) {
const pageFiles = await this.fileAnalyzer.findFilesByPattern(repositoryPath, '**/*.page.js');
const patterns = [];
for (const file of pageFiles) {
try {
const content = await fs.readFile(file, 'utf8');
const analysis = this.analyzePageObject(content);
patterns.push({
type: 'page_object',
file: path.relative(repositoryPath, file),
selectors: analysis.selectors,
methods: analysis.methods,
inheritance: analysis.inheritance,
best_practices: analysis.best_practices
});
} catch (error) {
console.warn(`Failed to analyze page file ${file}:`, error.message);
}
}
return patterns;
}
async analyzeSelectorPatterns(repositoryPath) {
const allFiles = await this.fileAnalyzer.findFilesByPattern(repositoryPath, '**/*.{js,ts}');
const selectorPatterns = new Map();
for (const file of allFiles) {
try {
const content = await fs.readFile(file, 'utf8');
const selectors = this.extractSelectors(content);
selectors.forEach(selector => {
const pattern = this.categorizeSelector(selector);
if (!selectorPatterns.has(pattern.category)) {
selectorPatterns.set(pattern.category, []);
}
selectorPatterns.get(pattern.category).push({
selector: selector.value,
file: path.relative(repositoryPath, file),
confidence: pattern.confidence
});
});
} catch (error) {
// Skip files that can't be read
}
}
return Array.from(selectorPatterns.entries()).map(([category, selectors]) => ({
type: 'selector_pattern',
category,
examples: selectors.slice(0, 10), // Limit examples
frequency: selectors.length,
reliability: this.calculateSelectorReliability(selectors)
}));
}
async analyzeDataPatterns(repositoryPath) {
const dataFiles = await this.fileAnalyzer.findFilesByPattern(repositoryPath, '**/*.data.js');
const patterns = [];
for (const file of dataFiles) {
try {
const content = await fs.readFile(file, 'utf8');
const dataStructures = this.extractDataStructures(content);
patterns.push({
type: 'data_pattern',
file: path.relative(repositoryPath, file),
structures: dataStructures,
consistency: this.calculateDataConsistency(dataStructures),
coverage: this.calculateDataCoverage(dataStructures)
});
} catch (error) {
console.warn(`Failed to analyze data file ${file}:`, error.message);
}
}
return patterns;
}
async generateRecommendations(scenario, existingPatterns) {
const recommendations = [];
// Analyze scenario for recommendations
const scenarioText = scenario.gherkin_syntax.toLowerCase();
// Check for existing similar patterns
const similarPatterns = this.findSimilarPatterns(scenario, existingPatterns);
if (similarPatterns.length > 0) {
recommendations.push({
type: 'reusability',
priority: 'high',
title: 'Reuse Existing Patterns',
description: `Found ${similarPatterns.length} similar existing patterns that can be reused`,
patterns: similarPatterns,
savings: this.calculateTimeSavings(similarPatterns)
});
}
// Check for selector best practices
const selectorRecommendations = this.generateSelectorRecommendations(scenario, existingPatterns);
recommendations.push(...selectorRecommendations);
// Check for architectural improvements
const architecturalRecommendations = this.generateArchitecturalRecommendations(scenario, existingPatterns);
recommendations.push(...architecturalRecommendations);
// Check for performance optimizations
const performanceRecommendations = this.generatePerformanceRecommendations(scenario, existingPatterns);
recommendations.push(...performanceRecommendations);
return recommendations.sort((a, b) => this.getPriorityWeight(b.priority) - this.getPriorityWeight(a.priority));
}
generateSelectorRecommendations(scenario, existingPatterns) {
const recommendations = [];
const selectorPatterns = existingPatterns.filter(p => p.type === 'selector_pattern');
// Recommend most reliable selector patterns
const reliablePatterns = selectorPatterns.filter(p => p.reliability > 0.8);
if (reliablePatterns.length > 0) {
recommendations.push({
type: 'selector_best_practice',
priority: 'medium',
title: 'Use Reliable Selector Patterns',
description: 'Leverage proven selector patterns from your codebase',
patterns: reliablePatterns.map(p => p.category),
example: reliablePatterns[0].examples[0]?.selector
});
}
// Warn about brittle selectors
const brittlePatterns = selectorPatterns.filter(p => p.reliability < 0.4);
if (brittlePatterns.length > 0) {
recommendations.push({
type: 'selector_warning',
priority: 'high',
title: 'Avoid Brittle Selector Patterns',
description: 'Some selector patterns in your codebase are unreliable',
patterns: brittlePatterns.map(p => p.category),
suggestion: 'Consider using data-testid attributes or more stable selectors'
});
}
return recommendations;
}
generateArchitecturalRecommendations(scenario, existingPatterns) {
const recommendations = [];
const pagePatterns = existingPatterns.filter(p => p.type === 'page_object');
// Check for consistent page object structure
if (pagePatterns.length > 0) {
const structures = pagePatterns.map(p => this.analyzePageStructure(p));
const consistencyScore = this.calculateStructuralConsistency(structures);
if (consistencyScore < 0.7) {
recommendations.push({
type: 'architecture',
priority: 'medium',
title: 'Improve Page Object Consistency',
description: 'Page objects show inconsistent structure patterns',
suggestion: 'Establish and follow consistent page object patterns',
current_score: consistencyScore
});
}
}
// Check for inheritance opportunities
const inheritanceOpportunities = this.findInheritanceOpportunities(pagePatterns);
if (inheritanceOpportunities.length > 0) {
recommendations.push({
type: 'architecture',
priority: 'low',
title: 'Consider Base Page Pattern',
description: 'Common functionality could be extracted to base classes',
opportunities: inheritanceOpportunities
});
}
return recommendations;
}
generatePerformanceRecommendations(scenario, existingPatterns) {
const recommendations = [];
// Check for potential performance issues
const stepPatterns = existingPatterns.filter(p => p.type === 'step_definition');
const complexSteps = stepPatterns.filter(p => p.complexity > 0.8);
if (complexSteps.length > 0) {
recommendations.push({
type: 'performance',
priority: 'medium',
title: 'Optimize Complex Steps',
description: 'Some step definitions are overly complex',
steps: complexSteps.map(s => s.file),
suggestion: 'Break down complex steps into smaller, focused steps'
});
}
// Check for wait strategies
const scenarioText = scenario.gherkin_syntax.toLowerCase();
if (scenarioText.includes('wait') || scenarioText.includes('load')) {
recommendations.push({
type: 'performance',
priority: 'high',
title: 'Implement Smart Wait Strategies',
description: 'Scenario involves waiting - use explicit waits over implicit delays',
suggestion: 'Use WebDriverIO waitUntil() methods for better performance'
});
}
return recommendations;
}
calculateReusabilityScore(scenario, existingPatterns) {
let score = 0;
let maxScore = 100;
// Check for reusable steps
const stepPatterns = existingPatterns.filter(p => p.type === 'step_definition');
const scenarioSteps = this.extractScenarioSteps(scenario.gherkin_syntax);
let reusableSteps = 0;
scenarioSteps.forEach(step => {
stepPatterns.forEach(pattern => {
if (pattern.patterns.some(p => this.calculateStepSimilarity(step, p) > 0.8)) {
reusableSteps++;
}
});
});
score += (reusableSteps / Math.max(scenarioSteps.length, 1)) * 40;
// Check for reusable selectors
const selectorPatterns = existingPatterns.filter(p => p.type === 'selector_pattern');
if (selectorPatterns.length > 0) {
score += 20; // Base score for having selector patterns
}
// Check for consistent patterns
const consistency = this.calculatePatternConsistency(existingPatterns);
score += consistency * 40;
return Math.min(score, maxScore);
}
async generateFileSuggestions(scenario, existingPatterns) {
const suggestions = {
feature: await this.suggestFeatureFileStructure(scenario, existingPatterns),
steps: await this.suggestStepsFileStructure(scenario, existingPatterns),
page: await this.suggestPageFileStructure(scenario, existingPatterns),
component: await this.suggestComponentFileStructure(scenario, existingPatterns)
};
return suggestions;
}
async suggestFeatureFileStructure(scenario, existingPatterns) {
const suggestion = {
recommended_location: this.suggestFeatureLocation(scenario),
naming_convention: this.suggestFeatureNaming(scenario),
tags: this.suggestFeatureTags(scenario),
structure: {
include_background: this.shouldIncludeBackground(scenario),
include_examples: this.shouldIncludeExamples(scenario),
scenario_outline: this.shouldUseScenarioOutline(scenario)
}
};
return suggestion;
}
async suggestStepsFileStructure(scenario, existingPatterns) {
const stepPatterns = existingPatterns.filter(p => p.type === 'step_definition');
return {
recommended_location: this.suggestStepsLocation(scenario),
naming_convention: this.suggestStepsNaming(scenario),
reusable_steps: this.findReusableSteps(scenario, stepPatterns),
new_steps_needed: this.identifyNewStepsNeeded(scenario, stepPatterns),
imports: this.suggestStepsImports(stepPatterns)
};
}
async suggestPageFileStructure(scenario, existingPatterns) {
const pagePatterns = existingPatterns.filter(p => p.type === 'page_object');
return {
recommended_location: this.suggestPageLocation(scenario),
naming_convention: this.suggestPageNaming(scenario),
inheritance: this.suggestPageInheritance(pagePatterns),
methods: this.suggestPageMethods(scenario),
selectors: this.suggestPageSelectors(scenario, existingPatterns)
};
}
async suggestComponentFileStructure(scenario, existingPatterns) {
const dataPatterns = existingPatterns.filter(p => p.type === 'data_pattern');
return {
recommended_location: this.suggestComponentLocation(scenario),
naming_convention: this.suggestComponentNaming(scenario),
data_structure: this.suggestDataStructure(scenario, dataPatterns),
validation: this.suggestDataValidation(scenario),
utilities: this.suggestDataUtilities(scenario)
};
}
generatePerformanceInsights(scenario, existingPatterns) {
const insights = {
estimated_execution_time: this.estimateExecutionTime(scenario),
complexity_metrics: this.calculateComplexityMetrics(scenario),
bottleneck_analysis: this.identifyPotentialBottlenecks(scenario),
optimization_opportunities: this.findOptimizationOpportunities(scenario, existingPatterns),
resource_usage: this.estimateResourceUsage(scenario)
};
return insights;
}
// Utility methods for analysis
calculateComplexity(scenario) {
const steps = this.extractScenarioSteps(scenario.gherkin_syntax);
const uniqueActions = new Set();
const interactions = [];
steps.forEach(step => {
const action = this.extractActionType(step);
uniqueActions.add(action);
if (this.isUserInteraction(step)) {
interactions.push(action);
}
});
const complexity = {
step_count: steps.length,
unique_actions: uniqueActions.size,
interaction_count: interactions.length,
has_loops: this.hasLoops(scenario.gherkin_syntax),
has_conditions: this.hasConditions(scenario.gherkin_syntax),
score: this.calculateComplexityScore(steps.length, uniqueActions.size, interactions.length)
};
return complexity;
}
identifyDomain(scenario) {
const text = (scenario.scenario_title + ' ' + scenario.gherkin_syntax).toLowerCase();
const domains = {
'ecommerce': ['buy', 'cart', 'checkout', 'payment', 'order', 'product'],
'authentication': ['login', 'register', 'password', 'user', 'account'],
'admin': ['admin', 'manage', 'configure', 'settings', 'dashboard'],
'api': ['api', 'endpoint', 'request', 'response', 'json'],
'form': ['form', 'input', 'submit', 'field', 'validation'],
'navigation': ['menu', 'navigate', 'page', 'link', 'redirect']
};
for (const [domain, keywords] of Object.entries(domains)) {
if (keywords.some(keyword => text.includes(keyword))) {
return domain;
}
}
return 'general';
}
classifyTestType(scenario) {
const text = scenario.gherkin_syntax.toLowerCase();
if (text.includes('api') || text.includes('endpoint')) return 'api';
if (text.includes('ui') || text.includes('browser') || text.includes('click')) return 'ui';
if (text.includes('mobile') || text.includes('app')) return 'mobile';
if (text.includes('performance') || text.includes('load')) return 'performance';
if (text.includes('security') || text.includes('auth')) return 'security';
return 'functional';
}
extractStepDefinitions(content) {
const stepRegex = /(Given|When|Then|And|But)\s*\(\s*['"](.*?)['"]/g;
const definitions = [];
let match;
while ((match = stepRegex.exec(content)) !== null) {
definitions.push({
type: match[1],
pattern: match[2],
full_match: match[0]
});
}
return definitions;
}
analyzePageObject(content) {
const analysis = {
selectors: this.extractPageSelectors(content),
methods: this.extractPageMethods(content),
inheritance: this.checkInheritance(content),
best_practices: this.checkPageObjectBestPractices(content)
};
return analysis;
}
extractSelectors(content) {
const selectorPatterns = [
/\$\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
/\$\$\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
/querySelector\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
/getElementById\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g
];
const selectors = [];
selectorPatterns.forEach(pattern => {
let match;
while ((match = pattern.exec(content)) !== null) {
selectors.push({
value: match[1],
type: this.determineSelectorType(match[1]),
line: content.substring(0, match.index).split('\n').length
});
}
});
return selectors;
}
categorizeSelector(selector) {
const value = selector.value;
let category = 'unknown';
let confidence = 0.5;
if (value.startsWith('[data-testid')) {
category = 'data-testid';
confidence = 0.9;
} else if (value.startsWith('#')) {
category = 'id';
confidence = 0.8;
} else if (value.startsWith('.')) {
category = 'class';
confidence = 0.6;
} else if (value.includes('[') && value.includes(']')) {
category = 'attribute';
confidence = 0.7;
} else if (/^[a-zA-Z][a-zA-Z0-9]*$/.test(value)) {
category = 'tag';
confidence = 0.4;
} else {
category = 'complex';
confidence = 0.3;
}
return { category, confidence };
}
extractDataStructures(content) {
const exportRegex = /export\s+(?:const|let|var)\s+(\w+)\s*=\s*({[\s\S]*?});?/g;
const structures = [];
let match;
while ((match = exportRegex.exec(content)) !== null) {
try {
const name = match[1];
const objectContent = match[2];
const structure = this.parseObjectStructure(objectContent);
structures.push({ name, structure });
} catch (error) {
// Skip invalid structures
}
}
return structures;
}
findSimilarPatterns(scenario, existingPatterns) {
const similar = [];
const scenarioSteps = this.extractScenarioSteps(scenario.gherkin_syntax);
existingPatterns.forEach(pattern => {
if (pattern.type === 'step_definition') {
pattern.patterns.forEach(stepPattern => {
scenarioSteps.forEach(scenarioStep => {
const similarity = this.calculateStepSimilarity(scenarioStep, stepPattern.pattern);
if (similarity > this.config.getStepSimilarityThreshold()) {
similar.push({
type: 'step',
pattern: stepPattern,
similarity,
scenario_step: scenarioStep,
file: pattern.file
});
}
});
});
}
});
return similar;
}
calculateStepSimilarity(step1, step2) {
const normalize = (str) => str.toLowerCase().replace(/['"]/g, '').replace(/\s+/g, ' ').trim();
const norm1 = normalize(step1);
const norm2 = normalize(step2);
if (norm1 === norm2) return 1.0;
const words1 = norm1.split(' ');
const words2 = norm2.split(' ');
const intersection = words1.filter(word => words2.includes(word));
const union = [...new Set([...words1, ...words2])];
return intersection.length / union.length;
}
extractScenarioSteps(gherkinSyntax) {
const stepRegex = /(Given|When|Then|And|But)\s+(.+)/g;
const steps = [];
let match;
while ((match = stepRegex.exec(gherkinSyntax)) !== null) {
steps.push(match[2].trim());
}
return steps;
}
// Helper methods for various calculations and utilities
calculateStepReusability(stepDefinitions) {
// Calculate based on genericity of step patterns
let score = 0;
stepDefinitions.forEach(step => {
if (step.pattern.includes('.*') || step.pattern.includes('\\w+')) {
score += 0.8;
} else if (step.pattern.length < 50) {
score += 0.6;
} else {
score += 0.3;
}
});
return score / Math.max(stepDefinitions.length, 1);
}
calculateStepComplexity(stepDefinitions) {
let totalComplexity = 0;
stepDefinitions.forEach(step => {
const patternComplexity = (step.pattern.match(/[.*+?^${}()|[\]\\]/g) || []).length;
totalComplexity += Math.min(patternComplexity / 10, 1);
});
return totalComplexity / Math.max(stepDefinitions.length, 1);
}
calculateSelectorReliability(selectors) {
let totalReliability = 0;
selectors.forEach(selector => {
totalReliability += selector.confidence || 0.5;
});
return totalReliability / Math.max(selectors.length, 1);
}
calculateDataConsistency(dataStructures) {
if (dataStructures.length < 2) return 1.0;
const fieldSets = dataStructures.map(ds => new Set(Object.keys(ds.structure || {})));
const allFields = new Set();
fieldSets.forEach(fields => fields.forEach(field => allFields.add(field)));
let consistencyScore = 0;
for (let i = 0; i < fieldSets.length - 1; i++) {
for (let j = i + 1; j < fieldSets.length; j++) {
const intersection = new Set([...fieldSets[i]].filter(x => fieldSets[j].has(x)));
const union = new Set([...fieldSets[i], ...fieldSets[j]]);
consistencyScore += intersection.size / union.size;
}
}
return consistencyScore / ((fieldSets.length * (fieldSets.length - 1)) / 2);
}
calculateDataCoverage(dataStructures) {
const allFields = new Set();
const coveredFields = new Set();
dataStructures.forEach(ds => {
const structure = ds.structure || {};
Object.keys(structure).forEach(field => {
allFields.add(field);
if (structure[field] !== null && structure[field] !== undefined) {
coveredFields.add(field);
}
});
});
return allFields.size > 0 ? coveredFields.size / allFields.size : 1.0;
}
generateCacheKey(repositoryPath, scenario) {
const keyData = {
path: repositoryPath,
title: scenario.scenario_title,
gherkin: scenario.gherkin_syntax.substring(0, 100) // First 100 chars for uniqueness
};
return Buffer.from(JSON.stringify(keyData)).toString('base64');
}
getFallbackAnalysis(scenario) {
return {
timestamp: new Date().toISOString(),
scenario_analysis: {
title: scenario.scenario_title,
complexity: { score: 0.5 },
domain: 'general',
test_type: 'functional'
},
patterns: [],
recommendations: [{
type: 'fallback',
priority: 'low',
title: 'Basic Analysis Only',
description: 'Full smart analysis unavailable, using basic patterns'
}],
file_suggestions: {},
reusability_score: 0,
performance_insights: {}
};
}
getPriorityWeight(priority) {
const weights = { high: 3, medium: 2, low: 1 };
return weights[priority] || 1;
}
// Additional utility methods would be implemented here
// Due to space constraints, showing key structure and main methods
clearCache() {
this.cache.clear();
console.log('Smart analysis cache cleared');
}
getAnalysisHistory() {
return this.analysisHistory;
}
exportAnalysis(analysis, format = 'json') {
if (format === 'json') {
return JSON.stringify(analysis, null, 2);
}
// Add other export formats as needed
return analysis;
}
}