UNPKG

ctrlshiftleft

Version:

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

400 lines (388 loc) 15.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generatePageObjectTemplate = generatePageObjectTemplate; const parser_1 = require("@babel/parser"); const traverse_1 = __importDefault(require("@babel/traverse")); const t = __importStar(require("@babel/types")); const path_1 = __importDefault(require("path")); /** * Generates a Page Object Model class for a component * @param sourcePath Path to the source component * @param sourceCode Source code of the component * @returns Test template with Page Object Model content */ function generatePageObjectTemplate(sourcePath, sourceCode) { const template = { content: '', format: 'typescript' }; // Extract component name from path const fileName = path_1.default.basename(sourcePath); const componentName = fileName.split('.')[0]; // Extract fields and their types from the component const fields = extractComponentFields(sourceCode, componentName); // Generate the Page Object Model template.content = `/** * Page Object Model for ${componentName} * Automatically generated by ctrl.shift.left */ import { Page } from '@playwright/test'; export class ${componentName}Page { readonly page: Page; // Selectors for component elements readonly selectors = { ${generateSelectors(fields)} }; constructor(page: Page) { this.page = page; } /** * Navigate to the page containing the ${componentName} */ async goto(url = 'http://localhost:3000') { await this.page.goto(url); } ${generateInteractionMethods(fields, componentName)} } `; return template; } /** * Extract fields and their types from the component source code */ function extractComponentFields(sourceCode, componentName) { const fields = []; try { // Parse the source code const ast = (0, parser_1.parse)(sourceCode, { sourceType: 'module', plugins: ['jsx', 'typescript'] }); // Track state variables and props const stateVars = {}; const props = []; // Traverse the AST (0, traverse_1.default)(ast, { // Find state variables VariableDeclarator(path) { if (t.isIdentifier(path.node.id) && t.isCallExpression(path.node.init) && t.isIdentifier(path.node.init.callee) && path.node.init.callee.name === 'useState') { const varName = path.node.id.name; const setterName = `set${varName.charAt(0).toUpperCase()}${varName.slice(1)}`; // Try to determine type from init value let varType = 'string'; if (path.node.init.arguments && path.node.init.arguments.length > 0) { const initArg = path.node.init.arguments[0]; if (t.isStringLiteral(initArg)) varType = 'string'; else if (t.isNumericLiteral(initArg)) varType = 'number'; else if (t.isBooleanLiteral(initArg)) varType = 'boolean'; else if (t.isObjectExpression(initArg)) varType = 'object'; else if (t.isArrayExpression(initArg)) varType = 'array'; } stateVars[varName] = varType; } }, // Find component props FunctionDeclaration(path) { if (path.node.id && path.node.id.name === componentName) { if (path.node.params.length > 0 && t.isIdentifier(path.node.params[0])) { // This is likely the props parameter const propsParam = path.node.params[0].name; // Find object destructuring of props path.traverse({ VariableDeclarator(innerPath) { if (t.isObjectPattern(innerPath.node.id) && t.isIdentifier(innerPath.node.init) && innerPath.node.init.name === propsParam) { innerPath.node.id.properties.forEach(prop => { if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { props.push(prop.key.name); } }); } } }); } } }, // Find form elements and input fields from JSX JSXOpeningElement(path) { const elementName = path.node.name.type === 'JSXIdentifier' ? path.node.name.name : ''; const attributes = path.node.attributes; // Extract data-testid attribute if present let testId = ''; let name = ''; let type = ''; let isForm = false; let isButton = false; let isInput = false; let isErrorMessage = false; let isSuccessMessage = false; for (const attr of attributes) { if (attr.type === 'JSXAttribute') { // Get data-testid if (attr.name.name === 'data-testid' && attr.value && attr.value.type === 'StringLiteral') { testId = attr.value.value; name = testId; } // Get name attribute if (attr.name.name === 'name' && attr.value && attr.value.type === 'StringLiteral') { name = attr.value.value; } // Get id attribute as fallback if (!name && attr.name.name === 'id' && attr.value && attr.value.type === 'StringLiteral') { name = attr.value.value; } // Get type attribute if (attr.name.name === 'type' && attr.value && attr.value.type === 'StringLiteral') { type = attr.value.value; } } } // Identify element type if (elementName === 'form') { isForm = true; name = name || 'form'; type = 'form'; } else if (elementName === 'button' || type === 'submit') { isButton = true; name = name || (type === 'submit' ? 'submit-button' : 'button'); type = 'button'; } else if (elementName === 'input' || elementName === 'select' || elementName === 'textarea') { isInput = true; name = name || 'input'; type = type || elementName; } else if (testId?.includes('error') || elementName.toLowerCase().includes('error')) { isErrorMessage = true; name = name || 'error-message'; type = 'error'; } else if (testId?.includes('success') || elementName.toLowerCase().includes('success')) { isSuccessMessage = true; name = name || 'success-message'; type = 'success'; } // Add field if it has a name if (name && (isForm || isButton || isInput || isErrorMessage || isSuccessMessage)) { fields.push({ name, type: type || 'element', testId, isForm, isButton, isInput, isErrorMessage, isSuccessMessage }); } } }); // Add state variables that don't have corresponding UI elements Object.entries(stateVars).forEach(([name, type]) => { // Check if this state var already has a UI element if (!fields.some(field => field.name === name)) { fields.push({ name, type, testId: undefined }); } }); } catch (error) { console.error('Error parsing component:', error); // Fallback with generic fields if parsing fails fields.push({ name: 'form', type: 'form', isForm: true }, { name: 'input', type: 'input', isInput: true }, { name: 'submit-button', type: 'button', isButton: true }, { name: 'error-message', type: 'error', isErrorMessage: true }, { name: 'success-message', type: 'success', isSuccessMessage: true }); } return fields; } /** * Generate selectors for the Page Object Model */ function generateSelectors(fields) { return fields.map(field => { const selectorName = field.name .replace(/[^a-zA-Z0-9]/g, '_') .replace(/_+/g, '_') .toLowerCase(); const testIdSelector = field.testId ? `'[data-testid="${field.testId}"]'` : `'[data-testid="${field.name.replace(/\s+/g, '-').toLowerCase()}${field.isInput ? '-input' : ''}"]'`; return ` ${selectorName}: ${testIdSelector},`; }).join('\n'); } /** * Generate interaction methods based on component fields */ function generateInteractionMethods(fields, componentName) { let methods = ''; // Form submission method if (fields.some(f => f.isForm)) { methods += ` /** * Submit the form */ async submitForm() { await this.page.click(this.selectors.submit_button); } `; } // Fill form method if there are input fields const inputFields = fields.filter(f => f.isInput); if (inputFields.length > 0) { methods += ` /** * Fill the form with the provided data */ async fillForm(data: { ${inputFields.map(field => ` ${field.name}?: string;`).join('\n')} }) { ${inputFields.map(field => ` if (data.${field.name} !== undefined) { await this.page.fill(this.selectors.${field.name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}, data.${field.name}); }`).join('\n')} } /** * Fill the form with valid test data */ async fillWithValidData() { await this.fillForm({ ${inputFields.map(field => { let validValue = '"test"'; if (field.name.includes('email')) validValue = '"test@example.com"'; if (field.name.includes('password')) validValue = '"Password123!"'; if (field.name.includes('card') && field.name.includes('number')) validValue = '"4111111111111111"'; if (field.name.includes('cvv') || field.name.includes('cvc')) validValue = '"123"'; if (field.name.includes('expiry') || field.name.includes('exp')) validValue = '"12/25"'; if (field.name.includes('amount') || field.name.includes('price')) validValue = '"100.00"'; return ` ${field.name}: ${validValue}`; }).join(',\n')} }); } `; } // Error message getter if there are error messages if (fields.some(f => f.isErrorMessage)) { methods += ` /** * Get error message text if present */ async getErrorMessage() { const errorSelector = this.selectors.error_message; await this.page.waitForSelector(errorSelector, { state: 'visible', timeout: 5000 }); return this.page.textContent(errorSelector); } `; } // Success message getter if there are success messages if (fields.some(f => f.isSuccessMessage)) { methods += ` /** * Get success message text if present */ async getSuccessMessage() { const successSelector = this.selectors.success_message; await this.page.waitForSelector(successSelector, { state: 'visible', timeout: 5000 }); return this.page.textContent(successSelector); } `; } // Generate validation methods methods += ` /** * Test if XSS payloads are sanitized */ async testXSSSanitization() { // Fill form with XSS payloads await this.fillForm(${inputFields.map(field => `{ ${field.name}: "Test<script>alert('XSS')</script>" }`).join(', ')}); await this.submitForm(); // Check if the page contains unsanitized script tags const content = await this.page.content(); return !content.includes('<script>alert'); } /** * Test CSRF token validation */ async testCSRFProtection(invalidToken = 'invalid-token') { await this.page.evaluate(({ token }) => { // Modify CSRF token in meta tag if present const csrfMetaTag = document.querySelector('meta[name="csrf-token"]'); if (csrfMetaTag) { csrfMetaTag.setAttribute('content', token); } // Modify CSRF token in sessionStorage if used sessionStorage.setItem('csrf-token', token); // Modify CSRF token in localStorage if used localStorage.setItem('csrf-token', token); }, { token: invalidToken }); await this.submitForm(); // Check for security error try { await this.page.waitForSelector('[data-testid*="error"]', { state: 'visible', timeout: 5000 }); return true; } catch (e) { return false; } } `; return methods; } //# sourceMappingURL=pageObjectTemplate.js.map