embedia
Version:
Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys
748 lines (628 loc) โข 22.5 kB
JavaScript
/**
* Functional Validator - Phase 2 Implementation
*
* ARCHITECTURAL PRINCIPLE: End-to-End Functionality Validation
*
* This validator performs comprehensive end-to-end testing to ensure
* the chatbot actually works in the browser, not just builds successfully.
* It validates API communication, component mounting, and user interaction.
*/
const { execSync, spawn } = require('child_process');
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const logger = require('../utils/logger');
class FunctionalValidator {
constructor(projectPath, contract) {
this.projectPath = projectPath;
this.contract = contract;
this.framework = contract.framework.name;
this.devServerProcess = null;
this.testResults = {
componentMounting: false,
apiCommunication: false,
userInteraction: false,
performance: {},
errors: [],
warnings: []
};
}
/**
* Run comprehensive functional validation
* @returns {Promise<Object>} Validation results
*/
async runFunctionalValidation() {
console.log(chalk.cyan('๐งช Running Functional Validation Suite...'));
const results = {
success: false,
score: 0,
maxScore: 100,
tests: [],
startTime: Date.now(),
endTime: null,
duration: null,
summary: {
passed: 0,
failed: 0,
skipped: 0
}
};
try {
// 1. Pre-validation checks
const preCheck = await this.runPreValidationChecks();
results.tests.push(preCheck);
if (!preCheck.success) {
console.log(chalk.yellow('โ ๏ธ Pre-validation checks failed, skipping functional tests'));
results.summary.skipped = 5;
return this.finalizeResults(results);
}
// 2. Component mounting validation
const mountingTest = await this.testComponentMounting();
results.tests.push(mountingTest);
if (mountingTest.success) results.summary.passed++;
else results.summary.failed++;
// 3. API communication validation
const apiTest = await this.testAPICommunication();
results.tests.push(apiTest);
if (apiTest.success) results.summary.passed++;
else results.summary.failed++;
// 4. Build and start validation
const buildTest = await this.testBuildAndStart();
results.tests.push(buildTest);
if (buildTest.success) results.summary.passed++;
else results.summary.failed++;
// 5. Browser compatibility validation (if possible)
const browserTest = await this.testBrowserCompatibility();
results.tests.push(browserTest);
if (browserTest.success) results.summary.passed++;
else if (browserTest.skipped) results.summary.skipped++;
else results.summary.failed++;
// 6. Performance validation
const perfTest = await this.testPerformance();
results.tests.push(perfTest);
if (perfTest.success) results.summary.passed++;
else results.summary.failed++;
// Calculate success
results.success = results.summary.failed === 0;
results.score = Math.round((results.summary.passed / (results.summary.passed + results.summary.failed)) * 100) || 0;
} catch (error) {
results.tests.push({
name: 'Functional Validation',
success: false,
error: error.message,
duration: 0
});
results.summary.failed++;
} finally {
// Cleanup
await this.cleanup();
}
return this.finalizeResults(results);
}
/**
* Run pre-validation checks to ensure functional testing is possible
*/
async runPreValidationChecks() {
const test = {
name: 'Pre-validation Checks',
success: false,
startTime: Date.now(),
checks: [],
errors: []
};
try {
// Check if package.json exists
const packageJsonPath = path.join(this.projectPath, 'package.json');
if (!await fs.pathExists(packageJsonPath)) {
test.errors.push('package.json not found');
} else {
test.checks.push('โ package.json exists');
}
// Check if dependencies are installed
const nodeModulesPath = path.join(this.projectPath, 'node_modules');
if (!await fs.pathExists(nodeModulesPath)) {
test.errors.push('node_modules not found - run npm install');
} else {
test.checks.push('โ Dependencies installed');
}
// Check for required scripts
const packageJson = await fs.readJson(packageJsonPath).catch(() => ({}));
const scripts = packageJson.scripts || {};
if (!scripts.build && !scripts['build:next'] && this.framework === 'nextjs') {
test.errors.push('No build script found');
} else {
test.checks.push('โ Build script available');
}
if (!scripts.dev && !scripts.start && !scripts['dev:next']) {
test.errors.push('No dev server script found');
} else {
test.checks.push('โ Dev server script available');
}
// Check for API routes
const apiPaths = [
'app/api/embedia/chat/route.js',
'app/api/embedia/chat/route.ts',
'src/app/api/embedia/chat/route.js',
'src/app/api/embedia/chat/route.ts',
'pages/api/embedia/chat.js',
'pages/api/embedia/chat.ts',
'src/pages/api/embedia/chat.js',
'src/pages/api/embedia/chat.ts'
];
let apiFound = false;
for (const apiPath of apiPaths) {
if (await fs.pathExists(path.join(this.projectPath, apiPath))) {
apiFound = true;
test.checks.push(`โ API route found: ${apiPath}`);
break;
}
}
if (!apiFound) {
test.errors.push('No API route found');
}
test.success = test.errors.length === 0;
} catch (error) {
test.errors.push(error.message);
}
test.duration = Date.now() - test.startTime;
console.log(chalk.gray(` Pre-validation: ${test.success ? 'โ' : 'โ'} (${test.checks.length} checks)`));
return test;
}
/**
* Test component mounting by checking generated files
*/
async testComponentMounting() {
const test = {
name: 'Component Mounting',
success: false,
startTime: Date.now(),
components: [],
errors: []
};
try {
// Check for component files
const componentPaths = [
'components/generated/embedia-chat/',
'components/EmbediaChatWrapper.jsx',
'components/EmbediaChatWrapper.tsx',
'components/EmbediaWebComponentLoader.jsx',
'components/EmbediaWebComponentLoader.tsx',
'public/embedia-chatbot.js'
];
let componentsFound = 0;
for (const componentPath of componentPaths) {
const fullPath = path.join(this.projectPath, componentPath);
if (await fs.pathExists(fullPath)) {
componentsFound++;
test.components.push(componentPath);
}
}
if (componentsFound === 0) {
test.errors.push('No chatbot components found');
}
// Check integration points
const integrationPoints = [
'app/layout.tsx',
'app/layout.jsx',
'src/app/layout.tsx',
'src/app/layout.jsx',
'pages/_app.tsx',
'pages/_app.jsx',
'src/pages/_app.tsx',
'src/pages/_app.jsx'
];
let integrationFound = false;
for (const integrationPath of integrationPoints) {
const fullPath = path.join(this.projectPath, integrationPath);
if (await fs.pathExists(fullPath)) {
const content = await fs.readFile(fullPath, 'utf8');
if (content.includes('embedia') || content.includes('Embedia')) {
integrationFound = true;
test.components.push(`Integration found in ${integrationPath}`);
break;
}
}
}
if (!integrationFound) {
test.errors.push('No integration point found in layout files');
}
test.success = componentsFound > 0 && integrationFound;
} catch (error) {
test.errors.push(error.message);
}
test.duration = Date.now() - test.startTime;
console.log(chalk.gray(` Component Mounting: ${test.success ? 'โ' : 'โ'} (${test.components.length} components)`));
return test;
}
/**
* Test API communication by validating API routes
*/
async testAPICommunication() {
const test = {
name: 'API Communication',
success: false,
startTime: Date.now(),
apis: [],
errors: []
};
try {
// Find API route files
const apiPaths = [
{ path: 'app/api/embedia/chat/route.js', type: 'app-router' },
{ path: 'app/api/embedia/chat/route.ts', type: 'app-router' },
{ path: 'src/app/api/embedia/chat/route.js', type: 'app-router' },
{ path: 'src/app/api/embedia/chat/route.ts', type: 'app-router' },
{ path: 'pages/api/embedia/chat.js', type: 'pages-router' },
{ path: 'pages/api/embedia/chat.ts', type: 'pages-router' },
{ path: 'src/pages/api/embedia/chat.js', type: 'pages-router' },
{ path: 'src/pages/api/embedia/chat.ts', type: 'pages-router' }
];
let apiValidated = false;
for (const { path: apiPath, type } of apiPaths) {
const fullPath = path.join(this.projectPath, apiPath);
if (await fs.pathExists(fullPath)) {
const content = await fs.readFile(fullPath, 'utf8');
// Validate API structure
const validation = this.validateAPIRoute(content, type);
test.apis.push({ path: apiPath, type, ...validation });
if (validation.valid) {
apiValidated = true;
} else {
test.errors.push(...validation.errors);
}
}
}
if (!apiValidated) {
test.errors.push('No valid API routes found');
}
test.success = apiValidated;
} catch (error) {
test.errors.push(error.message);
}
test.duration = Date.now() - test.startTime;
console.log(chalk.gray(` API Communication: ${test.success ? 'โ' : 'โ'} (${test.apis.length} APIs)`));
return test;
}
/**
* Test build and development server startup
*/
async testBuildAndStart() {
const test = {
name: 'Build and Start',
success: false,
startTime: Date.now(),
buildSuccess: false,
startSuccess: false,
errors: []
};
try {
// Test build
console.log(chalk.gray(' Testing build process...'));
try {
const buildOutput = execSync('npm run build', {
cwd: this.projectPath,
stdio: 'pipe',
timeout: 120000, // 2 minutes
encoding: 'utf8'
});
test.buildSuccess = true;
console.log(chalk.gray(' โ Build completed successfully'));
} catch (buildError) {
test.errors.push(`Build failed: ${buildError.message}`);
console.log(chalk.gray(' โ Build failed'));
}
// Test dev server start (if build succeeded)
if (test.buildSuccess) {
console.log(chalk.gray(' Testing dev server startup...'));
try {
const startResult = await this.testDevServerStart();
test.startSuccess = startResult.success;
if (!startResult.success) {
test.errors.push(...startResult.errors);
}
console.log(chalk.gray(` ${startResult.success ? 'โ' : 'โ'} Dev server startup test`));
} catch (startError) {
test.errors.push(`Dev server test failed: ${startError.message}`);
console.log(chalk.gray(' โ Dev server startup failed'));
}
}
test.success = test.buildSuccess && test.startSuccess && test.errors.length === 0;
} catch (error) {
test.errors.push(error.message);
}
test.duration = Date.now() - test.startTime;
console.log(chalk.gray(` Build and Start: ${test.success ? 'โ' : 'โ'} (Build: ${test.buildSuccess}, Start: ${test.startSuccess})`));
return test;
}
/**
* Test browser compatibility (basic check)
*/
async testBrowserCompatibility() {
const test = {
name: 'Browser Compatibility',
success: false,
skipped: false,
startTime: Date.now(),
checks: [],
errors: []
};
try {
// Check for modern JavaScript features that might cause issues
const componentFiles = await this.findComponentFiles();
if (componentFiles.length === 0) {
test.skipped = true;
test.checks.push('No component files to analyze');
return test;
}
for (const filePath of componentFiles) {
const content = await fs.readFile(filePath, 'utf8');
// Check for potential compatibility issues
const issues = this.checkCompatibilityIssues(content, filePath);
if (issues.length > 0) {
test.errors.push(...issues);
} else {
test.checks.push(`โ ${path.relative(this.projectPath, filePath)} - No compatibility issues`);
}
}
test.success = test.errors.length === 0;
} catch (error) {
test.errors.push(error.message);
}
test.duration = Date.now() - test.startTime;
const status = test.skipped ? 'Skipped' : (test.success ? 'โ' : 'โ');
console.log(chalk.gray(` Browser Compatibility: ${status} (${test.checks.length} files checked)`));
return test;
}
/**
* Test performance characteristics
*/
async testPerformance() {
const test = {
name: 'Performance',
success: false,
startTime: Date.now(),
metrics: {},
errors: []
};
try {
// Check bundle size
const bundleSize = await this.checkBundleSize();
test.metrics.bundleSize = bundleSize;
// Check component complexity
const complexity = await this.checkComponentComplexity();
test.metrics.complexity = complexity;
// Determine success based on performance criteria
const issues = [];
if (bundleSize.totalSize > 500000) { // 500KB
issues.push(`Large bundle size: ${Math.round(bundleSize.totalSize / 1024)}KB`);
}
if (complexity.cyclomatic > 10) {
issues.push(`High component complexity: ${complexity.cyclomatic}`);
}
test.errors = issues;
test.success = issues.length === 0;
} catch (error) {
test.errors.push(error.message);
}
test.duration = Date.now() - test.startTime;
console.log(chalk.gray(` Performance: ${test.success ? 'โ' : 'โ'} (${Object.keys(test.metrics).length} metrics)`));
return test;
}
// Helper methods
/**
* Validate API route structure
*/
validateAPIRoute(content, type) {
const validation = { valid: false, errors: [] };
if (type === 'app-router') {
if (!content.includes('export async function POST')) {
validation.errors.push('App Router API missing POST export');
}
if (!content.includes('NextResponse') && !content.includes('Response')) {
validation.errors.push('App Router API missing Response handling');
}
} else if (type === 'pages-router') {
if (!content.includes('export default')) {
validation.errors.push('Pages Router API missing default export');
}
if (!content.includes('req') || !content.includes('res')) {
validation.errors.push('Pages Router API missing req/res parameters');
}
}
// Check for AI integration
if (!content.includes('generative-ai') && !content.includes('openai') && !content.includes('anthropic')) {
validation.errors.push('API route missing AI integration');
}
validation.valid = validation.errors.length === 0;
return validation;
}
/**
* Test dev server startup
*/
async testDevServerStart() {
return new Promise((resolve) => {
const timeout = setTimeout(() => {
if (this.devServerProcess) {
this.devServerProcess.kill();
}
resolve({ success: false, errors: ['Dev server startup timeout'] });
}, 30000); // 30 seconds
try {
this.devServerProcess = spawn('npm', ['run', 'dev'], {
cwd: this.projectPath,
stdio: 'pipe'
});
let output = '';
let started = false;
this.devServerProcess.stdout.on('data', (data) => {
output += data.toString();
// Check for successful startup indicators
if (output.includes('Ready in') ||
output.includes('Local:') ||
output.includes('ready - started server') ||
output.includes('ready on')) {
started = true;
clearTimeout(timeout);
this.devServerProcess.kill();
resolve({ success: true, errors: [] });
}
});
this.devServerProcess.stderr.on('data', (data) => {
output += data.toString();
});
this.devServerProcess.on('close', (code) => {
if (!started) {
clearTimeout(timeout);
resolve({
success: false,
errors: [`Dev server exited with code ${code}`, output.substring(0, 500)]
});
}
});
} catch (error) {
clearTimeout(timeout);
resolve({ success: false, errors: [error.message] });
}
});
}
/**
* Find component files for analysis
*/
async findComponentFiles() {
const files = [];
const searchPaths = [
'components/generated/embedia-chat/',
'components/',
'public/'
];
for (const searchPath of searchPaths) {
const fullPath = path.join(this.projectPath, searchPath);
if (await fs.pathExists(fullPath)) {
const entries = await fs.readdir(fullPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.jsx') ||
entry.name.endsWith('.ts') || entry.name.endsWith('.tsx'))) {
files.push(path.join(fullPath, entry.name));
}
}
}
}
return files;
}
/**
* Check for browser compatibility issues
*/
checkCompatibilityIssues(content, filePath) {
const issues = [];
// Check for ES6+ features that might need polyfills
if (content.includes('?.') && !content.includes('babel')) {
issues.push(`${filePath}: Optional chaining may need polyfill`);
}
if (content.includes('??') && !content.includes('babel')) {
issues.push(`${filePath}: Nullish coalescing may need polyfill`);
}
// Check for async/await in older syntax
if (content.includes('async') && content.includes('function') &&
!content.includes('regenerator')) {
// This is just a warning, not necessarily an error
}
return issues;
}
/**
* Check bundle size
*/
async checkBundleSize() {
const sizes = { totalSize: 0, files: [] };
try {
const buildDir = path.join(this.projectPath, '.next');
if (await fs.pathExists(buildDir)) {
// Next.js build analysis
const staticDir = path.join(buildDir, 'static');
if (await fs.pathExists(staticDir)) {
const files = await this.getAllFiles(staticDir);
for (const file of files) {
const stats = await fs.stat(file);
sizes.totalSize += stats.size;
sizes.files.push({ path: path.relative(this.projectPath, file), size: stats.size });
}
}
}
// Check public assets
const publicDir = path.join(this.projectPath, 'public');
if (await fs.pathExists(publicDir)) {
const embediaFiles = await fs.readdir(publicDir);
for (const file of embediaFiles) {
if (file.includes('embedia')) {
const filePath = path.join(publicDir, file);
const stats = await fs.stat(filePath);
sizes.totalSize += stats.size;
sizes.files.push({ path: `public/${file}`, size: stats.size });
}
}
}
} catch (error) {
// If we can't analyze bundle size, that's not a critical failure
}
return sizes;
}
/**
* Check component complexity
*/
async checkComponentComplexity() {
const complexity = { cyclomatic: 0, files: 0 };
try {
const componentFiles = await this.findComponentFiles();
for (const file of componentFiles) {
const content = await fs.readFile(file, 'utf8');
// Simple cyclomatic complexity calculation
const conditions = (content.match(/if|else|while|for|switch|case|\?|\|\||&&/g) || []).length;
complexity.cyclomatic += conditions;
complexity.files++;
}
complexity.average = complexity.files > 0 ? complexity.cyclomatic / complexity.files : 0;
} catch (error) {
// Complexity analysis failure is not critical
}
return complexity;
}
/**
* Get all files recursively
*/
async getAllFiles(dir) {
const files = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...await this.getAllFiles(fullPath));
} else {
files.push(fullPath);
}
}
return files;
}
/**
* Cleanup resources
*/
async cleanup() {
if (this.devServerProcess) {
this.devServerProcess.kill();
this.devServerProcess = null;
}
}
/**
* Finalize validation results
*/
finalizeResults(results) {
results.endTime = Date.now();
results.duration = results.endTime - results.startTime;
// Add summary
const total = results.summary.passed + results.summary.failed + results.summary.skipped;
console.log(chalk.cyan(`\n๐งช Functional Validation Complete:`));
console.log(chalk.gray(` Duration: ${results.duration}ms`));
console.log(chalk.gray(` Tests: ${total} total, ${results.summary.passed} passed, ${results.summary.failed} failed, ${results.summary.skipped} skipped`));
console.log(chalk.gray(` Score: ${results.score}%`));
return results;
}
}
module.exports = FunctionalValidator;