UNPKG

smartui-migration-tool

Version:

Enterprise-grade CLI tool for migrating visual testing platforms to LambdaTest SmartUI

652 lines (545 loc) โ€ข 20.8 kB
const { Command, Flags } = require('@oclif/core'); const chalk = require('chalk'); const { ASCIILogos } = require('../utils/ascii-logos'); // Simplified modules for validation const fs = require('fs-extra'); const path = require('path'); const glob = require('glob'); class Validate extends Command { static description = 'Validate migration plan and check for issues'; static flags = { path: Flags.string({ char: 'p', description: 'Project path to validate' }), plan: Flags.string({ description: 'Migration plan file to validate' }), 'dry-run': Flags.boolean({ description: 'Validate without making changes' }), strict: Flags.boolean({ description: 'Use strict validation rules' }), 'fix-issues': Flags.boolean({ description: 'Automatically fix common issues' }), output: Flags.string({ char: 'o', description: 'Output validation report file' }), format: Flags.string({ description: 'Output format (json|yaml|table)', default: 'table' }) }; async run() { console.log(ASCIILogos.getMinimalLogo()); console.log(chalk.cyan.bold('\nโœ… SmartUI Migration Validation\n')); const { flags } = await this.parse(Validate); const projectPath = flags.path || process.cwd(); console.log(chalk.yellow.bold('๐Ÿ“ Validating Project:')); console.log(chalk.white(projectPath)); try { // Initialize validation modules const modules = await this.initializeValidationModules(); // Perform comprehensive validation const validation = await this.performValidation(projectPath, modules, flags); // Display validation results this.displayValidationResults(validation, flags); // Save validation report if requested if (flags.output) { await this.saveValidationReport(validation, flags.output, flags.format); } // Exit with appropriate code if (validation.issues.length > 0 && flags.strict) { this.exit(1); } } catch (error) { console.error(chalk.red.bold('\nโŒ Validation failed:')); console.error(chalk.red(error.message)); this.exit(1); } } async initializeValidationModules() { return { validator: this.createSimpleValidator(), patternMatcher: this.createSimplePatternMatcher(), codeTransformer: this.createSimpleCodeTransformer() }; } createSimpleValidator() { return { async validateProject(projectPath) { return { valid: true, issues: [] }; } }; } createSimplePatternMatcher() { return { async matchPatterns(projectPath, options) { try { const files = glob.sync(`${projectPath}/**/*.{js,ts,jsx,tsx,py,java,cs}`, { nodir: true }); const matches = []; for (const file of files.slice(0, 20)) { const content = await fs.readFile(file, 'utf8'); const fileMatches = this.findPatternsInContent(content, file); matches.push(...fileMatches); } return { matches: matches }; } catch (error) { return { matches: [] }; } } }; } createSimpleCodeTransformer() { return { async transformFile(filePath, transformations) { return { success: true, changes: [] }; } }; } async performValidation(projectPath, modules, flags) { const validation = { projectPath, timestamp: new Date().toISOString(), status: 'pending', issues: [], warnings: [], recommendations: [], statistics: {} }; console.log(chalk.blue(' ๐Ÿ” Validating project structure...')); const structureValidation = await this.validateProjectStructure(projectPath, modules); validation.issues.push(...structureValidation.issues); validation.warnings.push(...structureValidation.warnings); console.log(chalk.blue(' ๐Ÿง  Validating patterns...')); const patternValidation = await this.validatePatterns(projectPath, modules); validation.issues.push(...patternValidation.issues); validation.warnings.push(...patternValidation.warnings); console.log(chalk.blue(' ๐Ÿ“ฆ Validating dependencies...')); const dependencyValidation = await this.validateDependencies(projectPath, modules); validation.issues.push(...dependencyValidation.issues); validation.warnings.push(...dependencyValidation.warnings); console.log(chalk.blue(' โš™๏ธ Validating configuration...')); const configValidation = await this.validateConfiguration(projectPath, modules); validation.issues.push(...configValidation.issues); validation.warnings.push(...configValidation.warnings); console.log(chalk.blue(' ๐Ÿงช Validating test files...')); const testValidation = await this.validateTestFiles(projectPath, modules); validation.issues.push(...testValidation.issues); validation.warnings.push(...testValidation.warnings); // Generate recommendations validation.recommendations = await this.generateRecommendations(validation, modules); // Calculate statistics validation.statistics = this.calculateStatistics(validation); // Determine overall status validation.status = this.determineStatus(validation, flags); return validation; } async validateProjectStructure(projectPath, modules) { const issues = []; const warnings = []; try { // Check if project has required files const fs = require('fs-extra'); if (!await fs.pathExists(`${projectPath}/package.json`)) { issues.push({ type: 'structure', severity: 'error', message: 'package.json not found', file: 'package.json', suggestion: 'Create a package.json file for your project' }); } // Check for test directory const testDirs = ['tests', 'test', 'cypress', 'e2e', 'spec']; let hasTestDir = false; for (const dir of testDirs) { if (await fs.pathExists(`${projectPath}/${dir}`)) { hasTestDir = true; break; } } if (!hasTestDir) { warnings.push({ type: 'structure', severity: 'warning', message: 'No test directory found', suggestion: 'Create a test directory for your visual tests' }); } } catch (error) { issues.push({ type: 'structure', severity: 'error', message: `Structure validation failed: ${error.message}`, suggestion: 'Check project permissions and structure' }); } return { issues, warnings }; } async validatePatterns(projectPath, modules) { const issues = []; const warnings = []; try { // Check for visual testing patterns const patterns = this.getValidationPatterns(); const matches = await modules.patternMatcher.matchPatterns(projectPath, { patterns: patterns, confidence: 0.7, context: 'validation' }); // Check for outdated patterns const outdatedPatterns = matches.matches?.filter(m => m.platform && m.platform !== 'smartui') || []; if (outdatedPatterns.length > 0) { issues.push({ type: 'pattern', severity: 'error', message: `Found ${outdatedPatterns.length} outdated visual testing patterns`, details: outdatedPatterns.map(p => `${p.pattern} (${p.platform})`), suggestion: 'Update visual testing patterns to SmartUI' }); } // Check for missing SmartUI patterns const smartuiPatterns = matches.matches?.filter(m => m.platform === 'smartui') || []; if (smartuiPatterns.length === 0) { warnings.push({ type: 'pattern', severity: 'warning', message: 'No SmartUI patterns found', suggestion: 'Add SmartUI visual testing patterns to your tests' }); } } catch (error) { warnings.push({ type: 'pattern', severity: 'warning', message: `Pattern validation failed: ${error.message}`, suggestion: 'Check pattern matching configuration' }); } return { issues, warnings }; } async validateDependencies(projectPath, modules) { const issues = []; const warnings = []; try { const fs = require('fs-extra'); const packageJsonPath = `${projectPath}/package.json`; if (await fs.pathExists(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; // Check for outdated visual testing dependencies const outdatedDeps = this.checkOutdatedDependencies(dependencies); if (outdatedDeps.length > 0) { issues.push({ type: 'dependency', severity: 'error', message: `Found ${outdatedDeps.length} outdated dependencies`, details: outdatedDeps, suggestion: 'Update dependencies to latest versions' }); } // Check for missing SmartUI dependencies if (!dependencies['@lambdatest/smartui-cli']) { warnings.push({ type: 'dependency', severity: 'warning', message: 'SmartUI CLI not found in dependencies', suggestion: 'Install @lambdatest/smartui-cli package' }); } // Check for conflicting dependencies const conflicts = this.checkDependencyConflicts(dependencies); if (conflicts.length > 0) { warnings.push({ type: 'dependency', severity: 'warning', message: `Found ${conflicts.length} potential dependency conflicts`, details: conflicts, suggestion: 'Resolve dependency conflicts' }); } } } catch (error) { warnings.push({ type: 'dependency', severity: 'warning', message: `Dependency validation failed: ${error.message}`, suggestion: 'Check package.json format and dependencies' }); } return { issues, warnings }; } async validateConfiguration(projectPath, modules) { const issues = []; const warnings = []; try { const fs = require('fs-extra'); // Check for SmartUI configuration const configFiles = ['smartui.config.js', 'smartui.config.json', '.smartui.json']; let hasConfig = false; for (const configFile of configFiles) { if (await fs.pathExists(`${projectPath}/${configFile}`)) { hasConfig = true; break; } } if (!hasConfig) { warnings.push({ type: 'configuration', severity: 'warning', message: 'No SmartUI configuration found', suggestion: 'Create a smartui.config.js file' }); } // Check for test framework configuration const testConfigs = ['cypress.config.js', 'playwright.config.js', 'jest.config.js']; let hasTestConfig = false; for (const configFile of testConfigs) { if (await fs.pathExists(`${projectPath}/${configFile}`)) { hasTestConfig = true; break; } } if (!hasTestConfig) { warnings.push({ type: 'configuration', severity: 'warning', message: 'No test framework configuration found', suggestion: 'Configure your test framework (Cypress, Playwright, etc.)' }); } } catch (error) { warnings.push({ type: 'configuration', severity: 'warning', message: `Configuration validation failed: ${error.message}`, suggestion: 'Check configuration files' }); } return { issues, warnings }; } async validateTestFiles(projectPath, modules) { const issues = []; const warnings = []; try { const fs = require('fs-extra'); const glob = require('glob'); // Find test files const testFiles = glob.sync(`${projectPath}/**/*.{test,spec}.{js,ts,jsx,tsx}`, { nodir: true }); if (testFiles.length === 0) { warnings.push({ type: 'test', severity: 'warning', message: 'No test files found', suggestion: 'Create test files for your visual tests' }); } else { // Check for visual testing in test files let hasVisualTests = false; for (const testFile of testFiles) { const content = await fs.readFile(testFile, 'utf8'); if (content.includes('visual') || content.includes('snapshot') || content.includes('screenshot')) { hasVisualTests = true; break; } } if (!hasVisualTests) { warnings.push({ type: 'test', severity: 'warning', message: 'No visual tests found in test files', suggestion: 'Add visual testing to your test files' }); } } } catch (error) { warnings.push({ type: 'test', severity: 'warning', message: `Test validation failed: ${error.message}`, suggestion: 'Check test file structure and content' }); } return { issues, warnings }; } async generateRecommendations(validation, modules) { const recommendations = []; // Generate recommendations based on issues and warnings if (validation.issues.length > 0) { recommendations.push({ type: 'critical', message: 'Fix critical issues before proceeding with migration', priority: 'high' }); } if (validation.warnings.length > 0) { recommendations.push({ type: 'improvement', message: 'Address warnings to improve migration quality', priority: 'medium' }); } // Add specific recommendations based on validation results const hasStructureIssues = validation.issues.some(i => i.type === 'structure'); if (hasStructureIssues) { recommendations.push({ type: 'structure', message: 'Ensure project has proper structure with package.json and test directories', priority: 'high' }); } const hasPatternIssues = validation.issues.some(i => i.type === 'pattern'); if (hasPatternIssues) { recommendations.push({ type: 'pattern', message: 'Update visual testing patterns to SmartUI format', priority: 'high' }); } return recommendations; } calculateStatistics(validation) { return { totalIssues: validation.issues.length, totalWarnings: validation.warnings.length, totalRecommendations: validation.recommendations.length, issuesByType: this.groupByType(validation.issues), warningsByType: this.groupByType(validation.warnings), severityBreakdown: this.getSeverityBreakdown(validation.issues) }; } groupByType(items) { return items.reduce((groups, item) => { const type = item.type; groups[type] = (groups[type] || 0) + 1; return groups; }, {}); } getSeverityBreakdown(issues) { return issues.reduce((breakdown, issue) => { const severity = issue.severity; breakdown[severity] = (breakdown[severity] || 0) + 1; return breakdown; }, {}); } determineStatus(validation, flags) { if (validation.issues.length > 0) { return flags.strict ? 'failed' : 'warning'; } if (validation.warnings.length > 0) { return 'warning'; } return 'passed'; } getValidationPatterns() { return [ // Outdated patterns { id: 'percy-snapshot', pattern: 'percy\\.snapshot', platform: 'percy' }, { id: 'applitools-eyes', pattern: 'eyes\\.', platform: 'applitools' }, { id: 'sauce-visual', pattern: 'sauce.*visual', platform: 'sauce-labs' }, // SmartUI patterns { id: 'smartui-visual', pattern: 'smartui\\.visual', platform: 'smartui' }, { id: 'smartui-snapshot', pattern: 'smartui\\.snapshot', platform: 'smartui' } ]; } checkOutdatedDependencies(dependencies) { const outdated = []; const outdatedDeps = ['@percy/cli', '@percy/cypress', '@applitools/eyes-cypress', 'sauce-connect']; for (const dep of outdatedDeps) { if (dependencies[dep]) { outdated.push(dep); } } return outdated; } checkDependencyConflicts(dependencies) { const conflicts = []; // Check for conflicting visual testing tools const visualTools = ['@percy/cli', '@applitools/eyes-cypress', '@lambdatest/smartui-cli']; const installedTools = visualTools.filter(tool => dependencies[tool]); if (installedTools.length > 1) { conflicts.push(`Multiple visual testing tools: ${installedTools.join(', ')}`); } return conflicts; } displayValidationResults(validation, flags) { console.log(chalk.yellow.bold('\n๐Ÿ“Š Validation Results:')); // Overall status const statusColor = { 'passed': chalk.green, 'warning': chalk.yellow, 'failed': chalk.red }; const statusIcon = { 'passed': 'โœ…', 'warning': 'โš ๏ธ', 'failed': 'โŒ' }; console.log(chalk.white(`\nStatus: ${statusIcon[validation.status]} ${statusColor[validation.status](validation.status.toUpperCase())}`)); // Statistics console.log(chalk.blue('\n๐Ÿ“ˆ Statistics:')); console.log(chalk.white(` โ€ข Issues: ${validation.statistics.totalIssues}`)); console.log(chalk.white(` โ€ข Warnings: ${validation.statistics.totalWarnings}`)); console.log(chalk.white(` โ€ข Recommendations: ${validation.statistics.totalRecommendations}`)); // Issues if (validation.issues.length > 0) { console.log(chalk.red.bold('\nโŒ Issues:')); validation.issues.forEach((issue, index) => { console.log(chalk.red(` ${index + 1}. ${issue.message}`)); if (issue.suggestion) { console.log(chalk.gray(` ๐Ÿ’ก ${issue.suggestion}`)); } }); } // Warnings if (validation.warnings.length > 0) { console.log(chalk.yellow.bold('\nโš ๏ธ Warnings:')); validation.warnings.forEach((warning, index) => { console.log(chalk.yellow(` ${index + 1}. ${warning.message}`)); if (warning.suggestion) { console.log(chalk.gray(` ๐Ÿ’ก ${warning.suggestion}`)); } }); } // Recommendations if (validation.recommendations.length > 0) { console.log(chalk.blue.bold('\n๐Ÿ’ก Recommendations:')); validation.recommendations.forEach((rec, index) => { const priorityColor = { 'high': chalk.red, 'medium': chalk.yellow, 'low': chalk.green }; console.log(chalk.white(` ${index + 1}. ${rec.message}`)); console.log(priorityColor[rec.priority](` Priority: ${rec.priority.toUpperCase()}`)); }); } // Summary if (validation.status === 'passed') { console.log(chalk.green.bold('\n๐ŸŽ‰ Validation passed! Your project is ready for migration.')); } else if (validation.status === 'warning') { console.log(chalk.yellow.bold('\nโš ๏ธ Validation completed with warnings. Review and fix issues for optimal migration.')); } else { console.log(chalk.red.bold('\nโŒ Validation failed. Please fix critical issues before proceeding.')); } } async saveValidationReport(validation, outputPath, format) { const fs = require('fs-extra'); let content; if (format === 'json') { content = JSON.stringify(validation, null, 2); } else if (format === 'yaml') { const yaml = require('js-yaml'); content = yaml.dump(validation); } else { content = JSON.stringify(validation, null, 2); } await fs.writeFile(outputPath, content); console.log(chalk.green(`\nโœ… Validation report saved to: ${outputPath}`)); } findPatternsInContent(content, filePath) { const matches = []; const patterns = this.getValidationPatterns(); for (const pattern of patterns) { const regex = new RegExp(pattern.pattern, 'gi'); const matches_found = content.match(regex); if (matches_found) { matches.push({ pattern: pattern.pattern, platform: pattern.platform, file: filePath, matches: matches_found.length }); } } return matches; } } module.exports.default = Validate;