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; |