test: Optimize E2E testing infrastructure for certificates

- Add CertificatePage for certificate operations
- Create BasePage for common page object functionality
- Implement CertificateTestData for test data generation
- Create optimized certificate tests with improved stability
- Add test-certificate-filter.sh script for testing certificate filtering
- Improve test organization and reliability
This commit is contained in:
bengizmo 2025-05-21 09:53:36 -03:00
parent 8dde809062
commit 353d951ba7
5 changed files with 861 additions and 430 deletions

View file

@ -0,0 +1,172 @@
#!/bin/bash
# Certificate filtering test script
# This script runs certificate filtering tests with various filter combinations
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Change to the project root directory
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
cd "$SCRIPT_DIR/.." || exit 1
echo "Changed working directory to: $(pwd)"
echo -e "${BLUE}============================================${NC}"
echo -e "${BLUE} Certificate Filtering Test Runner ${NC}"
echo -e "${BLUE}============================================${NC}"
# Test data for filtering
TEST_EVENTS=(
"HVAC System Design Fundamentals"
"Advanced Refrigeration Technology"
"Building Automation Systems Workshop"
)
TEST_ATTENDEES=(
"John Smith"
"Sarah Johnson"
"example.com"
)
# Function to run a test with specific filter
run_certificate_test() {
local test_type=$1
local filter_value=$2
local additional_args=$3
echo -e "\n${YELLOW}Running certificate test with ${test_type} filter: ${filter_value}${NC}"
# Construct test command with proper escaping
local command="FILTER_TYPE=\"${test_type}\" FILTER_VALUE=\"${filter_value}\" npx playwright test tests/e2e/certificates.test.ts --config=tests/e2e/optimized-playwright.config.ts ${additional_args}"
echo -e "${YELLOW}$ ${command}${NC}"
# Run the test command
eval "$command"
# Check if test passed
local exit_code=$?
if [ $exit_code -eq 0 ]; then
echo -e "${GREEN}✓ Test passed${NC}"
else
echo -e "${RED}✗ Test failed with exit code ${exit_code}${NC}"
fi
echo -e "${YELLOW}-----------------------------------------------${NC}"
}
# Function to print test summary
print_summary() {
echo -e "\n${BLUE}============================================${NC}"
echo -e "${BLUE} Certificate Filtering Test Summary ${NC}"
echo -e "${BLUE}============================================${NC}"
echo -e "${YELLOW}Filter tests run:${NC} $1"
echo -e "${GREEN}Tests passed:${NC} $2"
echo -e "${RED}Tests failed:${NC} $3"
echo -e "${BLUE}============================================${NC}"
}
# Check if the certificates test exists
if [ ! -f "tests/e2e/certificates.test.ts" ]; then
echo -e "${RED}Error: certificates.test.ts not found!${NC}"
echo -e "Make sure the certificate test file exists at tests/e2e/certificates.test.ts"
exit 1
fi
# Check if optimized config exists
if [ ! -f "tests/e2e/optimized-playwright.config.ts" ]; then
echo -e "${RED}Error: optimized-playwright.config.ts not found!${NC}"
echo -e "Make sure the optimized configuration file exists at tests/e2e/optimized-playwright.config.ts"
exit 1
fi
# Ask if we should run all tests or just a specific one
echo -e "\n${YELLOW}Select test mode:${NC}"
echo "1) Run all certificate filter tests"
echo "2) Run event filtering tests only"
echo "3) Run attendee filtering tests only"
echo "4) Run custom filter test"
read -p "Enter your choice (1-4): " TEST_MODE
# Track test statistics
TOTAL_TESTS=0
PASSED_TESTS=0
FAILED_TESTS=0
case $TEST_MODE in
1)
# Run all test combinations
echo -e "\n${BLUE}Running all certificate filter tests...${NC}"
# Event filter tests
for event in "${TEST_EVENTS[@]}"; do
run_certificate_test "event" "$event" "--headed"
TOTAL_TESTS=$((TOTAL_TESTS+1))
[ $? -eq 0 ] && PASSED_TESTS=$((PASSED_TESTS+1)) || FAILED_TESTS=$((FAILED_TESTS+1))
done
# Attendee filter tests
for attendee in "${TEST_ATTENDEES[@]}"; do
run_certificate_test "attendee" "$attendee" "--headed"
TOTAL_TESTS=$((TOTAL_TESTS+1))
[ $? -eq 0 ] && PASSED_TESTS=$((PASSED_TESTS+1)) || FAILED_TESTS=$((FAILED_TESTS+1))
done
# Combined filter test (first event + first attendee)
run_certificate_test "combined" "${TEST_EVENTS[0]}|${TEST_ATTENDEES[0]}" "--headed"
TOTAL_TESTS=$((TOTAL_TESTS+1))
[ $? -eq 0 ] && PASSED_TESTS=$((PASSED_TESTS+1)) || FAILED_TESTS=$((FAILED_TESTS+1))
;;
2)
# Run event filter tests only
echo -e "\n${BLUE}Running event filter tests...${NC}"
for event in "${TEST_EVENTS[@]}"; do
run_certificate_test "event" "$event" "--headed"
TOTAL_TESTS=$((TOTAL_TESTS+1))
[ $? -eq 0 ] && PASSED_TESTS=$((PASSED_TESTS+1)) || FAILED_TESTS=$((FAILED_TESTS+1))
done
;;
3)
# Run attendee filter tests only
echo -e "\n${BLUE}Running attendee filter tests...${NC}"
for attendee in "${TEST_ATTENDEES[@]}"; do
run_certificate_test "attendee" "$attendee" "--headed"
TOTAL_TESTS=$((TOTAL_TESTS+1))
[ $? -eq 0 ] && PASSED_TESTS=$((PASSED_TESTS+1)) || FAILED_TESTS=$((FAILED_TESTS+1))
done
;;
4)
# Run custom filter test
echo -e "\n${BLUE}Running custom filter test...${NC}"
read -p "Enter filter type (event, attendee, combined): " CUSTOM_TYPE
read -p "Enter filter value: " CUSTOM_VALUE
read -p "Run with UI? (y/n): " WITH_UI
UI_FLAG=""
[ "$WITH_UI" = "y" ] && UI_FLAG="--headed"
run_certificate_test "$CUSTOM_TYPE" "$CUSTOM_VALUE" "$UI_FLAG"
TOTAL_TESTS=$((TOTAL_TESTS+1))
[ $? -eq 0 ] && PASSED_TESTS=$((PASSED_TESTS+1)) || FAILED_TESTS=$((FAILED_TESTS+1))
;;
*)
echo -e "${RED}Invalid option selected.${NC}"
exit 1
;;
esac
# Print test summary
print_summary $TOTAL_TESTS $PASSED_TESTS $FAILED_TESTS
exit 0

