/** * Event Editing Page Object Model * * Handles event editing and modification functionality including: * - Event detail editing and updates * - Status transitions (Draft → Pending → Published) * - Event modification workflows * - Version control and change tracking * * @package HVAC_Community_Events * @version 2.0.0 * @created 2025-08-27 * @author Agent-B-Event-Management */ const BasePage = require('../base/BasePage'); const { expect } = require('@playwright/test'); class EventEditing extends BasePage { constructor(page) { super(page); // Event editing form selectors this.selectors = { // Main form containers editEventForm: [ '[data-testid="edit-event-form"]', '.hvac-edit-event-form', '.tribe-events-community-form', '.event-edit-form', 'form#edit-event' ], // Form fields (similar to creation but for editing context) fields: { title: [ '[data-testid="event-title"]', 'input[name="event_title"]', 'input[name="post_title"]', '#event-title', '.event-title-input' ], description: [ '[data-testid="event-description"]', 'textarea[name="event_description"]', 'textarea[name="post_content"]', '#event-description', '.event-description-textarea' ], startDate: [ '[data-testid="event-start-date"]', 'input[name="event_start_date"]', 'input[name="EventStartDate"]', '#event-start-date', '.event-start-date' ], endDate: [ '[data-testid="event-end-date"]', 'input[name="event_end_date"]', 'input[name="EventEndDate"]', '#event-end-date', '.event-end-date' ], startTime: [ '[data-testid="event-start-time"]', 'input[name="event_start_time"]', 'input[name="EventStartTime"]', '#event-start-time', '.event-start-time' ], endTime: [ '[data-testid="event-end-time"]', 'input[name="event_end_time"]', 'input[name="EventEndTime"]', '#event-end-time', '.event-end-time' ], venue: [ '[data-testid="event-venue"]', 'select[name="venue"]', 'select[name="event_venue"]', '#event-venue', '.venue-select' ], organizer: [ '[data-testid="event-organizer"]', 'select[name="organizer"]', 'select[name="event_organizer"]', '#event-organizer', '.organizer-select' ], capacity: [ '[data-testid="event-capacity"]', 'input[name="event_capacity"]', 'input[name="_EventCapacity"]', '#event-capacity', '.event-capacity' ], cost: [ '[data-testid="event-cost"]', 'input[name="event_cost"]', 'input[name="_EventCost"]', '#event-cost', '.event-cost' ], status: [ '[data-testid="event-status"]', 'select[name="post_status"]', 'select[name="event_status"]', '#event-status', '.event-status-select' ] }, // Event status indicators status: { draft: [ '[data-testid="status-draft"]', '.status-draft', '.event-status-draft', 'text=Draft' ], pending: [ '[data-testid="status-pending"]', '.status-pending', '.event-status-pending', 'text=Pending Review' ], published: [ '[data-testid="status-published"]', '.status-published', '.event-status-published', 'text=Published' ], cancelled: [ '[data-testid="status-cancelled"]', '.status-cancelled', '.event-status-cancelled', 'text=Cancelled' ] }, // Action buttons buttons: { update: [ '[data-testid="update-event"]', 'input[value="Update Event"]', 'button[name="update"]', '#update-event', '.update-event-btn' ], publish: [ '[data-testid="publish-event"]', 'input[value="Publish"]', 'button[name="publish"]', '#publish-event', '.publish-event-btn' ], submitForReview: [ '[data-testid="submit-for-review"]', 'input[value="Submit for Review"]', 'button[name="submit_review"]', '#submit-for-review', '.submit-review-btn' ], saveDraft: [ '[data-testid="save-draft"]', 'input[value="Save Draft"]', 'button[name="save_draft"]', '#save-draft', '.save-draft-btn' ], delete: [ '[data-testid="delete-event"]', 'button[name="delete"]', '#delete-event', '.delete-event-btn', 'text=Delete Event' ], cancel: [ '[data-testid="cancel-event"]', 'button[name="cancel_event"]', '#cancel-event', '.cancel-event-btn', 'text=Cancel Event' ], preview: [ '[data-testid="preview-event"]', 'button[name="preview"]', '#preview-event', '.preview-event-btn' ] }, // Event info display eventInfo: { id: [ '[data-testid="event-id"]', '.event-id', '#event-id' ], createdDate: [ '[data-testid="created-date"]', '.created-date', '.event-created' ], lastModified: [ '[data-testid="last-modified"]', '.last-modified', '.event-modified' ], author: [ '[data-testid="event-author"]', '.event-author', '.post-author' ] }, // Attendee management attendees: { list: [ '[data-testid="attendees-list"]', '.attendees-list', '.event-attendees' ], count: [ '[data-testid="attendee-count"]', '.attendee-count', '.event-attendee-count' ], addAttendee: [ '[data-testid="add-attendee"]', '.add-attendee-btn', 'button.add-attendee' ], removeAttendee: [ '[data-testid="remove-attendee"]', '.remove-attendee-btn', 'button.remove-attendee' ] }, // Validation and messages validation: { errors: [ '[data-testid="form-errors"]', '.form-errors', '.tribe-community-notice-error', '.event-validation-errors', '.error-message' ], success: [ '[data-testid="form-success"]', '.form-success', '.tribe-community-notice-success', '.event-success-message', '.success-message' ], warnings: [ '[data-testid="form-warnings"]', '.form-warnings', '.event-warnings', '.warning-message' ] }, // Version control and history versions: { history: [ '[data-testid="version-history"]', '.version-history', '.event-revisions' ], compareLink: [ '[data-testid="compare-versions"]', '.compare-versions', 'a.compare-revisions' ] } }; this.urls = { editEvent: '/trainer/edit-event/', tecEditEvent: '/trainer/tec-edit-event/', customEditEvent: '/edit-event/', communityEdit: '/events/community/edit/' }; } /** * Navigate to event editing page */ async navigateToEdit(eventId, pageType = 'standard') { let url; switch (pageType) { case 'tec': url = `${this.urls.tecEditEvent}?event_id=${eventId}`; break; case 'community': url = `${this.urls.communityEdit}${eventId}/`; break; case 'custom': url = `${this.urls.customEditEvent}?id=${eventId}`; break; default: url = `${this.urls.editEvent}?event_id=${eventId}`; } await this.goto(url); await this.waitForEditFormLoad(); console.log(`✅ Navigated to event editing page: ${pageType} (ID: ${eventId})`); } /** * Wait for edit form to load completely */ async waitForEditFormLoad() { // Wait for form container await this.waitForVisible(this.selectors.editEventForm, { timeout: 10000 }); // Wait for essential fields await this.waitForVisible(this.selectors.fields.title, { timeout: 5000 }); await this.waitForVisible(this.selectors.fields.description, { timeout: 5000 }); // Wait for WordPress and AJAX to complete await this.waitForWordPressReady(); await this.waitForAjax(); console.log('✅ Event editing form loaded'); } /** * Get current event information */ async getEventInfo() { const eventInfo = {}; // Get event ID if available if (await this.isVisible(this.selectors.eventInfo.id)) { eventInfo.id = await this.getText(this.selectors.eventInfo.id); } // Get current values from form fields eventInfo.title = await this.getInputValue(this.selectors.fields.title); eventInfo.description = await this.getInputValue(this.selectors.fields.description); eventInfo.startDate = await this.getInputValue(this.selectors.fields.startDate); eventInfo.endDate = await this.getInputValue(this.selectors.fields.endDate); eventInfo.startTime = await this.getInputValue(this.selectors.fields.startTime); eventInfo.endTime = await this.getInputValue(this.selectors.fields.endTime); eventInfo.capacity = await this.getInputValue(this.selectors.fields.capacity); eventInfo.cost = await this.getInputValue(this.selectors.fields.cost); // Get status eventInfo.status = await this.getCurrentStatus(); // Get created/modified dates if available if (await this.isVisible(this.selectors.eventInfo.createdDate)) { eventInfo.createdDate = await this.getText(this.selectors.eventInfo.createdDate); } if (await this.isVisible(this.selectors.eventInfo.lastModified)) { eventInfo.lastModified = await this.getText(this.selectors.eventInfo.lastModified); } console.log('📋 Current event info:', eventInfo); return eventInfo; } /** * Update specific event fields */ async updateEventFields(updates) { console.log('✏️ Updating event fields:', Object.keys(updates)); for (const [fieldName, newValue] of Object.entries(updates)) { const fieldSelectors = this.selectors.fields[fieldName]; if (fieldSelectors && await this.isVisible(fieldSelectors)) { await this.clear(fieldSelectors); await this.fill(fieldSelectors, newValue); console.log(` ✓ Updated ${fieldName}: ${newValue}`); } else { console.warn(` ⚠️ Field '${fieldName}' not found or not visible`); } } // Wait for any auto-save or validation await this.waitForAjax(); } /** * Get current event status */ async getCurrentStatus() { // Check for status indicators const statusChecks = [ { status: 'published', selectors: this.selectors.status.published }, { status: 'pending', selectors: this.selectors.status.pending }, { status: 'draft', selectors: this.selectors.status.draft }, { status: 'cancelled', selectors: this.selectors.status.cancelled } ]; for (const { status, selectors } of statusChecks) { if (await this.isVisible(selectors)) { return status; } } // Check status dropdown if available if (await this.isVisible(this.selectors.fields.status)) { return await this.getSelectedValue(this.selectors.fields.status); } return 'unknown'; } /** * Change event status */ async changeStatus(newStatus) { console.log(`🔄 Changing event status to: ${newStatus}`); // If status dropdown is available if (await this.isVisible(this.selectors.fields.status)) { await this.selectByValue(this.selectors.fields.status, newStatus); console.log(`✅ Status changed via dropdown: ${newStatus}`); return; } // Use status-specific buttons const statusButtonMap = { 'draft': this.selectors.buttons.saveDraft, 'pending': this.selectors.buttons.submitForReview, 'published': this.selectors.buttons.publish }; const buttonSelectors = statusButtonMap[newStatus]; if (buttonSelectors && await this.isVisible(buttonSelectors)) { await this.click(buttonSelectors); console.log(`✅ Status changed via button: ${newStatus}`); } else { throw new Error(`Cannot change status to ${newStatus} - no available method found`); } } /** * Update event and save changes */ async updateEvent(options = {}) { const { waitForRedirect = true, expectedOutcome = 'success', timeout = 15000 } = options; console.log('💾 Updating event'); // Take screenshot before update await this.takeScreenshot('before-event-update'); // Click update button (priority order) const updateButtons = [ this.selectors.buttons.update, this.selectors.buttons.saveDraft, this.selectors.buttons.publish ]; let updateClicked = false; for (const buttonSelectors of updateButtons) { if (await this.isVisible(buttonSelectors)) { await this.click(buttonSelectors, { timeout }); updateClicked = true; break; } } if (!updateClicked) { throw new Error('No update button found on event edit form'); } if (waitForRedirect) { // Wait for form processing await this.waitForAjax(); // Wait for success/error message or URL change if (expectedOutcome === 'success') { await Promise.race([ this.waitForVisible(this.selectors.validation.success, { timeout }), this.waitForUrlChange(timeout) ]); console.log('✅ Event updated successfully'); } else if (expectedOutcome === 'error') { await this.waitForVisible(this.selectors.validation.errors, { timeout }); console.log('⚠️ Event update resulted in expected error'); } } // Take screenshot after update await this.takeScreenshot('after-event-update'); return await this.getUpdateResult(); } /** * Get update result with status and messages */ async getUpdateResult() { // Check for success messages if (await this.isVisible(this.selectors.validation.success)) { const successMessage = await this.getText(this.selectors.validation.success); return { success: true, message: successMessage, currentUrl: await this.page.url(), newStatus: await this.getCurrentStatus() }; } // Check for error messages if (await this.isVisible(this.selectors.validation.errors)) { const errorMessage = await this.getText(this.selectors.validation.errors); return { success: false, message: errorMessage, currentUrl: await this.page.url(), currentStatus: await this.getCurrentStatus() }; } // Check for warnings if (await this.isVisible(this.selectors.validation.warnings)) { const warningMessage = await this.getText(this.selectors.validation.warnings); return { success: true, warning: true, message: warningMessage, currentUrl: await this.page.url(), newStatus: await this.getCurrentStatus() }; } // Default success if no explicit messages return { success: true, message: 'Event updated (no confirmation message displayed)', currentUrl: await this.page.url(), newStatus: await this.getCurrentStatus() }; } /** * Delete event (with confirmation handling) */ async deleteEvent(confirmDelete = true) { console.log('🗑️ Attempting to delete event'); if (!await this.isVisible(this.selectors.buttons.delete)) { throw new Error('Delete button not available'); } await this.click(this.selectors.buttons.delete); // Handle confirmation dialog if (confirmDelete) { // Wait for browser confirmation dialog this.page.on('dialog', async dialog => { console.log(`🔔 Confirmation dialog: ${dialog.message()}`); await dialog.accept(); }); } await this.waitForAjax(); const currentUrl = await this.page.url(); const result = { deleted: !currentUrl.includes('edit-event'), currentUrl }; console.log(result.deleted ? '✅ Event deleted' : '⚠️ Event deletion may have failed'); return result; } /** * Cancel event (change status to cancelled) */ async cancelEvent(reason = '') { console.log('❌ Cancelling event'); // If there's a specific cancel button if (await this.isVisible(this.selectors.buttons.cancel)) { await this.click(this.selectors.buttons.cancel); // Handle reason input if available const reasonInput = await this.getVisibleSelector([ '[data-testid="cancel-reason"]', 'textarea[name="cancel_reason"]', '#cancel-reason' ]); if (reasonInput && reason) { await this.fill(reasonInput, reason); } await this.waitForAjax(); } else { // Use status change await this.changeStatus('cancelled'); } return await this.updateEvent(); } /** * Get attendee information */ async getAttendeeInfo() { const attendeeInfo = { count: 0, attendees: [], hasAttendeeManagement: false }; // Check if attendee management is available if (await this.isVisible(this.selectors.attendees.list)) { attendeeInfo.hasAttendeeManagement = true; // Get attendee count if (await this.isVisible(this.selectors.attendees.count)) { const countText = await this.getText(this.selectors.attendees.count); attendeeInfo.count = parseInt(countText.match(/\d+/)?.[0] || '0', 10); } // Get attendee list const attendeeElements = await this.page.locator(`${this.selectors.attendees.list.join(', ')} .attendee`).all(); for (const attendeeElement of attendeeElements) { const attendeeText = await attendeeElement.textContent(); attendeeInfo.attendees.push(attendeeText.trim()); } } console.log('👥 Attendee info:', attendeeInfo); return attendeeInfo; } /** * Test event status transitions */ async testStatusTransitions() { console.log('🔄 Testing status transitions'); const transitions = []; const currentStatus = await this.getCurrentStatus(); transitions.push({ from: null, to: currentStatus, timestamp: Date.now() }); // Test possible transitions based on current status const transitionMap = { 'draft': ['pending', 'published'], 'pending': ['published', 'draft'], 'published': ['draft'], 'cancelled': ['draft'] }; const possibleTransitions = transitionMap[currentStatus] || []; for (const targetStatus of possibleTransitions) { try { await this.changeStatus(targetStatus); await this.updateEvent({ waitForRedirect: false }); const newStatus = await this.getCurrentStatus(); transitions.push({ from: currentStatus, to: newStatus, expected: targetStatus, successful: newStatus === targetStatus, timestamp: Date.now() }); console.log(` ${currentStatus} → ${newStatus} ${newStatus === targetStatus ? '✅' : '❌'}`); } catch (error) { transitions.push({ from: currentStatus, to: targetStatus, expected: targetStatus, successful: false, error: error.message, timestamp: Date.now() }); console.log(` ${currentStatus} → ${targetStatus} ❌ (${error.message})`); } } return transitions; } /** * Compare current event data with expected data */ async compareEventData(expectedData) { const currentData = await this.getEventInfo(); const comparison = { matches: {}, differences: {}, missing: {} }; for (const [field, expectedValue] of Object.entries(expectedData)) { if (currentData.hasOwnProperty(field)) { const matches = currentData[field] === expectedValue; if (matches) { comparison.matches[field] = expectedValue; } else { comparison.differences[field] = { expected: expectedValue, actual: currentData[field] }; } } else { comparison.missing[field] = expectedValue; } } const overallMatch = Object.keys(comparison.differences).length === 0 && Object.keys(comparison.missing).length === 0; console.log('🔍 Event data comparison:', { overallMatch, ...comparison }); return { overallMatch, ...comparison }; } /** * Preview event */ async previewEvent() { if (await this.isVisible(this.selectors.buttons.preview)) { await this.click(this.selectors.buttons.preview); await this.waitForAjax(); console.log('👁️ Event preview opened'); return true; } console.log('⚠️ Preview not available'); return false; } /** * Get version history if available */ async getVersionHistory() { if (!await this.isVisible(this.selectors.versions.history)) { return { hasVersioning: false }; } const versions = []; const versionElements = await this.page.locator(`${this.selectors.versions.history.join(', ')} .revision`).all(); for (const versionElement of versionElements) { const versionText = await versionElement.textContent(); versions.push(versionText.trim()); } return { hasVersioning: true, versions, count: versions.length }; } /** * Take screenshot of edit form */ async screenshotEditForm(name = 'event-edit-form') { return await this.takeScreenshot(name, { fullPage: true }); } /** * Verify edit form access and functionality */ async verifyEditAccess() { const checks = { formVisible: await this.isVisible(this.selectors.editEventForm), fieldsEditable: await this.areFieldsEditable(), updateButtonAvailable: await this.isVisible(this.selectors.buttons.update), canChangeStatus: await this.canChangeStatus(), eventInfoDisplayed: await this.isVisible(this.selectors.eventInfo.id) }; const hasEditAccess = Object.values(checks).every(check => check === true); console.log('🔐 Edit access verification:', { hasEditAccess, ...checks }); return { hasEditAccess, checks }; } /** * Check if form fields are editable */ async areFieldsEditable() { const fieldsToCheck = ['title', 'description', 'startDate', 'capacity']; for (const fieldName of fieldsToCheck) { const fieldSelectors = this.selectors.fields[fieldName]; if (await this.isVisible(fieldSelectors)) { const isDisabled = await this.page.locator(fieldSelectors.join(', ')).first().isDisabled(); if (isDisabled) return false; } } return true; } /** * Check if user can change event status */ async canChangeStatus() { return await this.isVisible(this.selectors.fields.status) || await this.isVisible(this.selectors.buttons.publish) || await this.isVisible(this.selectors.buttons.submitForReview); } } module.exports = EventEditing;