/** * Base Page Object Model for HVAC Testing Framework * * Provides common page interaction methods with WordPress integration: * - Navigation and URL handling * - Element interaction with retry logic * - WordPress-specific waiting and assertions * - Screenshot and error handling utilities * * @package HVAC_Community_Events * @version 2.0.0 * @created 2025-08-27 */ const { expect } = require('@playwright/test'); const ConfigManager = require('../../framework/core/ConfigManager'); class BasePage { constructor(page) { this.page = page; this.config = ConfigManager; this.baseUrl = this.config.get('app.baseUrl'); this.defaultTimeout = this.config.get('framework.timeout'); // Common WordPress selectors this.wpSelectors = { adminBar: '#wpadminbar', loadingSpinner: '.spinner, .loading', ajaxLoader: '.ajax-loader', errorMessage: '.error, .notice-error', successMessage: '.success, .notice-success, .updated', wpNonce: '[name="_wpnonce"]', loggedInBody: 'body.logged-in' }; } /** * Navigate to URL with WordPress-aware waiting */ async goto(url) { const fullUrl = url.startsWith('http') ? url : `${this.baseUrl}${url}`; console.log(`🔗 Navigating to: ${fullUrl}`); await this.page.goto(fullUrl, { waitUntil: 'domcontentloaded', timeout: this.defaultTimeout }); // Wait for WordPress to be ready await this.waitForWordPressReady(); // Check for WordPress errors await this.checkForWordPressErrors(); } /** * Wait for WordPress to be ready */ async waitForWordPressReady() { // Wait for DOM to be ready await this.page.waitForFunction('document.readyState === "complete"', { timeout: this.defaultTimeout }); // Wait for jQuery if present try { await this.page.waitForFunction( 'typeof jQuery === "undefined" || jQuery.active === 0', { timeout: 5000 } ); } catch (error) { // jQuery may not be present, which is fine } } /** * Wait for AJAX requests to complete */ async waitForAjax(timeout = 5000) { try { await this.page.waitForFunction( 'window.hvacTestHelpers && window.hvacTestHelpers.waitForAjax()', { timeout } ); } catch (error) { // Fallback wait await this.page.waitForTimeout(500); } } /** * Check for WordPress errors on page */ async checkForWordPressErrors() { const errorSelectors = [ '.wp-die-message', '.error-message', '.notice-error', 'text=Fatal error', 'text=Parse error', 'text=Warning:', 'text=Notice:' ]; for (const selector of errorSelectors) { if (await this.isVisible(selector, { timeout: 1000 })) { const errorText = await this.getText(selector); console.warn(`⚠️ WordPress error detected: ${errorText}`); } } } /** * Find visible element from multiple selectors */ async getVisibleSelector(selectors, options = {}) { const { timeout = 2000 } = options; for (const selector of selectors) { try { if (await this.isVisible(selector, { timeout: timeout / selectors.length })) { return selector; } } catch (error) { continue; } } return null; } /** * Check if element is visible with multiple selector fallback */ async isVisible(selectors, options = {}) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; const { timeout = 2000 } = options; for (const selector of selectorArray) { try { const element = this.page.locator(selector).first(); await element.waitFor({ state: 'visible', timeout: timeout / selectorArray.length }); return true; } catch (error) { continue; } } return false; } /** * Wait for element to be visible */ async waitForVisible(selectors, options = {}) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; const { timeout = this.defaultTimeout } = options; for (const selector of selectorArray) { try { await this.page.locator(selector).first().waitFor({ state: 'visible', timeout: timeout / selectorArray.length }); return selector; } catch (error) { if (selector === selectorArray[selectorArray.length - 1]) { throw new Error(`Element not found with any selector: ${selectorArray.join(', ')}`); } continue; } } } /** * Wait for element to be hidden */ async waitForHidden(selectors, options = {}) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; const { timeout = this.defaultTimeout } = options; for (const selector of selectorArray) { try { await this.page.locator(selector).first().waitFor({ state: 'hidden', timeout: timeout / selectorArray.length }); return; } catch (error) { continue; } } } /** * Click element with multiple selector support */ async click(selectors, options = {}) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; const { timeout = this.defaultTimeout, waitForNavigation = false } = options; for (const selector of selectorArray) { try { const element = this.page.locator(selector).first(); await element.waitFor({ state: 'visible', timeout: timeout / selectorArray.length }); if (waitForNavigation) { await Promise.all([ this.page.waitForNavigation({ timeout }), element.click() ]); } else { await element.click(); } return; } catch (error) { if (selector === selectorArray[selectorArray.length - 1]) { throw new Error(`Cannot click element with any selector: ${selectorArray.join(', ')}`); } continue; } } } /** * Fill form field with multiple selector support */ async fill(selectors, value) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; for (const selector of selectorArray) { try { const element = this.page.locator(selector).first(); await element.waitFor({ state: 'visible', timeout: 2000 }); await element.fill(value); return; } catch (error) { if (selector === selectorArray[selectorArray.length - 1]) { throw new Error(`Cannot fill field with any selector: ${selectorArray.join(', ')}`); } continue; } } } /** * Clear form field */ async clear(selectors) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; for (const selector of selectorArray) { try { const element = this.page.locator(selector).first(); await element.waitFor({ state: 'visible', timeout: 2000 }); await element.clear(); return; } catch (error) { continue; } } } /** * Get text content from element */ async getText(selectors) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; for (const selector of selectorArray) { try { const element = this.page.locator(selector).first(); await element.waitFor({ state: 'visible', timeout: 2000 }); return await element.textContent(); } catch (error) { continue; } } return ''; } /** * Get input value from form field */ async getInputValue(selectors) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; for (const selector of selectorArray) { try { const element = this.page.locator(selector).first(); await element.waitFor({ state: 'visible', timeout: 2000 }); return await element.inputValue(); } catch (error) { continue; } } return ''; } /** * Select option by text */ async selectByText(selectors, text) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; for (const selector of selectorArray) { try { const element = this.page.locator(selector).first(); await element.waitFor({ state: 'visible', timeout: 2000 }); await element.selectOption({ label: text }); return; } catch (error) { continue; } } throw new Error(`Cannot select option with text '${text}' from any selector: ${selectorArray.join(', ')}`); } /** * Select option by value */ async selectByValue(selectors, value) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; for (const selector of selectorArray) { try { const element = this.page.locator(selector).first(); await element.waitFor({ state: 'visible', timeout: 2000 }); await element.selectOption(value); return; } catch (error) { continue; } } throw new Error(`Cannot select option with value '${value}' from any selector: ${selectorArray.join(', ')}`); } /** * Get selected value from dropdown */ async getSelectedValue(selectors) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; for (const selector of selectorArray) { try { const element = this.page.locator(selector).first(); await element.waitFor({ state: 'visible', timeout: 2000 }); return await element.inputValue(); } catch (error) { continue; } } return ''; } /** * Check checkbox or radio button */ async check(selectors) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; for (const selector of selectorArray) { try { const element = this.page.locator(selector).first(); await element.waitFor({ state: 'visible', timeout: 2000 }); await element.check(); return; } catch (error) { continue; } } } /** * Uncheck checkbox or radio button */ async uncheck(selectors) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; for (const selector of selectorArray) { try { const element = this.page.locator(selector).first(); await element.waitFor({ state: 'visible', timeout: 2000 }); await element.uncheck(); return; } catch (error) { continue; } } } /** * Get locator for element */ locator(selectors) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; return this.page.locator(selectorArray.join(', ')); } /** * Wait for URL change or specific URL pattern */ async waitForUrl(pattern, options = {}) { const { timeout = this.defaultTimeout } = options; await this.page.waitForURL(pattern, { timeout }); } /** * Wait for URL to change from current */ async waitForUrlChange(timeout = this.defaultTimeout) { const currentUrl = this.page.url(); await this.page.waitForFunction( (oldUrl) => window.location.href !== oldUrl, currentUrl, { timeout } ); } /** * Verify user role from page elements */ async verifyUserRole(expectedRole) { // Check body class for role const hasRoleClass = await this.page.evaluate((role) => { return document.body.classList.contains(`role-${role}`); }, expectedRole); if (hasRoleClass) { return true; } // Check admin bar user info if (await this.isVisible(this.wpSelectors.adminBar)) { const userInfo = await this.getText('#wp-admin-bar-my-account .display-name'); console.log(`Current user display name: ${userInfo}`); } // Additional WordPress role verification can be added here return hasRoleClass; } /** * Verify breadcrumbs navigation */ async verifyBreadcrumbs(expectedPath) { const breadcrumbSelectors = [ '.hvac-breadcrumb', '.breadcrumb', '[data-testid="breadcrumb"]', '.breadcrumb-nav' ]; for (const selector of breadcrumbSelectors) { if (await this.isVisible(selector)) { const breadcrumbText = await this.getText(selector); for (const pathItem of expectedPath) { if (!breadcrumbText.includes(pathItem)) { console.warn(`⚠️ Breadcrumb missing: ${pathItem}`); return false; } } console.log(`✅ Breadcrumbs verified: ${breadcrumbText}`); return true; } } console.log('⚠️ No breadcrumbs found'); return false; } /** * Take screenshot with context */ async takeScreenshot(name, options = {}) { const { fullPage = false } = options; try { const screenshotPath = `test-results/${name}-${Date.now()}.png`; await this.page.screenshot({ path: screenshotPath, fullPage }); console.log(`📸 Screenshot saved: ${screenshotPath}`); return screenshotPath; } catch (error) { console.warn('Could not take screenshot:', error.message); return null; } } /** * Scroll element into view */ async scrollIntoView(selectors) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; for (const selector of selectorArray) { try { const element = this.page.locator(selector).first(); await element.scrollIntoViewIfNeeded(); return; } catch (error) { continue; } } } /** * Handle WordPress nonce verification */ async verifyNonce(expectedAction) { if (await this.isVisible(this.wpSelectors.wpNonce)) { const nonce = await this.page.locator(this.wpSelectors.wpNonce).getAttribute('value'); console.log(`🔒 WordPress nonce present: ${nonce.substring(0, 8)}...`); return true; } console.warn('⚠️ WordPress nonce not found'); return false; } /** * Wait for network idle state */ async waitForNetworkIdle(timeout = 5000) { await this.page.waitForLoadState('networkidle', { timeout }); } /** * Get current page title */ async getPageTitle() { return await this.page.title(); } /** * Get current URL */ getCurrentUrl() { return this.page.url(); } /** * Refresh page with WordPress readiness check */ async refresh() { await this.page.reload({ waitUntil: 'domcontentloaded' }); await this.waitForWordPressReady(); await this.checkForWordPressErrors(); } } module.exports = BasePage;