UNPKG

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
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 };