/** * Base Page Object Model for HVAC Testing Framework * * Provides common functionality for all page objects: * - WordPress-specific element interactions * - Consistent selector patterns with data-testid support * - Error handling and retry mechanisms * - Screenshot management * - WordPress AJAX and loading state handling * * @package HVAC_Community_Events * @version 2.0.0 * @created 2025-08-27 */ const { expect } = require('@playwright/test'); const ConfigManager = require('../core/ConfigManager'); class BasePage { constructor(page) { this.page = page; this.config = ConfigManager; this.baseUrl = this.config.get('app.baseUrl'); this.timeout = this.config.get('framework.timeout'); } /** * Navigate to page with WordPress-aware waiting */ async goto(url, options = {}) { const fullUrl = url.startsWith('http') ? url : `${this.baseUrl}${url}`; console.log(`🔗 Navigating to: ${fullUrl}`); await this.page.goto(fullUrl, { waitUntil: 'networkidle', timeout: this.timeout, ...options }); // Wait for WordPress to be ready await this.waitForWordPressReady(); } /** * Wait for WordPress to be fully loaded */ async waitForWordPressReady() { // Wait for DOM to be ready await this.page.waitForFunction('document.readyState === "complete"'); // Wait for jQuery if present await this.page.waitForFunction(` !window.jQuery || jQuery.active === 0 `, { timeout: 5000 }).catch(() => { // jQuery not present or still active, that's fine }); // Wait for WordPress admin bar if present await this.page.waitForFunction(` !document.querySelector('#wpadminbar') || document.querySelector('#wpadminbar').style.display !== 'none' `, { timeout: 2000 }).catch(() => { // Admin bar not present, that's fine }); } /** * Find element using multiple selector strategies * Prioritizes data-testid, then data-role, then CSS selectors */ locator(selectors, options = {}) { if (typeof selectors === 'string') { selectors = [selectors]; } // Build selector list with priority const selectorList = []; for (const selector of selectors) { // Check if it's a test ID if (!selector.includes('[') && !selector.includes('.') && !selector.includes('#')) { selectorList.push(`[data-testid="${selector}"]`); } // Add the original selector selectorList.push(selector); } // Try selectors in order for (const selector of selectorList) { const locator = this.page.locator(selector); if (options.first) { return locator.first(); } return locator; } // Fallback to first selector return this.page.locator(selectors[0]); } /** * Click element with WordPress-aware waiting */ async click(selectors, options = {}) { const element = this.locator(selectors, options); // Wait for element to be visible and enabled await element.waitFor({ state: 'visible', timeout: options.timeout || this.timeout }); await element.waitFor({ state: 'attached' }); // Scroll into view if needed await element.scrollIntoViewIfNeeded(); // Click with optional navigation waiting if (options.waitForNavigation) { await Promise.all([ this.page.waitForLoadState('networkidle'), element.click() ]); } else { await element.click(); } // Wait for AJAX if WordPress if (!options.skipAjaxWait) { await this.waitForAjax(); } return element; } /** * Fill form field with validation */ async fill(selectors, value, options = {}) { const element = this.locator(selectors, options); await element.waitFor({ state: 'visible', timeout: options.timeout || this.timeout }); // Clear field first if not disabled if (options.clear !== false) { await element.clear(); } // Fill value await element.fill(value); // Validate the value was set correctly if (options.validate !== false) { const actualValue = await element.inputValue(); if (actualValue !== value) { throw new Error(`Expected field value "${value}", but got "${actualValue}"`); } } return element; } /** * Wait for element to be visible */ async waitForVisible(selectors, options = {}) { const element = this.locator(selectors, options); await element.waitFor({ state: 'visible', timeout: options.timeout || this.timeout }); return element; } /** * Wait for element to be hidden */ async waitForHidden(selectors, options = {}) { const element = this.locator(selectors, options); await element.waitFor({ state: 'hidden', timeout: options.timeout || this.timeout }); return element; } /** * Wait for WordPress AJAX requests to complete */ async waitForAjax(timeout = 5000) { try { await this.page.waitForFunction(` !window.jQuery || jQuery.active === 0 `, { timeout }); } catch (error) { // AJAX wait timed out, which is acceptable console.warn('AJAX wait timed out, continuing...'); } } /** * Check if element exists and is visible */ async isVisible(selectors, options = {}) { try { const element = this.locator(selectors, options); return await element.isVisible(); } catch (error) { return false; } } /** * Get text content from element */ async getText(selectors, options = {}) { const element = this.locator(selectors, options); await element.waitFor({ state: 'visible', timeout: options.timeout || 5000 }); return await element.textContent(); } /** * Get attribute value from element */ async getAttribute(selectors, attributeName, options = {}) { const element = this.locator(selectors, options); await element.waitFor({ state: 'attached', timeout: options.timeout || 5000 }); return await element.getAttribute(attributeName); } /** * Check WordPress nonce exists on page */ async verifyNonce(nonceName = '_wpnonce') { const nonceExists = await this.page.locator(`input[name="${nonceName}"]`).count() > 0; if (!nonceExists) { throw new Error(`WordPress nonce '${nonceName}' not found on page`); } return true; } /** * Wait for WordPress admin notice */ async waitForNotice(type = 'success', timeout = 5000) { const noticeSelector = `.notice-${type}, .updated, .notice.notice-${type}`; await this.page.waitForSelector(noticeSelector, { timeout }); return await this.page.locator(noticeSelector).textContent(); } /** * Dismiss WordPress admin notices */ async dismissNotices() { const dismissButtons = this.page.locator('.notice-dismiss, .dismiss'); const count = await dismissButtons.count(); for (let i = 0; i < count; i++) { try { await dismissButtons.nth(i).click(); await this.page.waitForTimeout(200); // Small delay between dismissals } catch (error) { // Notice already dismissed or not interactive } } console.log(`📄 Dismissed ${count} admin notices`); } /** * Handle WordPress modal dialogs */ async handleModal(action = 'accept', options = {}) { this.page.on('dialog', async dialog => { console.log(`🗨️ Dialog: ${dialog.message()}`); if (action === 'accept') { await dialog.accept(options.promptText); } else { await dialog.dismiss(); } }); } /** * Take screenshot with context */ async takeScreenshot(name, options = {}) { const screenshotPath = `${this.config.get('media.screenshotDir')}/${name}-${Date.now()}.png`; await this.page.screenshot({ path: screenshotPath, fullPage: options.fullPage || false, quality: options.quality || this.config.get('media.quality'), ...options }); console.log(`📸 Screenshot saved: ${screenshotPath}`); return screenshotPath; } /** * Scroll to element */ async scrollTo(selectors, options = {}) { const element = this.locator(selectors, options); await element.scrollIntoViewIfNeeded(); // Additional scroll offset if needed if (options.offset) { await this.page.evaluate((offset) => { window.scrollBy(0, offset); }, options.offset); } } /** * Wait for page URL to match pattern */ async waitForUrl(pattern, options = {}) { await this.page.waitForURL(pattern, { timeout: options.timeout || this.timeout, ...options }); } /** * Verify page title */ async verifyTitle(expectedTitle) { const actualTitle = await this.page.title(); expect(actualTitle).toContain(expectedTitle); } /** * Verify breadcrumb navigation */ async verifyBreadcrumbs(expectedBreadcrumbs) { const breadcrumbs = await this.page.locator('.hvac-breadcrumb a, .breadcrumb a').allTextContents(); for (const expectedCrumb of expectedBreadcrumbs) { expect(breadcrumbs).toContain(expectedCrumb); } } /** * Check if user is authenticated (WordPress-specific) */ async isAuthenticated() { const indicators = [ '#wpadminbar', // WordPress admin bar '.logged-in', // Body class for logged-in users '.hvac-trainer-nav', // HVAC trainer navigation '.hvac-master-nav' // HVAC master trainer navigation ]; for (const indicator of indicators) { if (await this.isVisible(indicator)) { return true; } } return false; } /** * Get current user role from page context */ async getCurrentUserRole() { return await this.page.evaluate(() => { // Check body classes const bodyClasses = document.body.className; if (bodyClasses.includes('role-hvac_master_trainer')) { return 'masterTrainer'; } else if (bodyClasses.includes('role-hvac_trainer')) { return 'trainer'; } else if (bodyClasses.includes('role-administrator')) { return 'admin'; } // Check navigation elements if (document.querySelector('.hvac-master-nav')) { return 'masterTrainer'; } else if (document.querySelector('.hvac-trainer-nav')) { return 'trainer'; } // Check URL patterns const path = window.location.pathname; if (path.includes('/master-trainer/')) { return 'masterTrainer'; } else if (path.includes('/trainer/')) { return 'trainer'; } else if (path.includes('/wp-admin/')) { return 'admin'; } return 'unknown'; }); } /** * Verify user has expected role */ async verifyUserRole(expectedRole) { const currentRole = await this.getCurrentUserRole(); expect(currentRole).toBe(expectedRole); } /** * Generic form submission with WordPress nonce handling */ async submitForm(formSelector, options = {}) { const form = this.locator(formSelector); await form.waitFor({ state: 'visible' }); // Verify nonce if required if (options.verifyNonce !== false) { try { await this.verifyNonce(options.nonceName); } catch (error) { console.warn('Nonce verification failed:', error.message); } } // Submit form if (options.submitButton) { await this.click(options.submitButton, { waitForNavigation: true }); } else { await form.press('Enter'); } // Wait for response await this.waitForAjax(); if (options.waitForNotice) { try { const noticeText = await this.waitForNotice(options.noticeType || 'success'); console.log(`✅ Form submitted successfully: ${noticeText}`); } catch (error) { console.warn('No success notice found after form submission'); } } } } module.exports = BasePage;