UNPKG

ctrlshiftleft

Version:

AI-powered toolkit for embedding QA and security testing into development workflows

453 lines (375 loc) 13.8 kB
#!/usr/bin/env node /** * Ctrl+Shift+Left Test Generator * Automatically generates Playwright tests for React components */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // Configuration const TARGET_FILE = process.argv[2] || '/Users/johngaspar/CascadeProjects/ctrlshiftleft/demo/src/components/PaymentForm.tsx'; const OUTPUT_DIR = process.argv[3] || '/Users/johngaspar/CascadeProjects/ctrlshiftleft/vscode-ext-test/generated-tests'; const COMPONENT_NAME = path.basename(TARGET_FILE, path.extname(TARGET_FILE)); // Create necessary directories function ensureDirectories() { // Main output directory if (!fs.existsSync(OUTPUT_DIR)) { console.log(`Creating output directory: ${OUTPUT_DIR}`); fs.mkdirSync(OUTPUT_DIR, { recursive: true }); } // Tests directory in project root const projectRoot = path.dirname(path.dirname(OUTPUT_DIR)); const testsDir = path.join(projectRoot, 'tests'); if (!fs.existsSync(testsDir)) { console.log(`Creating tests directory: ${testsDir}`); fs.mkdirSync(testsDir, { recursive: true }); } // Component-specific test directory const componentTestDir = path.join(testsDir, COMPONENT_NAME); if (!fs.existsSync(componentTestDir)) { console.log(`Creating component test directory: ${componentTestDir}`); fs.mkdirSync(componentTestDir, { recursive: true }); } return { outputDir: OUTPUT_DIR, testsDir, componentTestDir }; } // Function to extract props and state from a React component function extractComponentInfo(filePath) { console.log(`Analyzing component: ${filePath}`); if (!fs.existsSync(filePath)) { console.error(`File not found: ${filePath}`); return null; } const content = fs.readFileSync(filePath, 'utf8'); // Basic analysis - real implementation would use AST parsing const componentInfo = { name: COMPONENT_NAME, props: [], state: [], events: [], inputs: [], outputs: [], hasFetch: content.includes('fetch('), hasRouter: content.includes('useNavigate') || content.includes('useRouter'), hasAuth: content.includes('login') || content.includes('authentication') || content.includes('authorize'), hasForms: content.includes('form') || content.includes('onSubmit') || content.includes('onChange') }; // Extract prop types (basic implementation) const propMatches = content.match(/interface\s+\w+Props\s*\{[^}]+\}/g) || []; if (propMatches.length > 0) { const propContent = propMatches[0]; const propLines = propContent.split('\n').slice(1, -1); // Remove first and last lines (interface declaration and closing bracket) propLines.forEach(line => { const trimmedLine = line.trim(); if (trimmedLine && trimmedLine.includes(':')) { const [name, type] = trimmedLine.split(':').map(s => s.trim()); // Remove trailing semicolon or comma and optional marker const cleanName = name.replace(/[?;,]/g, ''); const cleanType = type.replace(/[;,]/g, ''); componentInfo.props.push({ name: cleanName, type: cleanType, required: !name.includes('?') }); } }); } // Extract form inputs const inputMatches = content.match(/<input[^>]*>/g) || []; inputMatches.forEach(input => { const typeMatch = input.match(/type=["']([^"']+)["']/); const nameMatch = input.match(/name=["']([^"']+)["']/); if (nameMatch) { componentInfo.inputs.push({ name: nameMatch[1], type: typeMatch ? typeMatch[1] : 'text' }); } }); // Look for forms const formMatches = content.match(/<form[^>]*>/g) || []; formMatches.forEach(form => { const onSubmitMatch = form.match(/onSubmit=\{([^}]+)\}/); if (onSubmitMatch) { componentInfo.events.push({ type: 'submit', handler: onSubmitMatch[1] }); } }); return componentInfo; } // Function to generate a Playwright test for a React component function generatePlaywrightTest(componentInfo) { if (!componentInfo) { return null; } const testFileName = `${componentInfo.name}.spec.ts`; const testFilePath = path.join(OUTPUT_DIR, testFileName); // Generate appropriate test content based on component analysis let testContent = `import { test, expect } from '@playwright/test'; /** * Playwright tests for ${componentInfo.name} component * Generated by Ctrl+Shift+Left */ // Base URL - update as needed const baseUrl = 'http://localhost:3000'; test.describe('${componentInfo.name} Component Tests', () => { test('should render the component properly', async ({ page }) => { // Navigate to the page containing the component await page.goto(\`\${baseUrl}\`); // Verify component is visible const component = page.locator('[data-testid="${componentInfo.name.toLowerCase()}"]'); await expect(component).toBeVisible(); }); `; // Add form tests if component has forms if (componentInfo.hasForms) { testContent += ` test('should validate form inputs', async ({ page }) => { await page.goto(\`\${baseUrl}\`); // Try to submit form without required fields const submitButton = page.locator('button[type="submit"]'); await submitButton.click(); // Expect validation error messages to be visible const errorMessage = page.locator('.error-message'); await expect(errorMessage).toBeVisible(); }); test('should successfully submit the form with valid data', async ({ page }) => { await page.goto(\`\${baseUrl}\`); `; // Add input filling based on detected inputs componentInfo.inputs.forEach(input => { if (input.type === 'text' || input.type === 'email') { testContent += ` await page.fill('input[name="${input.name}"]', 'test-${input.name}@example.com');\n`; } else if (input.type === 'password') { testContent += ` await page.fill('input[name="${input.name}"]', 'SecurePassword123!');\n`; } else if (input.type === 'checkbox') { testContent += ` await page.check('input[name="${input.name}"]');\n`; } else if (input.type === 'number') { testContent += ` await page.fill('input[name="${input.name}"]', '123');\n`; } }); testContent += ` // Submit the form const submitButton = page.locator('button[type="submit"]'); await submitButton.click(); // Check for success message or redirect await expect(page.locator('.success-message')).toBeVisible(); }); `; } // Add fetch/API mocking tests if component makes network requests if (componentInfo.hasFetch) { testContent += ` test('should handle API responses correctly', async ({ page }) => { await page.goto(\`\${baseUrl}\`); // Mock successful API response await page.route('**/api/**', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, message: 'Operation successful' }) }); }); // Trigger API call (e.g., by submitting form or clicking button) await page.click('button[type="submit"]'); // Verify success state is displayed await expect(page.locator('.success-message')).toBeVisible(); // Mock error API response for another test await page.route('**/api/**', (route) => { route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ success: false, message: 'Server error' }) }); }); // Trigger API call again await page.click('button[type="submit"]'); // Verify error state is displayed await expect(page.locator('.error-message')).toBeVisible(); }); `; } // Add security-focused tests if component deals with authentication if (componentInfo.hasAuth) { testContent += ` test('should handle authentication securely', async ({ page }) => { await page.goto(\`\${baseUrl}\`); // Attempt to bypass authentication // This tests if the component has proper auth checks await page.evaluate(() => { // Try to remove auth tokens from storage localStorage.removeItem('auth-token'); sessionStorage.removeItem('auth-token'); }); // Reload the page await page.reload(); // Verify user is redirected to login or sees auth error await expect(page.locator('.login-form, .auth-error')).toBeVisible(); }); `; } // Close the test suite testContent += ` }); `; // Write test file fs.writeFileSync(testFilePath, testContent); console.log(`Test file written to: ${testFilePath}`); return testFilePath; } // Function to create a Playwright config if it doesn't exist function ensurePlaywrightConfig() { const configPath = path.join(OUTPUT_DIR, 'playwright.config.ts'); if (!fs.existsSync(configPath)) { const configContent = `import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './', timeout: 30000, expect: { timeout: 5000 }, fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, ], webServer: { command: 'npm run start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, }); `; fs.writeFileSync(configPath, configContent); console.log(`Playwright config file written to: ${configPath}`); } } // Function to create a README file with instructions function createReadme() { const readmePath = path.join(OUTPUT_DIR, 'README.md'); const readmeContent = `# Generated Playwright Tests These tests were automatically generated by the Ctrl+Shift+Left test generator. ## Running the Tests 1. Install Playwright: \`\`\` npm init playwright@latest \`\`\` 2. Run the tests: \`\`\` npx playwright test \`\`\` 3. View the report: \`\`\` npx playwright show-report \`\`\` ## Test Structure - Each component has its own test file - Tests cover rendering, form submission, API interactions, and more - Security-specific tests are included where relevant ## Customizing Tests These tests are generated as a starting point. You may need to: 1. Update selectors to match your actual DOM structure 2. Adjust form input values to match your validation rules 3. Configure the correct base URL in playwright.config.ts ## Integrating with CI/CD Add this to your GitHub workflow: \`\`\`yaml - name: Run Playwright tests run: npx playwright test \`\`\` `; fs.writeFileSync(readmePath, readmeContent); console.log(`README file written to: ${readmePath}`); } // Main function function main() { console.log('Ctrl+Shift+Left Test Generator'); console.log('=============================='); // Create all necessary directories first const dirs = ensureDirectories(); console.log(`Directories created successfully`); let componentInfo = null; let testFilePath = null; let success = false; try { // Extract component information componentInfo = extractComponentInfo(TARGET_FILE); if (!componentInfo) { console.error('Failed to analyze component'); process.exit(1); } // Generate test file testFilePath = generatePlaywrightTest(componentInfo); // Also copy test to component-specific test directory const testFileName = `${componentInfo.name}.spec.ts`; const componentTestPath = path.join(dirs.componentTestDir, testFileName); fs.copyFileSync(testFilePath, componentTestPath); console.log(`Test also copied to: ${componentTestPath}`); // Create supporting files ensurePlaywrightConfig(); createReadme(); success = true; } catch (error) { console.error(`Error generating tests: ${error.message}`); console.error(error.stack); // Create minimal test file even in case of error const errorTestPath = path.join(OUTPUT_DIR, 'error-recovery.spec.ts'); fs.writeFileSync(errorTestPath, `// Error recovery test import { test, expect } from '@playwright/test'; test('minimal test', async ({ page }) => { // This is a fallback test created when an error occurred await page.goto('http://localhost:3000'); await expect(page).toHaveTitle(/.*/); }); `); console.log(`Created minimal recovery test at: ${errorTestPath}`); // Create a minimal componentInfo for summary if needed if (!componentInfo) { componentInfo = { name: path.basename(TARGET_FILE, path.extname(TARGET_FILE)), props: [], inputs: [], hasFetch: false, hasAuth: false }; } } // Print summary console.log('\nTest Generation Summary:'); console.log(`Component: ${componentInfo.name}`); console.log(`Props detected: ${componentInfo.props.length}`); console.log(`Form inputs detected: ${componentInfo.inputs.length}`); console.log(`Network requests: ${componentInfo.hasFetch ? 'Yes' : 'No'}`); console.log(`Authentication: ${componentInfo.hasAuth ? 'Yes' : 'No'}`); console.log(`Status: ${success ? '✅ Success' : '❌ Error (fallback test created)'}`); console.log('\nNext steps:'); console.log('1. Review and customize the generated tests'); console.log('2. Install Playwright: npm init playwright@latest'); console.log('3. Run the tests: npx playwright test'); } // Run the generator main();