upskill-event-manager/tests/test-modal-forms.js
ben 90193ea18c security: implement Phase 1 critical vulnerability fixes
- Add XSS protection with DOMPurify sanitization in rich text editor
- Implement comprehensive file upload security validation
- Enhance server-side content sanitization with wp_kses
- Add comprehensive security test suite with 194+ test cases
- Create security remediation plan documentation

Security fixes address:
- CRITICAL: XSS vulnerability in event description editor
- HIGH: File upload security bypass for malicious files
- HIGH: Enhanced CSRF protection verification
- MEDIUM: Input validation and error handling improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 18:53:23 -03:00

598 lines
No EOL
25 KiB
JavaScript

const { test, expect } = require('@playwright/test');
const { HVACTestBase } = require('./page-objects/HVACTestBase');
/**
* Modal Form Management Test Suite
*
* Tests the modal system for creating new entities including:
* - Organizer creation modal
* - Category creation modal
* - Venue creation modal
* - Form validation and error handling
* - AJAX submission and response handling
* - Modal lifecycle (open, close, reset)
* - Backdrop interaction and keyboard controls
* - Data persistence and form integration
*/
test.describe('Modal Form Management System', () => {
let hvacTest;
test.beforeEach(async ({ page }) => {
hvacTest = new HVACTestBase(page);
await hvacTest.loginAsTrainer();
await hvacTest.navigateToCreateEvent();
// Wait for selectors and modals to be ready
await expect(page.locator('.organizer-selector')).toBeVisible();
await expect(page.locator('.category-selector')).toBeVisible();
await expect(page.locator('.venue-selector')).toBeVisible();
});
test.describe('Organizer Modal', () => {
test('should open organizer creation modal correctly', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
// Modal should be visible
await expect(page.locator('#organizer-modal')).toBeVisible();
await expect(page.locator('#organizer-modal .modal-backdrop')).toBeVisible();
// Check modal structure
await expect(page.locator('#organizer-modal .modal-title')).toContainText('Create New Organizer');
await expect(page.locator('#organizer-modal .modal-close')).toBeVisible();
// Form fields should be visible and empty
await expect(page.locator('#new-organizer-name')).toBeVisible();
await expect(page.locator('#new-organizer-email')).toBeVisible();
await expect(page.locator('#new-organizer-phone')).toBeVisible();
await expect(page.locator('#new-organizer-organization')).toBeVisible();
// Verify initial state
const nameValue = await page.locator('#new-organizer-name').inputValue();
expect(nameValue).toBe('');
});
test('should close modal with close button', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
await expect(page.locator('#organizer-modal')).toBeVisible();
await page.click('#organizer-modal .modal-close');
await expect(page.locator('#organizer-modal')).not.toBeVisible();
});
test('should close modal with backdrop click', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
await expect(page.locator('#organizer-modal')).toBeVisible();
// Click on backdrop (outside modal content)
await page.click('#organizer-modal .modal-backdrop', { position: { x: 10, y: 10 } });
await expect(page.locator('#organizer-modal')).not.toBeVisible();
});
test('should close modal with Escape key', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
await expect(page.locator('#organizer-modal')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.locator('#organizer-modal')).not.toBeVisible();
});
test('should validate required fields', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
// Try to submit without required fields
await page.click('#save-organizer');
// Should show validation errors
await expect(page.locator('#new-organizer-name-error')).toBeVisible();
await expect(page.locator('#new-organizer-email-error')).toBeVisible();
// Modal should remain open
await expect(page.locator('#organizer-modal')).toBeVisible();
});
test('should validate email format', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
// Fill with invalid email
await page.fill('#new-organizer-name', 'John Doe');
await page.fill('#new-organizer-email', 'invalid-email');
await page.click('#save-organizer');
await expect(page.locator('#new-organizer-email-error')).toContainText('Invalid email format');
});
test('should create organizer successfully', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
// Fill valid form data
await page.fill('#new-organizer-name', 'John Doe');
await page.fill('#new-organizer-email', 'john@example.com');
await page.fill('#new-organizer-phone', '555-1234');
await page.fill('#new-organizer-organization', 'HVAC Corp');
// Mock successful AJAX response
await page.route('**/wp-admin/admin-ajax.php', async route => {
if (route.request().postData()?.includes('create_organizer')) {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
success: true,
data: {
id: 'temp_123',
name: 'John Doe',
email: 'john@example.com'
}
})
});
} else {
await route.continue();
}
});
await page.click('#save-organizer');
// Modal should close
await expect(page.locator('#organizer-modal')).not.toBeVisible();
// Organizer should be selected in the selector
await expect(page.locator('.organizer-selector .selected-item')).toContainText('John Doe');
});
test('should handle AJAX errors gracefully', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
await page.fill('#new-organizer-name', 'John Doe');
await page.fill('#new-organizer-email', 'john@example.com');
// Mock AJAX failure
await page.route('**/wp-admin/admin-ajax.php', route => route.abort());
await page.click('#save-organizer');
// Should show error message
await expect(page.locator('.modal-error')).toContainText('Failed to create organizer');
// Modal should remain open for retry
await expect(page.locator('#organizer-modal')).toBeVisible();
});
test('should reset form when closed and reopened', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
// Fill some data
await page.fill('#new-organizer-name', 'Test Name');
await page.fill('#new-organizer-email', 'test@example.com');
// Close modal
await page.click('#organizer-modal .modal-close');
// Reopen modal
await page.click('.organizer-selector .create-new-btn');
// Form should be reset
const nameValue = await page.locator('#new-organizer-name').inputValue();
const emailValue = await page.locator('#new-organizer-email').inputValue();
expect(nameValue).toBe('');
expect(emailValue).toBe('');
});
});
test.describe('Category Modal', () => {
test('should open category creation modal correctly', async ({ page }) => {
await page.click('.category-selector .create-new-btn');
await expect(page.locator('#category-modal')).toBeVisible();
await expect(page.locator('#category-modal .modal-title')).toContainText('Create New Category');
// Category-specific fields
await expect(page.locator('#new-category-name')).toBeVisible();
await expect(page.locator('#new-category-description')).toBeVisible();
await expect(page.locator('#new-category-parent')).toBeVisible();
});
test('should validate category name requirement', async ({ page }) => {
await page.click('.category-selector .create-new-btn');
await page.click('#save-category');
await expect(page.locator('#new-category-name-error')).toContainText('Category name is required');
});
test('should create category successfully', async ({ page }) => {
await page.click('.category-selector .create-new-btn');
await page.fill('#new-category-name', 'Advanced Training');
await page.fill('#new-category-description', 'Advanced HVAC training courses');
// Mock successful response
await page.route('**/wp-admin/admin-ajax.php', async route => {
if (route.request().postData()?.includes('create_category')) {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
success: true,
data: {
id: 'temp_456',
name: 'Advanced Training'
}
})
});
} else {
await route.continue();
}
});
await page.click('#save-category');
await expect(page.locator('#category-modal')).not.toBeVisible();
await expect(page.locator('.category-selector .selected-item')).toContainText('Advanced Training');
});
test('should handle parent category selection', async ({ page }) => {
await page.click('.category-selector .create-new-btn');
// Parent dropdown should show available categories
await page.click('#new-category-parent');
await expect(page.locator('#new-category-parent option')).toHaveCount({ min: 1 });
// Select a parent category
await page.selectOption('#new-category-parent', { index: 1 });
const selectedParent = await page.locator('#new-category-parent').inputValue();
expect(selectedParent).not.toBe('');
});
});
test.describe('Venue Modal', () => {
test('should open venue creation modal correctly', async ({ page }) => {
await page.click('.venue-selector .create-new-btn');
await expect(page.locator('#venue-modal')).toBeVisible();
await expect(page.locator('#venue-modal .modal-title')).toContainText('Create New Venue');
// Venue-specific fields
await expect(page.locator('#new-venue-name')).toBeVisible();
await expect(page.locator('#new-venue-address')).toBeVisible();
await expect(page.locator('#new-venue-city')).toBeVisible();
await expect(page.locator('#new-venue-state')).toBeVisible();
await expect(page.locator('#new-venue-zip')).toBeVisible();
await expect(page.locator('#new-venue-capacity')).toBeVisible();
await expect(page.locator('#new-venue-facilities')).toBeVisible();
});
test('should validate venue required fields', async ({ page }) => {
await page.click('.venue-selector .create-new-btn');
await page.click('#save-venue');
// Check for multiple required field errors
await expect(page.locator('#new-venue-name-error')).toBeVisible();
await expect(page.locator('#new-venue-address-error')).toBeVisible();
await expect(page.locator('#new-venue-city-error')).toBeVisible();
});
test('should validate capacity as number', async ({ page }) => {
await page.click('.venue-selector .create-new-btn');
await page.fill('#new-venue-name', 'Test Venue');
await page.fill('#new-venue-address', '123 Main St');
await page.fill('#new-venue-city', 'Test City');
await page.fill('#new-venue-capacity', 'not-a-number');
await page.click('#save-venue');
await expect(page.locator('#new-venue-capacity-error')).toContainText('Capacity must be a number');
});
test('should create venue successfully', async ({ page }) => {
await page.click('.venue-selector .create-new-btn');
// Fill comprehensive venue data
await page.fill('#new-venue-name', 'Conference Center');
await page.fill('#new-venue-address', '456 Business Ave');
await page.fill('#new-venue-city', 'Business City');
await page.fill('#new-venue-state', 'CA');
await page.fill('#new-venue-zip', '90210');
await page.fill('#new-venue-capacity', '200');
await page.fill('#new-venue-facilities', 'Projector, WiFi, Parking');
// Mock successful response
await page.route('**/wp-admin/admin-ajax.php', async route => {
if (route.request().postData()?.includes('create_venue')) {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
success: true,
data: {
id: 'temp_789',
name: 'Conference Center',
address: '456 Business Ave, Business City, CA 90210'
}
})
});
} else {
await route.continue();
}
});
await page.click('#save-venue');
await expect(page.locator('#venue-modal')).not.toBeVisible();
await expect(page.locator('.venue-selector .selected-venue')).toContainText('Conference Center');
});
});
test.describe('Modal System Behavior', () => {
test('should handle multiple modal opens correctly', async ({ page }) => {
// Open organizer modal
await page.click('.organizer-selector .create-new-btn');
await expect(page.locator('#organizer-modal')).toBeVisible();
// Close and open category modal
await page.keyboard.press('Escape');
await page.click('.category-selector .create-new-btn');
await expect(page.locator('#category-modal')).toBeVisible();
await expect(page.locator('#organizer-modal')).not.toBeVisible();
});
test('should prevent body scroll when modal is open', async ({ page }) => {
const initialOverflow = await page.evaluate(() => document.body.style.overflow);
await page.click('.organizer-selector .create-new-btn');
const modalOverflow = await page.evaluate(() => document.body.style.overflow);
expect(modalOverflow).toBe('hidden');
await page.keyboard.press('Escape');
const finalOverflow = await page.evaluate(() => document.body.style.overflow);
expect(finalOverflow).toBe(initialOverflow);
});
test('should focus management properly', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
// Focus should be on first form field
const focusedElement = await page.evaluate(() => document.activeElement.id);
expect(focusedElement).toBe('new-organizer-name');
// Tab should cycle through modal fields
await page.keyboard.press('Tab');
const nextFocused = await page.evaluate(() => document.activeElement.id);
expect(nextFocused).toBe('new-organizer-email');
});
test('should trap focus within modal', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
// Get all focusable elements in modal
const modalElements = await page.locator('#organizer-modal [tabindex]:not([tabindex="-1"]), #organizer-modal input, #organizer-modal button, #organizer-modal select, #organizer-modal textarea').count();
// Tab through all elements and ensure focus stays in modal
for (let i = 0; i < modalElements + 2; i++) {
await page.keyboard.press('Tab');
const activeElement = await page.evaluate(() => {
const active = document.activeElement;
return active.closest('#organizer-modal') !== null;
});
expect(activeElement).toBe(true);
}
});
test('should handle rapid modal interactions', async ({ page }) => {
// Rapidly open and close modals
for (let i = 0; i < 5; i++) {
await page.click('.organizer-selector .create-new-btn');
await expect(page.locator('#organizer-modal')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.locator('#organizer-modal')).not.toBeVisible();
}
// Should still work normally
await page.click('.organizer-selector .create-new-btn');
await expect(page.locator('#organizer-modal')).toBeVisible();
});
});
test.describe('Form Validation', () => {
test('should show real-time validation feedback', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
// Start typing invalid email
await page.fill('#new-organizer-email', 'invalid');
// Blur to trigger validation
await page.click('#new-organizer-name');
await expect(page.locator('#new-organizer-email-error')).toContainText('Invalid email format');
// Fix email
await page.fill('#new-organizer-email', 'valid@example.com');
await page.click('#new-organizer-name');
await expect(page.locator('#new-organizer-email-error')).not.toBeVisible();
});
test('should prevent submission with invalid data', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
// Fill partially invalid data
await page.fill('#new-organizer-name', 'Jo'); // Too short
await page.fill('#new-organizer-email', 'invalid-email');
const submitButton = page.locator('#save-organizer');
await submitButton.click();
// Should not submit
await expect(page.locator('#organizer-modal')).toBeVisible();
// Button should show loading state briefly then return to normal
await expect(submitButton).not.toHaveClass(/loading/);
});
test('should sanitize input data', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
// Try to inject HTML/JS
await page.fill('#new-organizer-name', '<script>alert("xss")</script>John Doe');
await page.fill('#new-organizer-organization', '<img src="x" onerror="alert(1)">');
const nameValue = await page.locator('#new-organizer-name').inputValue();
const orgValue = await page.locator('#new-organizer-organization').inputValue();
// Should contain cleaned values
expect(nameValue).not.toContain('<script>');
expect(orgValue).not.toContain('<img');
});
test('should handle server-side validation errors', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
await page.fill('#new-organizer-name', 'John Doe');
await page.fill('#new-organizer-email', 'existing@example.com');
// Mock server validation error
await page.route('**/wp-admin/admin-ajax.php', async route => {
if (route.request().postData()?.includes('create_organizer')) {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
success: false,
data: {
field_errors: {
email: 'Email already exists'
}
}
})
});
} else {
await route.continue();
}
});
await page.click('#save-organizer');
// Should show server error
await expect(page.locator('#new-organizer-email-error')).toContainText('Email already exists');
});
});
test.describe('Data Integration', () => {
test('should pass created entity data to parent selector', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
const testData = {
name: 'Test Organizer',
email: 'test@example.com',
phone: '555-0123',
organization: 'Test Org'
};
// Fill form
await page.fill('#new-organizer-name', testData.name);
await page.fill('#new-organizer-email', testData.email);
await page.fill('#new-organizer-phone', testData.phone);
await page.fill('#new-organizer-organization', testData.organization);
// Mock successful creation
await page.route('**/wp-admin/admin-ajax.php', async route => {
if (route.request().postData()?.includes('create_organizer')) {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
success: true,
data: {
id: 'temp_999',
...testData
}
})
});
} else {
await route.continue();
}
});
await page.click('#save-organizer');
// Verify data in selector
const selectedItem = page.locator('.organizer-selector .selected-item');
await expect(selectedItem).toContainText(testData.name);
// Verify hidden input has correct data
const hiddenInput = await page.locator('input[name="selected_organizers"]').inputValue();
const selectedData = JSON.parse(hiddenInput);
expect(selectedData).toEqual(expect.arrayContaining([expect.objectContaining({ id: 'temp_999' })]));
});
test('should update selector dropdown after creation', async ({ page }) => {
// Get initial organizer count
await page.click('.organizer-selector input');
const initialCount = await page.locator('.organizer-dropdown .option').count();
await page.keyboard.press('Escape');
// Create new organizer
await page.click('.organizer-selector .create-new-btn');
await page.fill('#new-organizer-name', 'New Organizer');
await page.fill('#new-organizer-email', 'new@example.com');
await page.route('**/wp-admin/admin-ajax.php', async route => {
if (route.request().postData()?.includes('create_organizer')) {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
success: true,
data: { id: 'temp_new', name: 'New Organizer' }
})
});
} else {
await route.continue();
}
});
await page.click('#save-organizer');
// Check dropdown has updated
await page.click('.organizer-selector input');
const updatedCount = await page.locator('.organizer-dropdown .option').count();
expect(updatedCount).toBe(initialCount + 1);
// New organizer should be in dropdown
await expect(page.locator('.organizer-dropdown .option')).toContainText(['New Organizer']);
});
test('should handle temporary IDs correctly', async ({ page }) => {
await page.click('.organizer-selector .create-new-btn');
await page.fill('#new-organizer-name', 'Temp ID Test');
await page.fill('#new-organizer-email', 'temp@example.com');
// Mock response with temporary ID
await page.route('**/wp-admin/admin-ajax.php', async route => {
if (route.request().postData()?.includes('create_organizer')) {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
success: true,
data: {
id: 'temp_' + Date.now(),
name: 'Temp ID Test',
is_temporary: true
}
})
});
} else {
await route.continue();
}
});
await page.click('#save-organizer');
// Verify temporary ID is marked appropriately
const selectedItem = page.locator('.organizer-selector .selected-item');
await expect(selectedItem).toHaveClass(/temporary/);
});
});
});