Refactor e2e tests for improved reliability and consistency

- Updated `auth-guard.spec.ts` to configure localStorage before navigation using `context.addInitScript`.
- Enhanced test stability with explicit `waitForLoadState` calls after page reloads.
- Refactored `admin-dashboard.spec.ts` for more descriptive test names aligning with chart updates. Adjusted lazy-loading behavior in the analytics section.
- Reworked `homepage.spec.ts` tests to improve headline and badge visibility checks. Added scroll-triggered animation handling for stats section.
- Enhanced MSW handler in `auth.ts` with mock data for user growth and registration activity charts. Added organization and user status distribution data.
This commit is contained in:
Felipe Cardoso
2025-11-24 20:55:04 +01:00
parent 6b970765ba
commit 570848cc2d
4 changed files with 110 additions and 55 deletions

View File

@@ -114,13 +114,19 @@ test.describe('Admin Dashboard - Analytics Charts', () => {
}); });
test('should display user growth chart', async ({ page }) => { 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(); await expect(page.getByText('Total and active users over the last 30 days')).toBeVisible();
}); });
test('should display session activity chart', async ({ page }) => { test('should display registration activity chart', async ({ page }) => {
await expect(page.getByText('Session Activity')).toBeVisible(); await expect(page.getByText('User Registration Activity')).toBeVisible();
await expect(page.getByText('Active and new sessions over the last 14 days')).toBeVisible(); await expect(page.getByText('New user registrations over the last 14 days')).toBeVisible();
}); });
test('should display organization distribution chart', async ({ page }) => { 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 }) => { 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 // All charts should be visible
const userGrowthChart = page.getByText('User Growth'); 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 orgDistributionChart = page.getByText('Organization Distribution');
const userStatusChart = page.getByText('User Status Distribution'); const userStatusChart = page.getByText('User Status Distribution');
await expect(userGrowthChart).toBeVisible(); await expect(userGrowthChart).toBeVisible({ timeout: 10000 });
await expect(sessionActivityChart).toBeVisible(); await expect(registrationActivityChart).toBeVisible({ timeout: 10000 });
await expect(orgDistributionChart).toBeVisible(); await expect(orgDistributionChart).toBeVisible({ timeout: 10000 });
await expect(userStatusChart).toBeVisible(); await expect(userStatusChart).toBeVisible({ timeout: 10000 });
}); });
}); });

View File

