feat: Add comprehensive certificate E2E tests

- Created CertificatePage class for testing certificate functionality
- Updated DashboardPage to support certificate links in navigation
- Implemented test data generator for certificate testing
- Added tests for certificate generation with checked-in users
- Added tests for certificate generation with non-checked-in users
- Added certificate management (view/email/revoke) tests
- Created comprehensive trainer journey test including certificates
- Added utility script to run certificate-specific tests

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
bengizmo 2025-05-20 20:44:42 -03:00
parent c417a6154b
commit fbc2d818c0
8 changed files with 1193 additions and 0 deletions

View file

@ -0,0 +1,123 @@
#!/bin/bash
# Script to run certificate-specific E2E tests
# This is helpful for testing only the certificate functionality
# Set default values
HEADLESS=true
VERBOSE=false
RETRY_FAILURES=false
REPORTER="list"
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Function to display usage information
function show_usage {
echo "Usage: $0 [options]"
echo "Options:"
echo " --all Run all certificate tests (default)"
echo " --checked-in Run only checked-in certificate tests"
echo " --non-checked-in Run only non-checked-in certificate tests"
echo " --management Run only certificate management tests"
echo " --journey Run only the certificate trainer journey"
echo " --no-headless Run tests with browser visible"
echo " --verbose Run tests with verbose output"
echo " --retry-failures Retry failed tests once"
echo " --html-report Generate HTML report"
echo " --help Show this help message"
echo ""
echo "Example: $0 --journey --no-headless --verbose"
}
# Parse command line arguments
TEST_PATTERN="@certificate|certificate-.*\\.test\\.ts"
ARGS=""
while [[ $# -gt 0 ]]; do
case "$1" in
--all)
TEST_PATTERN="@certificate|certificate-.*\\.test\\.ts"
shift
;;
--checked-in)
TEST_PATTERN="certificate-generation-checked-in\\.test\\.ts"
shift
;;
--non-checked-in)
TEST_PATTERN="certificate-generation-non-checked-in\\.test\\.ts"
shift
;;
--management)
TEST_PATTERN="certificate-management\\.test\\.ts"
shift
;;
--journey)
TEST_PATTERN="trainer-journey-with-certificates\\.test\\.ts"
shift
;;
--no-headless)
HEADLESS=false
shift
;;
--verbose)
VERBOSE=true
shift
;;
--retry-failures)
RETRY_FAILURES=true
shift
;;
--html-report)
REPORTER="html"
shift
;;
--help)
show_usage
exit 0
;;
*)
echo "Unknown option: $1"
show_usage
exit 1
;;
esac
done
# Build the command line arguments
if [ "$HEADLESS" = true ]; then
ARGS="$ARGS --headed false"
else
ARGS="$ARGS --headed"
fi
if [ "$VERBOSE" = true ]; then
ARGS="$ARGS --debug"
fi
if [ "$RETRY_FAILURES" = true ]; then
ARGS="$ARGS --retries=1"
fi
# Add reporter argument
ARGS="$ARGS --reporter=$REPORTER"
# Change to the project root directory
cd "$PROJECT_ROOT" || { echo "Failed to change to project root directory"; exit 1; }
# Run the tests
echo "Running certificate tests with pattern: $TEST_PATTERN"
echo "Arguments: $ARGS"
# Execute the tests
npx playwright test --config=playwright.config.ts --grep="$TEST_PATTERN" $ARGS
# Check exit status
EXIT_CODE=$?
# Display test results
if [ $EXIT_CODE -eq 0 ]; then
echo "Certificate tests completed successfully!"
else
echo "Certificate tests failed with exit code: $EXIT_CODE"
fi
exit $EXIT_CODE

View file

