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>
446 lines
No EOL
14 KiB
JavaScript
446 lines
No EOL
14 KiB
JavaScript
/**
|
|
* 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; |