- 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>
302 lines
No EOL
11 KiB
TypeScript
302 lines
No EOL
11 KiB
TypeScript
import { Page } from '@playwright/test';
|
|
import { BasePage } from './BasePage';
|
|
|
|
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
|
|
private readonly statsSection = '.hvac-stats-row';
|
|
private readonly statsColumns = '.hvac-stat-col';
|
|
private readonly totalEventsCard = '.hvac-stat-card:has-text("Total Events")';
|
|
private readonly upcomingEventsCard = '.hvac-stat-card:has-text("Upcoming Events")';
|
|
private readonly pastEventsCard = '.hvac-stat-card:has-text("Past Events")';
|
|
private readonly totalRevenueCard = '.hvac-stat-card:has-text("Total Revenue")';
|
|
|
|
// Event filters selectors
|
|
private readonly filterButtons = '.hvac-event-filters a';
|
|
private readonly allFilterButton = '.hvac-event-filters a:has-text("All")';
|
|
private readonly publishFilterButton = '.hvac-event-filters a:has-text("Publish")';
|
|
private readonly draftFilterButton = '.hvac-event-filters a:has-text("Draft")';
|
|
private readonly pendingFilterButton = '.hvac-event-filters a:has-text("Pending")';
|
|
private readonly privateFilterButton = '.hvac-event-filters a:has-text("Private")';
|
|
private readonly activeFilterClass = 'hvac-filter-active';
|
|
private readonly loadingIndicator = '.hvac-loading';
|
|
|
|
constructor(page: Page) {
|
|
super(page);
|
|
}
|
|
|
|
async navigate(): Promise<void> {
|
|
const STAGING_URL = 'https://wordpress-974670-5399585.cloudwaysapps.com';
|
|
await this.page.goto(`${STAGING_URL}/hvac-dashboard/`);
|
|
await this.page.waitForLoadState('networkidle');
|
|
}
|
|
|
|
async navigateToDashboard(): Promise<void> {
|
|
await this.navigate('/hvac-dashboard/');
|
|
}
|
|
|
|
async clickCreateEvent(): Promise<void> {
|
|
await this.click(this.createEventButton);
|
|
await this.waitForNavigation();
|
|
}
|
|
|
|
async clickViewProfile(): Promise<void> {
|
|
await this.click(this.viewProfileButton);
|
|
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();
|
|
}
|
|
|
|
async getStatistics(): Promise<{
|
|
totalEvents: string;
|
|
upcomingEvents: string;
|
|
pastEvents: string;
|
|
revenue: string;
|
|
}> {
|
|
return {
|
|
totalEvents: await this.page.locator(this.totalEventsCard).locator('p').textContent() || '0',
|
|
upcomingEvents: await this.page.locator(this.upcomingEventsCard).locator('p').textContent() || '0',
|
|
pastEvents: await this.page.locator(this.pastEventsCard).locator('p').textContent() || '0',
|
|
revenue: await this.page.locator(this.totalRevenueCard).locator('p').textContent() || '$0.00'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the number of stat columns in the row layout
|
|
*/
|
|
async getStatsColumnCount(): Promise<number> {
|
|
return await this.page.locator(this.statsColumns).count();
|
|
}
|
|
|
|
/**
|
|
* Check if the stats are displayed in a row layout
|
|
*/
|
|
async areStatsInRowLayout(): Promise<boolean> {
|
|
// First check if the row container exists
|
|
const statsRow = await this.isVisible(this.statsSection);
|
|
if (!statsRow) return false;
|
|
|
|
// Get columns
|
|
const columns = await this.page.locator(this.statsColumns);
|
|
const count = await columns.count();
|
|
|
|
// Need at least 2 columns to verify row layout
|
|
if (count < 2) return false;
|
|
|
|
// Get bounding boxes to verify horizontal layout
|
|
const box1 = await columns.nth(0).boundingBox();
|
|
const box2 = await columns.nth(1).boundingBox();
|
|
|
|
if (!box1 || !box2) return false;
|
|
|
|
// In a row layout, the second column should be to the right of the first
|
|
// (This may not be true on very narrow screens where they would wrap)
|
|
return box2.x > box1.x;
|
|
}
|
|
|
|
async isEventsTableVisible(): Promise<boolean> {
|
|
return await this.isVisible(this.eventsTable);
|
|
}
|
|
|
|
/**
|
|
* Get the count of filter buttons
|
|
*/
|
|
async getFilterButtonCount(): Promise<number> {
|
|
return await this.page.locator(this.filterButtons).count();
|
|
}
|
|
|
|
/**
|
|
* Get the currently active filter
|
|
*/
|
|
async getActiveFilter(): Promise<string> {
|
|
const activeFilter = await this.page.locator(`${this.filterButtons}.${this.activeFilterClass}`);
|
|
if (await activeFilter.count() === 0) {
|
|
return 'All'; // Default filter is All
|
|
}
|
|
return (await activeFilter.textContent() || '').trim();
|
|
}
|
|
|
|
/**
|
|
* Filter events by status
|
|
* @param status The filter status: 'All', 'Publish', 'Draft', 'Pending', or 'Private'
|
|
*/
|
|
async filterEvents(status: 'All' | 'Publish' | 'Draft' | 'Pending' | 'Private'): Promise<void> {
|
|
this.verbosity.log(`Filtering events by status: ${status}`);
|
|
|
|
// Get the appropriate selector based on status
|
|
let selector: string;
|
|
switch (status) {
|
|
case 'All':
|
|
selector = this.allFilterButton;
|
|
break;
|
|
case 'Publish':
|
|
selector = this.publishFilterButton;
|
|
break;
|
|
case 'Draft':
|
|
selector = this.draftFilterButton;
|
|
break;
|
|
case 'Pending':
|
|
selector = this.pendingFilterButton;
|
|
break;
|
|
case 'Private':
|
|
selector = this.privateFilterButton;
|
|
break;
|
|
default:
|
|
throw new Error(`Invalid filter status: ${status}`);
|
|
}
|
|
|
|
// Click the filter button
|
|
await this.click(selector);
|
|
|
|
// Wait for the loading indicator to disappear if it appears
|
|
const loadingElement = this.page.locator(this.loadingIndicator);
|
|
if (await loadingElement.isVisible()) {
|
|
await loadingElement.waitFor({ state: 'hidden', timeout: 5000 });
|
|
}
|
|
|
|
// Wait a moment for the AJAX content to update
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
|
|
/**
|
|
* Check if filter status appears in URL
|
|
*/
|
|
async doesUrlContainFilterStatus(status: string): Promise<boolean> {
|
|
const url = await this.getUrl();
|
|
|
|
// 'All' filter should not have event_status in URL
|
|
if (status.toLowerCase() === 'all') {
|
|
return !url.includes('event_status=');
|
|
}
|
|
|
|
// Other filters should have event_status=status in URL
|
|
return url.includes(`event_status=${status.toLowerCase()}`);
|
|
}
|
|
|
|
/**
|
|
* Check if table has been filtered by the given status
|
|
* If status is 'All', it checks if the table exists and has any events
|
|
* Otherwise it verifies that all visible events have the expected status
|
|
*/
|
|
async isTableFilteredByStatus(status: 'All' | 'Publish' | 'Draft' | 'Pending' | 'Private'): Promise<boolean> {
|
|
// First check if the table exists
|
|
if (!await this.isEventsTableVisible()) {
|
|
return false;
|
|
}
|
|
|
|
// If status is 'All', just check that the table exists
|
|
if (status === 'All') {
|
|
return true;
|
|
}
|
|
|
|
// Get all event rows
|
|
const rows = await this.page.locator(`${this.eventsTable} tbody tr`);
|
|
const count = await rows.count();
|
|
|
|
// If no events, can't verify status
|
|
if (count === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check for "No events found" message
|
|
if (count === 1) {
|
|
const firstRowText = await rows.first().textContent() || '';
|
|
if (firstRowText.includes('No events found')) {
|
|
// This is acceptable if we've filtered to a status with no events
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// For each row, check if the status column matches the expected status
|
|
for (let i = 0; i < count; i++) {
|
|
const rowData = await this.getEventRowData(i);
|
|
|
|
// If the status doesn't match, the filter isn't working correctly
|
|
if (rowData.status.toLowerCase() !== status.toLowerCase()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// All rows have the expected status
|
|
return true;
|
|
}
|
|
|
|
async getEventRowData(index: number): Promise<{
|
|
status: string;
|
|
name: string;
|
|
date: string;
|
|
organizer: string;
|
|
capacity: string;
|
|
soldTickets: string;
|
|
revenue: string;
|
|
}> {
|
|
const row = await this.page.locator(`${this.eventsTable} tbody tr`).nth(index);
|
|
|
|
// Check if this is a "No events found" row
|
|
const cellCount = await row.locator('td').count();
|
|
if (cellCount === 1) {
|
|
const text = await row.locator('td').textContent();
|
|
if (text?.includes('No events found')) {
|
|
return {
|
|
status: '',
|
|
name: text,
|
|
date: '',
|
|
organizer: '',
|
|
capacity: '',
|
|
soldTickets: '',
|
|
revenue: ''
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
status: await row.locator('td:nth-child(1)').textContent() || '',
|
|
name: await row.locator('td:nth-child(2)').textContent() || '',
|
|
date: await row.locator('td:nth-child(3)').textContent() || '',
|
|
organizer: await row.locator('td:nth-child(4)').textContent() || '',
|
|
capacity: await row.locator('td:nth-child(5)').textContent() || '',
|
|
soldTickets: await row.locator('td:nth-child(6)').textContent() || '',
|
|
revenue: await row.locator('td:nth-child(7)').textContent() || ''
|
|
};
|
|
}
|
|
|
|
async getEventCount(): Promise<number> {
|
|
const rows = await this.page.locator(`${this.eventsTable} tbody tr`).count();
|
|
// Check if the only row is "No events found"
|
|
if (rows === 1) {
|
|
const firstRow = await this.page.locator(`${this.eventsTable} tbody tr`).first();
|
|
const cellCount = await firstRow.locator('td').count();
|
|
if (cellCount === 1) {
|
|
const text = await firstRow.locator('td').textContent();
|
|
if (text?.includes('No events found')) {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
async clickEventName(eventName: string): Promise<void> {
|
|
await this.page.click(`${this.eventsTable} a:has-text("${eventName}")`);
|
|
await this.waitForNavigation();
|
|
}
|
|
} |