- 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>
598 lines
No EOL
25 KiB
JavaScript
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/);
|
|
});
|
|
});
|
|
}); |