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