View file

@ -0,0 +1,223 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { CertificatePage } from './pages/CertificatePage';
/**
* Certificate Tests
*
* These tests verify the certificate generation, reporting, and filtering functionality
*
* @group @certificate
*/
const STAGING_URL = 'https://wordpress-974670-5399585.cloudwaysapps.com';
// Test data for existing certificates to verify
const TEST_EVENTS = [
{ id: '5641', name: 'HVAC System Design Fundamentals' },
{ id: '5668', name: 'Advanced Refrigeration Technology' },
{ id: '5688', name: 'Building Automation Systems Workshop' }
];
// Test data for attendee filtering
const TEST_ATTENDEES = [
{ name: 'John Smith', email: 'john.smith@example.com' },
{ name: 'Sarah Johnson', email: 'sarah.johnson@example.com' },
{ name: 'Michael Brown', email: 'michael.brown@example.com' }
];
// Common setup for tests
async function setupTest(page) {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('test_trainer', 'Test123!');
// Verify login
const dashboardPage = new DashboardPage(page);
await expect(page).toHaveURL(/hvac-dashboard/);
return { loginPage, dashboardPage };
}
test.describe('Certificate Generation and Reports', () => {
test('Navigate to certificate generation page', async ({ page }) => {
const { dashboardPage } = await setupTest(page);
// Navigate to Generate Certificates page
await dashboardPage.clickGenerateCertificates();
// Verify page components
const certificatePage = new CertificatePage(page);
const pageVisible = await certificatePage.isGenerateCertificatesPageVisible();
expect(pageVisible).toBeTruthy();
// Check for event dropdown
await expect(page.locator('#event_id')).toBeVisible();
});
test('Navigate to certificate reports page', async ({ page }) => {
const { dashboardPage } = await setupTest(page);
// Navigate to Certificate Reports page
await dashboardPage.clickCertificateReports();
// Verify page components
const certificatePage = new CertificatePage(page);
const pageVisible = await certificatePage.isCertificateReportsPageVisible();
expect(pageVisible).toBeTruthy();
// Check for filter form
await expect(page.locator('form.hvac-certificate-filters')).toBeVisible();
});
test('Filter certificates by event', async ({ page }) => {
const { dashboardPage } = await setupTest(page);
// Navigate to Certificate Reports page
await dashboardPage.clickCertificateReports();
const certificatePage = new CertificatePage(page);
// Select an event to filter
const testEvent = TEST_EVENTS[0];
await certificatePage.searchCertificates(testEvent.name);
// Verify results
await page.waitForLoadState('networkidle');
const certificateCount = await certificatePage.getCertificateCount();
console.log(`Found ${certificateCount} certificates for event: ${testEvent.name}`);
// Should have at least some certificates (can be zero if no certificates exist)
expect(certificateCount).toBeGreaterThanOrEqual(0);
});
test('Search certificates by attendee', async ({ page }) => {
const { dashboardPage } = await setupTest(page);
// Navigate to Certificate Reports page
await dashboardPage.clickCertificateReports();
const certificatePage = new CertificatePage(page);
// Search for an attendee
const testAttendee = TEST_ATTENDEES[0];
await certificatePage.searchAttendee(testAttendee.name);
// Verify search results
await page.waitForLoadState('networkidle');
const certificateCount = await certificatePage.getCertificateCount();
console.log(`Found ${certificateCount} certificates for attendee: ${testAttendee.name}`);
// Verify filter was applied (search input should have the attendee name)
const searchValue = await page.inputValue('#search_attendee');
expect(searchValue).toEqual(testAttendee.name);
});
test('Reset certificate filters', async ({ page }) => {
const { dashboardPage } = await setupTest(page);
// Navigate to Certificate Reports page
await dashboardPage.clickCertificateReports();
const certificatePage = new CertificatePage(page);
// Apply a filter first
await certificatePage.searchAttendee('test');
// Get count with filter
const filteredCount = await certificatePage.getCertificateCount();
// Reset filters
await certificatePage.resetFilters();
// Get count after reset
const resetCount = await certificatePage.getCertificateCount();
// The search input should be empty after reset
const searchValue = await page.inputValue('#search_attendee');
expect(searchValue).toEqual('');
console.log(`Filtered certificates: ${filteredCount}, After reset: ${resetCount}`);
// We can't guarantee counts will differ, but the reset should work
});
test('View certificate details', async ({ page }) => {
const { dashboardPage } = await setupTest(page);
// Navigate to Certificate Reports page
await dashboardPage.clickCertificateReports();
const certificatePage = new CertificatePage(page);
// Get initial certificate count
const certificateCount = await certificatePage.getCertificateCount();
console.log(`Found ${certificateCount} certificates`);
// Skip test if no certificates
test.skip(certificateCount === 0, 'No certificates available to view');
if (certificateCount > 0) {
// View the first certificate
await certificatePage.viewCertificate(0);
// Verify preview is visible
await expect(page.locator('.hvac-certificate-preview')).toBeVisible();
// Close the preview
await certificatePage.closePreview();
// Preview should be hidden
await expect(page.locator('.hvac-certificate-preview')).not.toBeVisible();
}
});
});
// Comprehensive test for attendee search functionality
test('Attendee search functionality', async ({ page }) => {
const { dashboardPage } = await setupTest(page);
// Navigate to Certificate Reports page
await dashboardPage.clickCertificateReports();
const certificatePage = new CertificatePage(page);
// Test partial name search
await certificatePage.searchAttendee('John');
await page.waitForLoadState('networkidle');
// Verify search results
const nameSearchCount = await certificatePage.getCertificateCount();
console.log(`Found ${nameSearchCount} certificates for attendee name: John`);
// Reset filters
await certificatePage.resetFilters();
// Test email domain search
await certificatePage.searchAttendee('example.com');
await page.waitForLoadState('networkidle');
// Verify search results
const emailSearchCount = await certificatePage.getCertificateCount();
console.log(`Found ${emailSearchCount} certificates for email domain: example.com`);
// Reset filters
await certificatePage.resetFilters();
// Test combination of filters (event + attendee)
if (TEST_EVENTS.length > 0) {
const testEvent = TEST_EVENTS[0];
// Select an event
await certificatePage.searchCertificates(testEvent.name);
// Then search for an attendee
await certificatePage.searchAttendee('test');
// Verify combined results
const combinedCount = await certificatePage.getCertificateCount();
console.log(`Found ${combinedCount} certificates for event "${testEvent.name}" and attendee "test"`);
// Verify both filters were applied
const searchValue = await page.inputValue('#search_attendee');
expect(searchValue).toEqual('test');
// Reset filters
await certificatePage.resetFilters();
}
});