@ -0,0 +1,117 @@
import { test, expect } from '@playwright/test';
import { CertificatePage } from './pages/CertificatePage';
import { DashboardPage } from './pages/DashboardPage';
import { CertificateTestData } from './utils/CertificateTestData';
const STAGING_URL = 'https://wordpress-974670-5399585.cloudwaysapps.com';
test.describe('Certificate Generation for Checked-In Attendees @certificate', () => {
let eventName: string | null = null;
test.beforeAll(async ({ browser }) => {
console.log('Setting up test data for certificate tests...');
// Create a new browser context for data setup
const context = await browser.newContext();
const page = await context.newPage();
// Set up the test data
const testData = new CertificateTestData(page);
await testData.loginAsTrainer();
// Create a test event with attendees (some checked-in, some not)
eventName = await testData.setupCertificateTestEvent();
console.log(`Test event created: ${eventName}`);
// Close the setup context
await context.close();
});
test('Generate certificates for checked-in attendees', async ({ page }) => {
// Skip test if event creation failed
test.skip(!eventName, 'Test event creation failed in setup');
console.log('Step 1: Logging in...');
await page.goto(`${STAGING_URL}/community-login/`);
await page.fill('#user_login', 'test_trainer');
await page.fill('#user_pass', 'Test123!');
await page.click('#wp-submit');
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/hvac-dashboard/);
console.log('Step 2: Navigate to dashboard...');
const dashboardPage = new DashboardPage(page);
await dashboardPage.navigate();
console.log('Step 3: Verify certificate links are visible...');
await dashboardPage.clickGenerateCertificates();
console.log('Step 4: Generate certificates for checked-in attendees only...');
const certificatePage = new CertificatePage(page);
// Verify we're on the generate certificates page
const pageVisible = await certificatePage.isGenerateCertificatesPageVisible();
expect(pageVisible).toBeTruthy();
// Select the test event
if (eventName) {
await certificatePage.selectEvent(eventName);
// Get attendee counts
const totalAttendees = await certificatePage.getAttendeeCount();
const checkedInAttendees = await certificatePage.getCheckedInAttendeeCount();
console.log(`Found ${totalAttendees} total attendees, ${checkedInAttendees} checked-in`);
expect(totalAttendees).toBeGreaterThan(0);
expect(checkedInAttendees).toBeGreaterThan(0);
// Select only checked-in attendees
await certificatePage.selectCheckedInAttendees();
// Generate certificates
await certificatePage.generateCertificates();
// Verify success message
const success = await certificatePage.isSuccessMessageVisible();
expect(success).toBeTruthy();
const successMessage = await certificatePage.getSuccessMessage();
console.log(`Success message: ${successMessage}`);
expect(successMessage).toContain("success");
}
console.log('Step 5: Verify certificates in Certificate Reports...');
// Navigate to certificate reports
await dashboardPage.navigate();
await dashboardPage.clickCertificateReports();
// Verify we're on the certificate reports page
const reportsPageVisible = await certificatePage.isCertificateReportsPageVisible();
expect(reportsPageVisible).toBeTruthy();
// Filter certificates for the test event
if (eventName) {
await certificatePage.searchCertificates(eventName);
// Check certificate count
const certificateCount = await certificatePage.getCertificateCount();
console.log(`Found ${certificateCount} certificates for event`);
// We should have certificates equal to the number of checked-in attendees
// Note: This assumes that the test data setup created at least one checked-in attendee
expect(certificateCount).toBeGreaterThan(0);
// View a certificate
if (certificateCount > 0) {
await certificatePage.viewCertificate(0);
// Close the preview
await certificatePage.closePreview();
}
}
console.log('Certificate generation test for checked-in attendees completed successfully');
});
});

View file

