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();
 | |
|     }
 | |
|   });
 | |
| }); |