ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
453 lines (375 loc) • 13.8 kB
JavaScript
#!/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();