const { test, expect } = require('@playwright/test'); const { HVACTestBase } = require('./page-objects/HVACTestBase'); /** * Searchable Selector Components Test Suite * * Tests the advanced selector components including: * - Multi-select organizers (max 3) with autocomplete search * - Multi-select categories (max 3) with autocomplete search * - Single-select venue with autocomplete search * - "Create New" modal integration * - Search functionality and filtering * - Selection limits and validation * - Keyboard navigation and accessibility */ test.describe('Searchable Selector Components', () => { let hvacTest; test.beforeEach(async ({ page }) => { hvacTest = new HVACTestBase(page); await hvacTest.loginAsTrainer(); await hvacTest.navigateToCreateEvent(); // Wait for selectors 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 Multi-Select Component', () => { test('should initialize organizer selector correctly', async ({ page }) => { const organizerSelector = page.locator('.organizer-selector'); // Verify initial elements await expect(organizerSelector.locator('input[type="text"]')).toBeVisible(); await expect(organizerSelector.locator('.selected-items')).toBeVisible(); await expect(organizerSelector.locator('.create-new-btn')).toBeVisible(); // Verify placeholder text const placeholder = await organizerSelector.locator('input').getAttribute('placeholder'); expect(placeholder).toContain('Search organizers'); // Verify initial state const selectedCount = await organizerSelector.locator('.selected-item').count(); expect(selectedCount).toBe(0); }); test('should show dropdown on focus with all available options', async ({ page }) => { const input = page.locator('.organizer-selector input'); await input.click(); // Dropdown should appear await expect(page.locator('.organizer-dropdown')).toBeVisible(); // Should show available organizers const options = await page.locator('.organizer-dropdown .option').count(); expect(options).toBeGreaterThan(0); // Should show "Create New Organizer" option await expect(page.locator('.organizer-dropdown .create-new-option')).toBeVisible(); }); test('should filter options based on search input', async ({ page }) => { const input = page.locator('.organizer-selector input'); await input.click(); await input.type('john'); // Should filter results const visibleOptions = await page.locator('.organizer-dropdown .option:visible').count(); const totalOptions = await page.locator('.organizer-dropdown .option').count(); expect(visibleOptions).toBeLessThanOrEqual(totalOptions); // Options should contain search term const firstOption = await page.locator('.organizer-dropdown .option:visible').first().textContent(); expect(firstOption.toLowerCase()).toContain('john'); }); test('should select multiple organizers up to limit', async ({ page }) => { const input = page.locator('.organizer-selector input'); const selector = page.locator('.organizer-selector'); // Select first organizer await input.click(); await page.locator('.organizer-dropdown .option').first().click(); // Verify first selection let selectedCount = await selector.locator('.selected-item').count(); expect(selectedCount).toBe(1); // Select second organizer await input.click(); await page.locator('.organizer-dropdown .option').nth(1).click(); selectedCount = await selector.locator('.selected-item').count(); expect(selectedCount).toBe(2); // Select third organizer await input.click(); await page.locator('.organizer-dropdown .option').nth(2).click(); selectedCount = await selector.locator('.selected-item').count(); expect(selectedCount).toBe(3); // Try to select fourth (should be prevented) await input.click(); const fourthOption = page.locator('.organizer-dropdown .option').nth(3); if (await fourthOption.isVisible()) { await fourthOption.click(); // Should still be at limit selectedCount = await selector.locator('.selected-item').count(); expect(selectedCount).toBe(3); // Should show limit warning await expect(page.locator('.selection-limit-warning')).toContainText('Maximum 3 organizers'); } }); test('should allow removing selected organizers', async ({ page }) => { const input = page.locator('.organizer-selector input'); const selector = page.locator('.organizer-selector'); // Select an organizer await input.click(); await page.locator('.organizer-dropdown .option').first().click(); let selectedCount = await selector.locator('.selected-item').count(); expect(selectedCount).toBe(1); // Remove the selected organizer await selector.locator('.selected-item .remove-btn').click(); selectedCount = await selector.locator('.selected-item').count(); expect(selectedCount).toBe(0); }); test('should prevent duplicate selections', async ({ page }) => { const input = page.locator('.organizer-selector input'); const selector = page.locator('.organizer-selector'); // Select an organizer await input.click(); const firstOption = page.locator('.organizer-dropdown .option').first(); const firstOptionText = await firstOption.textContent(); await firstOption.click(); // Try to select the same organizer again await input.click(); await page.locator('.organizer-dropdown .option').filter({ hasText: firstOptionText }).click(); // Should still have only one selection const selectedCount = await selector.locator('.selected-item').count(); expect(selectedCount).toBe(1); }); test('should open create new organizer modal', async ({ page }) => { await page.locator('.organizer-selector .create-new-btn').click(); // Modal should open await expect(page.locator('#organizer-modal')).toBeVisible(); await expect(page.locator('#organizer-modal .modal-title')).toContainText('Create New Organizer'); // Form fields should be visible 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(); }); }); test.describe('Category Multi-Select Component', () => { test('should function similar to organizer selector', async ({ page }) => { const categorySelector = page.locator('.category-selector'); const input = categorySelector.locator('input'); // Test basic functionality await input.click(); await expect(page.locator('.category-dropdown')).toBeVisible(); // Select multiple categories (max 3) await page.locator('.category-dropdown .option').first().click(); await input.click(); await page.locator('.category-dropdown .option').nth(1).click(); await input.click(); await page.locator('.category-dropdown .option').nth(2).click(); const selectedCount = await categorySelector.locator('.selected-item').count(); expect(selectedCount).toBe(3); }); test('should enforce category selection limit', async ({ page }) => { const categorySelector = page.locator('.category-selector'); const input = categorySelector.locator('input'); // Select maximum categories for (let i = 0; i < 4; i++) { await input.click(); const option = page.locator('.category-dropdown .option').nth(i); if (await option.isVisible()) { await option.click(); } } // Should not exceed limit const selectedCount = await categorySelector.locator('.selected-item').count(); expect(selectedCount).toBeLessThanOrEqual(3); }); test('should open create new category modal', async ({ page }) => { await page.locator('.category-selector .create-new-btn').click(); await expect(page.locator('#category-modal')).toBeVisible(); await expect(page.locator('#new-category-name')).toBeVisible(); await expect(page.locator('#new-category-description')).toBeVisible(); }); }); test.describe('Venue Single-Select Component', () => { test('should initialize venue selector correctly', async ({ page }) => { const venueSelector = page.locator('.venue-selector'); await expect(venueSelector.locator('input[type="text"]')).toBeVisible(); await expect(venueSelector.locator('.selected-venue')).toBeVisible(); const placeholder = await venueSelector.locator('input').getAttribute('placeholder'); expect(placeholder).toContain('Search venues'); }); test('should allow single venue selection only', async ({ page }) => { const input = page.locator('.venue-selector input'); await input.click(); await expect(page.locator('.venue-dropdown')).toBeVisible(); // Select first venue await page.locator('.venue-dropdown .option').first().click(); // Should show selected venue await expect(page.locator('.selected-venue .venue-name')).toBeVisible(); // Select different venue (should replace first) await input.click(); await page.locator('.venue-dropdown .option').nth(1).click(); // Should have only one venue selected const selectedVenues = await page.locator('.selected-venue .venue-name').count(); expect(selectedVenues).toBe(1); }); test('should clear venue selection', async ({ page }) => { const input = page.locator('.venue-selector input'); // Select a venue await input.click(); await page.locator('.venue-dropdown .option').first().click(); // Clear selection await page.locator('.selected-venue .clear-btn').click(); // Should have no selection await expect(page.locator('.selected-venue .venue-name')).not.toBeVisible(); }); test('should open create new venue modal', async ({ page }) => { await page.locator('.venue-selector .create-new-btn').click(); await expect(page.locator('#venue-modal')).toBeVisible(); await expect(page.locator('#new-venue-name')).toBeVisible(); await expect(page.locator('#new-venue-address')).toBeVisible(); await expect(page.locator('#new-venue-capacity')).toBeVisible(); }); }); test.describe('Search Functionality', () => { test('should perform case-insensitive search', async ({ page }) => { const input = page.locator('.organizer-selector input'); await input.click(); await input.type('JOHN'); // Uppercase const options = await page.locator('.organizer-dropdown .option:visible').all(); for (const option of options) { const text = await option.textContent(); expect(text.toLowerCase()).toContain('john'); } }); test('should search across multiple fields', async ({ page }) => { const input = page.locator('.venue-selector input'); await input.click(); await input.type('conference'); // Could match name or description await expect(page.locator('.venue-dropdown .option:visible')).toHaveCount({ min: 1 }); }); test('should show no results message for empty search', async ({ page }) => { const input = page.locator('.organizer-selector input'); await input.click(); await input.type('zyxwvu'); // Non-existent search await expect(page.locator('.organizer-dropdown .no-results')).toBeVisible(); await expect(page.locator('.organizer-dropdown .no-results')).toContainText('No organizers found'); }); test('should clear search when input is cleared', async ({ page }) => { const input = page.locator('.organizer-selector input'); await input.click(); await input.type('john'); // Clear input await input.fill(''); // Should show all options again const visibleCount = await page.locator('.organizer-dropdown .option:visible').count(); const totalCount = await page.locator('.organizer-dropdown .option').count(); expect(visibleCount).toBe(totalCount); }); }); test.describe('Keyboard Navigation', () => { test('should support arrow key navigation in dropdown', async ({ page }) => { const input = page.locator('.organizer-selector input'); await input.click(); // Navigate with arrow keys await page.keyboard.press('ArrowDown'); await expect(page.locator('.organizer-dropdown .option.highlighted')).toBeVisible(); await page.keyboard.press('ArrowDown'); const highlightedOptions = await page.locator('.organizer-dropdown .option.highlighted').count(); expect(highlightedOptions).toBe(1); // Only one should be highlighted // Navigate up await page.keyboard.press('ArrowUp'); await expect(page.locator('.organizer-dropdown .option.highlighted')).toBeVisible(); }); test('should select option with Enter key', async ({ page }) => { const input = page.locator('.organizer-selector input'); const selector = page.locator('.organizer-selector'); await input.click(); await page.keyboard.press('ArrowDown'); await page.keyboard.press('Enter'); // Should have selected an organizer const selectedCount = await selector.locator('.selected-item').count(); expect(selectedCount).toBe(1); }); test('should close dropdown with Escape key', async ({ page }) => { const input = page.locator('.organizer-selector input'); await input.click(); await expect(page.locator('.organizer-dropdown')).toBeVisible(); await page.keyboard.press('Escape'); await expect(page.locator('.organizer-dropdown')).not.toBeVisible(); }); test('should support tab navigation between selectors', async ({ page }) => { // Start from organizer selector await page.locator('.organizer-selector input').click(); // Tab to category selector await page.keyboard.press('Tab'); const categoryFocused = await page.evaluate(() => document.activeElement.closest('.category-selector') !== null ); expect(categoryFocused).toBe(true); // Tab to venue selector await page.keyboard.press('Tab'); const venueFocused = await page.evaluate(() => document.activeElement.closest('.venue-selector') !== null ); expect(venueFocused).toBe(true); }); }); test.describe('Accessibility', () => { test('should have proper ARIA attributes', async ({ page }) => { const organizerInput = page.locator('.organizer-selector input'); // Check ARIA attributes await expect(organizerInput).toHaveAttribute('role', 'combobox'); await expect(organizerInput).toHaveAttribute('aria-expanded', 'false'); await expect(organizerInput).toHaveAttribute('aria-haspopup', 'listbox'); // Open dropdown await organizerInput.click(); await expect(organizerInput).toHaveAttribute('aria-expanded', 'true'); // Check dropdown ARIA const dropdown = page.locator('.organizer-dropdown'); await expect(dropdown).toHaveAttribute('role', 'listbox'); const options = page.locator('.organizer-dropdown .option'); await expect(options.first()).toHaveAttribute('role', 'option'); }); test('should announce selections to screen readers', async ({ page }) => { const input = page.locator('.organizer-selector input'); await input.click(); await page.locator('.organizer-dropdown .option').first().click(); // Check for live region announcement await expect(page.locator('[aria-live="polite"]')).toContainText('Organizer selected'); }); test('should support screen reader navigation', async ({ page }) => { const input = page.locator('.organizer-selector input'); await input.click(); // Options should be readable by screen reader const firstOption = page.locator('.organizer-dropdown .option').first(); const ariaLabel = await firstOption.getAttribute('aria-label'); expect(ariaLabel).toBeTruthy(); }); test('should handle focus management properly', async ({ page }) => { const input = page.locator('.organizer-selector input'); // Focus should return to input after selection await input.click(); await page.locator('.organizer-dropdown .option').first().click(); const focused = await page.evaluate(() => document.activeElement.classList.contains('selector-input') ); expect(focused).toBe(true); }); }); test.describe('Error Handling', () => { test('should handle AJAX errors gracefully', async ({ page }) => { // Mock AJAX failure await page.route('**/*organizer*', route => route.abort()); const input = page.locator('.organizer-selector input'); await input.click(); // Should show error message await expect(page.locator('.organizer-dropdown .error-message')).toBeVisible(); await expect(page.locator('.organizer-dropdown .error-message')).toContainText('Failed to load organizers'); }); test('should recover from network errors', async ({ page }) => { // Temporarily fail requests await page.route('**/*organizer*', route => route.abort()); const input = page.locator('.organizer-selector input'); await input.click(); await expect(page.locator('.organizer-dropdown .error-message')).toBeVisible(); // Restore network and retry await page.unroute('**/*organizer*'); await page.locator('.retry-btn').click(); await expect(page.locator('.organizer-dropdown .option')).toHaveCount({ min: 1 }); }); test('should validate selection limits client-side', async ({ page }) => { // Mock server that would allow over-limit selections await page.addInitScript(() => { window.organizerSelectionLimit = 3; }); const selector = page.locator('.organizer-selector'); const input = selector.locator('input'); // Try to exceed limit programmatically await page.evaluate(() => { const selector = document.querySelector('.organizer-selector'); // Simulate multiple rapid selections for (let i = 0; i < 5; i++) { const event = new CustomEvent('organizer-selected', { detail: { id: i, name: `Organizer ${i}` } }); selector.dispatchEvent(event); } }); // Should still respect client-side limit const selectedCount = await selector.locator('.selected-item').count(); expect(selectedCount).toBeLessThanOrEqual(3); }); }); test.describe('Performance', () => { test('should handle large datasets efficiently', async ({ page }) => { // Mock large dataset await page.addInitScript(() => { window.mockLargeDataset = true; window.organizerCount = 1000; }); const input = page.locator('.organizer-selector input'); const startTime = Date.now(); await input.click(); await expect(page.locator('.organizer-dropdown')).toBeVisible(); const endTime = Date.now(); // Should render within reasonable time expect(endTime - startTime).toBeLessThan(1000); // 1 second }); test('should virtualize long lists', async ({ page }) => { const input = page.locator('.organizer-selector input'); await input.click(); // Check if virtualization is implemented const visibleOptions = await page.locator('.organizer-dropdown .option:visible').count(); const totalOptions = await page.locator('.organizer-dropdown .option').count(); // If there are many options, only a subset should be visible if (totalOptions > 50) { expect(visibleOptions).toBeLessThan(totalOptions); } }); test('should debounce search requests', async ({ page }) => { let requestCount = 0; page.on('request', request => { if (request.url().includes('search')) { requestCount++; } }); const input = page.locator('.organizer-selector input'); await input.click(); // Type rapidly await input.type('john', { delay: 50 }); await page.waitForTimeout(1000); // Wait for debounce // Should not make a request for each keystroke expect(requestCount).toBeLessThan(4); // Less than number of characters typed }); }); });