upskill-event-manager/tests/framework/page-objects/BasePage.js
Ben 7c9ca65cf2
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
feat: add comprehensive test framework and test files
- 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>
2025-08-29 23:23:26 -03:00

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;