ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
713 lines (632 loc) • 34.4 kB
text/typescript
import { TestScenario, TestTemplate } from '../types/testTypes';
import path from 'path';
/**
* Generate a Playwright test file from test scenarios
* @param scenarios Test scenarios to include
* @param sourcePath Path to the source file (for documentation)
* @param usePageObjects Whether to use Page Object Model pattern
* @param useTestData Whether to use Test Data Factory pattern
* @returns Test template with content
*/
export function generatePlaywrightTest(
scenarios: TestScenario[],
sourcePath: string,
usePageObjects: boolean = true,
useTestData: boolean = true
): TestTemplate {
const template: TestTemplate = {
content: '',
format: 'playwright'
};
// Extract component name from source path for class naming
const fileName = path.basename(sourcePath);
const componentName = fileName.split('.')[0];
// Determine relative path for imports
const relativeImportPath = '../../helpers';
// Generate imports and header
template.content = `
import { test, expect } from '@playwright/test';
import type { Page } from '@playwright/test';
${usePageObjects ? `import { ${componentName}Page } from '${relativeImportPath}/${componentName}Page';
` : ''}${useTestData ? `import { ${componentName.toLowerCase()}Data, generateValidCardNumber, generateValidExpiryDate } from '${relativeImportPath}/testDataFactory';
` : ''}
/**
* End-to-end tests generated for: ${sourcePath}
* Generated by ctrl.shift.left
*
* This file contains comprehensive tests that cover:
* - UI/UX testing
* - API and integration testing
* - Security vulnerability testing
*/
// Test suite implementation for the Ctrl.shift.left test runner
export default {
async runTests(page: Page, options = {}) {
let total = 0;
let passed = 0;
let failed = 0;
let skipped = 0;
// Run all tests with the page instance
for (const testFn of Object.values(tests)) {
total++;
try {
await testFn(page);
passed++;
} catch (error) {
failed++;
console.error(error);
}
}
return { total, passed, failed, skipped };
}
};
// Helper to run tests programmatically through the ctrl.shift.left test runner
export default {
async runTests(page: Page, options = {}) {
let total = 0;
let passed = 0;
let failed = 0;
let skipped = 0;
// Run all tests with the page instance
for (const testFn of Object.values(tests)) {
total++;
try {
await testFn({ page });
passed++;
} catch (error) {
failed++;
console.error(error);
}
}
return { total, passed, failed, skipped };
}
};
// Test implementations that can be run individually or through the test runner
const tests = {
`;
// Determine if we need to set up page objects
const setupPageObject = usePageObjects ? ` // Setup - create page object
const ${componentName.toLowerCase()}Page = new ${componentName}Page(page);
test.setTimeout(60000);
// Navigate to form
await ${componentName.toLowerCase()}Page.goto();
` : ` test.setTimeout(60000);
// Navigate to the page
await page.goto("http://localhost:3000");
`;
// Convert scenarios to test implementations
const scenarioEntries = scenarios.map((scenario, index) => {
// Create valid JavaScript identifier for test name
const scenarioId = `test_${index + 1}_${(scenario.id || `scenario${index + 1}`).replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}`;
const testName = `${scenarioId}: async ({ page }) => {`;
const testSetup = setupPageObject;
// Map steps to code with meaningful selector placeholders
const stepsCode = scenario.steps
.map(step => {
// Handle JSON structured step objects
if (typeof step === 'object' && step !== null) {
try {
// For structured step objects, try to intelligently generate code
const stepObj = typeof step === 'string' ? JSON.parse(step) : step;
const action = stepObj.action?.toLowerCase();
const target = stepObj.target;
const value = stepObj.value;
if (action === 'fill_input' || action === 'type' || action === 'fill') {
if (usePageObjects) {
return ` // Fill ${target} field
await ${componentName.toLowerCase()}Page.fillForm({
${target}: '${value}'
});`;
} else {
return ` console.log('Filling ${target} with value: ${value}');
await page.fill('[data-testid="${target}${target.endsWith('-input') ? '' : '-input'}"]', '${value}');`;
}
} else if (action === 'click') {
if (usePageObjects && target === 'submit') {
return ` // Submit the form
await ${componentName.toLowerCase()}Page.submitForm();`;
} else {
return ` console.log('Clicking on ${target}');
await page.click('[data-testid="${target}"]');`;
}
} else if (action === 'navigate' || action === 'goto') {
if (usePageObjects) {
return ` // Navigate to page
await ${componentName.toLowerCase()}Page.goto('${value || 'http://localhost:3000'}');`;
} else {
return ` console.log('Navigating to ${value || 'http://localhost:3000'}');
await page.goto('${value || 'http://localhost:3000'}');`;
}
} else if (action === 'wait') {
return ` console.log('Waiting for ${value || 1000}ms');
await page.waitForTimeout(${value || 1000});`;
} else if (action === 'check' || action === 'uncheck') {
return ` console.log('${action.charAt(0).toUpperCase() + action.slice(1)}ing ${target}');
await page.${action}('[data-testid="${target}"]');`;
} else if (action === 'select') {
return ` console.log('Selecting option ${value} for ${target}');
await page.selectOption('[data-testid="${target}"]', '${value}');`;
} else if (action === 'set_state') {
return ` console.log('Simulating state change: ${JSON.stringify(value)}');\n // State manipulation typically requires interaction with app-specific APIs\n // For testing direct state manipulation, we'd need to use page.evaluate()\n await page.evaluate((stateData) => {\n // This would need to be customized based on your app's state management\n console.log('State change simulation', stateData);\n window.dispatchEvent(new CustomEvent('test:setState', { detail: stateData }));\n }, ${JSON.stringify(value)});`;
} else {
return ` console.log('Executing custom action: ${action}');\n // Implementation for ${action} on ${target} with value ${value}\n await page.evaluate((data) => {\n console.log('Custom action', data);\n }, { action: '${action}', target: '${target}', value: ${JSON.stringify(value)} });`;
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
return ` // ${JSON.stringify(step)}\n // TODO: Implement code for this step (Error parsing: ${errorMessage})`;
}
}
// Ensure step is a string for text-based steps
const stepStr = typeof step === 'string' ? step : JSON.stringify(step);
// Convert text step to actual code where possible
if (stepStr.toLowerCase().includes('navigate to')) {
return ` // ${stepStr}\n await page.goto('http://localhost:3000');`;
} else if (stepStr.toLowerCase().includes('enter') && stepStr.toLowerCase().includes('username')) {
return ` // ${stepStr}\n await page.fill('[data-testid="username-input"]', 'testuser');`;
} else if (stepStr.toLowerCase().includes('enter') && stepStr.toLowerCase().includes('password')) {
return ` // ${stepStr}\n await page.fill('[data-testid="password-input"]', 'password123');`;
} else if (stepStr.toLowerCase().includes('click') && stepStr.toLowerCase().includes('login')) {
return ` // ${stepStr}\n await page.click('[data-testid="login-button"]');`;
} else {
return ` // ${stepStr}\n // TODO: Implement code for this step`;
}
})
.join('\n');
// Map assertions to code with actual implementation
const assertionsCode = scenario.assertions
.map(assertion => {
if (typeof assertion === 'object' && assertion !== null) {
try {
const assertObj = typeof assertion === 'string' ? JSON.parse(assertion) : assertion;
const target = assertObj.target;
const condition = assertObj.condition?.toLowerCase();
const value = assertObj.value;
if (condition === 'exists' || condition === 'visible') {
return ` console.log('Asserting ${target} exists/is visible');\n await page.waitForSelector('[data-testid="${target}"]', { state: 'visible', timeout: 10000 });\n expect(await page.locator('[data-testid="${target}"]').isVisible()).toBeTruthy();`;
} else if (condition === 'not_exists' || condition === 'not_visible') {
return ` console.log('Asserting ${target} does not exist/is not visible');\n // Using a small timeout since we expect this element to not exist\n const isVisible = await page.locator('[data-testid="${target}"]').isVisible();\n expect(isVisible).toBeFalsy();`;
} else if (condition === 'contains' || condition === 'contains_text') {
return ` console.log('Asserting ${target} contains text: "${value}"');\n await page.waitForSelector('[data-testid="${target}"]', { state: 'visible', timeout: 10000 });\n const content = await page.textContent('[data-testid="${target}"]');\n expect(content).toContain('${value}');`;
} else if (condition === 'equals' || condition === 'equals_text') {
return ` console.log('Asserting ${target} text equals: "${value}"');\n await page.waitForSelector('[data-testid="${target}"]', { state: 'visible', timeout: 10000 });\n const content = await page.textContent('[data-testid="${target}"]');\n expect(content?.trim()).toBe('${value}');`;
} else if (condition === 'url') {
return ` console.log('Asserting URL contains: "${value}"');\n await page.waitForURL((url) => url.toString().includes('${value}'), { timeout: 10000 });\n expect(page.url()).toContain('${value}');`;
} else if (condition === 'count') {
return ` console.log('Asserting ${target} count equals: ${value}');\n await page.waitForSelector('[data-testid="${target}"]', { state: 'attached', timeout: 10000 });\n expect(await page.locator('[data-testid="${target}"]').count()).toBe(${value});`;
} else if (condition === 'value') {
return ` console.log('Asserting ${target} value equals: "${value}"');\n await page.waitForSelector('[data-testid="${target}"]', { state: 'visible', timeout: 10000 });\n const inputValue = await page.inputValue('[data-testid="${target}"]');\n expect(inputValue).toBe('${value}');`;
} else if (condition === 'attribute') {
const [attributeName, attributeValue] = value.split('=');
return ` console.log('Asserting ${target} has attribute ${attributeName} = "${attributeValue}"');\n await page.waitForSelector('[data-testid="${target}"]', { state: 'visible', timeout: 10000 });\n const attributeValue = await page.getAttribute('[data-testid="${target}"]', '${attributeName}');\n expect(attributeValue).toBe('${attributeValue}');`;
} else {
return ` console.log('Asserting custom condition on ${target}');\n await page.waitForSelector('[data-testid="${target}"]', { state: 'visible', timeout: 10000 });\n // Custom assertion for condition: ${condition} with value: ${value}\n const element = await page.locator('[data-testid="${target}"]');\n expect(await element.isVisible()).toBeTruthy();`;
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
return ` console.log('Executing assertion with parsing error: ${errorMessage}');
// Implementing a generic assertion as fallback
const element = await page.locator('[data-testid="error-message"]');
if (await element.isVisible()) {
expect(true).toBeTruthy(); // At least verify something is visible
}`;
}
}
// Ensure assertion is a string
const assertionStr = typeof assertion === 'string' ? assertion : JSON.stringify(assertion);
// Convert assertions to actual code
if (assertionStr.toLowerCase().includes('login button') && assertionStr.toLowerCase().includes('logging in')) {
return ` // ${assertionStr}\n const buttonText = await page.textContent('[data-testid="login-button"]');\n expect(buttonText).toContain('Logging in');`;
} else if (assertionStr.toLowerCase().includes('success') && assertionStr.toLowerCase().includes('message')) {
return ` // ${assertionStr}\n await page.waitForSelector('[data-testid="login-success"]', { state: 'visible', timeout: 5000 });\n const successMessage = await page.textContent('[data-testid="login-success"]');\n expect(successMessage).toContain('successful');`;
} else if (assertionStr.toLowerCase().includes('error') && assertionStr.toLowerCase().includes('message')) {
return ` // ${assertionStr}\n await page.waitForSelector('[data-testid="general-error"]', { state: 'visible', timeout: 5000 });\n const errorMessage = await page.textContent('[data-testid="general-error"]');\n expect(errorMessage).toContain('Invalid');`;
} else if (assertionStr.toLowerCase().includes('xss') || assertionStr.toLowerCase().includes('script')) {
// Enhanced security assertion for XSS
return ` // ${assertionStr}\n // Security test: Check for XSS vulnerabilities\n const content = await page.content();\n expect(content).not.toContain('<script>alert');\n expect(content).not.toContain('onerror=');`;
} else if (assertionStr.toLowerCase().includes('sanitize') || assertionStr.toLowerCase().includes('escape')) {
// Enhanced security assertion for input sanitization
return ` // ${assertionStr}\n // Security test: Verify input sanitization\n const displayedValue = await page.textContent('[data-testid="username-input"]');\n expect(displayedValue || '').not.toContain('<script>');`;
} else {
return ` // ${assertionStr}\n // TODO: Implement assertion`;
}
})
.join('\n');
return `${testName}\n${testSetup}\n // Test steps\n${stepsCode}\n\n // Assertions\n${assertionsCode}\n },`;
}).join('\n');
template.content += scenarioEntries;
// Create test file content
template.content += `
export { tests };
// Individual test implementations that can be run directly by Playwright
`;
// Generate individual Playwright tests for direct running
scenarios.forEach((scenario, index) => {
const scenarioName = scenario.name || `Test Scenario ${index + 1}`;
template.content += `test('${scenarioName}', async ({ page }) => {
`;
// Add Page Object Model initialization if enabled
if (usePageObjects) {
template.content += ` const ${componentName.toLowerCase()}Page = new ${componentName}Page(page);
await ${componentName.toLowerCase()}Page.goto();
`;
} else {
template.content += ` await page.goto('http://localhost:3000');
`;
}
// Add test steps
if (scenario.type === 'security' || scenarioName.toLowerCase().includes('security') || scenarioName.toLowerCase().includes('xss') || scenarioName.toLowerCase().includes('csrf')) {
// Special handling for security tests
if (scenarioName.toLowerCase().includes('xss')) {
if (usePageObjects && useTestData) {
template.content += ` // Enter XSS payloads
const xssPayload = '<script>alert("XSS")</script>';
await ${componentName.toLowerCase()}Page.fillForm({
${Object.keys(scenario.data || {}).map(key => `${key}: ${componentName.toLowerCase()}Data.security.xss.${key}`).join(',\n ')}
});
await ${componentName.toLowerCase()}Page.submitForm();
// Verify content doesn't contain unescaped script tags
const pageContent = await page.content();
expect(pageContent).not.toContain('<script>alert');
// Verify inputs were sanitized
const sanitized = await page.evaluate(() => {
const inputs = Array.from(document.querySelectorAll('input')) as HTMLInputElement[];
return !inputs.some(input =>
input.value.includes('<script>') ||
input.value.includes('alert("XSS")')
);
});
expect(sanitized).toBe(true);
`;
} else {
template.content += ` // Enter XSS payloads
const xssPayload = '<script>alert("XSS")</script>';
await page.fill('[data-testid="amount-input"]', '100.00' + xssPayload);
await page.fill('[data-testid="card-number-input"]', '4111' + xssPayload + '1111');
await page.click('[data-testid="submit-button"]');
// Verify content doesn't contain unescaped script tags
const pageContent = await page.content();
expect(pageContent).not.toContain('<script>alert');
`;
}
} else if (scenarioName.toLowerCase().includes('csrf')) {
if (usePageObjects) {
template.content += ` // Fill form with valid data
await ${componentName.toLowerCase()}Page.fillWithValidData();
// Tamper with CSRF token
await ${componentName.toLowerCase()}Page.testCSRFProtection('invalid-csrf-token');
// Verify security validation failed
const errorMessage = await ${componentName.toLowerCase()}Page.getErrorMessage();
expect(errorMessage).toMatch(/security|validation|csrf|token|invalid/i);
`;
} else {
template.content += ` // Fill form with valid data
await page.fill('[data-testid="amount-input"]', '100.00');
await page.fill('[data-testid="card-number-input"]', '4111111111111111');
// Tamper with CSRF token
await page.evaluate(() => {
const csrfMetaTag = document.querySelector('meta[name="csrf-token"]');
if (csrfMetaTag) csrfMetaTag.setAttribute('content', 'invalid-token');
sessionStorage.setItem('csrf-token', 'invalid-token');
});
// Submit form
await page.click('[data-testid="submit-button"]');
// Check for error message
await page.waitForSelector('[data-testid="error-message"]', { state: 'visible' });
const errorText = await page.textContent('[data-testid="error-message"]');
expect(errorText).toMatch(/security|validation|csrf|token|invalid/i);
`;
}
} else if (scenarioName.toLowerCase().includes('api') || scenarioName.toLowerCase().includes('error')) {
if (usePageObjects) {
template.content += ` // Fill form with valid data
await ${componentName.toLowerCase()}Page.fillWithValidData();
// Mock API failure response
await page.route('**/api/**', route => route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server error', message: 'Processing failed' })
}));
// Submit form
await ${componentName.toLowerCase()}Page.submitForm();
// Verify error message is displayed
await page.waitForSelector('[data-testid="server-error-message"]', { state: 'visible' });
const errorText = await page.textContent('[data-testid="server-error-message"]');
expect(errorText).toMatch(/error|failed|server/i);
`;
} else {
template.content += ` // Fill form with valid data
await page.fill('[data-testid="amount-input"]', '100.00');
await page.fill('[data-testid="card-number-input"]', '4111111111111111');
// Mock API failure
await page.route('**/api/**', route => route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server error', message: 'Processing failed' })
}));
// Submit form
await page.click('[data-testid="submit-button"]');
// Check for error message
await page.waitForSelector('[data-testid="error-message"]', { state: 'visible' });
`;
}
} else {
// Default security test implementation
template.content += ` // Test steps
${scenario.steps.map((step: string) => {
if (typeof step === 'string') {
return `// ${step}`;
} else {
try {
// For JSON objects describing steps
const stepObj = typeof step === 'string' ? JSON.parse(step) : step;
return `// ${stepObj.description || stepObj.action || JSON.stringify(step)}`;
} catch (error) {
return `// ${JSON.stringify(step)}`;
}
}
}).join('\n ')}
// Assertions
${scenario.assertions.map((assertion: string) => {
if (typeof assertion === 'string') {
return `// ${assertion}\n ${convertAssertionToCode(assertion)}`;
} else {
try {
const assertObj = typeof assertion === 'string' ? JSON.parse(assertion) : assertion;
return `// ${assertObj.description || assertObj.assertion || JSON.stringify(assertion)}\n ${convertAssertionToCode(assertObj.description || assertObj.assertion || '')}`;
} catch (error) {
return `// ${JSON.stringify(assertion)}\n // Wait for app to stabilize\n await page.waitForLoadState('networkidle');\n expect(await page.content()).toBeTruthy();`;
}
}
}).join('\n\n ')}
`;
}
} else if (scenario.type === 'ui' || scenarioName.toLowerCase().includes('accessibility') || scenarioName.toLowerCase().includes('responsive')) {
// UI/UX test implementation
if (scenarioName.toLowerCase().includes('accessibility')) {
template.content += ` // Test keyboard navigation through form
await page.keyboard.press('Tab');
let firstFocused = await page.evaluate(() => document.activeElement?.getAttribute('data-testid'));
expect(firstFocused).toBeTruthy();
// Verify ARIA attributes are present on form elements
const accessibilityAttributes = await page.evaluate(() => {
const results = {
allInputsHaveLabels: false,
formHasRole: false,
errorMessagesAreAccessible: false
};
// Check all input fields have labels
const inputs = Array.from(document.querySelectorAll('input')) as HTMLInputElement[];
results.allInputsHaveLabels = inputs.every(input => {
// Check for associated label
if (input.id && document.querySelector('label[for="' + input.id + '"]')) {
return true;
}
// Check for aria attributes
return input.hasAttribute('aria-label') || input.hasAttribute('aria-labelledby');
});
// Check form has appropriate role
const form = document.querySelector('form');
results.formHasRole = form ? (form.hasAttribute('role') || true) : false; // Forms have implicit role
// Check error messages have appropriate aria attributes
const errorMessages = Array.from(document.querySelectorAll('[data-testid$="-error"]'));
results.errorMessagesAreAccessible = errorMessages.length === 0 || errorMessages.every(el =>
el.hasAttribute('role') || el.hasAttribute('aria-live')
);
return results;
});
expect(accessibilityAttributes.allInputsHaveLabels).toBe(true);
expect(accessibilityAttributes.formHasRole).toBe(true);
expect(accessibilityAttributes.errorMessagesAreAccessible).toBe(true);
`;
} else if (scenarioName.toLowerCase().includes('responsive') || scenarioName.toLowerCase().includes('mobile')) {
template.content += ` // Test responsive design
// Mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
let isMobileLayoutCorrect = await page.evaluate(() => {
const form = document.querySelector('form');
const computedStyle = form ? window.getComputedStyle(form) : null;
return computedStyle ? ['flex', 'block'].includes(computedStyle.display) : false;
});
expect(isMobileLayoutCorrect).toBe(true);
// Tablet viewport
await page.setViewportSize({ width: 768, height: 1024 });
let isTabletLayoutCorrect = await page.evaluate(() => {
const form = document.querySelector('form');
const computedStyle = form ? window.getComputedStyle(form) : null;
return computedStyle ? ['flex', 'block'].includes(computedStyle.display) : false;
});
expect(isTabletLayoutCorrect).toBe(true);
// Desktop viewport
await page.setViewportSize({ width: 1280, height: 800 });
let isDesktopLayoutCorrect = await page.evaluate(() => {
const form = document.querySelector('form');
const computedStyle = form ? window.getComputedStyle(form) : null;
return computedStyle ? ['flex', 'block'].includes(computedStyle.display) : false;
});
expect(isDesktopLayoutCorrect).toBe(true);
`;
} else if (scenarioName.toLowerCase().includes('validation')) {
if (usePageObjects) {
template.content += ` // Test empty form submission
await ${componentName.toLowerCase()}Page.submitForm();
// Assert field validation errors appear
await page.waitForSelector('[data-testid*="error"]', { state: 'visible' });
// Test with invalid data
await ${componentName.toLowerCase()}Page.fillForm(${useTestData ? `${componentName.toLowerCase()}Data.invalid` : `{
amount: '-50',
cardNumber: '1234'
}`});
await ${componentName.toLowerCase()}Page.submitForm();
// Assert validation errors are shown
await page.waitForSelector('[data-testid*="error"]', { state: 'visible' });
// Test with valid data
await ${componentName.toLowerCase()}Page.fillWithValidData();
await ${componentName.toLowerCase()}Page.submitForm();
// Assert submission is successful
await page.waitForSelector('[data-testid="success-message"]', { state: 'visible' });
`;
} else {
template.content += ` // Test empty form submission
await page.click('[data-testid="submit-button"]');
// Assert field validation errors appear
await page.waitForSelector('[data-testid*="error"]', { state: 'visible' });
// Test with invalid data
await page.fill('[data-testid="amount-input"]', '-50');
await page.fill('[data-testid="card-number-input"]', '1234');
await page.click('[data-testid="submit-button"]');
// Assert validation errors are shown
await page.waitForSelector('[data-testid*="error"]', { state: 'visible' });
// Test with valid data
await page.fill('[data-testid="amount-input"]', '100.00');
await page.fill('[data-testid="card-number-input"]', '4111111111111111');
await page.click('[data-testid="submit-button"]');
// Assert submission is successful
await page.waitForSelector('[data-testid="success-message"]', { state: 'visible' });
`;
}
} else {
// Default UI/UX test implementation
template.content += ` // Test steps
${scenario.steps.map((step: string) => {
if (typeof step === 'string') {
return `// ${step}`;
} else {
try {
// For JSON objects describing steps
const stepObj = typeof step === 'string' ? JSON.parse(step) : step;
return `// ${stepObj.description || stepObj.action || JSON.stringify(step)}`;
} catch (error) {
return `// ${JSON.stringify(step)}`;
}
}
}).join('\n ')}
// Assertions
${scenario.assertions.map((assertion: string) => {
if (typeof assertion === 'string') {
return `// ${assertion}\n ${convertAssertionToCode(assertion)}`;
} else {
try {
const assertObj = typeof assertion === 'string' ? JSON.parse(assertion) : assertion;
return `// ${assertObj.description || assertObj.assertion || JSON.stringify(assertion)}\n ${convertAssertionToCode(assertObj.description || assertObj.assertion || '')}`;
} catch (error) {
return `// ${JSON.stringify(assertion)}\n // Wait for app to stabilize\n await page.waitForLoadState('networkidle');\n expect(await page.content()).toBeTruthy();`;
}
}
}).join('\n\n ')}
`;
}
} else {
// Default implementation for other test types
template.content += ` // Test steps
${scenario.steps.map((step: string) => {
if (typeof step === 'string') {
return `// ${step}`;
} else {
try {
// For JSON objects describing steps
const stepObj = typeof step === 'string' ? JSON.parse(step) : step;
return `// ${stepObj.description || stepObj.action || JSON.stringify(step)}`;
} catch (error) {
return `// ${JSON.stringify(step)}`;
}
}
}).join('\n ')}
// Assertions
${scenario.assertions.map((assertion: string) => {
if (typeof assertion === 'string') {
return `// ${assertion}\n ${convertAssertionToCode(assertion)}`;
} else {
try {
const assertObj = typeof assertion === 'string' ? JSON.parse(assertion) : assertion;
return `// ${assertObj.description || assertObj.assertion || JSON.stringify(assertion)}\n ${convertAssertionToCode(assertObj.description || assertObj.assertion || '')}`;
} catch (error) {
return `// ${JSON.stringify(assertion)}\n // Wait for app to stabilize\n await page.waitForLoadState('networkidle');\n expect(await page.content()).toBeTruthy();`;
}
}
}).join('\n\n ')}
`;
}
template.content += `});
`;
});
return template;
}
/**
* Convert a plain text assertion to Playwright code
* Enhanced implementation with detailed security-focused assertions
*/
function convertAssertionToCode(assertion: string): string {
// Improved conversion with comprehensive security assertions
if (assertion.toLowerCase().includes('url')) {
return `await page.waitForURL((url) => url.toString().includes('expected'), { timeout: 10000 });
expect(page.url()).toContain('expected');`;
} else if (assertion.toLowerCase().includes('title')) {
return `await page.waitForFunction(() => document.title.includes('Expected'), { timeout: 10000 });
expect(await page.title()).toContain('Expected');`;
} else if (assertion.toLowerCase().includes('visible') || assertion.toLowerCase().includes('see')) {
const selector = extractSelector(assertion) || '[data-testid="element"]';
return `await page.waitForSelector('${selector}', { state: 'visible', timeout: 10000 });
expect(await page.locator('${selector}').isVisible()).toBeTruthy();`;
} else if (assertion.toLowerCase().includes('text')) {
const selector = extractSelector(assertion) || '[data-testid="element"]';
const expectedText = extractExpectedText(assertion) || 'expected text';
return `await page.waitForSelector('${selector}', { state: 'visible', timeout: 10000 });
const textContent = await page.locator('${selector}').textContent();
expect(textContent).toContain('${expectedText}');`;
} else if (assertion.toLowerCase().includes('element')) {
const selector = extractSelector(assertion) || '[data-testid="element"]';
return `await page.waitForSelector('${selector}', { state: 'attached', timeout: 10000 });
expect(await page.locator('${selector}').count()).toBeGreaterThan(0);`;
} else if (assertion.toLowerCase().includes('xss') || assertion.toLowerCase().includes('injection') || assertion.toLowerCase().includes('sanitize')) {
// Comprehensive security assertion for XSS
return `// Security check for XSS protection
const dangerousInput = '<script>alert("XSS")</script>';
await page.fill('[data-testid="input"]', dangerousInput);
await page.click('[data-testid="submit"]');
// Verify content doesn't contain unescaped script tags
const pageContent = await page.content();
expect(pageContent).not.toContain('<script>alert');
// Check for error message
const errorElement = page.locator('[data-testid="error-message"]');
if (await errorElement.isVisible()) {
expect(await errorElement.textContent()).toContain('Invalid');
}`;
} else if (assertion.toLowerCase().includes('security') || assertion.toLowerCase().includes('validation')) {
return `// Security validation check
const invalidInput = assertion.toLowerCase().includes('sql') ? "' OR 1=1 --" : '<script>alert(1)</script>';
await page.fill('[data-testid="input"]', invalidInput);
await page.click('[data-testid="submit"]');
// Check for appropriate security validation
await page.waitForSelector('[data-testid="form-error"]', { state: 'visible', timeout: 10000 });
expect(await page.locator('[data-testid="form-error"]').isVisible()).toBeTruthy();
const errorText = await page.locator('[data-testid="form-error"]').textContent();
expect(errorText).toMatch(/invalid|error|failed/i);`;
} else if (assertion.toLowerCase().includes('auth') || assertion.toLowerCase().includes('password')) {
return `// Authentication security check
// Attempt with weak/invalid credentials
await page.fill('[data-testid="username-input"]', 'test');
await page.fill('[data-testid="password-input"]', 'weak');
await page.click('[data-testid="login-button"]');
// Verify proper auth validation
await page.waitForSelector('[data-testid="error-message"]', { state: 'visible', timeout: 10000 });
expect(await page.locator('[data-testid="error-message"]').isVisible()).toBeTruthy();`;
} else {
return `// Generic assertion for: ${assertion}
// Waiting for app to stabilize
await page.waitForLoadState('networkidle');
// Basic content verification
const pageContent = await page.content();
expect(pageContent).toBeTruthy();`;
}
}
// Helper functions for parsing assertions
function extractSelector(assertion: string): string | null {
// Simple extraction logic - would be enhanced in actual implementation
const selectorMatches = assertion.match(/['"](\[.*?\]|#\w+|\w+)['"]/);
return selectorMatches ? selectorMatches[1] : null;
}
function extractExpectedText(assertion: string): string | null {
// Simple extraction logic - would be enhanced in actual implementation
const textMatches = assertion.match(/['"](.*?)['"]/);
return textMatches ? textMatches[1] : null;
}