View file

@ -0,0 +1,92 @@
import { Page } from '@playwright/test';
/**
* Base page object that all page objects inherit from
* Contains common methods and properties used across pages
*/
export class BasePage {
protected readonly page: Page;
protected readonly baseUrl: string = 'https://wordpress-974670-5399585.cloudwaysapps.com';
constructor(page: Page) {
this.page = page;
}
/**
* Log a message to the console
*/
protected log(message: string): void {
console.log(`[${this.constructor.name}] ${message}`);
}
/**
* Wait for page to be fully loaded
*/
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState('domcontentloaded');
await this.page.waitForLoadState('networkidle');
}
/**
* Get the current page title
*/
async getTitle(): Promise<string> {
return await this.page.title();
}
/**
* Get the current page URL
*/
async getUrl(): Promise<string> {
return this.page.url();
}
/**
* Navigate to a specific URL path
* @param path The path to navigate to (relative to baseUrl)
*/
async navigateTo(path: string): Promise<void> {
await this.page.goto(`${this.baseUrl}${path}`);
await this.waitForPageLoad();
}
/**
* Take a screenshot and return the buffer
* @param name Optional name for the screenshot
*/
async takeScreenshot(name?: string): Promise<Buffer> {
const screenshotName = name || `${this.constructor.name}-${new Date().getTime()}`;
return await this.page.screenshot({
path: `./screenshots/${screenshotName}.png`,
fullPage: true
});
}
/**
* Check if an element is visible on the page
* @param selector The selector for the element
*/
async isVisible(selector: string): Promise<boolean> {
return await this.page.isVisible(selector);
}
/**
* Get text content of an element
* @param selector The selector for the element
*/
async getText(selector: string): Promise<string | null> {
return await this.page.textContent(selector);
}
/**
* Click an element and wait for navigation
* @param selector The selector for the element to click
*/
async clickAndWaitForNavigation(selector: string): Promise<void> {
await Promise.all([
this.page.waitForNavigation(),
this.page.click(selector)
]);
await this.waitForPageLoad();
}
}

View file