@ -0,0 +1,149 @@
import { test, expect } from '@playwright/test';
import { CertificatePage } from './pages/CertificatePage';
import { DashboardPage } from './pages/DashboardPage';
import { CertificateTestData } from './utils/CertificateTestData';
const STAGING_URL = 'https://wordpress-974670-5399585.cloudwaysapps.com';
test.describe('Certificate Generation for Non-Checked-In Attendees @certificate', () => {
let eventName: string | null = null;
test.beforeAll(async ({ browser }) => {
console.log('Setting up test data for non-checked-in certificate tests...');
// Create a new browser context for data setup
const context = await browser.newContext();
const page = await context.newPage();
// Set up the test data
const testData = new CertificateTestData(page);
await testData.loginAsTrainer();
// Create a test event with attendees (some checked-in, some not)
eventName = await testData.setupCertificateTestEvent();
console.log(`Test event created: ${eventName}`);
// Close the setup context
await context.close();
});
test('Generate certificates for non-checked-in attendees', async ({ page }) => {
// Skip test if event creation failed
test.skip(!eventName, 'Test event creation failed in setup');
console.log('Step 1: Logging in...');
await page.goto(`${STAGING_URL}/community-login/`);
await page.fill('#user_login', 'test_trainer');
await page.fill('#user_pass', 'Test123!');
await page.click('#wp-submit');
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/hvac-dashboard/);
console.log('Step 2: Navigate to dashboard...');
const dashboardPage = new DashboardPage(page);
await dashboardPage.navigate();
console.log('Step 3: Verify certificate links are visible...');
await dashboardPage.clickGenerateCertificates();
console.log('Step 4: Attempt to generate certificates for non-checked-in attendees...');
const certificatePage = new CertificatePage(page);
// Verify we're on the generate certificates page
const pageVisible = await certificatePage.isGenerateCertificatesPageVisible();
expect(pageVisible).toBeTruthy();
// Select the test event
if (eventName) {
await certificatePage.selectEvent(eventName);
// Get attendee counts
const totalAttendees = await certificatePage.getAttendeeCount();
const checkedInAttendees = await certificatePage.getCheckedInAttendeeCount();
const nonCheckedInAttendees = totalAttendees - checkedInAttendees;
console.log(`Found ${totalAttendees} total attendees, ${nonCheckedInAttendees} non-checked-in`);
expect(totalAttendees).toBeGreaterThan(0);
expect(nonCheckedInAttendees).toBeGreaterThan(0);
// Select only non-checked-in attendees
await certificatePage.selectNonCheckedInAttendees();
// Generate certificates
await certificatePage.generateCertificates();
// Check for success or warning message
// Note: The implementation could allow this with warnings,
// or it could block generation for non-checked-in attendees
const successVisible = await certificatePage.isSuccessMessageVisible();
const errorVisible = await certificatePage.isErrorMessageVisible();
if (successVisible) {
const successMessage = await certificatePage.getSuccessMessage();
console.log(`Success message: ${successMessage}`);
// If certificates were generated for non-checked-in attendees,
// check them in the reports page
console.log('Step 5: Verify certificates in Certificate Reports...');
// Navigate to certificate reports
await dashboardPage.navigate();
await dashboardPage.clickCertificateReports();
// Verify we're on the certificate reports page
const reportsPageVisible = await certificatePage.isCertificateReportsPageVisible();
expect(reportsPageVisible).toBeTruthy();
// Filter certificates for the test event
await certificatePage.searchCertificates(eventName);
// Check certificate count
const certificateCount = await certificatePage.getCertificateCount();
console.log(`Found ${certificateCount} certificates for event`);
// We should have certificates for the non-checked-in attendees
expect(certificateCount).toBeGreaterThan(0);
// View a certificate
if (certificateCount > 0) {
await certificatePage.viewCertificate(0);
// Close the preview
await certificatePage.closePreview();
}
} else if (errorVisible) {
// If the system prevents generating certificates for non-checked-in attendees,
// verify the appropriate error message is shown
const errorMessage = await certificatePage.getErrorMessage();
console.log(`Error message: ${errorMessage}`);
expect(errorMessage).toContain("check-in") || expect(errorMessage).toContain("attendance");
// Verify no certificates were generated for non-checked-in attendees
console.log('Verifying no certificates were generated...');
// Navigate to certificate reports
await dashboardPage.navigate();
await dashboardPage.clickCertificateReports();
// Filter certificates for the test event
await certificatePage.searchCertificates(eventName);
// Search for any certificates generated since we started the test
const certificateCount = await certificatePage.getCertificateCount();
// There shouldn't be any new certificates for non-checked-in attendees
// Note: This simple verification might be inaccurate if there were already certificates
// for this event that we didn't account for
console.log(`Found ${certificateCount} certificates for event`);
} else {
// If neither success nor error message is visible, something unexpected happened
console.log('No success or error message found after certificate generation attempt');
expect(false).toBeTruthy(); // Fail the test
}
}
console.log('Certificate generation test for non-checked-in attendees completed');
});
});

