muspe-cli
Version:
MusPE Advanced Framework v2.1.3 - Mobile User-friendly Simple Progressive Engine with Enhanced CLI Tools, Specialized E-Commerce Templates, Material Design 3, Progressive Enhancement, Mobile Optimizations, Performance Analysis, and Enterprise-Grade Develo
623 lines (518 loc) • 17 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const ora = require('ora');
const spawn = require('cross-spawn');
async function testProject(options = {}) {
const spinner = ora('Running MusPE tests...').start();
try {
const projectRoot = findProjectRoot();
if (!projectRoot) {
spinner.fail('Not in a MusPE project directory');
return false;
}
const testSuite = options.suite || 'all';
const verbose = options.verbose || false;
let results = {};
switch (testSuite) {
case 'unit':
results = await runUnitTests(projectRoot, options);
break;
case 'e2e':
results = await runE2ETests(projectRoot, options);
break;
case 'component':
results = await runComponentTests(projectRoot, options);
break;
case 'performance':
results = await runPerformanceTests(projectRoot, options);
break;
case 'accessibility':
results = await runAccessibilityTests(projectRoot, options);
break;
case 'all':
default:
results.unit = await runUnitTests(projectRoot, options);
results.component = await runComponentTests(projectRoot, options);
results.e2e = await runE2ETests(projectRoot, options);
results.performance = await runPerformanceTests(projectRoot, options);
results.accessibility = await runAccessibilityTests(projectRoot, options);
break;
}
// Display results summary
displayTestResults(results, verbose);
spinner.succeed('Test execution completed');
// Return true if all tests passed
const allPassed = Object.values(results).every(result =>
result && result.success
);
return allPassed;
} catch (error) {
spinner.fail('Test execution failed');
console.error(chalk.red(error.message));
return false;
}
}
async function runUnitTests(projectRoot, options) {
const testDir = path.join(projectRoot, 'tests', 'unit');
if (!await fs.pathExists(testDir)) {
if (options.verbose) {
console.log(chalk.yellow('No unit tests found, creating test structure...'));
}
await createUnitTestStructure(projectRoot);
}
try {
// Run Jest or similar test runner
const jestConfig = path.join(projectRoot, 'jest.config.js');
const hasJest = await fs.pathExists(jestConfig);
if (!hasJest) {
await createJestConfig(projectRoot);
}
const result = await runCommand('npm', ['test', '--', '--testPathPattern=unit'], {
cwd: projectRoot,
stdio: options.verbose ? 'inherit' : 'pipe'
});
return {
success: result.code === 0,
type: 'unit',
tests: result.tests || 0,
passed: result.passed || 0,
failed: result.failed || 0,
duration: result.duration || 0
};
} catch (error) {
return {
success: false,
type: 'unit',
error: error.message
};
}
}
async function runComponentTests(projectRoot, options) {
const testDir = path.join(projectRoot, 'tests', 'components');
if (!await fs.pathExists(testDir)) {
if (options.verbose) {
console.log(chalk.yellow('No component tests found, creating examples...'));
}
await createComponentTestStructure(projectRoot);
}
try {
// Component testing with testing-library or similar
const result = await runCommand('npm', ['test', '--', '--testPathPattern=components'], {
cwd: projectRoot,
stdio: options.verbose ? 'inherit' : 'pipe'
});
return {
success: result.code === 0,
type: 'component',
tests: result.tests || 0,
passed: result.passed || 0,
failed: result.failed || 0,
duration: result.duration || 0
};
} catch (error) {
return {
success: false,
type: 'component',
error: error.message
};
}
}
async function runE2ETests(projectRoot, options) {
const testDir = path.join(projectRoot, 'tests', 'e2e');
if (!await fs.pathExists(testDir)) {
if (options.verbose) {
console.log(chalk.yellow('No E2E tests found, creating examples...'));
}
await createE2ETestStructure(projectRoot);
}
try {
// Cypress or Playwright E2E tests
const cypressConfig = path.join(projectRoot, 'cypress.config.js');
const hasCypress = await fs.pathExists(cypressConfig);
if (!hasCypress) {
await createCypressConfig(projectRoot);
}
const result = await runCommand('npx', ['cypress', 'run', '--headless'], {
cwd: projectRoot,
stdio: options.verbose ? 'inherit' : 'pipe'
});
return {
success: result.code === 0,
type: 'e2e',
tests: result.tests || 0,
passed: result.passed || 0,
failed: result.failed || 0,
duration: result.duration || 0
};
} catch (error) {
return {
success: false,
type: 'e2e',
error: error.message
};
}
}
async function runPerformanceTests(projectRoot, options) {
try {
// Lighthouse performance audits
const result = await runCommand('npx', ['lighthouse', 'http://localhost:3000', '--chrome-flags="--headless"', '--output=json'], {
cwd: projectRoot,
stdio: 'pipe'
});
if (result.code === 0 && result.stdout) {
const lighthouse = JSON.parse(result.stdout);
const scores = lighthouse.lhr.categories;
return {
success: true,
type: 'performance',
scores: {
performance: Math.round(scores.performance.score * 100),
accessibility: Math.round(scores.accessibility.score * 100),
bestPractices: Math.round(scores['best-practices'].score * 100),
seo: Math.round(scores.seo.score * 100)
}
};
}
return {
success: false,
type: 'performance',
error: 'Failed to run Lighthouse audit'
};
} catch (error) {
return {
success: false,
type: 'performance',
error: error.message
};
}
}
async function runAccessibilityTests(projectRoot, options) {
try {
// axe-core accessibility testing
const result = await runCommand('npx', ['axe', 'http://localhost:3000'], {
cwd: projectRoot,
stdio: 'pipe'
});
return {
success: result.code === 0,
type: 'accessibility',
violations: result.violations || 0,
passes: result.passes || 0
};
} catch (error) {
return {
success: false,
type: 'accessibility',
error: error.message
};
}
}
async function createUnitTestStructure(projectRoot) {
const testDir = path.join(projectRoot, 'tests', 'unit');
await fs.ensureDir(testDir);
// Create example unit test
const exampleTest = `// Example Unit Test for MusPE
const { MusPE } = require('../../src/core/muspe');
describe('MusPE Core', () => {
test('should initialize correctly', () => {
expect(MusPE).toBeDefined();
expect(typeof MusPE.dom).toBe('object');
expect(typeof MusPE.http).toBe('object');
});
test('should create DOM elements', () => {
const element = MusPE.dom.create('div', {
class: 'test-element',
textContent: 'Hello World'
});
expect(element.tagName).toBe('DIV');
expect(element.className).toBe('test-element');
expect(element.textContent).toBe('Hello World');
});
test('should handle HTTP requests', async () => {
// Mock fetch for testing
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ test: 'data' })
})
);
const result = await MusPE.http.get('/api/test');
expect(result).toEqual({ test: 'data' });
expect(fetch).toHaveBeenCalledWith('/api/test', expect.any(Object));
});
});`;
await fs.writeFile(path.join(testDir, 'muspe.test.js'), exampleTest);
}
async function createComponentTestStructure(projectRoot) {
const testDir = path.join(projectRoot, 'tests', 'components');
await fs.ensureDir(testDir);
// Create example component test
const componentTest = `// Component Test Example
import { render, screen, fireEvent } from '@testing-library/dom';
import { AppHeader } from '../../src/components/AppHeader';
describe('AppHeader Component', () => {
test('renders correctly', () => {
const header = new AppHeader({
title: 'Test App',
subtitle: 'Test Subtitle'
});
const element = header.render();
document.body.appendChild(element);
expect(screen.getByText('Test App')).toBeInTheDocument();
expect(screen.getByText('Test Subtitle')).toBeInTheDocument();
});
test('updates title correctly', () => {
const header = new AppHeader({
title: 'Original Title'
});
const element = header.render();
document.body.appendChild(element);
header.update('New Title', 'New Subtitle');
expect(screen.getByText('New Title')).toBeInTheDocument();
expect(screen.getByText('New Subtitle')).toBeInTheDocument();
});
test('cleans up correctly', () => {
const header = new AppHeader();
const element = header.render();
document.body.appendChild(element);
header.destroy();
expect(document.querySelector('.app-header')).not.toBeInTheDocument();
});
});`;
await fs.writeFile(path.join(testDir, 'AppHeader.test.js'), componentTest);
}
async function createE2ETestStructure(projectRoot) {
const testDir = path.join(projectRoot, 'tests', 'e2e');
await fs.ensureDir(testDir);
// Create example E2E test
const e2eTest = `// E2E Test Example
describe('MusPE App E2E', () => {
beforeEach(() => {
cy.visit('/');
});
it('should load the homepage', () => {
cy.contains('Welcome to');
cy.get('.app-header').should('be.visible');
cy.get('.welcome-card').should('be.visible');
});
it('should navigate using buttons', () => {
cy.get('.cta-button').click();
cy.get('.modal-overlay').should('be.visible');
cy.contains('Welcome to MusPE!');
});
it('should be responsive', () => {
// Test mobile viewport
cy.viewport(375, 667);
cy.get('.app-header').should('be.visible');
cy.get('.features').should('have.class', 'features');
// Test tablet viewport
cy.viewport(768, 1024);
cy.get('.container').should('have.css', 'max-width');
});
it('should work with Material.io components', () => {
cy.get('md-filled-button').should('exist');
cy.get('md-outlined-button').should('exist');
});
it('should handle Swiper components', () => {
cy.get('.demo-button').click();
cy.get('.swiper-demo').should('be.visible');
cy.get('.swiper-slide').should('have.length.greaterThan', 1);
});
});`;
await fs.writeFile(path.join(testDir, 'app.cy.js'), e2eTest);
}
async function createJestConfig(projectRoot) {
const jestConfig = `module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
testMatch: [
'<rootDir>/tests/**/*.test.js',
'<rootDir>/tests/**/*.spec.js'
],
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js',
'!src/**/node_modules/**'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1'
},
transform: {
'^.+\\.js$': 'babel-jest'
}
};`;
await fs.writeFile(path.join(projectRoot, 'jest.config.js'), jestConfig);
// Create test setup file
const setupFile = `// Test Setup
import '@testing-library/jest-dom';
// Mock MusPE globals
global.MusPE = {
dom: {
create: jest.fn(),
append: jest.fn(),
remove: jest.fn(),
addClass: jest.fn(),
removeClass: jest.fn()
},
http: {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
delete: jest.fn()
}
};
// Mock Material.io components
global.customElements = {
define: jest.fn(),
get: jest.fn(),
whenDefined: jest.fn(() => Promise.resolve())
};`;
const testsDir = path.join(projectRoot, 'tests');
await fs.ensureDir(testsDir);
await fs.writeFile(path.join(testsDir, 'setup.js'), setupFile);
}
async function createCypressConfig(projectRoot) {
const cypressConfig = `const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
specPattern: 'tests/e2e/**/*.cy.js',
supportFile: 'tests/e2e/support/e2e.js',
videosFolder: 'tests/e2e/videos',
screenshotsFolder: 'tests/e2e/screenshots',
viewportWidth: 1280,
viewportHeight: 720,
video: false,
screenshot: true
},
component: {
devServer: {
framework: 'muspe',
bundler: 'vite'
},
specPattern: 'tests/components/**/*.cy.js'
}
});`;
await fs.writeFile(path.join(projectRoot, 'cypress.config.js'), cypressConfig);
// Create Cypress support file
const supportDir = path.join(projectRoot, 'tests', 'e2e', 'support');
await fs.ensureDir(supportDir);
const supportFile = `// Cypress E2E Support
import './commands';
// Global test setup
beforeEach(() => {
// Clear any existing data
cy.clearLocalStorage();
cy.clearCookies();
});
// Handle uncaught exceptions
Cypress.on('uncaught:exception', (err, runnable) => {
// Don't fail tests on Material.io component errors
if (err.message.includes('material') || err.message.includes('md-')) {
return false;
}
return true;
});`;
await fs.writeFile(path.join(supportDir, 'e2e.js'), supportFile);
const commandsFile = `// Custom Cypress Commands
Cypress.Commands.add('getByTestId', (testId) => {
cy.get(\`[data-testid="\${testId}"]\`);
});
Cypress.Commands.add('waitForMaterial', () => {
cy.window().then((win) => {
return new Promise((resolve) => {
if (win.customElements && win.customElements.whenDefined) {
Promise.all([
win.customElements.whenDefined('md-filled-button'),
win.customElements.whenDefined('md-outlined-button'),
win.customElements.whenDefined('md-card')
]).then(resolve);
} else {
resolve();
}
});
});
});
Cypress.Commands.add('checkAccessibility', () => {
cy.injectAxe();
cy.checkA11y();
});`;
await fs.writeFile(path.join(supportDir, 'commands.js'), commandsFile);
}
function displayTestResults(results, verbose) {
console.log(chalk.cyan('\n📊 Test Results Summary\n'));
Object.entries(results).forEach(([type, result]) => {
if (!result) return;
const icon = result.success ? '✅' : '❌';
const color = result.success ? chalk.green : chalk.red;
console.log(color(`${icon} ${type.toUpperCase()} Tests`));
if (result.error) {
console.log(chalk.red(` Error: ${result.error}`));
} else if (result.tests !== undefined) {
console.log(chalk.gray(` Tests: ${result.tests} | Passed: ${result.passed} | Failed: ${result.failed}`));
if (result.duration) {
console.log(chalk.gray(` Duration: ${result.duration}ms`));
}
} else if (result.scores) {
console.log(chalk.gray(` Performance: ${result.scores.performance}%`));
console.log(chalk.gray(` Accessibility: ${result.scores.accessibility}%`));
console.log(chalk.gray(` Best Practices: ${result.scores.bestPractices}%`));
console.log(chalk.gray(` SEO: ${result.scores.seo}%`));
} else if (result.violations !== undefined) {
console.log(chalk.gray(` Violations: ${result.violations} | Passes: ${result.passes}`));
}
console.log();
});
const totalSuccess = Object.values(results).filter(r => r && r.success).length;
const totalTests = Object.keys(results).length;
if (totalSuccess === totalTests) {
console.log(chalk.green('🎉 All tests passed!'));
} else {
console.log(chalk.red(`⚠️ ${totalTests - totalSuccess} test suite(s) failed`));
}
}
function runCommand(command, args, options = {}) {
return new Promise((resolve) => {
const child = spawn(command, args, {
stdio: 'pipe',
...options
});
let stdout = '';
let stderr = '';
if (child.stdout) {
child.stdout.on('data', (data) => {
stdout += data.toString();
});
}
if (child.stderr) {
child.stderr.on('data', (data) => {
stderr += data.toString();
});
}
child.on('close', (code) => {
resolve({
code,
stdout,
stderr
});
});
});
}
function findProjectRoot() {
let currentDir = process.cwd();
while (currentDir !== path.dirname(currentDir)) {
const configPath = path.join(currentDir, 'muspe.config.js');
const packagePath = path.join(currentDir, 'package.json');
if (fs.existsSync(configPath) ||
(fs.existsSync(packagePath) &&
JSON.parse(fs.readFileSync(packagePath, 'utf-8')).muspe)) {
return currentDir;
}
currentDir = path.dirname(currentDir);
}
return null;
}
module.exports = { testProject };