upskill-event-manager/tests/test-toggle-controls.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

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();
});
});
});