smartui-migration-tool
Version:
Enterprise-grade CLI tool for migrating visual testing platforms to LambdaTest SmartUI
652 lines (545 loc) โข 20.8 kB
JavaScript
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;