View file

@ -0,0 +1,188 @@
import { test, expect } from '@playwright/test';
import { CertificatePage } from './pages/CertificatePage';
import { DashboardPage } from './pages/DashboardPage';
import { CertificateTestData } from './utils/CertificateTestData';
const STAGING_URL = 'https://wordpress-974670-5399585.cloudwaysapps.com';
test.describe('Certificate Management @certificate-management', () => {
let eventName: string | null = null;
test.beforeAll(async ({ browser }) => {
console.log('Setting up test data for certificate management tests...');
// Create a new browser context for data setup
const context = await browser.newContext();
const page = await context.newPage();
// Set up the test data
const testData = new CertificateTestData(page);
await testData.loginAsTrainer();
// Create a test event with attendees (some checked-in, some not)
eventName = await testData.setupCertificateTestEvent();
console.log(`Test event created: ${eventName}`);
// Generate certificates for the test event
if (eventName) {
const certificatePage = new CertificatePage(page);
// Navigate to generate certificates page
await page.goto(`${STAGING_URL}/generate-certificates/`);
await page.waitForLoadState('networkidle');
// Select the test event
await certificatePage.selectEvent(eventName);
// Select all attendees
await certificatePage.selectAllAttendees();
// Generate certificates
await certificatePage.generateCertificates();
console.log('Generated certificates for test event');
}
// Close the setup context
await context.close();
});
test('View, email, and revoke certificates', async ({ page }) => {
// Skip test if event creation failed
test.skip(!eventName, 'Test event creation failed in setup');
console.log('Step 1: Logging in...');
await page.goto(`${STAGING_URL}/community-login/`);
await page.fill('#user_login', 'test_trainer');
await page.fill('#user_pass', 'Test123!');
await page.click('#wp-submit');
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/hvac-dashboard/);
console.log('Step 2: Navigate to dashboard...');
const dashboardPage = new DashboardPage(page);
await dashboardPage.navigate();
console.log('Step 3: Navigate to Certificate Reports...');
await dashboardPage.clickCertificateReports();
const certificatePage = new CertificatePage(page);
// Verify we're on the certificate reports page
const pageVisible = await certificatePage.isCertificateReportsPageVisible();
expect(pageVisible).toBeTruthy();
// Filter certificates for the test event
if (eventName) {
console.log('Step 4: Search for certificates from test event...');
await certificatePage.searchCertificates(eventName);
// Check certificate count
const certificateCount = await certificatePage.getCertificateCount();
console.log(`Found ${certificateCount} certificates for event`);
expect(certificateCount).toBeGreaterThan(0);
if (certificateCount > 0) {
// Test View Certificate
console.log('Step 5: Testing View Certificate functionality...');
await certificatePage.viewCertificate(0);
// Close the preview
await certificatePage.closePreview();
// Test Email Certificate
console.log('Step 6: Testing Email Certificate functionality...');
await certificatePage.emailCertificate(0);
// Check for success message after email
const emailSuccess = await certificatePage.isSuccessMessageVisible();
expect(emailSuccess).toBeTruthy();
// Test Revoke Certificate (if more than one certificate exists)
if (certificateCount > 1) {
console.log('Step 7: Testing Revoke Certificate functionality...');
await certificatePage.revokeCertificate(1);
// Check for success message after revocation
const revokeSuccess = await certificatePage.isSuccessMessageVisible();
expect(revokeSuccess).toBeTruthy();
// Verify certificate count decreased
await certificatePage.searchCertificates(eventName); // Refresh the list
const newCertificateCount = await certificatePage.getCertificateCount();
expect(newCertificateCount).toBeLessThan(certificateCount);
console.log(`Certificate count after revocation: ${newCertificateCount}`);
} else {
console.log('Only one certificate found, skipping revocation test');
}
}
}
console.log('Certificate management test completed successfully');
});
test('Pagination and filtering in Certificate Reports', async ({ page }) => {
// Skip test if event creation failed
test.skip(!eventName, 'Test event creation failed in setup');
console.log('Step 1: Logging in...');
await page.goto(`${STAGING_URL}/community-login/`);
await page.fill('#user_login', 'test_trainer');
await page.fill('#user_pass', 'Test123!');
await page.click('#wp-submit');
await page.waitForLoadState('networkidle');
console.log('Step 2: Navigate to Certificate Reports...');
const dashboardPage = new DashboardPage(page);
await dashboardPage.navigate();
await dashboardPage.clickCertificateReports();
const certificatePage = new CertificatePage(page);
// Verify we're on the certificate reports page
const pageVisible = await certificatePage.isCertificateReportsPageVisible();
expect(pageVisible).toBeTruthy();
// Test filtering functionality
console.log('Step 3: Testing filtering functionality...');
// 1. Filter by event name
if (eventName) {
await certificatePage.searchCertificates(eventName);
// Verify results contain only certificates for the test event
const certificateCount = await certificatePage.getCertificateCount();
console.log(`Found ${certificateCount} certificates for event "${eventName}"`);
expect(certificateCount).toBeGreaterThan(0);
}
// 2. Filter by a non-existent name (should show no results)
const randomText = `non-existent-event-${Math.random().toString(36).substring(2, 8)}`;
await certificatePage.searchCertificates(randomText);
// Verify no results
const noResultsCount = await certificatePage.getCertificateCount();
console.log(`Found ${noResultsCount} certificates for random text "${randomText}"`);
expect(noResultsCount).toBe(0);
// Test pagination if available
console.log('Step 4: Testing pagination functionality (if available)...');
// Clear the search first
await certificatePage.searchCertificates('');
// Check if pagination is visible (this might not be if there aren't enough certificates)
const isPaginationVisible = await certificatePage.isPaginationVisible();
if (isPaginationVisible) {
console.log('Pagination is visible, testing pagination functionality...');
// Add specific pagination testing here if there's pagination in the UI
// This would involve clicking next/previous buttons and verifying different results
} else {
console.log('No pagination visible, skipping pagination tests');
}
console.log('Certificate reporting pagination and filtering test completed');
});
});

