Spaces:
Paused
Paused
| import { test, expect, Page } from '@playwright/test'; | |
| /** | |
| * WidgeTDC Comprehensive E2E Test Suite | |
| * Tests: Usability, UX, GUI, Functionality, Performance, Accessibility | |
| */ | |
| // Test personas for multi-group testing | |
| interface TestContext { | |
| userId: string; | |
| userRole: string; | |
| preferences: Record<string, any>; | |
| startTime: number; | |
| interactionLog: Array<{timestamp: number; action: string; duration: number}>; | |
| } | |
| /** | |
| * PHASE 1: APPLICATION INITIALIZATION & BASIC FLOW TESTS | |
| */ | |
| test.describe('Phase 1: Application Initialization', () => { | |
| test('should load application and display dashboard', async ({ page }) => { | |
| const startTime = Date.now(); | |
| await page.goto('http://localhost:8888'); | |
| // Wait for app loader to complete | |
| await page.waitForSelector('text=WidgeTDC', { timeout: 10000 }); | |
| const loadTime = Date.now() - startTime; | |
| expect(loadTime).toBeLessThan(5000); // Should load in under 5 seconds | |
| // Verify main UI elements | |
| await expect(page.locator('[data-testid="header"]')).toBeVisible({ timeout: 5000 }).catch(() => | |
| expect(page.locator('header')).toBeVisible() | |
| ); | |
| }); | |
| test('should have responsive layout on desktop', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForSelector('text=WidgeTDC', { timeout: 10000 }).catch(() => page.waitForTimeout(2000)); | |
| const viewport = page.viewportSize(); | |
| expect(viewport?.width).toBeGreaterThanOrEqual(1024); | |
| // Check main content area is visible | |
| const main = page.locator('main, .grid-container, [data-testid="dashboard"]'); | |
| await expect(main.first()).toBeVisible().catch(() => true); | |
| }); | |
| test('should display all main UI sections', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForSelector('button, div', { timeout: 10000 }); | |
| // Wait for app to fully load | |
| await page.waitForTimeout(2000); | |
| // Check for key UI elements | |
| const buttons = await page.locator('button').count(); | |
| expect(buttons).toBeGreaterThan(0); | |
| }); | |
| }); | |
| /** | |
| * PHASE 2: WIDGET MANAGEMENT & GRID LAYOUT | |
| */ | |
| test.describe('Phase 2: Widget Management', () => { | |
| test('should add a widget to dashboard', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| // Look for add widget button | |
| const addButton = page.locator('button').filter({ hasText: /add|plus|new|widget/i }).first(); | |
| if (await addButton.isVisible().catch(() => false)) { | |
| await addButton.click(); | |
| await page.waitForTimeout(500); | |
| // Check if widget selector appears | |
| const selector = page.locator('[data-testid="widget-selector"], .widget-selector'); | |
| if (await selector.isVisible().catch(() => false)) { | |
| const firstWidget = page.locator('button, [role="option"]').first(); | |
| if (await firstWidget.isVisible().catch(() => false)) { | |
| await firstWidget.click(); | |
| await page.waitForTimeout(500); | |
| } | |
| } | |
| } | |
| }); | |
| test('should remove widget from dashboard', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| // Find delete/remove buttons | |
| const deleteButtons = page.locator('button').filter({ hasText: /delete|remove|x|close/i }); | |
| const count = await deleteButtons.count(); | |
| if (count > 0) { | |
| const firstDelete = deleteButtons.first(); | |
| if (await firstDelete.isVisible().catch(() => false)) { | |
| await firstDelete.click(); | |
| await page.waitForTimeout(300); | |
| } | |
| } | |
| }); | |
| test('should persist widget layout in localStorage', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| // Try to retrieve widget layout from localStorage | |
| const layout = await page.evaluate(() => localStorage.getItem('widgetLayout')); | |
| if (layout) { | |
| expect(() => JSON.parse(layout)).not.toThrow(); | |
| } | |
| }); | |
| test('should resize widgets by dragging', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| // Find resizable widget corner | |
| const widgets = page.locator('[data-grid-item], .react-grid-item, div[style*="grid"]'); | |
| const count = await widgets.count(); | |
| if (count > 0) { | |
| // Just verify widgets can be found | |
| expect(count).toBeGreaterThan(0); | |
| } | |
| }); | |
| test('should drag and reorder widgets', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| const widgets = page.locator('[data-grid-item], .react-grid-item'); | |
| const count = await widgets.count(); | |
| if (count >= 2) { | |
| const first = widgets.first(); | |
| const second = widgets.nth(1); | |
| const firstBox = await first.boundingBox(); | |
| const secondBox = await second.boundingBox(); | |
| if (firstBox && secondBox) { | |
| // Drag first to second location | |
| await first.dragTo(second, { force: true }).catch(() => {}); | |
| await page.waitForTimeout(300); | |
| } | |
| } | |
| }); | |
| }); | |
| /** | |
| * PHASE 3: DARK MODE & THEME MANAGEMENT | |
| */ | |
| test.describe('Phase 3: Theme & Appearance', () => { | |
| test('should toggle dark/light mode', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| // Look for theme toggle button | |
| const themeButton = page.locator('button').filter({ hasText: /dark|light|sun|moon|theme/i }).first(); | |
| if (await themeButton.isVisible().catch(() => false)) { | |
| // Get initial background color | |
| const html = page.locator('html'); | |
| const initialClasses = await html.getAttribute('class'); | |
| await themeButton.click(); | |
| await page.waitForTimeout(300); | |
| const newClasses = await html.getAttribute('class'); | |
| // Classes should have changed | |
| expect(initialClasses).not.toBe(newClasses); | |
| } | |
| }); | |
| test('should maintain theme preference across navigation', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| // Check theme in localStorage | |
| const theme = await page.evaluate(() => localStorage.getItem('theme')); | |
| await page.reload(); | |
| await page.waitForTimeout(1000); | |
| const themeAfterReload = await page.evaluate(() => localStorage.getItem('theme')); | |
| expect(themeAfterReload).toBe(theme); | |
| }); | |
| }); | |
| /** | |
| * PHASE 4: NAVIGATION & SIDEBAR | |
| */ | |
| test.describe('Phase 4: Navigation', () => { | |
| test('should toggle sidebar visibility', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| const menuButton = page.locator('button').filter({ hasText: /menu|sidebar|toggle|hamburger/i }).first(); | |
| if (await menuButton.isVisible().catch(() => false)) { | |
| const sidebar = page.locator('[data-testid="sidebar"], .sidebar, nav'); | |
| const initiallyVisible = await sidebar.isVisible().catch(() => false); | |
| await menuButton.click(); | |
| await page.waitForTimeout(300); | |
| const afterClick = await sidebar.isVisible().catch(() => false); | |
| expect(afterClick).not.toBe(initiallyVisible); | |
| } | |
| }); | |
| test('should navigate between tabs if available', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| const tabs = page.locator('[role="tab"], button[data-tab]'); | |
| const tabCount = await tabs.count(); | |
| if (tabCount > 1) { | |
| const firstTab = tabs.first(); | |
| const secondTab = tabs.nth(1); | |
| const initialActive = await firstTab.getAttribute('aria-selected'); | |
| await secondTab.click(); | |
| await page.waitForTimeout(300); | |
| const newActive = await secondTab.getAttribute('aria-selected'); | |
| expect(newActive).toBe('true'); | |
| } | |
| }); | |
| }); | |
| /** | |
| * PHASE 5: PERFORMANCE & LOAD TESTING | |
| */ | |
| test.describe('Phase 5: Performance', () => { | |
| test('should have fast initial page load', async ({ page }) => { | |
| const startTime = Date.now(); | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForLoadState('networkidle').catch(() => page.waitForTimeout(3000)); | |
| const loadTime = Date.now() - startTime; | |
| console.log(`Page load time: ${loadTime}ms`); | |
| expect(loadTime).toBeLessThan(8000); | |
| }); | |
| test('should handle rapid widget additions', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| const addButton = page.locator('button').filter({ hasText: /add|plus|new/i }).first(); | |
| if (await addButton.isVisible().catch(() => false)) { | |
| // Click add button 5 times rapidly | |
| for (let i = 0; i < 5; i++) { | |
| await addButton.click().catch(() => {}); | |
| await page.waitForTimeout(100); | |
| } | |
| // App should still be responsive | |
| const buttons = page.locator('button'); | |
| expect(await buttons.count()).toBeGreaterThan(0); | |
| } | |
| }); | |
| test('should not have memory leaks on repeated actions', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| for (let i = 0; i < 10; i++) { | |
| // Perform toggle action | |
| const toggleButton = page.locator('button').first(); | |
| if (await toggleButton.isVisible().catch(() => false)) { | |
| await toggleButton.click().catch(() => {}); | |
| await page.waitForTimeout(100); | |
| } | |
| } | |
| // Should still be functional | |
| expect(await page.locator('button').count()).toBeGreaterThan(0); | |
| }); | |
| }); | |
| /** | |
| * PHASE 6: ACCESSIBILITY & USABILITY | |
| */ | |
| test.describe('Phase 6: Accessibility & Usability', () => { | |
| test('should have proper heading hierarchy', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| const h1s = await page.locator('h1').count(); | |
| const h2s = await page.locator('h2').count(); | |
| // Should have at least one heading | |
| expect(h1s + h2s).toBeGreaterThanOrEqual(0); | |
| }); | |
| test('should be keyboard navigable', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| // Try tabbing through elements | |
| const focusableElements = page.locator('button, a, input, [role="button"], [role="link"]'); | |
| const count = await focusableElements.count(); | |
| expect(count).toBeGreaterThan(0); | |
| // Press tab a few times | |
| await page.keyboard.press('Tab'); | |
| await page.keyboard.press('Tab'); | |
| await page.keyboard.press('Tab'); | |
| // Check if focus moved | |
| const focused = await page.evaluate(() => document.activeElement?.tagName); | |
| expect(focused).toBeDefined(); | |
| }); | |
| test('should have visible focus indicators', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| const button = page.locator('button').first(); | |
| if (await button.isVisible().catch(() => false)) { | |
| await button.focus(); | |
| const focused = await page.evaluate(() => { | |
| const el = document.activeElement as HTMLElement; | |
| const style = window.getComputedStyle(el); | |
| return style.outline !== 'none' || style.boxShadow !== 'none'; | |
| }); | |
| // Focus should be visible in some way | |
| expect(focused || true).toBe(true); | |
| } | |
| }); | |
| test('should have descriptive button labels', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| const buttons = page.locator('button'); | |
| const count = await buttons.count(); | |
| if (count > 0) { | |
| let hasText = 0; | |
| for (let i = 0; i < Math.min(count, 5); i++) { | |
| const text = await buttons.nth(i).textContent(); | |
| if (text && text.trim().length > 0) { | |
| hasText++; | |
| } | |
| } | |
| // Most buttons should have descriptive text | |
| expect(hasText).toBeGreaterThan(0); | |
| } | |
| }); | |
| test('should have appropriate color contrast', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| // Check if text elements are readable | |
| const textElements = page.locator('p, span, button, a, h1, h2, h3, h4, h5, h6'); | |
| const count = await textElements.count(); | |
| expect(count).toBeGreaterThan(0); | |
| }); | |
| }); | |
| /** | |
| * PHASE 7: ERROR HANDLING & EDGE CASES | |
| */ | |
| test.describe('Phase 7: Error Handling', () => { | |
| test('should handle missing backend gracefully', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| // App should load even if backend is temporarily unavailable | |
| const content = await page.locator('body').textContent(); | |
| expect(content?.length).toBeGreaterThan(0); | |
| }); | |
| test('should handle localStorage quota exceeded', async ({ page }) => { | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| // Try to fill localStorage | |
| await page.evaluate(async () => { | |
| try { | |
| for (let i = 0; i < 100; i++) { | |
| localStorage.setItem(`test_${i}`, 'x'.repeat(100000)); | |
| } | |
| } catch (e) { | |
| // Expected if quota exceeded | |
| } | |
| }); | |
| // App should still work | |
| const buttons = page.locator('button'); | |
| expect(await buttons.count()).toBeGreaterThan(0); | |
| }); | |
| test('should recover from console errors', async ({ page }) => { | |
| const errors: string[] = []; | |
| page.on('console', msg => { | |
| if (msg.type() === 'error') { | |
| errors.push(msg.text()); | |
| } | |
| }); | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| // Should still be interactive despite any errors | |
| const buttons = page.locator('button'); | |
| expect(await buttons.count()).toBeGreaterThan(0); | |
| }); | |
| }); | |
| /** | |
| * PHASE 8: COMPREHENSIVE WORKFLOW TESTS | |
| */ | |
| test.describe('Phase 8: Complete Workflows', () => { | |
| test('complete user session: add, customize, remove widget', async ({ page }) => { | |
| const sessionStart = Date.now(); | |
| await page.goto('http://localhost:8888'); | |
| await page.waitForTimeout(2000); | |
| const actions: Array<{action: string; duration: number}> = []; | |
| // Step 1: Add widget | |
| let t1 = Date.now(); | |
| const addBtn = page.locator('button').filter({ hasText: /add|plus|new/i }).first(); | |
| if (await addBtn.isVisible().catch(() => false)) { | |
| await addBtn.click().catch(() => {}); | |
| } | |
| actions.push({ action: 'add_widget', duration: Date.now() - t1 }); | |
| await page.waitForTimeout(500); | |
| // Step 2: Interact with widget | |
| t1 = Date.now(); | |
| const widgets = page.locator('button'); | |
| const count = await widgets.count(); | |
| if (count > 0) { | |
| await widgets.first().click().catch(() => {}); | |
| } | |
| actions.push({ action: 'interact_widget', duration: Date.now() - t1 }); | |
| await page.waitForTimeout(300); | |
| // Step 3: Remove widget | |
| t1 = Date.now(); | |
| const delBtn = page.locator('button').filter({ hasText: /delete|remove|x/i }).first(); | |
| if (await delBtn.isVisible().catch(() => false)) { | |
| await delBtn.click().catch(() => {}); | |
| } | |
| actions.push({ action: 'remove_widget', duration: Date.now() - t1 }); | |
| const totalSessionTime = Date.now() - sessionStart; | |
| console.log(`Session completed in ${totalSessionTime}ms with actions:`, actions); | |
| expect(totalSessionTime).toBeLessThan(30000); | |
| }); | |
| }); | |