@@ -43,10 +43,9 @@ test.describe('AuthGuard - Route Protection', () => {
await expect(page.locator('h2')).toContainText('Reset your password'); await expect(page.locator('h2')).toContainText('Reset your password');
}); });
test('should persist authentication across page reloads', async ({ page }) => { test('should persist authentication across page reloads', async ({ page, context }) => {
// Manually set a mock token in localStorage for testing // Set localStorage before navigation using context
await page.goto('/en'); await context.addInitScript(() => {
await page.evaluate(() => {
const mockToken = { const mockToken = {
access_token: 'mock-access-token', access_token: 'mock-access-token',
refresh_token: 'mock-refresh-token', refresh_token: 'mock-refresh-token',
@@ -61,8 +60,13 @@ test.describe('AuthGuard - Route Protection', () => {
localStorage.setItem('auth_token', JSON.stringify(mockToken)); 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 // Reload the page
await page.reload(); await page.reload();
await page.waitForLoadState('networkidle');
// Should still have the token // Should still have the token
const hasToken = await page.evaluate(() => { const hasToken = await page.evaluate(() => {
@@ -72,8 +76,11 @@ test.describe('AuthGuard - Route Protection', () => {
}); });
test('should clear authentication on logout', async ({ page }) => { test('should clear authentication on logout', async ({ page }) => {
// Set up authenticated state // Navigate first without any auth
await page.goto('/en'); await page.goto('/en');
await page.waitForLoadState('networkidle');
// Now inject auth token after page is loaded
await page.evaluate(() => { await page.evaluate(() => {
const mockToken = { const mockToken = {
access_token: 'mock-access-token', access_token: 'mock-access-token',
@@ -89,8 +96,11 @@ test.describe('AuthGuard - Route Protection', () => {
localStorage.setItem('auth_token', JSON.stringify(mockToken)); localStorage.setItem('auth_token', JSON.stringify(mockToken));
}); });
// Reload to apply token // Verify token was set
await page.reload(); const hasToken = await page.evaluate(() => {
return localStorage.getItem('auth_token') !== null;
});
expect(hasToken).toBe(true);
// Simulate logout by clearing storage // Simulate logout by clearing storage
await page.evaluate(() => { await page.evaluate(() => {
@@ -100,18 +110,18 @@ test.describe('AuthGuard - Route Protection', () => {
// Reload page // Reload page
await page.reload(); await page.reload();
await page.waitForLoadState('networkidle');
// Storage should be clear // Storage should be clear after reload
const hasToken = await page.evaluate(() => { const tokenCleared = await page.evaluate(() => {
return localStorage.getItem('auth_token') === null; 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 }) => { test('should not allow access to auth pages when already logged in', async ({ page, context }) => {
// Set up authenticated state // Set up authenticated state before navigation
await page.goto('/en'); await context.addInitScript(() => {
await page.evaluate(() => {
const mockToken = { const mockToken = {
access_token: 'mock-access-token', access_token: 'mock-access-token',
refresh_token: 'mock-refresh-token', refresh_token: 'mock-refresh-token',
@@ -128,6 +138,7 @@ test.describe('AuthGuard - Route Protection', () => {
// Try to access login page // Try to access login page
await page.goto('/en/login'); await page.goto('/en/login');
await page.waitForLoadState('networkidle');
// Wait a bit for potential redirect // Wait a bit for potential redirect
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
@@ -139,10 +150,9 @@ test.describe('AuthGuard - Route Protection', () => {
expect(currentUrl).toBeTruthy(); expect(currentUrl).toBeTruthy();
}); });
test('should handle expired tokens gracefully', async ({ page }) => { test('should handle expired tokens gracefully', async ({ page, context }) => {
// Set up authenticated state with expired token // Set up authenticated state with expired token before navigation
await page.goto('/en'); await context.addInitScript(() => {
await page.evaluate(() => {
const expiredToken = { const expiredToken = {
access_token: 'expired-access-token', access_token: 'expired-access-token',
refresh_token: 'expired-refresh-token', refresh_token: 'expired-refresh-token',
@@ -159,7 +169,8 @@ test.describe('AuthGuard - Route Protection', () => {
// Try to access a protected route // Try to access a protected route
// Backend should return 401, triggering logout // Backend should return 401, triggering logout
await page.reload(); await page.goto('/en');
await page.waitForLoadState('networkidle');
// Wait for potential redirect to login // Wait for potential redirect to login
await page.waitForTimeout(3000); await page.waitForTimeout(3000);
@@ -168,13 +179,12 @@ test.describe('AuthGuard - Route Protection', () => {
// This depends on token refresh logic // 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 // This is a nice-to-have feature that requires protected routes
// For now, just verify the test doesn't crash // For now, just verify the test doesn't crash
await page.goto('/en');
// Login (via localStorage for testing) // Login (via localStorage for testing)
await page.evaluate(() => { await context.addInitScript(() => {
const mockToken = { const mockToken = {
access_token: 'mock-access-token', access_token: 'mock-access-token',
refresh_token: 'mock-refresh-token', refresh_token: 'mock-refresh-token',
@@ -189,9 +199,9 @@ test.describe('AuthGuard - Route Protection', () => {
localStorage.setItem('auth_token', JSON.stringify(mockToken)); localStorage.setItem('auth_token', JSON.stringify(mockToken));
}); });
// Reload page // Navigate with auth already set
await page.reload(); await page.goto('/en');
await page.waitForTimeout(1000); await page.waitForLoadState('networkidle');
// Verify page loaded successfully // Verify page loaded successfully
expect(page.url()).toBeTruthy(); expect(page.url()).toBeTruthy();

View File

@@ -353,17 +353,50 @@ export async function setupSuperuserMocks(page: Page): Promise<void> {
} }
}); });
// 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) => { await page.route(`${baseURL}/api/v1/admin/stats`, async (route: Route) => {
if (route.request().method() === 'GET') { 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({ await route.fulfill({
status: 200, status: 200,
contentType: 'application/json', contentType: 'application/json',
body: JSON.stringify({ body: JSON.stringify({
total_users: 150, user_growth: userGrowth,
active_users: 120, registration_activity: registrationActivity,
total_organizations: 25, organization_distribution: [
active_sessions: 45, { 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 { } else {

View File

@@ -227,22 +227,25 @@ test.describe('Homepage - Hero Section', () => {
}); });
test('should display main headline', async ({ page }) => { test('should display main headline', async ({ page }) => {
await expect( await expect(page.getByRole('heading', { name: /The Pragmatic/i }).first()).toBeVisible();
page.getByRole('heading', { name: /Everything You Need to Build/i }).first() await expect(page.getByRole('heading', { name: /Full-Stack Template/i }).first()).toBeVisible();
).toBeVisible();
await expect(page.getByText(/Modern Web Applications/i).first()).toBeVisible();
}); });
test('should display badge with key highlights', async ({ page }) => { test('should display badge with key highlights', async ({ page }) => {
await expect(page.getByText('MIT Licensed').first()).toBeVisible(); await expect(page.getByText('MIT Licensed').first()).toBeVisible();
await expect(page.getByText(/97% Test Coverage/).first()).toBeVisible(); await expect(page.getByText('Comprehensive Tests').first()).toBeVisible();
await expect(page.getByText('Production Ready').first()).toBeVisible(); await expect(page.getByText('Pragmatic by Design').first()).toBeVisible();
}); });
test('should display test coverage stats', async ({ page }) => { test('should display quality stats section', async ({ page }) => {
await expect(page.getByText('97%').first()).toBeVisible(); // Scroll to stats section to trigger animations
await expect(page.getByText('743').first()).toBeVisible(); const statsSection = page.getByText('Built with Quality in Mind').first();
await expect(page.getByText(/Passing Tests/).first()).toBeVisible(); 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 }) => { 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 }) => { test('should display tech stack section', async ({ page }) => {
await expect( await expect(page.getByRole('heading', { name: /A Stack You Can Trust/i })).toBeVisible();
page.getByRole('heading', { name: /Modern, Type-Safe, Production-Grade Stack/i })
).toBeVisible();
// Check for key technologies // Check for key technologies
await expect(page.getByText('FastAPI').first()).toBeVisible(); 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(); await expect(page.getByText('PostgreSQL').first()).toBeVisible();
}); });
test('should display philosophy section', async ({ page }) => { test('should display philosophy section', async ({ page }) => {
await expect(page.getByRole('heading', { name: /Why This Template Exists/i })).toBeVisible(); await expect(page.getByRole('heading', { name: /Why PragmaStack/i })).toBeVisible();
await expect(page.getByText(/Free forever, MIT licensed/i)).toBeVisible(); await expect(page.getByText(/MIT licensed/i).first()).toBeVisible();
}); });
}); });