View file

@ -0,0 +1,251 @@
import { Page } from '@playwright/test';
import { BasePage } from './BasePage';
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';
// 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")';
constructor(page: Page) {
super(page);
}
// Common methods
async navigateToGenerateCertificates(): Promise<void> {
const STAGING_URL = 'https://wordpress-974670-5399585.cloudwaysapps.com';
await this.page.goto(`${STAGING_URL}/generate-certificates/`);
await this.page.waitForLoadState('networkidle');
await this.screenshot('generate-certificates-page');
}
async navigateToCertificateReports(): Promise<void> {
const STAGING_URL = 'https://wordpress-974670-5399585.cloudwaysapps.com';
await this.page.goto(`${STAGING_URL}/certificate-reports/`);
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(500); // Wait for search results
}
// Select the event from dropdown
await this.page.selectOption(this.eventSelector, { label: eventName });
await this.page.waitForTimeout(1000); // Wait for attendee list to load
// 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: 5000 });
}
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: 10000 });
}
await this.page.waitForTimeout(2000); // 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(500); // 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(1000); // 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: 5000 });
}
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(2000); // 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(2000); // Wait for revocation to complete
}
await this.screenshot('revoke-certificate');
}
async isPaginationVisible(): Promise<boolean> {
return await this.isVisible(this.certificatePagination);
}
}

View file

