- 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>
560 lines
No EOL
22 KiB
JavaScript
560 lines
No EOL
22 KiB
JavaScript
const { test, expect } = require('@playwright/test');
|
|
const { HVACTestBase } = require('./page-objects/HVACTestBase');
|
|
|
|
/**
|
|
* Toggle Controls Test Suite
|
|
*
|
|
* Tests the interactive toggle switches that show/hide form sections:
|
|
* - Virtual Event toggle → Virtual event configuration fields
|
|
* - Enable RSVP toggle → RSVP configuration options
|
|
* - Enable Ticketing toggle → Ticketing subsection fields
|
|
* - State management and persistence
|
|
* - Accessibility and keyboard support
|
|
* - Animation and visual feedback
|
|
*/
|
|
test.describe('Toggle Controls System', () => {
|
|
let hvacTest;
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
hvacTest = new HVACTestBase(page);
|
|
await hvacTest.loginAsTrainer();
|
|
await hvacTest.navigateToCreateEvent();
|
|
|
|
// Wait for toggle controls to be ready
|
|
await expect(page.locator('.virtual-event-toggle')).toBeVisible();
|
|
await expect(page.locator('.rsvp-toggle')).toBeVisible();
|
|
await expect(page.locator('.ticketing-toggle')).toBeVisible();
|
|
});
|
|
|
|
test.describe('Virtual Event Toggle', () => {
|
|
test('should initialize in correct default state', async ({ page }) => {
|
|
const toggle = page.locator('.virtual-event-toggle');
|
|
const configSection = page.locator('.virtual-event-config');
|
|
|
|
// Toggle should be off by default
|
|
const isChecked = await toggle.isChecked();
|
|
expect(isChecked).toBe(false);
|
|
|
|
// Config section should be hidden
|
|
const isVisible = await configSection.isVisible();
|
|
expect(isVisible).toBe(false);
|
|
});
|
|
|
|
test('should show virtual event fields when enabled', async ({ page }) => {
|
|
const toggle = page.locator('.virtual-event-toggle');
|
|
const configSection = page.locator('.virtual-event-config');
|
|
|
|
// Enable virtual event
|
|
await toggle.click();
|
|
|
|
// Should be checked
|
|
const isChecked = await toggle.isChecked();
|
|
expect(isChecked).toBe(true);
|
|
|
|
// Config section should be visible
|
|
await expect(configSection).toBeVisible();
|
|
|
|
// Virtual event fields should be visible
|
|
await expect(page.locator('#virtual-meeting-url')).toBeVisible();
|
|
await expect(page.locator('#virtual-meeting-platform')).toBeVisible();
|
|
await expect(page.locator('#virtual-meeting-id')).toBeVisible();
|
|
await expect(page.locator('#virtual-meeting-password')).toBeVisible();
|
|
await expect(page.locator('#virtual-meeting-instructions')).toBeVisible();
|
|
});
|
|
|
|
test('should hide virtual event fields when disabled', async ({ page }) => {
|
|
const toggle = page.locator('.virtual-event-toggle');
|
|
const configSection = page.locator('.virtual-event-config');
|
|
|
|
// Enable then disable
|
|
await toggle.click();
|
|
await expect(configSection).toBeVisible();
|
|
|
|
await toggle.click();
|
|
|
|
// Should be unchecked
|
|
const isChecked = await toggle.isChecked();
|
|
expect(isChecked).toBe(false);
|
|
|
|
// Config section should be hidden
|
|
await expect(configSection).not.toBeVisible();
|
|
});
|
|
|
|
test('should preserve field values when toggled', async ({ page }) => {
|
|
const toggle = page.locator('.virtual-event-toggle');
|
|
const urlField = page.locator('#virtual-meeting-url');
|
|
|
|
// Enable and fill data
|
|
await toggle.click();
|
|
await urlField.fill('https://zoom.us/j/123456789');
|
|
|
|
// Disable toggle
|
|
await toggle.click();
|
|
|
|
// Re-enable toggle
|
|
await toggle.click();
|
|
|
|
// Value should be preserved
|
|
const preservedValue = await urlField.inputValue();
|
|
expect(preservedValue).toBe('https://zoom.us/j/123456789');
|
|
});
|
|
|
|
test('should validate virtual event URL format', async ({ page }) => {
|
|
const toggle = page.locator('.virtual-event-toggle');
|
|
const urlField = page.locator('#virtual-meeting-url');
|
|
|
|
await toggle.click();
|
|
|
|
// Test invalid URLs
|
|
const invalidUrls = [
|
|
'not-a-url',
|
|
'ftp://invalid.com',
|
|
'javascript:alert(1)',
|
|
'http://'
|
|
];
|
|
|
|
for (const url of invalidUrls) {
|
|
await urlField.fill(url);
|
|
await urlField.blur();
|
|
|
|
await expect(page.locator('#virtual-meeting-url-error')).toContainText('Invalid URL format');
|
|
}
|
|
|
|
// Test valid URL
|
|
await urlField.fill('https://zoom.us/j/123456789');
|
|
await urlField.blur();
|
|
|
|
await expect(page.locator('#virtual-meeting-url-error')).not.toBeVisible();
|
|
});
|
|
|
|
test('should show platform-specific fields', async ({ page }) => {
|
|
const toggle = page.locator('.virtual-event-toggle');
|
|
const platformSelect = page.locator('#virtual-meeting-platform');
|
|
|
|
await toggle.click();
|
|
|
|
// Test Zoom platform
|
|
await platformSelect.selectOption('zoom');
|
|
await expect(page.locator('#zoom-meeting-id-field')).toBeVisible();
|
|
await expect(page.locator('#zoom-passcode-field')).toBeVisible();
|
|
|
|
// Test Teams platform
|
|
await platformSelect.selectOption('teams');
|
|
await expect(page.locator('#teams-meeting-id-field')).toBeVisible();
|
|
await expect(page.locator('#zoom-meeting-id-field')).not.toBeVisible();
|
|
|
|
// Test Generic platform
|
|
await platformSelect.selectOption('generic');
|
|
await expect(page.locator('#generic-instructions-field')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('RSVP Toggle', () => {
|
|
test('should initialize in correct default state', async ({ page }) => {
|
|
const toggle = page.locator('.rsvp-toggle');
|
|
const configSection = page.locator('.rsvp-config');
|
|
|
|
// Toggle should be off by default
|
|
const isChecked = await toggle.isChecked();
|
|
expect(isChecked).toBe(false);
|
|
|
|
// Config section should be hidden
|
|
const isVisible = await configSection.isVisible();
|
|
expect(isVisible).toBe(false);
|
|
});
|
|
|
|
test('should show RSVP configuration when enabled', async ({ page }) => {
|
|
const toggle = page.locator('.rsvp-toggle');
|
|
const configSection = page.locator('.rsvp-config');
|
|
|
|
await toggle.click();
|
|
|
|
await expect(configSection).toBeVisible();
|
|
|
|
// RSVP fields should be visible
|
|
await expect(page.locator('#rsvp-deadline')).toBeVisible();
|
|
await expect(page.locator('#rsvp-capacity')).toBeVisible();
|
|
await expect(page.locator('#rsvp-waitlist')).toBeVisible();
|
|
await expect(page.locator('#rsvp-confirmation-message')).toBeVisible();
|
|
});
|
|
|
|
test('should validate RSVP deadline is in future', async ({ page }) => {
|
|
const toggle = page.locator('.rsvp-toggle');
|
|
const deadlineField = page.locator('#rsvp-deadline');
|
|
|
|
await toggle.click();
|
|
|
|
// Set deadline to yesterday
|
|
const yesterday = new Date();
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
|
|
|
await deadlineField.fill(yesterdayStr);
|
|
await deadlineField.blur();
|
|
|
|
await expect(page.locator('#rsvp-deadline-error')).toContainText('RSVP deadline must be in the future');
|
|
});
|
|
|
|
test('should validate RSVP capacity is positive number', async ({ page }) => {
|
|
const toggle = page.locator('.rsvp-toggle');
|
|
const capacityField = page.locator('#rsvp-capacity');
|
|
|
|
await toggle.click();
|
|
|
|
// Test invalid capacities
|
|
const invalidValues = ['0', '-5', 'not-a-number'];
|
|
|
|
for (const value of invalidValues) {
|
|
await capacityField.fill(value);
|
|
await capacityField.blur();
|
|
|
|
await expect(page.locator('#rsvp-capacity-error')).toContainText('Capacity must be a positive number');
|
|
}
|
|
|
|
// Test valid capacity
|
|
await capacityField.fill('50');
|
|
await capacityField.blur();
|
|
|
|
await expect(page.locator('#rsvp-capacity-error')).not.toBeVisible();
|
|
});
|
|
|
|
test('should show waitlist options when enabled', async ({ page }) => {
|
|
const rsvpToggle = page.locator('.rsvp-toggle');
|
|
const waitlistToggle = page.locator('#rsvp-waitlist');
|
|
|
|
await rsvpToggle.click();
|
|
await waitlistToggle.click();
|
|
|
|
// Waitlist configuration should appear
|
|
await expect(page.locator('.waitlist-config')).toBeVisible();
|
|
await expect(page.locator('#waitlist-size')).toBeVisible();
|
|
await expect(page.locator('#waitlist-notification-message')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Ticketing Toggle', () => {
|
|
test('should initialize in correct default state', async ({ page }) => {
|
|
const toggle = page.locator('.ticketing-toggle');
|
|
const configSection = page.locator('.ticketing-config');
|
|
|
|
// Toggle should be off by default
|
|
const isChecked = await toggle.isChecked();
|
|
expect(isChecked).toBe(false);
|
|
|
|
// Config section should be hidden
|
|
const isVisible = await configSection.isVisible();
|
|
expect(isVisible).toBe(false);
|
|
});
|
|
|
|
test('should show ticketing configuration when enabled', async ({ page }) => {
|
|
const toggle = page.locator('.ticketing-toggle');
|
|
const configSection = page.locator('.ticketing-config');
|
|
|
|
await toggle.click();
|
|
|
|
await expect(configSection).toBeVisible();
|
|
|
|
// Ticketing fields should be visible
|
|
await expect(page.locator('#ticket-name')).toBeVisible();
|
|
await expect(page.locator('#ticket-price')).toBeVisible();
|
|
await expect(page.locator('#ticket-capacity')).toBeVisible();
|
|
await expect(page.locator('#ticket-sales-start')).toBeVisible();
|
|
await expect(page.locator('#ticket-sales-end')).toBeVisible();
|
|
});
|
|
|
|
test('should validate ticket price format', async ({ page }) => {
|
|
const toggle = page.locator('.ticketing-toggle');
|
|
const priceField = page.locator('#ticket-price');
|
|
|
|
await toggle.click();
|
|
|
|
// Test invalid prices
|
|
const invalidPrices = ['-10', 'abc', '10.999', ''];
|
|
|
|
for (const price of invalidPrices) {
|
|
await priceField.fill(price);
|
|
await priceField.blur();
|
|
|
|
await expect(page.locator('#ticket-price-error')).toContainText('Invalid price format');
|
|
}
|
|
|
|
// Test valid prices
|
|
const validPrices = ['0', '10', '99.99', '100.00'];
|
|
|
|
for (const price of validPrices) {
|
|
await priceField.fill(price);
|
|
await priceField.blur();
|
|
|
|
await expect(page.locator('#ticket-price-error')).not.toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('should validate ticket sales date range', async ({ page }) => {
|
|
const toggle = page.locator('.ticketing-toggle');
|
|
const startField = page.locator('#ticket-sales-start');
|
|
const endField = page.locator('#ticket-sales-end');
|
|
|
|
await toggle.click();
|
|
|
|
// Set end date before start date
|
|
const today = new Date();
|
|
const tomorrow = new Date(today);
|
|
tomorrow.setDate(today.getDate() + 1);
|
|
|
|
await startField.fill(tomorrow.toISOString().split('T')[0]);
|
|
await endField.fill(today.toISOString().split('T')[0]);
|
|
await endField.blur();
|
|
|
|
await expect(page.locator('#ticket-sales-end-error')).toContainText('End date must be after start date');
|
|
});
|
|
|
|
test('should support multiple ticket types', async ({ page }) => {
|
|
const toggle = page.locator('.ticketing-toggle');
|
|
|
|
await toggle.click();
|
|
|
|
// Add second ticket type
|
|
await page.click('#add-ticket-type');
|
|
|
|
// Should have two ticket sections
|
|
const ticketSections = await page.locator('.ticket-type-section').count();
|
|
expect(ticketSections).toBe(2);
|
|
|
|
// Each should have independent fields
|
|
await expect(page.locator('#ticket-name-0')).toBeVisible();
|
|
await expect(page.locator('#ticket-name-1')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Toggle Interactions', () => {
|
|
test('should handle conflicting configurations', async ({ page }) => {
|
|
const rsvpToggle = page.locator('.rsvp-toggle');
|
|
const ticketingToggle = page.locator('.ticketing-toggle');
|
|
|
|
// Enable both RSVP and ticketing
|
|
await rsvpToggle.click();
|
|
await ticketingToggle.click();
|
|
|
|
// Should show warning about conflicting features
|
|
await expect(page.locator('.feature-conflict-warning')).toContainText('RSVP and paid tickets cannot be used together');
|
|
|
|
// Should provide options to resolve conflict
|
|
await expect(page.locator('.conflict-resolution')).toBeVisible();
|
|
});
|
|
|
|
test('should update form submission data based on toggles', async ({ page }) => {
|
|
const virtualToggle = page.locator('.virtual-event-toggle');
|
|
const rsvpToggle = page.locator('.rsvp-toggle');
|
|
|
|
// Enable virtual event
|
|
await virtualToggle.click();
|
|
await page.fill('#virtual-meeting-url', 'https://zoom.us/j/123456789');
|
|
|
|
// Check hidden form data
|
|
let hiddenData = await page.locator('input[name="event_meta"]').inputValue();
|
|
let metaData = JSON.parse(hiddenData);
|
|
expect(metaData.virtual_event).toBe(true);
|
|
expect(metaData.virtual_url).toBe('https://zoom.us/j/123456789');
|
|
|
|
// Enable RSVP
|
|
await rsvpToggle.click();
|
|
await page.fill('#rsvp-capacity', '100');
|
|
|
|
hiddenData = await page.locator('input[name="event_meta"]').inputValue();
|
|
metaData = JSON.parse(hiddenData);
|
|
expect(metaData.rsvp_enabled).toBe(true);
|
|
expect(metaData.rsvp_capacity).toBe('100');
|
|
});
|
|
|
|
test('should maintain toggle states during form autosave', async ({ page }) => {
|
|
const virtualToggle = page.locator('.virtual-event-toggle');
|
|
const rsvpToggle = page.locator('.rsvp-toggle');
|
|
|
|
// Set initial states
|
|
await virtualToggle.click();
|
|
await rsvpToggle.click();
|
|
|
|
// Trigger autosave
|
|
await page.fill('#event_title', 'Autosave Test Event');
|
|
await page.waitForTimeout(3000); // Wait for autosave
|
|
|
|
// States should be preserved
|
|
expect(await virtualToggle.isChecked()).toBe(true);
|
|
expect(await rsvpToggle.isChecked()).toBe(true);
|
|
|
|
// Config sections should still be visible
|
|
await expect(page.locator('.virtual-event-config')).toBeVisible();
|
|
await expect(page.locator('.rsvp-config')).toBeVisible();
|
|
});
|
|
|
|
test('should handle rapid toggle clicks', async ({ page }) => {
|
|
const toggle = page.locator('.virtual-event-toggle');
|
|
const configSection = page.locator('.virtual-event-config');
|
|
|
|
// Rapidly click toggle multiple times
|
|
for (let i = 0; i < 10; i++) {
|
|
await toggle.click();
|
|
await page.waitForTimeout(50);
|
|
}
|
|
|
|
// Final state should be consistent
|
|
const isChecked = await toggle.isChecked();
|
|
const isVisible = await configSection.isVisible();
|
|
|
|
expect(isChecked).toBe(isVisible);
|
|
});
|
|
});
|
|
|
|
test.describe('Accessibility', () => {
|
|
test('should have proper ARIA labels and roles', async ({ page }) => {
|
|
const toggles = [
|
|
'.virtual-event-toggle',
|
|
'.rsvp-toggle',
|
|
'.ticketing-toggle'
|
|
];
|
|
|
|
for (const toggleSelector of toggles) {
|
|
const toggle = page.locator(toggleSelector);
|
|
|
|
// Should have proper role
|
|
await expect(toggle).toHaveAttribute('role', 'switch');
|
|
|
|
// Should have aria-label or aria-labelledby
|
|
const hasLabel = await toggle.getAttribute('aria-label') ||
|
|
await toggle.getAttribute('aria-labelledby');
|
|
expect(hasLabel).toBeTruthy();
|
|
|
|
// Should have aria-checked attribute
|
|
const ariaChecked = await toggle.getAttribute('aria-checked');
|
|
expect(['true', 'false']).toContain(ariaChecked);
|
|
}
|
|
});
|
|
|
|
test('should support keyboard navigation', async ({ page }) => {
|
|
// Focus should move to toggles with Tab
|
|
await page.keyboard.press('Tab'); // Navigate to first toggle
|
|
|
|
const focused = await page.evaluate(() =>
|
|
document.activeElement.classList.contains('virtual-event-toggle')
|
|
);
|
|
expect(focused).toBe(true);
|
|
|
|
// Space should toggle the switch
|
|
await page.keyboard.press('Space');
|
|
|
|
const isChecked = await page.locator('.virtual-event-toggle').isChecked();
|
|
expect(isChecked).toBe(true);
|
|
|
|
// Enter should also toggle
|
|
await page.keyboard.press('Enter');
|
|
|
|
const isStillChecked = await page.locator('.virtual-event-toggle').isChecked();
|
|
expect(isStillChecked).toBe(false);
|
|
});
|
|
|
|
test('should announce state changes to screen readers', async ({ page }) => {
|
|
const toggle = page.locator('.virtual-event-toggle');
|
|
|
|
// Monitor for aria-live announcements
|
|
const announcements = [];
|
|
page.on('console', msg => {
|
|
if (msg.text().includes('Virtual event enabled') || msg.text().includes('Virtual event disabled')) {
|
|
announcements.push(msg.text());
|
|
}
|
|
});
|
|
|
|
await toggle.click();
|
|
await page.waitForTimeout(100);
|
|
|
|
await toggle.click();
|
|
await page.waitForTimeout(100);
|
|
|
|
// Should have announced state changes
|
|
expect(announcements.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should have proper focus management', async ({ page }) => {
|
|
const virtualToggle = page.locator('.virtual-event-toggle');
|
|
|
|
// Focus toggle and activate
|
|
await virtualToggle.focus();
|
|
await page.keyboard.press('Space');
|
|
|
|
// Focus should move to first field in opened section
|
|
await page.keyboard.press('Tab');
|
|
|
|
const focusedElement = await page.evaluate(() => document.activeElement.id);
|
|
expect(focusedElement).toBe('virtual-meeting-url');
|
|
});
|
|
});
|
|
|
|
test.describe('Visual Feedback and Animation', () => {
|
|
test('should show loading state during toggle transitions', async ({ page }) => {
|
|
// Mock slow response for toggle action
|
|
await page.addInitScript(() => {
|
|
window.TOGGLE_DELAY = 500;
|
|
});
|
|
|
|
const toggle = page.locator('.virtual-event-toggle');
|
|
|
|
await toggle.click();
|
|
|
|
// Should show loading state briefly
|
|
await expect(page.locator('.toggle-loading')).toBeVisible();
|
|
|
|
// Loading should disappear
|
|
await expect(page.locator('.toggle-loading')).not.toBeVisible();
|
|
});
|
|
|
|
test('should animate section visibility changes', async ({ page }) => {
|
|
const toggle = page.locator('.virtual-event-toggle');
|
|
const configSection = page.locator('.virtual-event-config');
|
|
|
|
await toggle.click();
|
|
|
|
// Section should have animation class during transition
|
|
await expect(configSection).toHaveClass(/animating/);
|
|
|
|
// Wait for animation to complete
|
|
await page.waitForTimeout(300);
|
|
await expect(configSection).not.toHaveClass(/animating/);
|
|
});
|
|
|
|
test('should provide visual feedback for validation errors', async ({ page }) => {
|
|
const virtualToggle = page.locator('.virtual-event-toggle');
|
|
const urlField = page.locator('#virtual-meeting-url');
|
|
|
|
await virtualToggle.click();
|
|
|
|
// Enter invalid URL
|
|
await urlField.fill('invalid-url');
|
|
await urlField.blur();
|
|
|
|
// Field should have error styling
|
|
await expect(urlField).toHaveClass(/error/);
|
|
await expect(page.locator('#virtual-meeting-url-error')).toBeVisible();
|
|
|
|
// Fix the error
|
|
await urlField.fill('https://zoom.us/j/123456789');
|
|
await urlField.blur();
|
|
|
|
// Error styling should be removed
|
|
await expect(urlField).not.toHaveClass(/error/);
|
|
await expect(page.locator('#virtual-meeting-url-error')).not.toBeVisible();
|
|
});
|
|
|
|
test('should show success feedback for completed configurations', async ({ page }) => {
|
|
const virtualToggle = page.locator('.virtual-event-toggle');
|
|
|
|
await virtualToggle.click();
|
|
|
|
// Fill all required virtual event fields
|
|
await page.fill('#virtual-meeting-url', 'https://zoom.us/j/123456789');
|
|
await page.selectOption('#virtual-meeting-platform', 'zoom');
|
|
await page.fill('#virtual-meeting-id', '123456789');
|
|
|
|
// Should show completion indicator
|
|
await expect(page.locator('.virtual-event-config .config-complete')).toBeVisible();
|
|
});
|
|
});
|
|
}); |