/** * Event Management Dashboard Page Object Model * * Handles event management dashboard functionality including: * - Event listing and filtering * - Bulk operations on events * - Event status management * - Attendee management dashboard * - Event analytics and reporting * * @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 EventManagement extends BasePage { constructor(page) { super(page); // Event management dashboard selectors this.selectors = { // Main dashboard containers managementDashboard: [ '[data-testid="event-management-dashboard"]', '.hvac-event-management', '.event-management-dashboard', '.manage-events-dashboard' ], // Event listing eventsList: [ '[data-testid="events-list"]', '.events-list', '.hvac-events-list', '.manage-events-list', 'table.events-table' ], eventRow: [ '[data-testid="event-row"]', '.event-row', 'tr.event-item', '.event-listing-item' ], // Event actions actions: { createNew: [ '[data-testid="create-event-btn"]', '.create-event-btn', 'a[href*="create-event"]', 'button.new-event' ], bulkActions: [ '[data-testid="bulk-actions"]', 'select[name="bulk_action"]', '.bulk-actions-select' ], applyBulk: [ '[data-testid="apply-bulk"]', 'button[name="apply_bulk"]', '.apply-bulk-btn' ], export: [ '[data-testid="export-events"]', '.export-events-btn', 'button.export' ], import: [ '[data-testid="import-events"]', '.import-events-btn', 'button.import' ] }, // Event row actions rowActions: { edit: [ '[data-testid="edit-event"]', '.edit-event', 'a.edit-link', 'text=Edit' ], view: [ '[data-testid="view-event"]', '.view-event', 'a.view-link', 'text=View' ], delete: [ '[data-testid="delete-event"]', '.delete-event', 'button.delete-link', 'text=Delete' ], duplicate: [ '[data-testid="duplicate-event"]', '.duplicate-event', 'button.duplicate-link', 'text=Duplicate' ], cancel: [ '[data-testid="cancel-event"]', '.cancel-event', 'button.cancel-link', 'text=Cancel' ] }, // Filtering and search filters: { search: [ '[data-testid="events-search"]', 'input[name="search"]', '#events-search', '.search-events' ], status: [ '[data-testid="filter-status"]', 'select[name="status_filter"]', '.status-filter' ], dateRange: [ '[data-testid="date-range-filter"]', 'input[name="date_range"]', '.date-range-filter' ], category: [ '[data-testid="category-filter"]', 'select[name="category_filter"]', '.category-filter' ], venue: [ '[data-testid="venue-filter"]', 'select[name="venue_filter"]', '.venue-filter' ] }, // Sorting sorting: { title: [ '[data-testid="sort-title"]', 'th.sort-title', '.sort-by-title' ], date: [ '[data-testid="sort-date"]', 'th.sort-date', '.sort-by-date' ], status: [ '[data-testid="sort-status"]', 'th.sort-status', '.sort-by-status' ], attendees: [ '[data-testid="sort-attendees"]', 'th.sort-attendees', '.sort-by-attendees' ] }, // Pagination pagination: { container: [ '[data-testid="pagination"]', '.pagination', '.events-pagination' ], previous: [ '[data-testid="prev-page"]', '.prev-page', 'a.prev' ], next: [ '[data-testid="next-page"]', '.next-page', 'a.next' ], pageNumbers: [ '[data-testid="page-numbers"]', '.page-numbers', '.pagination-numbers' ] }, // Event details in list view eventDetails: { title: [ '[data-testid="event-title"]', '.event-title', 'td.title' ], date: [ '[data-testid="event-date"]', '.event-date', 'td.date' ], status: [ '[data-testid="event-status"]', '.event-status', 'td.status' ], attendeeCount: [ '[data-testid="attendee-count"]', '.attendee-count', 'td.attendees' ], venue: [ '[data-testid="event-venue"]', '.event-venue', 'td.venue' ], organizer: [ '[data-testid="event-organizer"]', '.event-organizer', 'td.organizer' ] }, // Dashboard statistics stats: { totalEvents: [ '[data-testid="total-events"]', '.total-events-count', '.stat-total-events' ], upcomingEvents: [ '[data-testid="upcoming-events"]', '.upcoming-events-count', '.stat-upcoming' ], draftEvents: [ '[data-testid="draft-events"]', '.draft-events-count', '.stat-drafts' ], publishedEvents: [ '[data-testid="published-events"]', '.published-events-count', '.stat-published' ], totalAttendees: [ '[data-testid="total-attendees"]', '.total-attendees-count', '.stat-attendees' ] }, // Quick actions panel quickActions: { panel: [ '[data-testid="quick-actions"]', '.quick-actions-panel', '.management-quick-actions' ], bulkPublish: [ '[data-testid="bulk-publish"]', '.bulk-publish-btn', 'button.quick-publish' ], bulkCancel: [ '[data-testid="bulk-cancel"]', '.bulk-cancel-btn', 'button.quick-cancel' ], exportSelected: [ '[data-testid="export-selected"]', '.export-selected-btn', 'button.export-selected' ] }, // Messages and notifications messages: { success: [ '[data-testid="success-message"]', '.success-message', '.notice-success' ], error: [ '[data-testid="error-message"]', '.error-message', '.notice-error' ], warning: [ '[data-testid="warning-message"]', '.warning-message', '.notice-warning' ] }, // Loading states loading: [ '[data-testid="loading"]', '.loading-events', '.management-loading' ] }; this.urls = { management: '/trainer/manage-event/', events: '/trainer/events/', dashboard: '/trainer/dashboard/', tecMyEvents: '/trainer/tec-my-events/' }; } /** * Navigate to event management dashboard */ async navigate(dashboardType = 'standard') { let url; switch (dashboardType) { case 'tec': url = this.urls.tecMyEvents; break; case 'events': url = this.urls.events; break; default: url = this.urls.management; } await this.goto(url); await this.waitForDashboardLoad(); console.log(`✅ Navigated to event management: ${dashboardType}`); } /** * Wait for management dashboard to load */ async waitForDashboardLoad() { // Wait for main dashboard container await this.waitForVisible(this.selectors.managementDashboard, { timeout: 10000 }); // Wait for events list to load await this.waitForVisible(this.selectors.eventsList, { timeout: 8000 }); // Wait for WordPress and AJAX await this.waitForWordPressReady(); await this.waitForAjax(); // Wait for any loading indicators to disappear await this.waitForHidden(this.selectors.loading, { timeout: 5000 }); console.log('✅ Event management dashboard loaded'); } /** * Get dashboard statistics */ async getDashboardStats() { const stats = {}; // Get various statistics if available const statFields = [ 'totalEvents', 'upcomingEvents', 'draftEvents', 'publishedEvents', 'totalAttendees' ]; for (const statField of statFields) { const statSelectors = this.selectors.stats[statField]; if (await this.isVisible(statSelectors)) { const statText = await this.getText(statSelectors); stats[statField] = this.extractNumber(statText); } } console.log('📊 Dashboard statistics:', stats); return stats; } /** * Get list of events with their details */ async getEventsList() { const events = []; // Find all event rows const eventRows = await this.page.locator(this.selectors.eventRow.join(', ')).all(); for (const row of eventRows) { const event = {}; // Extract event details from each row try { const titleElement = await row.locator(this.selectors.eventDetails.title.join(', ')).first(); event.title = await titleElement.textContent(); const dateElement = await row.locator(this.selectors.eventDetails.date.join(', ')).first(); event.date = await dateElement.textContent(); const statusElement = await row.locator(this.selectors.eventDetails.status.join(', ')).first(); event.status = await statusElement.textContent(); // Optional fields if (await row.locator(this.selectors.eventDetails.attendeeCount.join(', ')).count() > 0) { const attendeeElement = await row.locator(this.selectors.eventDetails.attendeeCount.join(', ')).first(); event.attendees = await attendeeElement.textContent(); } if (await row.locator(this.selectors.eventDetails.venue.join(', ')).count() > 0) { const venueElement = await row.locator(this.selectors.eventDetails.venue.join(', ')).first(); event.venue = await venueElement.textContent(); } events.push(event); } catch (error) { console.warn('Could not extract event details from row:', error.message); } } console.log(`📋 Found ${events.length} events in list`); return events; } /** * Search for events */ async searchEvents(searchTerm) { console.log(`🔍 Searching for events: "${searchTerm}"`); if (!await this.isVisible(this.selectors.filters.search)) { throw new Error('Search functionality not available'); } // Clear existing search and enter new term await this.clear(this.selectors.filters.search); await this.fill(this.selectors.filters.search, searchTerm); await this.page.keyboard.press('Enter'); // Wait for results to update await this.waitForAjax(); const results = await this.getEventsList(); console.log(`📋 Search returned ${results.length} results`); return results; } /** * Filter events by status */ async filterByStatus(status) { console.log(`🎯 Filtering events by status: ${status}`); if (await this.isVisible(this.selectors.filters.status)) { await this.selectByValue(this.selectors.filters.status, status); await this.waitForAjax(); const results = await this.getEventsList(); console.log(`📋 Status filter returned ${results.length} results`); return results; } throw new Error('Status filter not available'); } /** * Filter events by date range */ async filterByDateRange(startDate, endDate) { console.log(`📅 Filtering events by date range: ${startDate} to ${endDate}`); if (await this.isVisible(this.selectors.filters.dateRange)) { const dateRange = `${startDate} - ${endDate}`; await this.fill(this.selectors.filters.dateRange, dateRange); await this.waitForAjax(); const results = await this.getEventsList(); console.log(`📋 Date filter returned ${results.length} results`); return results; } throw new Error('Date range filter not available'); } /** * Sort events by specified field */ async sortEvents(sortBy, direction = 'asc') { console.log(`⬆️ Sorting events by ${sortBy} (${direction})`); const sortSelectors = this.selectors.sorting[sortBy]; if (!sortSelectors || !await this.isVisible(sortSelectors)) { throw new Error(`Sort option '${sortBy}' not available`); } // Click sort header await this.click(sortSelectors); // Click again if we need descending order if (direction === 'desc') { await this.click(sortSelectors); } await this.waitForAjax(); console.log(`✅ Events sorted by ${sortBy} (${direction})`); } /** * Select events for bulk operations */ async selectEvents(eventTitles) { console.log(`☑️ Selecting events for bulk operation:`, eventTitles); const selectedCount = 0; const eventRows = await this.page.locator(this.selectors.eventRow.join(', ')).all(); for (const row of eventRows) { try { const titleElement = await row.locator(this.selectors.eventDetails.title.join(', ')).first(); const title = await titleElement.textContent(); if (eventTitles.includes(title.trim())) { // Find and click checkbox in this row const checkbox = await row.locator('input[type="checkbox"]').first(); await checkbox.click(); selectedCount++; } } catch (error) { console.warn('Could not select event in row:', error.message); } } console.log(`✅ Selected ${selectedCount} events`); return selectedCount; } /** * Perform bulk action on selected events */ async performBulkAction(action) { console.log(`⚡ Performing bulk action: ${action}`); if (!await this.isVisible(this.selectors.actions.bulkActions)) { throw new Error('Bulk actions not available'); } // Select the bulk action await this.selectByValue(this.selectors.actions.bulkActions, action); // Apply the bulk action await this.click(this.selectors.actions.applyBulk); // Wait for action to complete await this.waitForAjax(); // Check for success/error messages const result = await this.getBulkActionResult(); console.log(`${result.success ? '✅' : '❌'} Bulk action result:`, result.message); return result; } /** * Get bulk action result */ async getBulkActionResult() { // Check for success message if (await this.isVisible(this.selectors.messages.success)) { return { success: true, message: await this.getText(this.selectors.messages.success) }; } // Check for error message if (await this.isVisible(this.selectors.messages.error)) { return { success: false, message: await this.getText(this.selectors.messages.error) }; } // Default success if no messages return { success: true, message: 'Bulk action completed (no confirmation message)' }; } /** * Edit specific event */ async editEvent(eventTitle) { console.log(`✏️ Editing event: ${eventTitle}`); const eventRow = await this.findEventRow(eventTitle); if (!eventRow) { throw new Error(`Event '${eventTitle}' not found`); } // Find and click edit button in this row const editButton = await eventRow.locator(this.selectors.rowActions.edit.join(', ')).first(); await editButton.click(); // Wait for navigation to edit page await this.waitForUrlChange(); console.log(`✅ Navigated to edit page for: ${eventTitle}`); } /** * View specific event */ async viewEvent(eventTitle) { console.log(`👁️ Viewing event: ${eventTitle}`); const eventRow = await this.findEventRow(eventTitle); if (!eventRow) { throw new Error(`Event '${eventTitle}' not found`); } // Find and click view button in this row const viewButton = await eventRow.locator(this.selectors.rowActions.view.join(', ')).first(); await viewButton.click(); // Wait for navigation or modal to open await this.waitForAjax(); console.log(`✅ Viewing event: ${eventTitle}`); } /** * Delete specific event */ async deleteEvent(eventTitle, confirmDelete = true) { console.log(`🗑️ Deleting event: ${eventTitle}`); const eventRow = await this.findEventRow(eventTitle); if (!eventRow) { throw new Error(`Event '${eventTitle}' not found`); } // Find and click delete button const deleteButton = await eventRow.locator(this.selectors.rowActions.delete.join(', ')).first(); await deleteButton.click(); // Handle confirmation dialog if it appears if (confirmDelete) { this.page.on('dialog', async dialog => { console.log(`🔔 Confirmation: ${dialog.message()}`); await dialog.accept(); }); } await this.waitForAjax(); // Verify deletion const stillExists = await this.findEventRow(eventTitle); const deleted = !stillExists; console.log(`${deleted ? '✅' : '❌'} Event deletion: ${eventTitle}`); return { deleted, eventTitle }; } /** * Duplicate specific event */ async duplicateEvent(eventTitle) { console.log(`📋 Duplicating event: ${eventTitle}`); const eventRow = await this.findEventRow(eventTitle); if (!eventRow) { throw new Error(`Event '${eventTitle}' not found`); } const duplicateButton = await eventRow.locator(this.selectors.rowActions.duplicate.join(', ')).first(); await duplicateButton.click(); await this.waitForAjax(); // Look for success message or new duplicate event const result = await this.getBulkActionResult(); console.log(`✅ Event duplicated: ${eventTitle}`); return result; } /** * Find event row by title */ async findEventRow(eventTitle) { const eventRows = await this.page.locator(this.selectors.eventRow.join(', ')).all(); for (const row of eventRows) { try { const titleElement = await row.locator(this.selectors.eventDetails.title.join(', ')).first(); const title = await titleElement.textContent(); if (title.trim() === eventTitle.trim()) { return row; } } catch (error) { continue; } } return null; } /** * Navigate through pagination */ async navigateToPage(pageNumber) { console.log(`📄 Navigating to page: ${pageNumber}`); if (!await this.isVisible(this.selectors.pagination.container)) { console.log('⚠️ Pagination not available'); return false; } // Look for specific page number link const pageLink = await this.getVisibleSelector([ `text=${pageNumber}`, `[data-page="${pageNumber}"]`, `.page-${pageNumber}` ]); if (pageLink) { await this.click(pageLink); await this.waitForAjax(); console.log(`✅ Navigated to page ${pageNumber}`); return true; } console.log(`⚠️ Page ${pageNumber} not found`); return false; } /** * Export events */ async exportEvents(format = 'csv') { console.log(`📤 Exporting events in ${format} format`); if (!await this.isVisible(this.selectors.actions.export)) { throw new Error('Export functionality not available'); } // Click export button await this.click(this.selectors.actions.export); // Handle format selection if needed const formatSelector = await this.getVisibleSelector([ `select[name="export_format"]`, `input[value="${format}"]`, `.format-${format}` ]); if (formatSelector) { if (formatSelector.includes('select')) { await this.selectByValue(formatSelector, format); } else { await this.click(formatSelector); } } // Wait for export to process await this.waitForAjax(); console.log(`✅ Events export initiated (${format})`); return { format, initiated: true }; } /** * Get event management metrics */ async getManagementMetrics() { const metrics = { totalEvents: 0, eventsByStatus: {}, averageAttendeesPerEvent: 0, upcomingEventsCount: 0 }; // Get dashboard stats const stats = await this.getDashboardStats(); metrics.totalEvents = stats.totalEvents || 0; metrics.upcomingEventsCount = stats.upcomingEvents || 0; // Get events list for detailed analysis const events = await this.getEventsList(); // Analyze events by status events.forEach(event => { const status = event.status?.toLowerCase() || 'unknown'; metrics.eventsByStatus[status] = (metrics.eventsByStatus[status] || 0) + 1; }); // Calculate average attendees (if available) const eventsWithAttendees = events.filter(event => event.attendees && !isNaN(parseInt(event.attendees))); if (eventsWithAttendees.length > 0) { const totalAttendees = eventsWithAttendees.reduce((sum, event) => { return sum + parseInt(event.attendees); }, 0); metrics.averageAttendeesPerEvent = Math.round(totalAttendees / eventsWithAttendees.length); } console.log('📊 Management metrics:', metrics); return metrics; } /** * Verify management dashboard functionality */ async verifyDashboardFunctionality() { const functionality = { canViewEvents: await this.isVisible(this.selectors.eventsList), canCreateEvents: await this.isVisible(this.selectors.actions.createNew), canSearch: await this.isVisible(this.selectors.filters.search), canFilter: await this.isVisible(this.selectors.filters.status), canSort: await this.isVisible(this.selectors.sorting.date), hasBulkActions: await this.isVisible(this.selectors.actions.bulkActions), canExport: await this.isVisible(this.selectors.actions.export), hasPagination: await this.isVisible(this.selectors.pagination.container), hasStats: await this.isVisible(this.selectors.stats.totalEvents) }; const functionalityScore = Object.values(functionality).filter(Boolean).length; const totalFeatures = Object.keys(functionality).length; console.log(`🔧 Dashboard functionality: ${functionalityScore}/${totalFeatures} features available`); return { ...functionality, score: functionalityScore, total: totalFeatures }; } /** * Extract number from text */ extractNumber(text) { const match = text.match(/\d+/); return match ? parseInt(match[0], 10) : 0; } /** * Take screenshot of management dashboard */ async screenshotDashboard(name = 'event-management-dashboard') { return await this.takeScreenshot(name, { fullPage: true }); } } module.exports = EventManagement;