@ -5,6 +5,8 @@ export class DashboardPage extends BasePage {
private readonly createEventButton = 'a:has-text("Create Event")';
private readonly viewProfileButton = 'a:has-text("View Profile")';
private readonly logoutButton = 'a:has-text("Logout")';
private readonly generateCertificatesButton = 'a:has-text("Generate Certificates")';
private readonly certificateReportsButton = 'a:has-text("Certificate Reports")';
private readonly eventsTable = 'table';
// Updated Stats row layout selectors
@ -49,6 +51,16 @@ export class DashboardPage extends BasePage {
await this.waitForNavigation();
}
async clickGenerateCertificates(): Promise<void> {
await this.click(this.generateCertificatesButton);
await this.waitForNavigation();
}
async clickCertificateReports(): Promise<void> {
await this.click(this.certificateReportsButton);
await this.waitForNavigation();
}
async logout(): Promise<void> {
await this.click(this.logoutButton);
await this.waitForNavigation();

View file

@ -0,0 +1,172 @@
import { test, expect } from '@playwright/test';
import { DashboardPage } from './pages/DashboardPage';
import { CertificatePage } from './pages/CertificatePage';
import { CertificateTestData } from './utils/CertificateTestData';
const STAGING_URL = 'https://wordpress-974670-5399585.cloudwaysapps.com';
test.describe('Complete Trainer Journey with Certificates @trainer-journey @certificates', () => {
test('Full trainer workflow including certificate generation', async ({ page }) => {
console.log('Starting comprehensive trainer journey test with certificates...');
// Step 1: Login as test_trainer
console.log('Step 1: Logging in...');
await page.goto(`${STAGING_URL}/community-login/`);
await page.fill('#user_login', 'test_trainer');
await page.fill('#user_pass', 'Test123!');
await page.click('#wp-submit');
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/hvac-dashboard/);
console.log('Login successful');
// Initialize page objects
const dashboardPage = new DashboardPage(page);
const certificatePage = new CertificatePage(page);
// Step 2: Verify dashboard shows essential elements
console.log('Step 2: Verifying dashboard content...');
await dashboardPage.navigate();
// Check for certificate links in the navigation
await page.waitForSelector(page.locator('a:has-text("Generate Certificates")').first());
await page.waitForSelector(page.locator('a:has-text("Certificate Reports")').first());
// Verify statistics are displayed
const stats = await dashboardPage.getStatistics();
console.log('Dashboard statistics:', stats);
// Verify events table is visible
const eventsTableVisible = await dashboardPage.isEventsTableVisible();
expect(eventsTableVisible).toBeTruthy();
// Step 3: Create a new event for testing
console.log('Step 3: Creating a new event...');
await dashboardPage.clickCreateEvent();
// Fill in event details
const eventName = `Certificate Test Event ${new Date().getTime()}`;
await page.fill('#post_title, input[name="post_title"]', eventName);
// Add description
const newEventFrame = page.frameLocator('iframe[id*="_ifr"]');
const newEventBody = newEventFrame.locator('body');
await newEventBody.fill(`This is a test event created for certificate journey testing: ${eventName}`);
// Set future 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 page.fill('input[name="EventStartDate"]', dateString);
await page.fill('input[name="EventStartTime"]', '10:00 AM');
await page.fill('input[name="EventEndDate"]', dateString);
await page.fill('input[name="EventEndTime"]', '04:00 PM');
// Add a ticket
// Try to find the ticket UI
const addTicketSection = page.locator('a:has-text("Add Tickets")');
if (await addTicketSection.isVisible()) {
await addTicketSection.click();
await page.waitForTimeout(1000);
}
const ticketNameField = page.locator('#tribe-tickets-editor-tickets-name');
const ticketPriceField = page.locator('#tribe-tickets-editor-tickets-price');
const addTicketButton = page.locator('button:has-text("Add Ticket")');
if (await ticketNameField.isVisible()) {
await ticketNameField.fill('Standard Admission');
await ticketPriceField.fill('99.99');
await addTicketButton.click();
await page.waitForTimeout(2000);
console.log('Added ticket to event');
} else {
console.log('Ticket UI not found, continuing without adding ticket');
}
// Submit the event
const submitButton = page.locator('input[value="Submit Event"], button:has-text("Submit Event")');
await submitButton.click();
await page.waitForLoadState('networkidle');
// Verify submission success
const successMessage = page.locator('text=/success|submitted/i');
await expect(successMessage.first()).toBeVisible({ timeout: 10000 });
console.log(`New event "${eventName}" created successfully`);
// Step 4: Navigate to Generate Certificates page
console.log('Step 4: Navigating to Generate Certificates page...');
await dashboardPage.navigate();
await dashboardPage.clickGenerateCertificates();
// Verify we're on the generate certificates page
const generatePageVisible = await certificatePage.isGenerateCertificatesPageVisible();
expect(generatePageVisible).toBeTruthy();
// Check if the newly created event is available in the dropdown
// If it is, we could verify event selection functionality
try {
await certificatePage.selectEvent(eventName);
console.log('Event found in certificate generation dropdown');
// If there are no attendees yet, the attendee list might be empty
const attendeeCount = await certificatePage.getAttendeeCount();
console.log(`Found ${attendeeCount} attendees for the new event`);
// Since this is a brand new event with no attendees yet,
// no certificates can be generated at this point
} catch (error) {
console.log('New event not yet available in certificate generation, continuing test');
}
// Step 5: Navigate to Certificate Reports page
console.log('Step 5: Navigating to Certificate Reports page...');
await dashboardPage.navigate();
await dashboardPage.clickCertificateReports();
// Verify we're on the certificate reports page
const reportsPageVisible = await certificatePage.isCertificateReportsPageVisible();
expect(reportsPageVisible).toBeTruthy();
// Step 6: Search for any existing certificates
console.log('Step 6: Searching for existing certificates...');
// Clear any existing filters
await certificatePage.searchCertificates('');
// Get the number of existing certificates
const existingCertificateCount = await certificatePage.getCertificateCount();
console.log(`Found ${existingCertificateCount} existing certificates`);
// If certificates exist, test viewing one
if (existingCertificateCount > 0) {
await certificatePage.viewCertificate(0);
await certificatePage.closePreview();
console.log('Successfully viewed an existing certificate');
}
// Step 7: Navigate back to My Events page to check the new event
console.log('Step 7: Navigating to My Events page...');
await dashboardPage.navigate();
await page.goto(`${STAGING_URL}/my-events/`);
await page.waitForLoadState('networkidle');
// Check for the newly created event
const newEventListing = page.locator(`text="${eventName}"`);
await expect(newEventListing).toBeVisible({ timeout: 10000 });
console.log('New event found in My Events list');
// Step 8: Verify the complete trainer journey
console.log('Step 8: Final verification...');
// Return to dashboard
await dashboardPage.navigate();
// Take a final screenshot
await page.screenshot({ path: 'trainer-journey-with-certificates-complete.png', fullPage: true });
console.log('Comprehensive trainer journey test with certificates completed successfully!');
});
});

View file

@ -0,0 +1,181 @@
import { Page } from '@playwright/test';
import { VerbosityController } from './VerbosityController';
/**
* 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
*/
export class CertificateTestData {
private page: Page;
private verbosity: VerbosityController;
private readonly STAGING_URL = 'https://wordpress-974670-5399585.cloudwaysapps.com';
constructor(page: Page) {
this.page = page;
this.verbosity = VerbosityController.getInstance();
}
/**
* Login as the test trainer
*/
async loginAsTrainer(): Promise<void> {
this.verbosity.log('Logging in as test_trainer');
await this.page.goto(`${this.STAGING_URL}/community-login/`);
await this.page.fill('#user_login', 'test_trainer');
await this.page.fill('#user_pass', 'Test123!');
await this.page.click('#wp-submit');
await this.page.waitForLoadState('networkidle');
}
/**
* 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(`${this.STAGING_URL}/manage-event/`);
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(1000);
}
}
// Fill in ticket details
if (await ticketNameField.isVisible()) {
await ticketNameField.fill(ticketName);
await ticketPriceField.fill(price);
await addTicketButton.click();
await this.page.waitForTimeout(2000);
} 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(`${this.STAGING_URL}/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', 'test_trainer');
await this.page.fill('#user_pass', 'Test123!');
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(1000);
}
// 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(500);
// 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(1000);
} 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;
}
}