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>
505 lines
No EOL
17 KiB
JavaScript
505 lines
No EOL
17 KiB
JavaScript
/**
|
|
* Public Pages Object Models
|
|
*
|
|
* Page objects for public-facing pages including authentication flows
|
|
* and public access pages for comprehensive E2E testing.
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @version 2.0.0
|
|
* @created 2025-08-27
|
|
*/
|
|
|
|
const BasePage = require('../../framework/base/BasePage');
|
|
|
|
/**
|
|
* Training Login Page Object
|
|
*/
|
|
class TrainingLoginPage extends BasePage {
|
|
constructor() {
|
|
super();
|
|
this.url = '/training-login/';
|
|
this.selectors = {
|
|
loginForm: 'form#loginform, form.login-form, form[name="loginform"]',
|
|
usernameField: '#user_login, input[name="log"], input[name="username"]',
|
|
passwordField: '#user_pass, input[name="pwd"], input[name="password"]',
|
|
submitButton: '#wp-submit, input[type="submit"], button[type="submit"]',
|
|
errorMessage: '.login_error, .error, .notice-error, .warning',
|
|
forgotPasswordLink: 'a[href*="lostpassword"], a[href*="forgot"], a:has-text("Forgot")',
|
|
rememberMe: '#rememberme, input[name="rememberme"]'
|
|
};
|
|
}
|
|
|
|
async waitForPageReady() {
|
|
await this.waitForElement(this.selectors.loginForm, 30000);
|
|
}
|
|
|
|
async login(username, password) {
|
|
await this.fillField(this.selectors.usernameField, username);
|
|
await this.fillField(this.selectors.passwordField, password);
|
|
await this.clickElement(this.selectors.submitButton);
|
|
|
|
// Wait for redirect or error
|
|
await this.page.waitForTimeout(3000);
|
|
}
|
|
|
|
async getErrorMessage() {
|
|
try {
|
|
return await this.getElementText(this.selectors.errorMessage);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async clickForgotPassword() {
|
|
await this.clickElement(this.selectors.forgotPasswordLink);
|
|
}
|
|
|
|
async hasRememberMeOption() {
|
|
return await this.hasElement(this.selectors.rememberMe);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Trainer Registration Page Object
|
|
*/
|
|
class TrainerRegistrationPage extends BasePage {
|
|
constructor() {
|
|
super();
|
|
this.url = '/trainer-registration/';
|
|
this.selectors = {
|
|
registrationForm: 'form.registration-form, form#registerform, form[name="registerform"]',
|
|
usernameField: '#user_login, input[name="user_login"], input[name="username"]',
|
|
emailField: '#user_email, input[name="user_email"], input[name="email"]',
|
|
passwordField: '#user_pass, input[name="user_pass"], input[name="password"]',
|
|
confirmPasswordField: '#user_pass_confirm, input[name="user_pass_confirm"]',
|
|
firstNameField: 'input[name="first_name"], input[name="user_firstname"]',
|
|
lastNameField: 'input[name="last_name"], input[name="user_lastname"]',
|
|
phoneField: 'input[name="phone"], input[name="user_phone"]',
|
|
companyField: 'input[name="company"], input[name="user_company"]',
|
|
submitButton: 'input[type="submit"], button[type="submit"]',
|
|
errorMessages: '.error, .notice-error, .registration-errors',
|
|
successMessage: '.success, .notice-success, .registration-success',
|
|
requiredFieldIndicators: '.required, .asterisk, span[title*="required"]'
|
|
};
|
|
}
|
|
|
|
async waitForPageReady() {
|
|
await this.waitForElement(this.selectors.registrationForm, 15000);
|
|
}
|
|
|
|
async fillRegistrationForm(userData) {
|
|
if (await this.hasElement(this.selectors.usernameField)) {
|
|
await this.fillField(this.selectors.usernameField, userData.username);
|
|
}
|
|
|
|
if (await this.hasElement(this.selectors.emailField)) {
|
|
await this.fillField(this.selectors.emailField, userData.email);
|
|
}
|
|
|
|
if (await this.hasElement(this.selectors.passwordField)) {
|
|
await this.fillField(this.selectors.passwordField, userData.password);
|
|
}
|
|
|
|
if (await this.hasElement(this.selectors.confirmPasswordField)) {
|
|
await this.fillField(this.selectors.confirmPasswordField, userData.password);
|
|
}
|
|
|
|
if (await this.hasElement(this.selectors.firstNameField)) {
|
|
await this.fillField(this.selectors.firstNameField, userData.firstName);
|
|
}
|
|
|
|
if (await this.hasElement(this.selectors.lastNameField)) {
|
|
await this.fillField(this.selectors.lastNameField, userData.lastName);
|
|
}
|
|
|
|
if (await this.hasElement(this.selectors.phoneField)) {
|
|
await this.fillField(this.selectors.phoneField, userData.phone || '555-0123');
|
|
}
|
|
|
|
if (await this.hasElement(this.selectors.companyField)) {
|
|
await this.fillField(this.selectors.companyField, userData.company || 'Test Company');
|
|
}
|
|
}
|
|
|
|
async submitRegistration() {
|
|
await this.clickElement(this.selectors.submitButton);
|
|
await this.page.waitForTimeout(5000); // Wait for submission processing
|
|
}
|
|
|
|
async getRegistrationErrors() {
|
|
try {
|
|
const errorElements = await this.page.locator(this.selectors.errorMessages).all();
|
|
const errors = [];
|
|
for (const element of errorElements) {
|
|
const text = await element.textContent();
|
|
if (text && text.trim()) {
|
|
errors.push(text.trim());
|
|
}
|
|
}
|
|
return errors;
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async hasSuccessMessage() {
|
|
return await this.hasElement(this.selectors.successMessage);
|
|
}
|
|
|
|
async getRequiredFields() {
|
|
const requiredFields = [];
|
|
const indicators = await this.page.locator(this.selectors.requiredFieldIndicators).all();
|
|
|
|
for (const indicator of indicators) {
|
|
const parentForm = await indicator.locator('..').first();
|
|
const inputField = await parentForm.locator('input').first();
|
|
const fieldName = await inputField.getAttribute('name');
|
|
if (fieldName) {
|
|
requiredFields.push(fieldName);
|
|
}
|
|
}
|
|
|
|
return requiredFields;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registration Pending Page Object
|
|
*/
|
|
class RegistrationPendingPage extends BasePage {
|
|
constructor() {
|
|
super();
|
|
this.url = '/registration-pending/';
|
|
this.selectors = {
|
|
pendingMessage: '.pending-message, .approval-notice, .registration-pending',
|
|
statusIndicator: '.status, [data-status="pending"]',
|
|
contactInfo: '.contact-info, .admin-contact',
|
|
nextStepsInfo: '.next-steps, .what-happens-next',
|
|
timeframeInfo: '.timeframe, .approval-time'
|
|
};
|
|
}
|
|
|
|
async getPendingMessage() {
|
|
try {
|
|
return await this.getElementText(this.selectors.pendingMessage);
|
|
} catch (error) {
|
|
return await this.getElementText('body'); // Fallback to body content
|
|
}
|
|
}
|
|
|
|
async hasContactInformation() {
|
|
return await this.hasElement(this.selectors.contactInfo);
|
|
}
|
|
|
|
async hasNextStepsInformation() {
|
|
return await this.hasElement(this.selectors.nextStepsInfo);
|
|
}
|
|
|
|
async getApprovalTimeframe() {
|
|
try {
|
|
return await this.getElementText(this.selectors.timeframeInfo);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Account Pending Page Object
|
|
*/
|
|
class AccountPendingPage extends BasePage {
|
|
constructor() {
|
|
super();
|
|
this.url = '/trainer-account-pending/';
|
|
this.selectors = {
|
|
pendingMessage: '.account-pending, .approval-pending',
|
|
statusDisplay: '.account-status, [data-account-status]',
|
|
adminContact: '.admin-contact, .contact-administrator',
|
|
submittedDate: '.submission-date, .request-date',
|
|
accountDetails: '.account-details, .pending-account-info'
|
|
};
|
|
}
|
|
|
|
async getAccountStatus() {
|
|
try {
|
|
return await this.getElementText(this.selectors.statusDisplay);
|
|
} catch (error) {
|
|
return 'pending'; // Default status
|
|
}
|
|
}
|
|
|
|
async getSubmissionDate() {
|
|
try {
|
|
return await this.getElementText(this.selectors.submittedDate);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async hasAdminContactInfo() {
|
|
return await this.hasElement(this.selectors.adminContact);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Account Disabled Page Object
|
|
*/
|
|
class AccountDisabledPage extends BasePage {
|
|
constructor() {
|
|
super();
|
|
this.url = '/trainer-account-disabled/';
|
|
this.selectors = {
|
|
disabledMessage: '.account-disabled, .account-suspended, .disabled-notice',
|
|
reasonDisplay: '.disable-reason, .suspension-reason',
|
|
reactivationInfo: '.reactivation-info, .restore-account',
|
|
contactInfo: '.contact-admin, .appeal-info',
|
|
disabledDate: '.disabled-date, .suspension-date'
|
|
};
|
|
}
|
|
|
|
async getDisabledReason() {
|
|
try {
|
|
return await this.getElementText(this.selectors.reasonDisplay);
|
|
} catch (error) {
|
|
return 'Account has been disabled';
|
|
}
|
|
}
|
|
|
|
async hasReactivationInstructions() {
|
|
return await this.hasElement(this.selectors.reactivationInfo);
|
|
}
|
|
|
|
async getDisabledDate() {
|
|
try {
|
|
return await this.getElementText(this.selectors.disabledDate);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async hasAppealProcess() {
|
|
return await this.hasElement(this.selectors.contactInfo);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find Trainer Page Object (Public Directory)
|
|
*/
|
|
class FindTrainerPage extends BasePage {
|
|
constructor() {
|
|
super();
|
|
this.url = '/find-trainer/';
|
|
this.selectors = {
|
|
searchForm: '.trainer-search, .directory-search, form[role="search"]',
|
|
searchField: 'input[type="search"], input[name="search"], .search-input',
|
|
searchButton: 'button[type="submit"], input[type="submit"], .search-submit',
|
|
trainerCards: '.trainer-card, .trainer-item, .trainer-listing, article.trainer',
|
|
trainerName: '.trainer-name, .trainer-title, h3, h2',
|
|
trainerLocation: '.trainer-location, .location',
|
|
trainerSpecialty: '.trainer-specialty, .specialties',
|
|
contactButton: '.contact-trainer, .trainer-contact',
|
|
filterOptions: '.filter-options, .directory-filters',
|
|
locationFilter: 'select[name="location"], input[name="location"]',
|
|
specialtyFilter: 'select[name="specialty"], input[name="specialty"]',
|
|
sortOptions: '.sort-options, select[name="sort"]',
|
|
resultsCount: '.results-count, .trainer-count',
|
|
noResultsMessage: '.no-results, .no-trainers-found',
|
|
paginationLinks: '.pagination, .page-numbers'
|
|
};
|
|
}
|
|
|
|
async waitForPageReady() {
|
|
await this.waitForElement('body', 10000);
|
|
// Wait for dynamic content to load
|
|
await this.page.waitForTimeout(2000);
|
|
}
|
|
|
|
async searchTrainers(searchTerm) {
|
|
if (await this.hasElement(this.selectors.searchField)) {
|
|
await this.fillField(this.selectors.searchField, searchTerm);
|
|
|
|
if (await this.hasElement(this.selectors.searchButton)) {
|
|
await this.clickElement(this.selectors.searchButton);
|
|
} else {
|
|
await this.page.press(this.selectors.searchField, 'Enter');
|
|
}
|
|
|
|
// Wait for search results
|
|
await this.page.waitForTimeout(3000);
|
|
}
|
|
}
|
|
|
|
async getTrainerCount() {
|
|
return await this.page.locator(this.selectors.trainerCards).count();
|
|
}
|
|
|
|
async getTrainerDetails(index = 0) {
|
|
const trainerCard = this.page.locator(this.selectors.trainerCards).nth(index);
|
|
|
|
if (!(await trainerCard.isVisible())) {
|
|
return null;
|
|
}
|
|
|
|
const details = {};
|
|
|
|
try {
|
|
details.name = await trainerCard.locator(this.selectors.trainerName).textContent();
|
|
} catch (error) {
|
|
details.name = 'Unknown';
|
|
}
|
|
|
|
try {
|
|
details.location = await trainerCard.locator(this.selectors.trainerLocation).textContent();
|
|
} catch (error) {
|
|
details.location = '';
|
|
}
|
|
|
|
try {
|
|
details.specialty = await trainerCard.locator(this.selectors.trainerSpecialty).textContent();
|
|
} catch (error) {
|
|
details.specialty = '';
|
|
}
|
|
|
|
return details;
|
|
}
|
|
|
|
async filterByLocation(location) {
|
|
if (await this.hasElement(this.selectors.locationFilter)) {
|
|
await this.selectOption(this.selectors.locationFilter, location);
|
|
await this.page.waitForTimeout(2000);
|
|
}
|
|
}
|
|
|
|
async filterBySpecialty(specialty) {
|
|
if (await this.hasElement(this.selectors.specialtyFilter)) {
|
|
await this.selectOption(this.selectors.specialtyFilter, specialty);
|
|
await this.page.waitForTimeout(2000);
|
|
}
|
|
}
|
|
|
|
async sortResults(sortOption) {
|
|
if (await this.hasElement(this.selectors.sortOptions)) {
|
|
await this.selectOption(this.selectors.sortOptions, sortOption);
|
|
await this.page.waitForTimeout(2000);
|
|
}
|
|
}
|
|
|
|
async hasNoResults() {
|
|
return await this.hasElement(this.selectors.noResultsMessage);
|
|
}
|
|
|
|
async getResultsCount() {
|
|
try {
|
|
const countText = await this.getElementText(this.selectors.resultsCount);
|
|
const match = countText.match(/\d+/);
|
|
return match ? parseInt(match[0]) : 0;
|
|
} catch (error) {
|
|
return await this.getTrainerCount();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Documentation Page Object
|
|
*/
|
|
class DocumentationPage extends BasePage {
|
|
constructor() {
|
|
super();
|
|
this.url = '/documentation/';
|
|
this.selectors = {
|
|
docNavigation: '.doc-nav, .documentation-nav, .help-nav, nav',
|
|
searchField: '.doc-search, .help-search, input[type="search"]',
|
|
searchButton: '.search-submit, button[type="submit"]',
|
|
articleLinks: '.doc-link, .help-link, .article-link, a[href*="help"], a[href*="doc"]',
|
|
articleTitle: '.doc-title, .help-title, .article-title, h1, h2',
|
|
articleContent: '.doc-content, .help-content, .article-content, .content',
|
|
breadcrumbs: '.breadcrumbs, .breadcrumb',
|
|
categoryLinks: '.category-link, .doc-category',
|
|
tableOfContents: '.toc, .table-of-contents',
|
|
relatedArticles: '.related-articles, .see-also',
|
|
backToTop: '.back-to-top, a[href="#top"]',
|
|
printButton: '.print-doc, .print-page',
|
|
lastUpdated: '.last-updated, .updated-date'
|
|
};
|
|
}
|
|
|
|
async waitForPageReady() {
|
|
await this.waitForElement('body', 10000);
|
|
await this.page.waitForTimeout(1000);
|
|
}
|
|
|
|
async searchDocumentation(searchTerm) {
|
|
if (await this.hasElement(this.selectors.searchField)) {
|
|
await this.fillField(this.selectors.searchField, searchTerm);
|
|
|
|
if (await this.hasElement(this.selectors.searchButton)) {
|
|
await this.clickElement(this.selectors.searchButton);
|
|
} else {
|
|
await this.page.press(this.selectors.searchField, 'Enter');
|
|
}
|
|
|
|
await this.page.waitForTimeout(3000);
|
|
}
|
|
}
|
|
|
|
async getArticleLinks() {
|
|
const links = await this.page.locator(this.selectors.articleLinks).all();
|
|
const linkData = [];
|
|
|
|
for (const link of links) {
|
|
const text = await link.textContent();
|
|
const href = await link.getAttribute('href');
|
|
if (text && href) {
|
|
linkData.push({ text: text.trim(), href });
|
|
}
|
|
}
|
|
|
|
return linkData;
|
|
}
|
|
|
|
async navigateToArticle(linkText) {
|
|
const link = this.page.locator(this.selectors.articleLinks).filter({ hasText: linkText });
|
|
if (await link.isVisible()) {
|
|
await link.click();
|
|
await this.page.waitForLoadState('networkidle');
|
|
}
|
|
}
|
|
|
|
async hasTableOfContents() {
|
|
return await this.hasElement(this.selectors.tableOfContents);
|
|
}
|
|
|
|
async getArticleTitle() {
|
|
try {
|
|
return await this.getElementText(this.selectors.articleTitle);
|
|
} catch (error) {
|
|
return await this.getPageTitle();
|
|
}
|
|
}
|
|
|
|
async hasSearchFunctionality() {
|
|
return await this.hasElement(this.selectors.searchField);
|
|
}
|
|
|
|
async getLastUpdatedDate() {
|
|
try {
|
|
return await this.getElementText(this.selectors.lastUpdated);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async hasRelatedArticles() {
|
|
return await this.hasElement(this.selectors.relatedArticles);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
TrainingLoginPage,
|
|
TrainerRegistrationPage,
|
|
RegistrationPendingPage,
|
|
AccountPendingPage,
|
|
AccountDisabledPage,
|
|
FindTrainerPage,
|
|
DocumentationPage
|
|
}; |