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 }) => {
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 });
});
});

View File

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

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) => {
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 {

View File

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