upskill-event-manager/tests/test-searchable-selectors.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

553 lines
No EOL
23 KiB
JavaScript

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