task-master-neo-sdlc
Version:
Enhanced task management system with Neo SDLC agents and MCP tools for comprehensive, AI-driven software development lifecycle management.
718 lines (658 loc) • 24.1 kB
JavaScript
/**
* Test Frameworks Configuration
*
* Defines supported test frameworks, their configurations, and utilities.
*/
import { log } from '../../../utils/logging.js';
import path from 'path';
import fs from 'fs';
import { execSync } from 'child_process';
/**
* Supported test frameworks
*/
export const TEST_FRAMEWORKS = {
// Unit and Component Testing
JEST: {
name: 'jest',
type: 'unit',
fileExtension: '.test.js',
tsFileExtension: '.test.ts',
reactFileExtension: '.test.jsx',
reactTsFileExtension: '.test.tsx',
defaultDir: 'tests',
configFile: 'jest.config.js',
installCmd: 'npm install --save-dev jest @testing-library/react @testing-library/jest-dom',
runCmd: 'npx jest',
coverageCmd: 'npx jest --coverage',
watchCmd: 'npx jest --watch',
initCmd: 'npx jest --init',
supports: ['unit', 'integration', 'snapshot'],
capabilities: ['react', 'vue', 'angular', 'node', 'typescript']
},
VITEST: {
name: 'vitest',
type: 'unit',
fileExtension: '.test.js',
tsFileExtension: '.test.ts',
reactFileExtension: '.test.jsx',
reactTsFileExtension: '.test.tsx',
defaultDir: 'tests',
configFile: 'vitest.config.js',
installCmd: 'npm install --save-dev vitest @testing-library/react jsdom @testing-library/jest-dom',
runCmd: 'npx vitest run',
coverageCmd: 'npx vitest run --coverage',
watchCmd: 'npx vitest',
initCmd: 'npm install --save-dev vitest',
supports: ['unit', 'integration', 'snapshot'],
capabilities: ['react', 'vue', 'svelte', 'node', 'typescript', 'vite']
},
MOCHA: {
name: 'mocha',
type: 'unit',
fileExtension: '.spec.js',
tsFileExtension: '.spec.ts',
defaultDir: 'test',
configFile: '.mocharc.js',
installCmd: 'npm install --save-dev mocha chai sinon',
runCmd: 'npx mocha',
coverageCmd: 'npx nyc mocha',
watchCmd: 'npx mocha --watch',
initCmd: 'npx mocha --init',
supports: ['unit', 'integration'],
capabilities: ['node', 'browser', 'typescript']
},
JASMINE: {
name: 'jasmine',
type: 'unit',
fileExtension: '.spec.js',
tsFileExtension: '.spec.ts',
defaultDir: 'spec',
configFile: 'jasmine.json',
installCmd: 'npm install --save-dev jasmine',
runCmd: 'npx jasmine',
coverageCmd: 'npx nyc jasmine',
watchCmd: 'npx jasmine-node --autotest .',
initCmd: 'npx jasmine init',
supports: ['unit', 'integration'],
capabilities: ['node', 'browser', 'angular']
},
AVA: {
name: 'ava',
type: 'unit',
fileExtension: '.test.js',
tsFileExtension: '.test.ts',
defaultDir: 'test',
configFile: 'ava.config.js',
installCmd: 'npm install --save-dev ava',
runCmd: 'npx ava',
coverageCmd: 'npx c8 ava',
watchCmd: 'npx ava --watch',
initCmd: 'npx ava --init',
supports: ['unit', 'integration'],
capabilities: ['node', 'typescript', 'esm']
},
RTL: {
name: 'rtl',
type: 'component',
fileExtension: '.test.js',
tsFileExtension: '.test.ts',
reactFileExtension: '.test.jsx',
reactTsFileExtension: '.test.tsx',
defaultDir: 'tests',
configFile: 'jest.config.js', // Usually used with Jest
installCmd: 'npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event',
runCmd: 'npx jest',
coverageCmd: 'npx jest --coverage',
watchCmd: 'npx jest --watch',
initCmd: null, // Uses Jest init
supports: ['component', 'unit', 'integration'],
capabilities: ['react', 'hooks', 'dom', 'events']
},
// End-to-End Testing
CYPRESS: {
name: 'cypress',
type: 'e2e',
fileExtension: '.cy.js',
tsFileExtension: '.cy.ts',
reactFileExtension: '.cy.jsx',
reactTsFileExtension: '.cy.tsx',
defaultDir: 'cypress/e2e',
configFile: 'cypress.config.js',
installCmd: 'npm install --save-dev cypress',
runCmd: 'npx cypress run',
coverageCmd: 'npx cypress run --env coverage=true',
watchCmd: 'npx cypress open',
initCmd: 'npx cypress open',
supports: ['e2e', 'component', 'visual', 'api'],
capabilities: ['react', 'vue', 'angular', 'web']
},
PLAYWRIGHT: {
name: 'playwright',
type: 'e2e',
fileExtension: '.spec.js',
tsFileExtension: '.spec.ts',
defaultDir: 'tests',
configFile: 'playwright.config.js',
installCmd: 'npm install --save-dev @playwright/test',
runCmd: 'npx playwright test',
coverageCmd: 'npx playwright test --reporter=html',
watchCmd: 'npx playwright test --ui',
initCmd: 'npx playwright install',
supports: ['e2e', 'visual', 'api', 'accessibility'],
capabilities: ['react', 'vue', 'angular', 'web', 'mobile-web', 'cross-browser']
},
PUPPETEER: {
name: 'puppeteer',
type: 'e2e',
fileExtension: '.test.js',
tsFileExtension: '.test.ts',
defaultDir: 'tests/e2e',
configFile: null, // Puppeteer doesn't have a standard config file
installCmd: 'npm install --save-dev puppeteer jest',
runCmd: 'npx jest',
coverageCmd: 'npx jest --coverage',
watchCmd: 'npx jest --watch',
initCmd: 'npm install --save-dev puppeteer',
supports: ['e2e', 'visual', 'performance'],
capabilities: ['web', 'headless', 'screenshots', 'pdf']
},
WEBDRIVERIO: {
name: 'webdriverio',
type: 'e2e',
fileExtension: '.spec.js',
tsFileExtension: '.spec.ts',
defaultDir: 'test/specs',
configFile: 'wdio.conf.js',
installCmd: 'npm install --save-dev @wdio/cli',
runCmd: 'npx wdio run wdio.conf.js',
coverageCmd: 'npx wdio run wdio.conf.js --coverage',
watchCmd: 'npx wdio run wdio.conf.js --watch',
initCmd: 'npx wdio config',
supports: ['e2e', 'mobile', 'api', 'visual'],
capabilities: ['web', 'mobile', 'cross-browser', 'parallel']
},
// API Testing
SUPERTEST: {
name: 'supertest',
type: 'api',
fileExtension: '.api.test.js',
tsFileExtension: '.api.test.ts',
defaultDir: 'tests/api',
configFile: null, // Usually used with Jest or Mocha
installCmd: 'npm install --save-dev supertest',
runCmd: 'npx jest',
coverageCmd: 'npx jest --coverage',
watchCmd: 'npx jest --watch',
initCmd: 'npm install --save-dev supertest',
supports: ['api', 'integration'],
capabilities: ['rest', 'http', 'express', 'node']
},
POSTMAN: {
name: 'newman',
type: 'api',
fileExtension: '.postman_collection.json',
defaultDir: 'tests/api',
configFile: null,
installCmd: 'npm install --save-dev newman',
runCmd: 'npx newman run',
coverageCmd: 'npx newman run --reporters cli,html',
watchCmd: null,
initCmd: null, // Collections are created in Postman UI
supports: ['api', 'integration'],
capabilities: ['rest', 'graphql', 'soap', 'websocket']
},
// Visual Regression Testing
STORYBOOK: {
name: 'storybook',
type: 'visual',
fileExtension: '.stories.js',
tsFileExtension: '.stories.ts',
reactFileExtension: '.stories.jsx',
reactTsFileExtension: '.stories.tsx',
defaultDir: 'src/stories',
configFile: '.storybook/main.js',
installCmd: 'npx storybook init',
runCmd: 'npm run storybook',
coverageCmd: null,
watchCmd: 'npm run storybook',
initCmd: 'npx storybook init',
supports: ['visual', 'component', 'accessibility'],
capabilities: ['react', 'vue', 'angular', 'web', 'design-system']
}
};
/**
* Test types with descriptions
*/
export const TEST_TYPES = {
UNIT: {
name: 'unit',
description: 'Tests individual functions, methods, or classes in isolation',
frameworks: ['jest', 'vitest', 'mocha', 'jasmine', 'ava', 'rtl'],
priority: 1
},
INTEGRATION: {
name: 'integration',
description: 'Tests interactions between different parts of the application',
frameworks: ['jest', 'vitest', 'mocha', 'jasmine', 'supertest', 'ava', 'rtl'],
priority: 2
},
E2E: {
name: 'e2e',
description: 'Tests the application from end to end, simulating user behavior',
frameworks: ['cypress', 'playwright', 'puppeteer', 'webdriverio'],
priority: 3
},
API: {
name: 'api',
description: 'Tests API endpoints and responses',
frameworks: ['supertest', 'newman', 'cypress', 'playwright'],
priority: 2
},
VISUAL: {
name: 'visual',
description: 'Tests the visual appearance of components',
frameworks: ['storybook', 'cypress', 'playwright', 'puppeteer'],
priority: 4
},
PERFORMANCE: {
name: 'performance',
description: 'Tests the performance of the application',
frameworks: ['lighthouse', 'puppeteer'],
priority: 5
},
ACCESSIBILITY: {
name: 'accessibility',
description: 'Tests the accessibility of the application',
frameworks: ['storybook', 'cypress', 'playwright'],
priority: 4
},
SNAPSHOT: {
name: 'snapshot',
description: 'Tests that components render consistently',
frameworks: ['jest', 'vitest'],
priority: 3
}
};
/**
* Detect test frameworks in a project
* @param {string} projectRoot - Project root directory
* @returns {Array<string>} Detected test frameworks
*/
export function detectTestFrameworks(projectRoot) {
try {
const packageJsonPath = path.join(projectRoot, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return ['jest']; // Default to Jest if no package.json
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies
};
const detectedFrameworks = [];
// Check for each framework
if (dependencies.jest) detectedFrameworks.push('jest');
if (dependencies.vitest) detectedFrameworks.push('vitest');
if (dependencies.mocha) detectedFrameworks.push('mocha');
if (dependencies.jasmine) detectedFrameworks.push('jasmine');
if (dependencies.ava) detectedFrameworks.push('ava');
if (dependencies['@testing-library/react']) detectedFrameworks.push('rtl');
if (dependencies.cypress) detectedFrameworks.push('cypress');
if (dependencies['@playwright/test']) detectedFrameworks.push('playwright');
if (dependencies.puppeteer) detectedFrameworks.push('puppeteer');
if (dependencies['@wdio/cli']) detectedFrameworks.push('webdriverio');
if (dependencies.supertest) detectedFrameworks.push('supertest');
if (dependencies.newman) detectedFrameworks.push('newman');
if (dependencies['@storybook/react'] || dependencies['@storybook/vue']) detectedFrameworks.push('storybook');
// Check for config files if no frameworks detected in dependencies
if (detectedFrameworks.length === 0) {
if (fs.existsSync(path.join(projectRoot, 'jest.config.js'))) detectedFrameworks.push('jest');
if (fs.existsSync(path.join(projectRoot, 'vitest.config.js'))) detectedFrameworks.push('vitest');
if (fs.existsSync(path.join(projectRoot, '.mocharc.js'))) detectedFrameworks.push('mocha');
if (fs.existsSync(path.join(projectRoot, 'jasmine.json'))) detectedFrameworks.push('jasmine');
if (fs.existsSync(path.join(projectRoot, 'ava.config.js'))) detectedFrameworks.push('ava');
if (fs.existsSync(path.join(projectRoot, 'cypress.config.js'))) detectedFrameworks.push('cypress');
if (fs.existsSync(path.join(projectRoot, 'playwright.config.js'))) detectedFrameworks.push('playwright');
if (fs.existsSync(path.join(projectRoot, 'wdio.conf.js'))) detectedFrameworks.push('webdriverio');
if (fs.existsSync(path.join(projectRoot, '.storybook'))) detectedFrameworks.push('storybook');
// Check for RTL by looking for testing-library imports in test files
const testFiles = findTestFiles(projectRoot);
if (testFiles.some(file => {
try {
const content = fs.readFileSync(file, 'utf8');
return content.includes('@testing-library/react') ||
content.includes('@testing-library/vue') ||
content.includes('render(') ||
content.includes('screen.');
} catch (e) {
return false;
}
})) {
detectedFrameworks.push('rtl');
}
}
return detectedFrameworks.length > 0 ? detectedFrameworks : ['jest']; // Default to Jest if nothing detected
} catch (error) {
log.warn(`Error detecting test frameworks: ${error.message}`);
return ['jest']; // Default to Jest on error
}
}
/**
* Install a test framework
* @param {string} framework - Framework name
* @param {string} projectRoot - Project root directory
* @returns {Promise<boolean>} Whether installation was successful
*/
export async function installTestFramework(framework, projectRoot) {
try {
const frameworkConfig = Object.values(TEST_FRAMEWORKS).find(f => f.name === framework);
if (!frameworkConfig) {
throw new Error(`Unknown test framework: ${framework}`);
}
log.info(`Installing ${framework} in ${projectRoot}...`);
// Execute the install command
execSync(frameworkConfig.installCmd, {
cwd: projectRoot,
stdio: 'inherit'
});
// Run init command if it exists
if (frameworkConfig.initCmd) {
execSync(frameworkConfig.initCmd, {
cwd: projectRoot,
stdio: 'inherit'
});
}
log.info(`Successfully installed ${framework}`);
return true;
} catch (error) {
log.error(`Error installing ${framework}: ${error.message}`);
return false;
}
}
/**
* Run tests with a specific framework
* @param {string} framework - Framework name
* @param {string} testPath - Path to test file or directory
* @param {Object} options - Options for running tests
* @param {boolean} options.coverage - Whether to collect coverage
* @param {boolean} options.watch - Whether to run in watch mode
* @param {string} options.projectRoot - Project root directory
* @returns {Promise<Object>} Test results
*/
export async function runTests(framework, testPath, options = {}) {
try {
const { coverage = false, watch = false, projectRoot = process.cwd() } = options;
const frameworkConfig = Object.values(TEST_FRAMEWORKS).find(f => f.name === framework);
if (!frameworkConfig) {
throw new Error(`Unknown test framework: ${framework}`);
}
let command;
if (coverage && frameworkConfig.coverageCmd) {
command = `${frameworkConfig.coverageCmd} ${testPath}`;
} else if (watch && frameworkConfig.watchCmd) {
command = `${frameworkConfig.watchCmd} ${testPath}`;
} else {
command = `${frameworkConfig.runCmd} ${testPath}`;
}
log.info(`Running tests with ${framework}: ${command}`);
// Execute the command
const output = execSync(command, {
cwd: projectRoot,
encoding: 'utf8'
});
// Parse the output to extract test results
// This is a simplified version; in reality, you'd need to parse the output based on the framework
const passed = !output.includes('FAIL') && !output.includes('ERROR');
const failures = (output.match(/FAIL/g) || []).length;
const totalTests = (output.match(/PASS|FAIL/g) || []).length;
// Extract coverage if available
let coverageResult = null;
if (coverage) {
const coverageMatch = output.match(/All files[^\n]*?(\d+(?:\.\d+)?)%/);
if (coverageMatch) {
coverageResult = parseFloat(coverageMatch[1]);
}
}
return {
passed,
failures,
totalTests,
coverage: coverageResult,
output,
framework,
testPath
};
} catch (error) {
log.error(`Error running tests with ${framework}: ${error.message}`);
return {
passed: false,
failures: 1,
totalTests: 1,
coverage: null,
output: error.message,
framework,
testPath,
error: error.message
};
}
}
/**
* Recommend test frameworks based on project type
* @param {Object} projectInfo - Project information
* @param {string} projectInfo.type - Project type (react, vue, node, etc.)
* @param {Array<string>} projectInfo.features - Project features
* @param {string} projectInfo.projectRoot - Project root directory
* @returns {Object} Recommended test frameworks
*/
export function recommendTestFrameworks(projectInfo) {
const { type, features = [], projectRoot } = projectInfo;
// Default recommendations
const recommendations = {
unit: 'jest',
integration: 'jest',
e2e: 'cypress',
api: 'supertest',
visual: 'storybook'
};
// Check existing frameworks
const existingFrameworks = detectTestFrameworks(projectRoot);
// Adjust recommendations based on project type
switch (type) {
case 'react':
recommendations.unit = existingFrameworks.includes('vitest') ? 'vitest' : 'jest';
recommendations.e2e = existingFrameworks.includes('playwright') ? 'playwright' : 'cypress';
break;
case 'vue':
recommendations.unit = existingFrameworks.includes('vitest') ? 'vitest' : 'jest';
break;
case 'angular':
recommendations.unit = 'jasmine'; // Angular uses Jasmine by default
break;
case 'node':
recommendations.unit = existingFrameworks.includes('mocha') ? 'mocha' : 'jest';
recommendations.api = 'supertest';
break;
default:
// Use detected frameworks if available
if (existingFrameworks.length > 0) {
const unitFrameworks = existingFrameworks.filter(f =>
['jest', 'vitest', 'mocha', 'jasmine'].includes(f)
);
if (unitFrameworks.length > 0) {
recommendations.unit = unitFrameworks[0];
recommendations.integration = unitFrameworks[0];
}
const e2eFrameworks = existingFrameworks.filter(f =>
['cypress', 'playwright', 'puppeteer'].includes(f)
);
if (e2eFrameworks.length > 0) {
recommendations.e2e = e2eFrameworks[0];
}
}
}
// Adjust based on features
if (features.includes('api')) {
recommendations.api = 'supertest';
}
if (features.includes('graphql')) {
recommendations.api = 'apollo-client-testing';
}
if (features.includes('design-system')) {
recommendations.visual = 'storybook';
}
if (features.includes('accessibility')) {
recommendations.e2e = 'playwright'; // Playwright has better accessibility testing
}
return recommendations;
}
/**
* Get framework configuration by name
* @param {string} frameworkName - Framework name
* @returns {Object} Framework configuration
*/
export function getFrameworkConfig(frameworkName) {
return Object.values(TEST_FRAMEWORKS).find(f => f.name === frameworkName) || TEST_FRAMEWORKS.JEST;
}
/**
* Get test type configuration by name
* @param {string} typeName - Test type name
* @returns {Object} Test type configuration
*/
export function getTestTypeConfig(typeName) {
return Object.values(TEST_TYPES).find(t => t.name === typeName) || TEST_TYPES.UNIT;
}
/**
* Find test files in a project
* @param {string} projectRoot - Project root directory
* @returns {Array<string>} Test file paths
*/
function findTestFiles(projectRoot) {
try {
const testFiles = [];
const testPatterns = [
'**/*.test.js', '**/*.test.jsx', '**/*.test.ts', '**/*.test.tsx',
'**/*.spec.js', '**/*.spec.jsx', '**/*.spec.ts', '**/*.spec.tsx',
'**/test/**/*.js', '**/test/**/*.jsx', '**/test/**/*.ts', '**/test/**/*.tsx',
'**/tests/**/*.js', '**/tests/**/*.jsx', '**/tests/**/*.ts', '**/tests/**/*.tsx',
'**/spec/**/*.js', '**/spec/**/*.jsx', '**/spec/**/*.ts', '**/spec/**/*.tsx',
'**/cypress/e2e/**/*.js', '**/cypress/e2e/**/*.ts'
];
// This is a simplified implementation
// In a real implementation, you would use a glob library to find files
// For now, we'll just check a few common directories
const commonDirs = ['test', 'tests', 'spec', 'specs', 'src', 'cypress/e2e'];
for (const dir of commonDirs) {
const dirPath = path.join(projectRoot, dir);
if (fs.existsSync(dirPath)) {
const files = findFilesInDir(dirPath, (file) => {
return file.endsWith('.test.js') || file.endsWith('.test.jsx') ||
file.endsWith('.test.ts') || file.endsWith('.test.tsx') ||
file.endsWith('.spec.js') || file.endsWith('.spec.jsx') ||
file.endsWith('.spec.ts') || file.endsWith('.spec.tsx');
});
testFiles.push(...files);
}
}
return testFiles;
} catch (error) {
log.warn(`Error finding test files: ${error.message}`);
return [];
}
}
/**
* Find files in a directory recursively
* @param {string} dir - Directory to search
* @param {Function} filter - Filter function
* @returns {Array<string>} File paths
*/
function findFilesInDir(dir, filter) {
if (!fs.existsSync(dir)) {
return [];
}
const files = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...findFilesInDir(fullPath, filter));
} else if (filter(fullPath)) {
files.push(fullPath);
}
}
return files;
}
/**
* Handle ESM/CJS interoperability issues
* @param {string} projectRoot - Project root directory
* @param {string} framework - Test framework
* @returns {Promise<Object>} Configuration result
*/
export async function handleModuleInterop(projectRoot, framework) {
try {
const packageJsonPath = path.join(projectRoot, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return { success: false, message: 'package.json not found' };
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const isESM = packageJson.type === 'module';
// Handle Jest with ESM
if (framework === 'jest' && isESM) {
const jestConfigPath = path.join(projectRoot, 'jest.config.js');
// Create Jest config for ESM if it doesn't exist
if (!fs.existsSync(jestConfigPath)) {
const jestConfig = `export default {
transform: {},
extensionsToTreatAsEsm: ['.js', '.jsx', '.ts', '.tsx'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
testEnvironment: 'node',
};
`;
fs.writeFileSync(jestConfigPath, jestConfig, 'utf8');
log.info('Created Jest config for ESM compatibility');
} else {
// Update existing Jest config
let jestConfig = fs.readFileSync(jestConfigPath, 'utf8');
if (!jestConfig.includes('extensionsToTreatAsEsm')) {
// This is a simplified approach; in a real implementation, you would parse the config
if (jestConfig.includes('module.exports')) {
// Convert to ESM
jestConfig = jestConfig.replace('module.exports', 'export default');
}
// Add ESM configuration
jestConfig = jestConfig.replace('export default {', `export default {
transform: {},
extensionsToTreatAsEsm: ['.js', '.jsx', '.ts', '.tsx'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},`);
fs.writeFileSync(jestConfigPath, jestConfig, 'utf8');
log.info('Updated Jest config for ESM compatibility');
}
}
return { success: true, message: 'Configured Jest for ESM compatibility' };
}
// Handle Vitest with CJS
if (framework === 'vitest' && !isESM) {
const vitestConfigPath = path.join(projectRoot, 'vitest.config.js');
// Create Vitest config for CJS if it doesn't exist
if (!fs.existsSync(vitestConfigPath)) {
const vitestConfig = `import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
},
});
`;
fs.writeFileSync(vitestConfigPath, vitestConfig, 'utf8');
log.info('Created Vitest config for CJS compatibility');
}
return { success: true, message: 'Configured Vitest for CJS compatibility' };
}
return { success: true, message: 'No module interop issues detected' };
} catch (error) {
log.error(`Error handling module interop: ${error.message}`);
return { success: false, message: error.message };
}
}