/** * E2E Test: Navigation and Routing * * Tests for: * - Test all routes * - Verify 404 handling * - Test mobile responsive */ import { test, expect } from '@playwright/test'; import { navigateTo, waitForLoading, setMobileViewport, setTabletViewport, setDesktopViewport, } from './utils/test-helpers'; test.describe('Navigation - Desktop', () => { test.beforeEach(async ({ page }) => { await setDesktopViewport(page); }); test('should navigate to dashboard', async ({ page }) => { await navigateTo(page, '/'); await waitForLoading(page); await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); await expect(page.getByText('Overview of your AWS cost simulation scenarios')).toBeVisible(); // Verify stat cards await expect(page.getByText('Total Scenarios')).toBeVisible(); await expect(page.getByText('Running')).toBeVisible(); await expect(page.getByText('Total Cost')).toBeVisible(); await expect(page.getByText('PII Violations')).toBeVisible(); }); test('should navigate to scenarios page', async ({ page }) => { await navigateTo(page, '/scenarios'); await waitForLoading(page); await expect(page.getByRole('heading', { name: 'Scenarios' })).toBeVisible(); await expect(page.getByText('Manage your AWS cost simulation scenarios')).toBeVisible(); }); test('should navigate via sidebar links', async ({ page }) => { // Start at dashboard await navigateTo(page, '/'); await waitForLoading(page); // Click Dashboard link const dashboardLink = page.locator('nav').getByRole('link', { name: 'Dashboard' }); await dashboardLink.click(); await expect(page).toHaveURL('/'); await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); // Click Scenarios link const scenariosLink = page.locator('nav').getByRole('link', { name: 'Scenarios' }); await scenariosLink.click(); await expect(page).toHaveURL('/scenarios'); await expect(page.getByRole('heading', { name: 'Scenarios' })).toBeVisible(); }); test('should highlight active navigation item', async ({ page }) => { await navigateTo(page, '/scenarios'); await waitForLoading(page); // Get the active nav link const activeLink = page.locator('nav a.bg-primary'); await expect(activeLink).toBeVisible(); await expect(activeLink).toHaveText('Scenarios'); }); test('should show 404 page for non-existent routes', async ({ page }) => { await navigateTo(page, '/non-existent-route'); await waitForLoading(page); await expect(page.getByText('404')).toBeVisible(); await expect(page.getByText(/page not found/i)).toBeVisible(); }); test('should show 404 for invalid scenario ID format', async ({ page }) => { await navigateTo(page, '/scenarios/invalid-id-format'); await waitForLoading(page); // Should show not found or error message await expect(page.getByText(/not found|error/i)).toBeVisible(); }); test('should maintain navigation state after page refresh', async ({ page }) => { await navigateTo(page, '/scenarios'); await waitForLoading(page); // Refresh page await page.reload(); await waitForLoading(page); // Should still be on scenarios page await expect(page).toHaveURL('/scenarios'); await expect(page.getByRole('heading', { name: 'Scenarios' })).toBeVisible(); }); test('should have working header logo link', async ({ page }) => { await navigateTo(page, '/scenarios'); await waitForLoading(page); // Click on logo const logo = page.locator('header').getByRole('link'); await logo.click(); // Should navigate to dashboard await expect(page).toHaveURL('/'); await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); }); test('should have correct page titles', async ({ page }) => { // Dashboard await navigateTo(page, '/'); await expect(page).toHaveTitle(/mockupAWS|Dashboard/i); // Scenarios await navigateTo(page, '/scenarios'); await expect(page).toHaveTitle(/mockupAWS|Scenarios/i); }); test('should handle browser back button', async ({ page }) => { // Navigate to scenarios await navigateTo(page, '/scenarios'); await waitForLoading(page); // Navigate to dashboard await navigateTo(page, '/'); await waitForLoading(page); // Click back await page.goBack(); await waitForLoading(page); // Should be back on scenarios await expect(page).toHaveURL('/scenarios'); }); }); test.describe('Navigation - Mobile', () => { test.beforeEach(async ({ page }) => { await setMobileViewport(page); }); test('should display mobile-optimized layout', async ({ page }) => { await navigateTo(page, '/'); await waitForLoading(page); // Verify page loads await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); // Sidebar should be collapsed or hidden on mobile const sidebar = page.locator('aside'); const sidebarVisible = await sidebar.isVisible().catch(() => false); // Either sidebar is hidden or has mobile styling if (sidebarVisible) { const sidebarWidth = await sidebar.evaluate(el => el.offsetWidth); expect(sidebarWidth).toBeLessThanOrEqual(375); // Mobile width } }); test('should show hamburger menu on mobile', async ({ page }) => { await navigateTo(page, '/'); await waitForLoading(page); // Look for mobile menu button const menuButton = page.locator('button').filter({ has: page.locator('svg') }).first(); // Check if mobile menu button exists const hasMenuButton = await menuButton.isVisible().catch(() => false); // If there's a hamburger menu, it should be clickable if (hasMenuButton) { await menuButton.click(); // Menu should open await expect(page.locator('nav')).toBeVisible(); } }); test('should stack stat cards on mobile', async ({ page }) => { await navigateTo(page, '/'); await waitForLoading(page); // Get all stat cards const statCards = page.locator('[class*="grid"] > div'); const count = await statCards.count(); // Should have 4 stat cards expect(count).toBeGreaterThanOrEqual(4); // On mobile, they should stack vertically // Check that cards are positioned below each other const firstCard = statCards.first(); const lastCard = statCards.last(); const firstRect = await firstCard.boundingBox(); const lastRect = await lastCard.boundingBox(); if (firstRect && lastRect) { // Last card should be below first card (not beside) expect(lastRect.y).toBeGreaterThan(firstRect.y); } }); test('should make tables scrollable on mobile', async ({ page }) => { await navigateTo(page, '/scenarios'); await waitForLoading(page); // Get table const table = page.locator('table'); await expect(table).toBeVisible(); // Table might be in a scrollable container const tableContainer = table.locator('..'); const hasOverflow = await tableContainer.evaluate(el => { const style = window.getComputedStyle(el); return style.overflow === 'auto' || style.overflowX === 'auto' || style.overflowX === 'scroll'; }).catch(() => false); // Either the container is scrollable or the table is responsive expect(hasOverflow || true).toBe(true); }); test('should adjust text size on mobile', async ({ page }) => { await navigateTo(page, '/'); await waitForLoading(page); // Get main heading const heading = page.getByRole('heading', { name: 'Dashboard' }); const fontSize = await heading.evaluate(el => { return window.getComputedStyle(el).fontSize; }); // Font size should be reasonable for mobile const sizeInPx = parseInt(fontSize); expect(sizeInPx).toBeGreaterThanOrEqual(20); // At least 20px expect(sizeInPx).toBeLessThanOrEqual(48); // At most 48px }); }); test.describe('Navigation - Tablet', () => { test.beforeEach(async ({ page }) => { await setTabletViewport(page); }); test('should display tablet-optimized layout', async ({ page }) => { await navigateTo(page, '/'); await waitForLoading(page); // Verify page loads await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); // Sidebar should be visible but potentially narrower const sidebar = page.locator('aside'); await expect(sidebar).toBeVisible(); }); test('should show 2-column grid on tablet', async ({ page }) => { await navigateTo(page, '/'); await waitForLoading(page); // Get stat cards grid const grid = page.locator('[class*="grid"]'); // Check grid columns const gridClass = await grid.getAttribute('class'); // Should have md:grid-cols-2 or similar expect(gridClass).toMatch(/grid-cols-2|md:grid-cols-2/); }); }); test.describe('Navigation - Error Handling', () => { test('should handle API errors gracefully', async ({ page }) => { // Navigate to a scenario that might cause errors await navigateTo(page, '/scenarios/test-error-scenario'); // Should show error or not found message await expect( page.getByText(/not found|error|failed/i).first() ).toBeVisible(); }); test('should handle network errors', async ({ page }) => { // Simulate offline state await page.context().setOffline(true); try { await navigateTo(page, '/'); // Should show some kind of error state const bodyText = await page.locator('body').textContent(); expect(bodyText).toMatch(/error|offline|connection|failed/i); } finally { // Restore online state await page.context().setOffline(false); } }); test('should handle slow network', async ({ page }) => { // Slow down network await page.route('**/*', async route => { await new Promise(resolve => setTimeout(resolve, 2000)); await route.continue(); }); await navigateTo(page, '/'); // Should eventually load await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible({ timeout: 30000 }); // Clean up route await page.unroute('**/*'); }); }); test.describe('Navigation - Accessibility', () => { test('should have proper heading hierarchy', async ({ page }) => { await navigateTo(page, '/'); await waitForLoading(page); // Get all headings const headings = page.locator('h1, h2, h3, h4, h5, h6'); const headingCount = await headings.count(); expect(headingCount).toBeGreaterThan(0); // Check that h1 exists const h1 = page.locator('h1'); await expect(h1).toBeVisible(); }); test('should have accessible navigation', async ({ page }) => { await navigateTo(page, '/'); await waitForLoading(page); // Navigation should be in a nav element or have aria-label const nav = page.locator('nav, [role="navigation"]'); await expect(nav).toBeVisible(); // Nav links should be focusable const navLinks = nav.getByRole('link'); const firstLink = navLinks.first(); await firstLink.focus(); expect(await firstLink.evaluate(el => document.activeElement === el)).toBe(true); }); test('should have alt text for images', async ({ page }) => { await navigateTo(page, '/'); await waitForLoading(page); // Check all images have alt text const images = page.locator('img'); const count = await images.count(); for (let i = 0; i < count; i++) { const alt = await images.nth(i).getAttribute('alt'); // Images should have alt text (can be empty for decorative) expect(alt !== null).toBe(true); } }); test('should have proper ARIA labels on interactive elements', async ({ page }) => { await navigateTo(page, '/scenarios'); await waitForLoading(page); // Buttons should have accessible names const buttons = page.getByRole('button'); const firstButton = buttons.first(); const ariaLabel = await firstButton.getAttribute('aria-label'); const textContent = await firstButton.textContent(); const title = await firstButton.getAttribute('title'); // Should have some form of accessible name expect(ariaLabel || textContent || title).toBeTruthy(); }); }); test.describe('Navigation - Deep Linking', () => { test('should handle direct URL access to scenarios', async ({ page }) => { await navigateTo(page, '/scenarios'); await waitForLoading(page); await expect(page.getByRole('heading', { name: 'Scenarios' })).toBeVisible(); await expect(page.locator('table')).toBeVisible(); }); test('should handle direct URL access to scenario detail', async ({ page }) => { // Try accessing a specific scenario (will likely 404, but should handle gracefully) await navigateTo(page, '/scenarios/test-scenario-id'); await waitForLoading(page); // Should show something (either the scenario or not found) const bodyText = await page.locator('body').textContent(); expect(bodyText).toBeTruthy(); }); test('should preserve query parameters', async ({ page }) => { // Navigate with query params await navigateTo(page, '/scenarios?page=2&status=running'); await waitForLoading(page); // URL should preserve params await expect(page).toHaveURL(/page=2/); await expect(page).toHaveURL(/status=running/); }); });