Some checks are pending
		
		
	
	HVAC Plugin CI/CD Pipeline / Security Analysis (push) Waiting to run
				
			HVAC Plugin CI/CD Pipeline / Code Quality & Standards (push) Waiting to run
				
			HVAC Plugin CI/CD Pipeline / Unit Tests (push) Waiting to run
				
			HVAC Plugin CI/CD Pipeline / Integration Tests (push) Waiting to run
				
			HVAC Plugin CI/CD Pipeline / Deploy to Staging (push) Blocked by required conditions
				
			HVAC Plugin CI/CD Pipeline / Deploy to Production (push) Blocked by required conditions
				
			HVAC Plugin CI/CD Pipeline / Notification (push) Blocked by required conditions
				
			Security Monitoring & Compliance / Dependency Vulnerability Scan (push) Waiting to run
				
			Security Monitoring & Compliance / Secrets & Credential Scan (push) Waiting to run
				
			Security Monitoring & Compliance / WordPress Security Analysis (push) Waiting to run
				
			Security Monitoring & Compliance / Static Code Security Analysis (push) Waiting to run
				
			Security Monitoring & Compliance / Security Compliance Validation (push) Waiting to run
				
			Security Monitoring & Compliance / Security Summary Report (push) Blocked by required conditions
				
			Security Monitoring & Compliance / Security Team Notification (push) Blocked by required conditions
				
			- Add 90+ test files including E2E, unit, and integration tests - Implement Page Object Model (POM) architecture - Add Docker testing environment with comprehensive services - Include modernized test framework with error recovery - Add specialized test suites for master trainer and trainer workflows - Update .gitignore to properly track test infrastructure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			563 lines
		
	
	
		
			No EOL
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			563 lines
		
	
	
		
			No EOL
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * 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; |