Some checks are pending
HVAC Plugin CI/CD Pipeline / Security Analysis (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Code Quality & Standards (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Unit Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Integration Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Deploy to Staging (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Deploy to Production (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Notification (push) Blocked by required conditions
Security Monitoring & Compliance / Dependency Vulnerability Scan (push) Waiting to run
Security Monitoring & Compliance / Secrets & Credential Scan (push) Waiting to run
Security Monitoring & Compliance / WordPress Security Analysis (push) Waiting to run
Security Monitoring & Compliance / Static Code Security Analysis (push) Waiting to run
Security Monitoring & Compliance / Security Compliance Validation (push) Waiting to run
Security Monitoring & Compliance / Security Summary Report (push) Blocked by required conditions
Security Monitoring & Compliance / Security Team Notification (push) Blocked by required conditions
- Add 90+ test files including E2E, unit, and integration tests - Implement Page Object Model (POM) architecture - Add Docker testing environment with comprehensive services - Include modernized test framework with error recovery - Add specialized test suites for master trainer and trainer workflows - Update .gitignore to properly track test infrastructure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
535 lines
No EOL
20 KiB
JavaScript
535 lines
No EOL
20 KiB
JavaScript
// @ts-check
|
|
const { test, expect } = require('@playwright/test');
|
|
|
|
/**
|
|
* E2E Tests for HVAC Trainer Announcements System
|
|
*
|
|
* Tests the complete user journey for:
|
|
* - Master trainers creating and managing announcements
|
|
* - Regular trainers viewing announcements
|
|
* - Email notifications
|
|
* - Security and permissions
|
|
*/
|
|
|
|
// Test configuration
|
|
const BASE_URL = process.env.UPSKILL_STAGING_URL || 'https://upskill-staging.measurequick.com';
|
|
const ADMIN_USER = process.env.STAGING_ADMIN_USER || 'benr';
|
|
const ADMIN_PASS = process.env.STAGING_ADMIN_PASS || 'Xy8$kP#mN2@Lq9Z!';
|
|
|
|
// Test users
|
|
const MASTER_TRAINER = {
|
|
username: 'JoeMedosch@gmail.com',
|
|
password: 'JoeTrainer2025@',
|
|
displayName: 'Joe Medosch'
|
|
};
|
|
|
|
const REGULAR_TRAINER = {
|
|
username: 'test_trainer',
|
|
password: 'TestTrainer123!',
|
|
displayName: 'Test Trainer'
|
|
};
|
|
|
|
// Helper function to login
|
|
async function loginAsUser(page, username, password) {
|
|
await page.goto(`${BASE_URL}/wp-login.php`);
|
|
await page.fill('#user_login', username);
|
|
await page.fill('#user_pass', password);
|
|
await page.click('#wp-submit');
|
|
await page.waitForURL(/\/wp-admin|\/trainer\//);
|
|
}
|
|
|
|
// Helper function to navigate to trainer page
|
|
async function navigateToTrainerPage(page, path) {
|
|
await page.goto(`${BASE_URL}/trainer/${path}/`);
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
|
|
test.describe('HVAC Announcements - Master Trainer Features', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginAsUser(page, MASTER_TRAINER.username, MASTER_TRAINER.password);
|
|
});
|
|
|
|
test('Master trainer can access announcements management page', async ({ page }) => {
|
|
// Navigate to master announcements page
|
|
await navigateToTrainerPage(page, 'master-trainer/manage-announcements');
|
|
|
|
// Verify page loaded
|
|
await expect(page.locator('h1')).toContainText('Manage Announcements');
|
|
|
|
// Verify admin interface elements
|
|
await expect(page.locator('#announcements-admin-container')).toBeVisible();
|
|
await expect(page.locator('.announcement-header')).toBeVisible();
|
|
await expect(page.locator('#announcement-list')).toBeVisible();
|
|
|
|
// Verify action buttons
|
|
await expect(page.locator('#new-announcement-btn')).toBeVisible();
|
|
await expect(page.locator('#new-announcement-btn')).toHaveText('New Announcement');
|
|
});
|
|
|
|
test('Master trainer can create a new announcement', async ({ page }) => {
|
|
await navigateToTrainerPage(page, 'master-trainer/manage-announcements');
|
|
|
|
// Click new announcement button
|
|
await page.click('#new-announcement-btn');
|
|
|
|
// Wait for form to appear
|
|
await expect(page.locator('#announcement-form')).toBeVisible();
|
|
|
|
// Fill in announcement details
|
|
const testTitle = `E2E Test Announcement ${Date.now()}`;
|
|
const testContent = 'This is a test announcement created by Playwright E2E tests.';
|
|
const testExcerpt = 'Test excerpt for E2E';
|
|
|
|
await page.fill('#announcement-title', testTitle);
|
|
|
|
// Handle rich text editor (TinyMCE or plain textarea)
|
|
const editorFrame = page.frameLocator('#announcement-content_ifr');
|
|
const hasIframe = await editorFrame.locator('body').count() > 0;
|
|
|
|
if (hasIframe) {
|
|
// TinyMCE editor
|
|
await editorFrame.locator('body').fill(testContent);
|
|
} else {
|
|
// Plain textarea
|
|
await page.fill('#announcement-content', testContent);
|
|
}
|
|
|
|
await page.fill('#announcement-excerpt', testExcerpt);
|
|
|
|
// Select status
|
|
await page.selectOption('#announcement-status', 'draft');
|
|
|
|
// Add categories (if available)
|
|
const categoryCheckboxes = await page.locator('.category-checkbox').count();
|
|
if (categoryCheckboxes > 0) {
|
|
await page.locator('.category-checkbox').first().check();
|
|
}
|
|
|
|
// Add tags
|
|
await page.fill('#announcement-tags', 'e2e-test, playwright, automated');
|
|
|
|
// Save announcement
|
|
await page.click('#save-announcement-btn');
|
|
|
|
// Wait for success message
|
|
await expect(page.locator('.notice-success')).toBeVisible({ timeout: 10000 });
|
|
await expect(page.locator('.notice-success')).toContainText(/created|saved|success/i);
|
|
|
|
// Verify announcement appears in list
|
|
await expect(page.locator('#announcement-list')).toContainText(testTitle);
|
|
});
|
|
|
|
test('Master trainer can edit an existing announcement', async ({ page }) => {
|
|
await navigateToTrainerPage(page, 'master-trainer/manage-announcements');
|
|
|
|
// Wait for announcements to load
|
|
await page.waitForSelector('.announcement-item', { timeout: 10000 });
|
|
|
|
// Click edit on first announcement
|
|
const firstAnnouncement = page.locator('.announcement-item').first();
|
|
await firstAnnouncement.locator('.edit-announcement').click();
|
|
|
|
// Wait for form to load with data
|
|
await expect(page.locator('#announcement-form')).toBeVisible();
|
|
await expect(page.locator('#announcement-title')).not.toBeEmpty();
|
|
|
|
// Edit the title
|
|
const currentTitle = await page.locator('#announcement-title').inputValue();
|
|
const updatedTitle = currentTitle + ' (Updated)';
|
|
await page.fill('#announcement-title', updatedTitle);
|
|
|
|
// Update status to publish
|
|
await page.selectOption('#announcement-status', 'publish');
|
|
|
|
// Save changes
|
|
await page.click('#save-announcement-btn');
|
|
|
|
// Wait for success message
|
|
await expect(page.locator('.notice-success')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify updated title in list
|
|
await expect(page.locator('#announcement-list')).toContainText(updatedTitle);
|
|
});
|
|
|
|
test('Master trainer can delete an announcement', async ({ page }) => {
|
|
await navigateToTrainerPage(page, 'master-trainer/manage-announcements');
|
|
|
|
// Wait for announcements to load
|
|
await page.waitForSelector('.announcement-item', { timeout: 10000 });
|
|
|
|
// Count initial announcements
|
|
const initialCount = await page.locator('.announcement-item').count();
|
|
|
|
// Find an announcement with "E2E Test" in title (if exists)
|
|
const testAnnouncements = page.locator('.announcement-item:has-text("E2E Test")');
|
|
const hasTestAnnouncement = await testAnnouncements.count() > 0;
|
|
|
|
if (hasTestAnnouncement) {
|
|
// Click delete on test announcement
|
|
await testAnnouncements.first().locator('.delete-announcement').click();
|
|
|
|
// Confirm deletion
|
|
page.on('dialog', dialog => dialog.accept());
|
|
|
|
// Wait for announcement to be removed
|
|
await expect(page.locator('.notice-success')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify count decreased
|
|
const newCount = await page.locator('.announcement-item').count();
|
|
expect(newCount).toBeLessThan(initialCount);
|
|
}
|
|
});
|
|
|
|
test('Master trainer can filter announcements by status', async ({ page }) => {
|
|
await navigateToTrainerPage(page, 'master-trainer/manage-announcements');
|
|
|
|
// Wait for filter controls
|
|
await expect(page.locator('#announcement-status-filter')).toBeVisible();
|
|
|
|
// Filter by published
|
|
await page.selectOption('#announcement-status-filter', 'publish');
|
|
await page.waitForTimeout(1000); // Wait for filter to apply
|
|
|
|
// Verify only published announcements shown
|
|
const statusBadges = await page.locator('.status-badge').allTextContents();
|
|
statusBadges.forEach(status => {
|
|
expect(status.toLowerCase()).toContain('publish');
|
|
});
|
|
|
|
// Filter by draft
|
|
await page.selectOption('#announcement-status-filter', 'draft');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Verify only draft announcements shown (if any)
|
|
const draftBadges = await page.locator('.status-badge').allTextContents();
|
|
if (draftBadges.length > 0) {
|
|
draftBadges.forEach(status => {
|
|
expect(status.toLowerCase()).toContain('draft');
|
|
});
|
|
}
|
|
});
|
|
|
|
test('Master trainer can search announcements', async ({ page }) => {
|
|
await navigateToTrainerPage(page, 'master-trainer/manage-announcements');
|
|
|
|
// Wait for search box
|
|
await expect(page.locator('#announcement-search')).toBeVisible();
|
|
|
|
// Search for specific term
|
|
await page.fill('#announcement-search', 'training');
|
|
await page.press('#announcement-search', 'Enter');
|
|
|
|
// Wait for search results
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Verify search results contain search term (if any results)
|
|
const searchResults = await page.locator('.announcement-item').count();
|
|
if (searchResults > 0) {
|
|
const titles = await page.locator('.announcement-title').allTextContents();
|
|
const contents = await page.locator('.announcement-excerpt').allTextContents();
|
|
|
|
const allText = [...titles, ...contents].join(' ').toLowerCase();
|
|
expect(allText).toContain('training');
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('HVAC Announcements - Regular Trainer Features', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginAsUser(page, REGULAR_TRAINER.username, REGULAR_TRAINER.password);
|
|
});
|
|
|
|
test('Regular trainer can view announcements on resources page', async ({ page }) => {
|
|
// Navigate to resources page
|
|
await navigateToTrainerPage(page, 'resources');
|
|
|
|
// Verify page loaded
|
|
await expect(page.locator('h1')).toContainText('Training Resources');
|
|
|
|
// Verify announcements timeline is visible
|
|
await expect(page.locator('.hvac-announcements-timeline')).toBeVisible();
|
|
|
|
// Verify at least one announcement is displayed (if any published)
|
|
const announcementCount = await page.locator('.timeline-item').count();
|
|
if (announcementCount > 0) {
|
|
// Verify announcement structure
|
|
await expect(page.locator('.timeline-item').first()).toBeVisible();
|
|
await expect(page.locator('.timeline-title').first()).toBeVisible();
|
|
await expect(page.locator('.timeline-date').first()).toBeVisible();
|
|
await expect(page.locator('.timeline-excerpt').first()).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('Regular trainer can click to view announcement details', async ({ page }) => {
|
|
await navigateToTrainerPage(page, 'resources');
|
|
|
|
// Wait for announcements to load
|
|
const announcements = await page.locator('.announcement-link').count();
|
|
|
|
if (announcements > 0) {
|
|
// Click first announcement
|
|
await page.locator('.announcement-link').first().click();
|
|
|
|
// Wait for modal to appear
|
|
await expect(page.locator('#announcement-modal')).toBeVisible();
|
|
await expect(page.locator('.modal-content')).toBeVisible();
|
|
|
|
// Verify modal content
|
|
await expect(page.locator('.announcement-full')).toBeVisible();
|
|
await expect(page.locator('.announcement-header h2')).toBeVisible();
|
|
await expect(page.locator('.announcement-content')).toBeVisible();
|
|
|
|
// Close modal
|
|
await page.locator('.modal-close').click();
|
|
await expect(page.locator('#announcement-modal')).not.toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('Regular trainer cannot access management page', async ({ page }) => {
|
|
// Try to access master announcements page
|
|
const response = await page.goto(`${BASE_URL}/trainer/master-trainer/manage-announcements/`);
|
|
|
|
// Should be redirected or show error
|
|
expect(response.status()).not.toBe(200);
|
|
|
|
// Verify error message or redirect
|
|
const url = page.url();
|
|
expect(url).not.toContain('/manage-announcements');
|
|
});
|
|
|
|
test('Regular trainer can use load more functionality', async ({ page }) => {
|
|
await navigateToTrainerPage(page, 'resources');
|
|
|
|
// Check if load more button exists
|
|
const hasLoadMore = await page.locator('.load-more-announcements').count() > 0;
|
|
|
|
if (hasLoadMore) {
|
|
// Count initial announcements
|
|
const initialCount = await page.locator('.timeline-item').count();
|
|
|
|
// Click load more
|
|
await page.click('.load-more-announcements');
|
|
|
|
// Wait for new announcements to load
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Verify more announcements loaded
|
|
const newCount = await page.locator('.timeline-item').count();
|
|
expect(newCount).toBeGreaterThan(initialCount);
|
|
}
|
|
});
|
|
|
|
test('Regular trainer can view Google Drive resources', async ({ page }) => {
|
|
await navigateToTrainerPage(page, 'resources');
|
|
|
|
// Check if Google Drive embed exists
|
|
const hasDriveEmbed = await page.locator('.hvac-google-drive-embed').count() > 0;
|
|
|
|
if (hasDriveEmbed) {
|
|
// Verify iframe is present
|
|
await expect(page.locator('.hvac-google-drive-embed iframe')).toBeVisible();
|
|
|
|
// Verify iframe has correct attributes
|
|
const iframe = page.locator('.hvac-google-drive-embed iframe');
|
|
await expect(iframe).toHaveAttribute('src', /drive\.google\.com/);
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('HVAC Announcements - Security and Permissions', () => {
|
|
test('Unauthenticated user cannot access announcements', async ({ page }) => {
|
|
// Try to access resources page without login
|
|
await page.goto(`${BASE_URL}/trainer/resources/`);
|
|
|
|
// Should be redirected to login
|
|
await expect(page).toHaveURL(/wp-login\.php/);
|
|
});
|
|
|
|
test('Subscriber cannot view announcements', async ({ page }) => {
|
|
// Create or use a subscriber account
|
|
// This would need a subscriber test account to be set up
|
|
// For now, we'll test that non-trainer roles are blocked
|
|
|
|
// Attempt to access resources page directly
|
|
const response = await page.goto(`${BASE_URL}/trainer/resources/`);
|
|
|
|
// Should not have access
|
|
if (response.ok()) {
|
|
// If page loads, verify no announcements shown
|
|
const hasPermissionError = await page.locator(':text("You do not have permission")').count() > 0;
|
|
expect(hasPermissionError).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
test('XSS prevention in announcement content', async ({ page }) => {
|
|
await loginAsUser(page, MASTER_TRAINER.username, MASTER_TRAINER.password);
|
|
await navigateToTrainerPage(page, 'master-trainer/manage-announcements');
|
|
|
|
// Create announcement with potential XSS
|
|
await page.click('#new-announcement-btn');
|
|
await page.fill('#announcement-title', 'XSS Test <script>alert("XSS")</script>');
|
|
await page.fill('#announcement-content', '<img src=x onerror="alert(\'XSS\')">Test content');
|
|
await page.fill('#announcement-excerpt', '<script>alert("XSS")</script>Excerpt');
|
|
await page.selectOption('#announcement-status', 'publish');
|
|
await page.click('#save-announcement-btn');
|
|
|
|
// Wait for save
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Now view as regular trainer
|
|
await loginAsUser(page, REGULAR_TRAINER.username, REGULAR_TRAINER.password);
|
|
await navigateToTrainerPage(page, 'resources');
|
|
|
|
// Verify no script execution
|
|
page.on('dialog', dialog => {
|
|
// Fail test if any alert appears
|
|
expect(dialog.message()).not.toContain('XSS');
|
|
dialog.dismiss();
|
|
});
|
|
|
|
// Check that dangerous content is escaped
|
|
const pageContent = await page.content();
|
|
expect(pageContent).not.toContain('<script>alert');
|
|
expect(pageContent).not.toContain('onerror=');
|
|
});
|
|
});
|
|
|
|
test.describe('HVAC Announcements - Responsive Design', () => {
|
|
[
|
|
{ name: 'Mobile', width: 375, height: 667 },
|
|
{ name: 'Tablet', width: 768, height: 1024 },
|
|
{ name: 'Desktop', width: 1920, height: 1080 }
|
|
].forEach(({ name, width, height }) => {
|
|
test(`Announcements display correctly on ${name}`, async ({ page }) => {
|
|
// Set viewport
|
|
await page.setViewportSize({ width, height });
|
|
|
|
await loginAsUser(page, REGULAR_TRAINER.username, REGULAR_TRAINER.password);
|
|
await navigateToTrainerPage(page, 'resources');
|
|
|
|
// Verify timeline is responsive
|
|
await expect(page.locator('.hvac-announcements-timeline')).toBeVisible();
|
|
|
|
// Check if elements stack properly on mobile
|
|
if (width < 768) {
|
|
// Mobile layout checks
|
|
const timelineItems = await page.locator('.timeline-item').count();
|
|
if (timelineItems > 0) {
|
|
const item = page.locator('.timeline-item').first();
|
|
const box = await item.boundingBox();
|
|
expect(box.width).toBeLessThanOrEqual(width - 40); // Account for padding
|
|
}
|
|
}
|
|
|
|
// Test modal on different screen sizes
|
|
const hasAnnouncements = await page.locator('.announcement-link').count() > 0;
|
|
if (hasAnnouncements) {
|
|
await page.locator('.announcement-link').first().click();
|
|
|
|
// Verify modal is responsive
|
|
const modal = page.locator('.modal-content');
|
|
await expect(modal).toBeVisible();
|
|
|
|
const modalBox = await modal.boundingBox();
|
|
expect(modalBox.width).toBeLessThanOrEqual(width);
|
|
|
|
// Close modal
|
|
await page.locator('.modal-close').click();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
test.describe('HVAC Announcements - Performance', () => {
|
|
test('Announcements page loads within acceptable time', async ({ page }) => {
|
|
const startTime = Date.now();
|
|
|
|
await loginAsUser(page, REGULAR_TRAINER.username, REGULAR_TRAINER.password);
|
|
await navigateToTrainerPage(page, 'resources');
|
|
|
|
// Wait for main content to load
|
|
await expect(page.locator('.hvac-announcements-timeline')).toBeVisible();
|
|
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
// Page should load within 5 seconds
|
|
expect(loadTime).toBeLessThan(5000);
|
|
});
|
|
|
|
test('Pagination loads efficiently', async ({ page }) => {
|
|
await loginAsUser(page, REGULAR_TRAINER.username, REGULAR_TRAINER.password);
|
|
await navigateToTrainerPage(page, 'resources');
|
|
|
|
const hasLoadMore = await page.locator('.load-more-announcements').count() > 0;
|
|
|
|
if (hasLoadMore) {
|
|
const startTime = Date.now();
|
|
|
|
// Click load more
|
|
await page.click('.load-more-announcements');
|
|
|
|
// Wait for new content
|
|
await page.waitForTimeout(1000);
|
|
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
// Additional content should load within 2 seconds
|
|
expect(loadTime).toBeLessThan(2000);
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('HVAC Announcements - Accessibility', () => {
|
|
test('Announcements are keyboard navigable', async ({ page }) => {
|
|
await loginAsUser(page, REGULAR_TRAINER.username, REGULAR_TRAINER.password);
|
|
await navigateToTrainerPage(page, 'resources');
|
|
|
|
// Tab through announcement links
|
|
const announcements = await page.locator('.announcement-link').count();
|
|
|
|
if (announcements > 0) {
|
|
// Focus first announcement with Tab
|
|
await page.keyboard.press('Tab');
|
|
|
|
// Keep tabbing until we reach an announcement link
|
|
for (let i = 0; i < 20; i++) {
|
|
const focused = await page.evaluate(() => document.activeElement.className);
|
|
if (focused.includes('announcement-link')) {
|
|
break;
|
|
}
|
|
await page.keyboard.press('Tab');
|
|
}
|
|
|
|
// Press Enter to open modal
|
|
await page.keyboard.press('Enter');
|
|
|
|
// Verify modal opened
|
|
await expect(page.locator('#announcement-modal')).toBeVisible();
|
|
|
|
// Press Escape to close
|
|
await page.keyboard.press('Escape');
|
|
|
|
// Verify modal closed
|
|
await expect(page.locator('#announcement-modal')).not.toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('Announcements have proper ARIA labels', async ({ page }) => {
|
|
await loginAsUser(page, REGULAR_TRAINER.username, REGULAR_TRAINER.password);
|
|
await navigateToTrainerPage(page, 'resources');
|
|
|
|
// Check for ARIA attributes
|
|
const hasAnnouncements = await page.locator('.timeline-item').count() > 0;
|
|
|
|
if (hasAnnouncements) {
|
|
// Check article elements have proper roles
|
|
const articles = page.locator('.timeline-item');
|
|
await expect(articles.first()).toHaveAttribute('role', 'article');
|
|
|
|
// Check links have accessible text
|
|
const link = page.locator('.announcement-link').first();
|
|
const linkText = await link.textContent();
|
|
expect(linkText).not.toBeEmpty();
|
|
}
|
|
});
|
|
}); |