diff --git a/frontend/e2e/admin-dashboard.spec.ts b/frontend/e2e/admin-dashboard.spec.ts index 40b0daf..05b3769 100644 --- a/frontend/e2e/admin-dashboard.spec.ts +++ b/frontend/e2e/admin-dashboard.spec.ts @@ -114,13 +114,19 @@ test.describe('Admin Dashboard - Analytics Charts', () => { }); test('should display user growth chart', async ({ page }) => { - await expect(page.getByText('User Growth')).toBeVisible(); + // Scroll to charts section and wait for it to load + const chartsHeading = page.getByRole('heading', { name: 'Analytics Overview' }); + await chartsHeading.scrollIntoViewIfNeeded(); + await page.waitForTimeout(500); // Wait for any lazy-loaded components + + const userGrowthHeading = page.getByText('User Growth'); + await expect(userGrowthHeading).toBeVisible({ timeout: 10000 }); await expect(page.getByText('Total and active users over the last 30 days')).toBeVisible(); }); - test('should display session activity chart', async ({ page }) => { - await expect(page.getByText('Session Activity')).toBeVisible(); - await expect(page.getByText('Active and new sessions over the last 14 days')).toBeVisible(); + test('should display registration activity chart', async ({ page }) => { + await expect(page.getByText('User Registration Activity')).toBeVisible(); + await expect(page.getByText('New user registrations over the last 14 days')).toBeVisible(); }); test('should display organization distribution chart', async ({ page }) => { @@ -134,16 +140,21 @@ test.describe('Admin Dashboard - Analytics Charts', () => { }); test('should display all four charts in grid layout', async ({ page }) => { + // Scroll to charts section and wait for lazy-loaded components + const chartsHeading = page.getByRole('heading', { name: 'Analytics Overview' }); + await chartsHeading.scrollIntoViewIfNeeded(); + await page.waitForTimeout(500); + // All charts should be visible const userGrowthChart = page.getByText('User Growth'); - const sessionActivityChart = page.getByText('Session Activity'); + const registrationActivityChart = page.getByText('User Registration Activity'); const orgDistributionChart = page.getByText('Organization Distribution'); const userStatusChart = page.getByText('User Status Distribution'); - await expect(userGrowthChart).toBeVisible(); - await expect(sessionActivityChart).toBeVisible(); - await expect(orgDistributionChart).toBeVisible(); - await expect(userStatusChart).toBeVisible(); + await expect(userGrowthChart).toBeVisible({ timeout: 10000 }); + await expect(registrationActivityChart).toBeVisible({ timeout: 10000 }); + await expect(orgDistributionChart).toBeVisible({ timeout: 10000 }); + await expect(userStatusChart).toBeVisible({ timeout: 10000 }); }); }); diff --git a/frontend/e2e/auth-guard.spec.ts b/frontend/e2e/auth-guard.spec.ts index 05ceb3b..638852a 100644 --- a/frontend/e2e/auth-guard.spec.ts +++ b/frontend/e2e/auth-guard.spec.ts @@ -43,10 +43,9 @@ test.describe('AuthGuard - Route Protection', () => { await expect(page.locator('h2')).toContainText('Reset your password'); }); - test('should persist authentication across page reloads', async ({ page }) => { - // Manually set a mock token in localStorage for testing - await page.goto('/en'); - await page.evaluate(() => { + test('should persist authentication across page reloads', async ({ page, context }) => { + // Set localStorage before navigation using context + await context.addInitScript(() => { const mockToken = { access_token: 'mock-access-token', refresh_token: 'mock-refresh-token', @@ -61,8 +60,13 @@ test.describe('AuthGuard - Route Protection', () => { localStorage.setItem('auth_token', JSON.stringify(mockToken)); }); + // Now navigate - localStorage will already be set + await page.goto('/en'); + await page.waitForLoadState('networkidle'); + // Reload the page await page.reload(); + await page.waitForLoadState('networkidle'); // Should still have the token const hasToken = await page.evaluate(() => { @@ -72,8 +76,11 @@ test.describe('AuthGuard - Route Protection', () => { }); test('should clear authentication on logout', async ({ page }) => { - // Set up authenticated state + // Navigate first without any auth await page.goto('/en'); + await page.waitForLoadState('networkidle'); + + // Now inject auth token after page is loaded await page.evaluate(() => { const mockToken = { access_token: 'mock-access-token', @@ -89,8 +96,11 @@ test.describe('AuthGuard - Route Protection', () => { localStorage.setItem('auth_token', JSON.stringify(mockToken)); }); - // Reload to apply token - await page.reload(); + // Verify token was set + const hasToken = await page.evaluate(() => { + return localStorage.getItem('auth_token') !== null; + }); + expect(hasToken).toBe(true); // Simulate logout by clearing storage await page.evaluate(() => { @@ -100,18 +110,18 @@ test.describe('AuthGuard - Route Protection', () => { // Reload page await page.reload(); + await page.waitForLoadState('networkidle'); - // Storage should be clear - const hasToken = await page.evaluate(() => { + // Storage should be clear after reload + const tokenCleared = await page.evaluate(() => { return localStorage.getItem('auth_token') === null; }); - expect(hasToken).toBe(true); + expect(tokenCleared).toBe(true); }); - test('should not allow access to auth pages when already logged in', async ({ page }) => { - // Set up authenticated state - await page.goto('/en'); - await page.evaluate(() => { + test('should not allow access to auth pages when already logged in', async ({ page, context }) => { + // Set up authenticated state before navigation + await context.addInitScript(() => { const mockToken = { access_token: 'mock-access-token', refresh_token: 'mock-refresh-token', @@ -128,6 +138,7 @@ test.describe('AuthGuard - Route Protection', () => { // Try to access login page await page.goto('/en/login'); + await page.waitForLoadState('networkidle'); // Wait a bit for potential redirect await page.waitForTimeout(2000); @@ -139,10 +150,9 @@ test.describe('AuthGuard - Route Protection', () => { expect(currentUrl).toBeTruthy(); }); - test('should handle expired tokens gracefully', async ({ page }) => { - // Set up authenticated state with expired token - await page.goto('/en'); - await page.evaluate(() => { + test('should handle expired tokens gracefully', async ({ page, context }) => { + // Set up authenticated state with expired token before navigation + await context.addInitScript(() => { const expiredToken = { access_token: 'expired-access-token', refresh_token: 'expired-refresh-token', @@ -159,7 +169,8 @@ test.describe('AuthGuard - Route Protection', () => { // Try to access a protected route // Backend should return 401, triggering logout - await page.reload(); + await page.goto('/en'); + await page.waitForLoadState('networkidle'); // Wait for potential redirect to login await page.waitForTimeout(3000); @@ -168,13 +179,12 @@ test.describe('AuthGuard - Route Protection', () => { // This depends on token refresh logic }); - test('should preserve intended destination after login', async ({ page }) => { + test('should preserve intended destination after login', async ({ page, context }) => { // This is a nice-to-have feature that requires protected routes // For now, just verify the test doesn't crash - await page.goto('/en'); // Login (via localStorage for testing) - await page.evaluate(() => { + await context.addInitScript(() => { const mockToken = { access_token: 'mock-access-token', refresh_token: 'mock-refresh-token', @@ -189,9 +199,9 @@ test.describe('AuthGuard - Route Protection', () => { localStorage.setItem('auth_token', JSON.stringify(mockToken)); }); - // Reload page - await page.reload(); - await page.waitForTimeout(1000); + // Navigate with auth already set + await page.goto('/en'); + await page.waitForLoadState('networkidle'); // Verify page loaded successfully expect(page.url()).toBeTruthy(); diff --git a/frontend/e2e/helpers/auth.ts b/frontend/e2e/helpers/auth.ts index faa7b45..f368fce 100644 --- a/frontend/e2e/helpers/auth.ts +++ b/frontend/e2e/helpers/auth.ts @@ -353,17 +353,50 @@ export async function setupSuperuserMocks(page: Page): Promise { } }); - // Mock GET /api/v1/admin/stats - Get dashboard statistics + // Mock GET /api/v1/admin/stats - Get dashboard statistics with chart data await page.route(`${baseURL}/api/v1/admin/stats`, async (route: Route) => { if (route.request().method() === 'GET') { + // Generate user growth data for last 30 days + const userGrowth = []; + const today = new Date(); + for (let i = 29; i >= 0; i--) { + const date = new Date(today); + date.setDate(date.getDate() - i); + userGrowth.push({ + date: date.toISOString().split('T')[0], + total_users: 50 + Math.floor((29 - i) * 1.5), + active_users: Math.floor((50 + (29 - i) * 1.5) * 0.8), + }); + } + + // Generate registration activity for last 14 days + const registrationActivity = []; + for (let i = 13; i >= 0; i--) { + const date = new Date(today); + date.setDate(date.getDate() - i); + registrationActivity.push({ + date: date.toISOString().split('T')[0], + count: Math.floor(Math.random() * 5) + 1, + }); + } + await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ - total_users: 150, - active_users: 120, - total_organizations: 25, - active_sessions: 45, + user_growth: userGrowth, + registration_activity: registrationActivity, + organization_distribution: [ + { name: 'Acme Corporation', value: 12 }, + { name: 'Tech Innovators', value: 8 }, + { name: 'Global Solutions Inc', value: 25 }, + { name: 'Startup Ventures', value: 5 }, + { name: 'Inactive Corp', value: 3 }, + ], + user_status: [ + { name: 'Active', value: 89 }, + { name: 'Inactive', value: 11 }, + ], }), }); } else { diff --git a/frontend/e2e/homepage.spec.ts b/frontend/e2e/homepage.spec.ts index 3d0b64e..885b228 100644 --- a/frontend/e2e/homepage.spec.ts +++ b/frontend/e2e/homepage.spec.ts @@ -227,22 +227,25 @@ test.describe('Homepage - Hero Section', () => { }); test('should display main headline', async ({ page }) => { - await expect( - page.getByRole('heading', { name: /Everything You Need to Build/i }).first() - ).toBeVisible(); - await expect(page.getByText(/Modern Web Applications/i).first()).toBeVisible(); + await expect(page.getByRole('heading', { name: /The Pragmatic/i }).first()).toBeVisible(); + await expect(page.getByRole('heading', { name: /Full-Stack Template/i }).first()).toBeVisible(); }); test('should display badge with key highlights', async ({ page }) => { await expect(page.getByText('MIT Licensed').first()).toBeVisible(); - await expect(page.getByText(/97% Test Coverage/).first()).toBeVisible(); - await expect(page.getByText('Production Ready').first()).toBeVisible(); + await expect(page.getByText('Comprehensive Tests').first()).toBeVisible(); + await expect(page.getByText('Pragmatic by Design').first()).toBeVisible(); }); - test('should display test coverage stats', async ({ page }) => { - await expect(page.getByText('97%').first()).toBeVisible(); - await expect(page.getByText('743').first()).toBeVisible(); - await expect(page.getByText(/Passing Tests/).first()).toBeVisible(); + test('should display quality stats section', async ({ page }) => { + // Scroll to stats section to trigger animations + const statsSection = page.getByText('Built with Quality in Mind').first(); + await statsSection.scrollIntoViewIfNeeded(); + await expect(statsSection).toBeVisible(); + + // Wait for animated counter to render (it starts at 0 and counts up) + await page.waitForTimeout(500); + await expect(page.getByText('Open Source').first()).toBeVisible(); }); test('should navigate to GitHub when clicking View on GitHub', async ({ page }) => { @@ -444,19 +447,17 @@ test.describe('Homepage - Feature Sections', () => { }); test('should display tech stack section', async ({ page }) => { - await expect( - page.getByRole('heading', { name: /Modern, Type-Safe, Production-Grade Stack/i }) - ).toBeVisible(); + await expect(page.getByRole('heading', { name: /A Stack You Can Trust/i })).toBeVisible(); // Check for key technologies await expect(page.getByText('FastAPI').first()).toBeVisible(); - await expect(page.getByText('Next.js 15').first()).toBeVisible(); + await expect(page.getByText('Next.js').first()).toBeVisible(); await expect(page.getByText('PostgreSQL').first()).toBeVisible(); }); test('should display philosophy section', async ({ page }) => { - await expect(page.getByRole('heading', { name: /Why This Template Exists/i })).toBeVisible(); - await expect(page.getByText(/Free forever, MIT licensed/i)).toBeVisible(); + await expect(page.getByRole('heading', { name: /Why PragmaStack/i })).toBeVisible(); + await expect(page.getByText(/MIT licensed/i).first()).toBeVisible(); }); });