@ -1,270 +1,195 @@
import { Page, expect } from '@playwright/test';
import { BasePage } from './BasePage';
import { Config } from '../utils/Config';
/**
* Page object representing the Certificate-related pages
* Handles both Certificate Generation and Certificate Reports pages
*/
export class CertificatePage extends BasePage {
// Generate Certificates page selectors
private readonly generateCertificatesTitle = 'h1:has-text("Generate Certificates")';
private readonly eventSelector = 'select[name="event_id"]';
private readonly eventSearchInput = 'input[name="event_search"]';
private readonly selectAllCheckbox = 'input[name="select_all"]';
private readonly attendeeCheckboxes = 'input[name="attendees[]"]';
private readonly generateButton = 'button:has-text("Generate Certificates")';
private readonly previewButton = 'button:has-text("Preview Certificate")';
private readonly successMessage = '.hvac-success-message';
private readonly errorMessage = '.hvac-error-message';
private readonly attendeeList = '.hvac-attendee-list';
private readonly attendeeItem = '.hvac-attendee-item';
private readonly checkinStatusAttribute = 'data-checkin-status';
private readonly loadingIndicator = '.hvac-loading';
// Selectors for Generate Certificates page
private readonly generatePageTitle = 'h1:text("Generate Certificates")';
private readonly eventSelectDropdown = '#event_id';
private readonly selectedEventName = '.hvac-selected-event strong';
private readonly attendeeList = '.hvac-attendee-list';
private readonly attendeeItems = '.hvac-attendee-item';
private readonly checkedInLabel = '.hvac-checked-in';
private readonly checkAllButton = 'button:text("Select All")';
private readonly checkCheckedInButton = 'button:text("Select Checked-In")';
private readonly generateButton = 'button[type="submit"]:text("Generate Certificates")';
private readonly successMessage = '.hvac-success-message';
private readonly errorMessage = '.hvac-error-message';
// Certificate Reports page selectors
private readonly certificateReportsTitle = 'h1:has-text("Certificate Reports")';
private readonly certificateFilterInput = 'input[name="certificate_search"]';
private readonly certificateTable = '.hvac-certificate-table';
private readonly certificateTableRows = '.hvac-certificate-table tbody tr';
private readonly viewCertificateButton = 'button:has-text("View")';
private readonly emailCertificateButton = 'button:has-text("Email")';
private readonly revokeCertificateButton = 'button:has-text("Revoke")';
private readonly certificatePagination = '.hvac-pagination';
private readonly certificateModal = '.hvac-certificate-modal';
private readonly certificatePreview = '.hvac-certificate-preview';
private readonly closeModalButton = '.hvac-modal-close';
private readonly confirmRevocationButton = 'button:has-text("Confirm Revocation")';
private readonly confirmEmailButton = 'button:has-text("Send Email")';
private readonly previousPageButton = '.hvac-pagination-prev';
private readonly nextPageButton = '.hvac-pagination-next';
// Selectors for Certificate Reports page
private readonly reportsPageTitle = 'h1:text("Certificate Reports")';
private readonly filterForm = 'form.hvac-certificate-filters';
private readonly eventFilterSelect = '#filter_event';
private readonly attendeeSearchInput = '#search_attendee';
private readonly revokedFilterSelect = '#filter_revoked';
private readonly filterButton = 'button[type="submit"]:text("Filter")';
private readonly resetButton = 'button[type="reset"]:text("Reset Filters")';
private readonly certificateTable = '.hvac-certificate-table';
private readonly certificateItems = '.hvac-certificate-item';
private readonly viewCertificateLinks = 'a:text("View")';
private readonly certificatePreview = '.hvac-certificate-preview';
private readonly closePreviewButton = '.hvac-preview-close';
constructor(page: Page) {
super(page);
constructor(page: Page) {
super(page);
}
/**
* Navigate to the Generate Certificates page
*/
async navigateToGenerateCertificates(): Promise<void> {
await this.page.goto('/generate-certificates/');
await this.page.waitForLoadState('networkidle');
await this.page.waitForSelector(this.generatePageTitle);
}
/**
* Navigate to the Certificate Reports page
*/
async navigateToCertificateReports(): Promise<void> {
await this.page.goto('/certificate-reports/');
await this.page.waitForLoadState('networkidle');
await this.page.waitForSelector(this.reportsPageTitle);
}
/**
* Check if the Generate Certificates page is visible
*/
async isGenerateCertificatesPageVisible(): Promise<boolean> {
return await this.page.isVisible(this.generatePageTitle);
}
/**
* Check if the Certificate Reports page is visible
*/
async isCertificateReportsPageVisible(): Promise<boolean> {
return await this.page.isVisible(this.reportsPageTitle);
}
/**
* Select an event from the dropdown on Generate Certificates page
*/
async selectEvent(eventName: string): Promise<void> {
await this.page.selectOption(this.eventSelectDropdown, {
label: new RegExp(eventName, 'i')
});
await this.page.waitForSelector(this.attendeeList);
// Verify the selected event
const selectedText = await this.page.textContent(this.selectedEventName);
expect(selectedText).toContain(eventName);
}
/**
* Get total number of attendees listed
*/
async getAttendeeCount(): Promise<number> {
return await this.page.locator(this.attendeeItems).count();
}
/**
* Get number of checked-in attendees
*/
async getCheckedInAttendeeCount(): Promise<number> {
return await this.page.locator(this.checkedInLabel).count();
}
/**
* Select all attendees
*/
async selectAllAttendees(): Promise<void> {
await this.page.click(this.checkAllButton);
}
/**
* Select only checked-in attendees
*/
async selectCheckedInAttendees(): Promise<void> {
await this.page.click(this.checkCheckedInButton);
}
/**
* Generate certificates for selected attendees
*/
async generateCertificates(): Promise<void> {
await this.page.click(this.generateButton);
await this.page.waitForLoadState('networkidle');
// Wait for either success or error message
await this.page.waitForSelector(`${this.successMessage}, ${this.errorMessage}`);
}
/**
* Check if success message is visible
*/
async isSuccessMessageVisible(): Promise<boolean> {
return await this.page.isVisible(this.successMessage);
}
/**
* Get success message text
*/
async getSuccessMessage(): Promise<string | null> {
return await this.page.textContent(this.successMessage);
}
/**
* Filter certificates by event name
*/
async searchCertificates(eventName: string): Promise<void> {
await this.page.selectOption(this.eventFilterSelect, {
label: new RegExp(eventName, 'i')
});
await this.page.click(this.filterButton);
await this.page.waitForLoadState('networkidle');
}
/**
* Filter certificates by attendee name or email
*/
async searchAttendee(searchTerm: string): Promise<void> {
await this.page.fill(this.attendeeSearchInput, searchTerm);
await this.page.click(this.filterButton);
await this.page.waitForLoadState('networkidle');
}
/**
* Reset all filters
*/
async resetFilters(): Promise<void> {
await this.page.click(this.resetButton);
await this.page.waitForLoadState('networkidle');
}
/**
* Get the number of certificates in the table
*/
async getCertificateCount(): Promise<number> {
return await this.page.locator(this.certificateItems).count();
}
/**
* View a certificate by index
*/
async viewCertificate(index: number): Promise<void> {
const viewLinks = this.page.locator(this.viewCertificateLinks);
const count = await viewLinks.count();
if (index >= count) {
throw new Error(`Cannot view certificate at index ${index}. Only ${count} certificates available.`);
}
await viewLinks.nth(index).click();
await this.page.waitForSelector(this.certificatePreview);
}
// Common methods
async navigateToGenerateCertificates(): Promise<void> {
await this.page.goto(Config.generateCertificatesUrl);
await this.page.waitForLoadState('networkidle');
await this.screenshot('generate-certificates-page');
}
async navigateToCertificateReports(): Promise<void> {
await this.page.goto(Config.certificateReportsUrl);
await this.page.waitForLoadState('networkidle');
await this.screenshot('certificate-reports-page');
}
// Generate Certificates page methods
async isGenerateCertificatesPageVisible(): Promise<boolean> {
return await this.isVisible(this.generateCertificatesTitle);
}
async selectEvent(eventName: string): Promise<void> {
// If there's a search input, try using it
if (await this.isVisible(this.eventSearchInput)) {
await this.fill(this.eventSearchInput, eventName);
await this.page.waitForTimeout(Config.shortWait);
}
// Select the event from dropdown
await this.page.selectOption(this.eventSelector, { label: eventName });
await this.page.waitForTimeout(Config.shortWait);
// Wait for loading indicator to disappear if it's present
const loadingElement = this.page.locator(this.loadingIndicator);
if (await loadingElement.isVisible()) {
await loadingElement.waitFor({ state: 'hidden', timeout: Config.defaultTimeout });
}
await this.screenshot('event-selected');
}
async getAttendeeCount(): Promise<number> {
return await this.page.locator(this.attendeeItem).count();
}
async getCheckedInAttendeeCount(): Promise<number> {
let checkedInCount = 0;
const attendees = this.page.locator(this.attendeeItem);
const count = await attendees.count();
for (let i = 0; i < count; i++) {
const status = await attendees.nth(i).getAttribute(this.checkinStatusAttribute);
if (status === 'checked-in') {
checkedInCount++;
}
}
return checkedInCount;
}
async selectAllAttendees(): Promise<void> {
await this.click(this.selectAllCheckbox);
await this.screenshot('all-attendees-selected');
}
async selectCheckedInAttendees(): Promise<void> {
// Deselect "Select All" if it's checked
const selectAllChecked = await this.page.isChecked(this.selectAllCheckbox);
if (selectAllChecked) {
await this.click(this.selectAllCheckbox);
}
// Select only checked-in attendees
const attendees = this.page.locator(this.attendeeItem);
const count = await attendees.count();
for (let i = 0; i < count; i++) {
const status = await attendees.nth(i).getAttribute(this.checkinStatusAttribute);
if (status === 'checked-in') {
const checkbox = attendees.nth(i).locator('input[type="checkbox"]');
await checkbox.check();
}
}
await this.screenshot('checked-in-attendees-selected');
}
async selectNonCheckedInAttendees(): Promise<void> {
// Deselect "Select All" if it's checked
const selectAllChecked = await this.page.isChecked(this.selectAllCheckbox);
if (selectAllChecked) {
await this.click(this.selectAllCheckbox);
}
// Select only non-checked-in attendees
const attendees = this.page.locator(this.attendeeItem);
const count = await attendees.count();
for (let i = 0; i < count; i++) {
const status = await attendees.nth(i).getAttribute(this.checkinStatusAttribute);
if (status !== 'checked-in') {
const checkbox = attendees.nth(i).locator('input[type="checkbox"]');
await checkbox.check();
}
}
await this.screenshot('non-checked-in-attendees-selected');
}
async generateCertificates(): Promise<void> {
await this.click(this.generateButton);
// Wait for loading indicator to disappear if it's present
const loadingElement = this.page.locator(this.loadingIndicator);
if (await loadingElement.isVisible()) {
await loadingElement.waitFor({ state: 'hidden', timeout: Config.longWait });
}
await this.page.waitForTimeout(Config.mediumWait); // Additional wait for any post-processing
await this.screenshot('certificates-generated');
}
async previewCertificate(): Promise<void> {
await this.click(this.previewButton);
// Wait for the preview modal to appear
await this.waitForElement(this.certificateModal);
await this.screenshot('certificate-preview');
}
async closePreview(): Promise<void> {
if (await this.isVisible(this.closeModalButton)) {
await this.click(this.closeModalButton);
await this.page.waitForTimeout(Config.shortWait); // Wait for modal to close
}
}
async isSuccessMessageVisible(): Promise<boolean> {
return await this.isVisible(this.successMessage);
}
async isErrorMessageVisible(): Promise<boolean> {
return await this.isVisible(this.errorMessage);
}
async getSuccessMessage(): Promise<string> {
return await this.getText(this.successMessage);
}
async getErrorMessage(): Promise<string> {
return await this.getText(this.errorMessage);
}
// Certificate Reports page methods
async isCertificateReportsPageVisible(): Promise<boolean> {
return await this.isVisible(this.certificateReportsTitle);
}
async searchCertificates(query: string): Promise<void> {
await this.fill(this.certificateFilterInput, query);
await this.page.waitForTimeout(Config.shortWait); // Wait for search results
// Wait for loading indicator to disappear if it's present
const loadingElement = this.page.locator(this.loadingIndicator);
if (await loadingElement.isVisible()) {
await loadingElement.waitFor({ state: 'hidden', timeout: Config.defaultTimeout });
}
await this.screenshot('certificate-search');
}
async getCertificateCount(): Promise<number> {
return await this.page.locator(this.certificateTableRows).count();
}
async viewCertificate(index: number = 0): Promise<void> {
const viewButtons = this.page.locator(this.viewCertificateButton);
await viewButtons.nth(index).click();
// Wait for the preview modal to appear
await this.waitForElement(this.certificateModal);
await this.screenshot('view-certificate');
}
async emailCertificate(index: number = 0): Promise<void> {
const emailButtons = this.page.locator(this.emailCertificateButton);
await emailButtons.nth(index).click();
// Wait for the email confirmation dialog
if (await this.isVisible(this.confirmEmailButton)) {
await this.click(this.confirmEmailButton);
await this.page.waitForTimeout(Config.mediumWait); // Wait for email to be sent
}
await this.screenshot('email-certificate');
}
async revokeCertificate(index: number = 0): Promise<void> {
const revokeButtons = this.page.locator(this.revokeCertificateButton);
await revokeButtons.nth(index).click();
// Wait for the revocation confirmation dialog
if (await this.isVisible(this.confirmRevocationButton)) {
await this.click(this.confirmRevocationButton);
await this.page.waitForTimeout(Config.mediumWait); // Wait for revocation to complete
}
await this.screenshot('revoke-certificate');
}
async isPaginationVisible(): Promise<boolean> {
return await this.isVisible(this.certificatePagination);
}
async goToNextPage(): Promise<boolean> {
if (await this.isVisible(this.nextPageButton)) {
await this.click(this.nextPageButton);
await this.page.waitForTimeout(Config.shortWait);
return true;
}
return false;
}
async goToPreviousPage(): Promise<boolean> {
if (await this.isVisible(this.previousPageButton)) {
await this.click(this.previousPageButton);
await this.page.waitForTimeout(Config.shortWait);
return true;
}
return false;
}
/**
* Close the certificate preview
*/
async closePreview(): Promise<void> {
await this.page.click(this.closePreviewButton);
await this.page.waitForSelector(this.certificatePreview, { state: 'hidden' });
}
}

View file

@ -1,181 +1,200 @@
import { Page } from '@playwright/test';
import { VerbosityController } from './VerbosityController';
import { Config } from './Config';
/**
* Utility class to set up test data for certificate testing
* This helps ensure we have events with both checked-in and non-checked-in attendees
* Utility class for creating test data for certificate tests
*
* This class handles:
* 1. Setting up test events with attendees
* 2. Checking in some attendees
* 3. Cleaning up test data after tests
*/
export class CertificateTestData {
private page: Page;
private verbosity: VerbosityController;
private page: Page;
private readonly baseUrl = 'https://wordpress-974670-5399585.cloudwaysapps.com';
private readonly loginUrl = '/community-login/';
private readonly dashboardUrl = '/hvac-dashboard/';
private readonly adminUrl = '/wp-admin/';
private readonly username = 'test_trainer';
private readonly password = 'Test123!';
private testEventId: string | null = null;
private testEventName: string | null = null;
constructor(page: Page) {
this.page = page;
}
/**
* Login as a trainer user for test setup
*/
async loginAsTrainer(): Promise<void> {
await this.page.goto(`${this.baseUrl}${this.loginUrl}`);
await this.page.fill('#user_login', this.username);
await this.page.fill('#user_pass', this.password);
await this.page.click('#wp-submit');
await this.page.waitForLoadState('networkidle');
constructor(page: Page) {
this.page = page;
this.verbosity = VerbosityController.getInstance();
// Verify login was successful
const url = this.page.url();
if (!url.includes(this.dashboardUrl)) {
throw new Error(`Login failed. Expected URL to contain ${this.dashboardUrl}, but got ${url}`);
}
}
/**
* Set up a test event with attendees for certificate testing
* @returns The name of the created test event
*/
async setupCertificateTestEvent(): Promise<string | null> {
// Generate a unique event name with timestamp
const timestamp = new Date().getTime();
this.testEventName = `Certificate Test Event ${timestamp}`;
// Navigate to create event page
await this.page.goto(`${this.baseUrl}/community-events/`);
await this.page.waitForLoadState('networkidle');
// Populate the event form with test data
await this.page.fill('#post_title', this.testEventName);
// Set event date (today + 1 day)
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const dateString = tomorrow.toISOString().split('T')[0]; // Format as YYYY-MM-DD
// Fill event date fields
await this.page.fill('input[name="EventStartDate"]', dateString);
await this.page.fill('input[name="EventEndDate"]', dateString);
// Set content using TinyMCE if available
const tinyMceFrame = this.page.frameLocator('.mce-edit-area iframe');
if (await tinyMceFrame.count() > 0) {
await tinyMceFrame.locator('body').fill('This is a test event for certificate generation testing.');
} else {
// Fallback to regular textarea
await this.page.fill('#post_content', 'This is a test event for certificate generation testing.');
}
/**
* Login as the test trainer
*/
async loginAsTrainer(): Promise<void> {
this.verbosity.log('Logging in as test_trainer');
await this.page.goto(Config.loginUrl);
await this.page.fill('#user_login', Config.testTrainer.username);
await this.page.fill('#user_pass', Config.testTrainer.password);
// Fill other required fields
await this.page.fill('#EventVenueName', 'Test Venue');
await this.page.fill('#EventVenueAddress', '123 Test Street');
await this.page.fill('#EventVenueCity', 'Test City');
await this.page.fill('#EventVenueCountry', 'United States');
await this.page.fill('#EventVenueZip', '12345');
// Create ticket
await this.page.click('text=Tickets');
await this.page.click('text=Add a new ticket');
await this.page.fill('.tribe-ticket-field-name input', 'General Admission');
await this.page.fill('.tribe-ticket-field-price input', '10');
await this.page.fill('.tribe-ticket-field-capacity input', '100');
// Save event
await this.page.click('#community-events-submit');
await this.page.waitForLoadState('networkidle');
// Extract the event ID from the URL or response
const url = this.page.url();
const match = url.match(/post=(\d+)/);
if (match && match[1]) {
this.testEventId = match[1];
console.log(`Created test event with ID: ${this.testEventId}`);
} else {
console.error('Failed to extract event ID from URL:', url);
}
// Create test attendees via API for efficiency
await this.createTestAttendees();
return this.testEventName;
}
/**
* Create test attendees for the event
* Creates 5 attendees: 3 checked-in, 2 not checked-in
*/
private async createTestAttendees(): Promise<void> {
if (!this.testEventId) {
throw new Error('Cannot create attendees: No test event ID available');
}
// Login to admin to create test attendees
await this.page.goto(`${this.baseUrl}${this.adminUrl}`);
await this.page.fill('#user_login', this.username);
await this.page.fill('#user_pass', this.password);
await this.page.click('#wp-submit');
await this.page.waitForLoadState('networkidle');
// Create 5 test attendees
const attendeeData = [
{ name: 'Test Attendee 1', email: 'test1@example.com', checkedIn: true },
{ name: 'Test Attendee 2', email: 'test2@example.com', checkedIn: true },
{ name: 'Test Attendee 3', email: 'test3@example.com', checkedIn: true },
{ name: 'Test Attendee 4', email: 'test4@example.com', checkedIn: false },
{ name: 'Test Attendee 5', email: 'test5@example.com', checkedIn: false }
];
// Navigate to Event Tickets > Attendees page
await this.page.goto(`${this.baseUrl}${this.adminUrl}edit.php?post_type=tribe_events&page=tickets-attendees&event_id=${this.testEventId}`);
await this.page.waitForLoadState('networkidle');
for (const attendee of attendeeData) {
// Add attendee manually
await this.page.click('a:text("Add a new attendee")');
await this.page.waitForSelector('form.tribe-attendees-page-add-attendee-form');
// Fill attendee details
await this.page.selectOption('select[name="ticket_id"]', { index: 0 }); // Select the first ticket
await this.page.fill('input[name="attendee[full_name]"]', attendee.name);
await this.page.fill('input[name="attendee[email]"]', attendee.email);
// Submit the form
await this.page.click('button:text("Add")');
await this.page.waitForLoadState('networkidle');
// Check in the attendee if needed
if (attendee.checkedIn) {
// Find the attendee row
const attendeeRow = this.page.locator(`tr:has-text("${attendee.email}")`).first();
// Click the check-in button
await attendeeRow.locator('.check-in').click();
await this.page.waitForLoadState('networkidle');
}
}
console.log(`Created ${attendeeData.length} test attendees for event ${this.testEventId}`);
}
/**
* Clean up test data
*/
async cleanup(): Promise<void> {
if (!this.testEventId) {
console.log('No test data to clean up');
return;
}
try {
// Login to admin if needed
if (!this.page.url().includes(this.adminUrl)) {
await this.page.goto(`${this.baseUrl}${this.adminUrl}`);
await this.page.fill('#user_login', this.username);
await this.page.fill('#user_pass', this.password);
await this.page.click('#wp-submit');
await this.page.waitForLoadState('networkidle');
}
// Move the event to trash
await this.page.goto(`${this.baseUrl}${this.adminUrl}post.php?post=${this.testEventId}&action=trash`);
await this.page.waitForLoadState('networkidle');
console.log(`Cleaned up test event with ID: ${this.testEventId}`);
this.testEventId = null;
this.testEventName = null;
} catch (error) {
console.error('Error during test data cleanup:', error);
}
/**
* Creates a test event with a specified name and future date
*/
async createTestEvent(eventName: string): Promise<string | null> {
this.verbosity.log(`Creating test event: ${eventName}`);
await this.page.goto(Config.createEventUrl);
await this.page.waitForLoadState('networkidle');
// Fill in event details
await this.page.fill('#post_title, input[name="post_title"]', eventName);
// Add description
const newEventFrame = this.page.frameLocator('iframe[id*="_ifr"]');
const newEventBody = newEventFrame.locator('body');
await newEventBody.fill(`This is a test event created for certificate testing: ${eventName}`);
// Set dates (30 days from now)
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 30);
const dateString = `${(futureDate.getMonth() + 1).toString().padStart(2, '0')}/${futureDate.getDate().toString().padStart(2, '0')}/${futureDate.getFullYear()}`;
await this.page.fill('input[name="EventStartDate"]', dateString);
await this.page.fill('input[name="EventStartTime"]', '10:00 AM');
await this.page.fill('input[name="EventEndDate"]', dateString);
await this.page.fill('input[name="EventEndTime"]', '04:00 PM');
// Add a ticket for $100
await this.addTicket('Certificate Test Ticket', '100');
// Submit the event
const submitButton = this.page.locator('input[value="Submit Event"], button:has-text("Submit Event")');
await submitButton.click();
await this.page.waitForLoadState('networkidle');
// Get the event ID from the URL if possible
const url = this.page.url();
const match = url.match(/post=(\d+)/);
if (match && match[1]) {
return match[1];
}
return null;
}
/**
* Adds a ticket to an event
*/
private async addTicket(ticketName: string, price: string): Promise<void> {
// Look for ticket creation UI elements
const ticketNameField = this.page.locator('#tribe-tickets-editor-tickets-name');
const ticketPriceField = this.page.locator('#tribe-tickets-editor-tickets-price');
const addTicketButton = this.page.locator('button:has-text("Add Ticket")');
// If the ticket UI isn't visible, try to open it
if (!await ticketNameField.isVisible()) {
const addTicketsSection = this.page.locator('a:has-text("Add Tickets")');
if (await addTicketsSection.isVisible()) {
await addTicketsSection.click();
await this.page.waitForTimeout(Config.shortWait);
}
}
// Fill in ticket details
if (await ticketNameField.isVisible()) {
await ticketNameField.fill(ticketName);
await ticketPriceField.fill(price);
await addTicketButton.click();
await this.page.waitForTimeout(Config.mediumWait);
} else {
this.verbosity.log('Warning: Ticket creation UI not found');
}
}
/**
* Simulates attendee registrations for an event
* Creates a mix of checked-in and non-checked-in attendees
*/
async createTestAttendees(eventId: string, count: number = 5): Promise<void> {
this.verbosity.log(`Creating ${count} test attendees for event ${eventId}`);
// First, navigate to the admin area to access the event
await this.page.goto(`${Config.stagingUrl}/wp-admin/post.php?post=${eventId}&action=edit`);
// Check if we're on the login page and log in if needed
if (this.page.url().includes('wp-login.php')) {
await this.page.fill('#user_login', Config.testTrainer.username);
await this.page.fill('#user_pass', Config.testTrainer.password);
await this.page.click('#wp-submit');
await this.page.waitForLoadState('networkidle');
}
// Navigate to the attendees tab - this is implementation-specific and may need adjustment
const attendeesTab = this.page.locator('a:has-text("Attendees")');
if (await attendeesTab.isVisible()) {
await attendeesTab.click();
await this.page.waitForTimeout(Config.shortWait);
}
// Look for "Add New" button
const addNewButton = this.page.locator('a:has-text("Add attendee"), button:has-text("Add attendee")');
for (let i = 1; i <= count; i++) {
// Click "Add New" for each attendee
if (await addNewButton.isVisible()) {
await addNewButton.click();
await this.page.waitForTimeout(Config.shortWait);
// Fill in attendee info
await this.page.fill('input[name="attendee[email]"]', `test.attendee${i}@example.com`);
await this.page.fill('input[name="attendee[full_name]"]', `Test Attendee ${i}`);
// Mark every other attendee as checked in
if (i % 2 === 0) {
const checkinCheckbox = this.page.locator('input[name="attendee[check_in]"]');
if (await checkinCheckbox.isVisible()) {
await checkinCheckbox.check();
}
}
// Save the attendee
const saveButton = this.page.locator('button:has-text("Add")');
await saveButton.click();
await this.page.waitForTimeout(Config.shortWait);
} else {
this.verbosity.log('Warning: Add attendee button not found');
break;
}
}
}
/**
* Creates a complete test event with attendees for certificate testing
*/
async setupCertificateTestEvent(): Promise<string | null> {
// Create a uniquely named event
const timestamp = new Date().getTime();
const eventName = `Certificate Test Event ${timestamp}`;
// Create the event
const eventId = await this.createTestEvent(eventName);
if (!eventId) {
this.verbosity.log('Failed to create test event');
return null;
}
// Add test attendees (mix of checked-in and non-checked-in)
await this.createTestAttendees(eventId, 6);
return eventName;
}
}
}