veza/apps/web/e2e/navigation.spec.ts
2026-01-07 19:39:21 +01:00

283 lines
11 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test';
import {
TEST_CONFIG,
loginAsUser,
setupErrorCapture,
navigateViaHref,
} from './utils/test-helpers';
/**
* Navigation E2E Test Suite
*
* Tests the complete navigation flow of the application:
* - Sidebar navigation
* - Route guards (protected routes)
* - Deep linking
* - Browser back/forward navigation
* - Active route highlighting
* - Mobile navigation (responsive)
*/
test.describe('Navigation Flow', () => {
let consoleErrors: string[] = [];
let networkErrors: Array<{ url: string; status: number; method: string }> = [];
test.beforeEach(async ({ page }) => {
const errorCapture = setupErrorCapture(page);
consoleErrors = errorCapture.consoleErrors;
networkErrors = errorCapture.networkErrors;
});
test.describe('Authenticated Navigation', () => {
test.beforeEach(async ({ page }) => {
await loginAsUser(page);
});
test('should navigate to dashboard from sidebar', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/library`);
await page.waitForLoadState('networkidle');
// Click dashboard link in sidebar
const dashboardLink = page.locator('nav a[href="/dashboard"], nav a[href="/"]').first();
await expect(dashboardLink).toBeVisible();
await dashboardLink.click();
await expect(page).toHaveURL(new RegExp(`${TEST_CONFIG.FRONTEND_URL}/?(dashboard)?$`));
});
test('should navigate to library from sidebar', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
await page.waitForLoadState('networkidle');
const libraryLink = page.locator('nav a[href="/library"]').first();
await expect(libraryLink).toBeVisible();
await libraryLink.click();
await expect(page).toHaveURL(new RegExp(`${TEST_CONFIG.FRONTEND_URL}/library`));
});
test('should navigate to playlists from sidebar', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
await page.waitForLoadState('networkidle');
const playlistsLink = page.locator('nav a[href="/playlists"]').first();
await expect(playlistsLink).toBeVisible();
await playlistsLink.click();
await expect(page).toHaveURL(new RegExp(`${TEST_CONFIG.FRONTEND_URL}/playlists`));
});
test('should navigate to profile from sidebar', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
await page.waitForLoadState('networkidle');
// Profile link might be in a dropdown menu
const profileLink = page.locator('nav a[href*="/profile"], nav a[href*="/user"]').first();
if (await profileLink.isVisible({ timeout: 2000 }).catch(() => false)) {
await profileLink.click();
await expect(page).toHaveURL(new RegExp(`${TEST_CONFIG.FRONTEND_URL}/(profile|user)`));
} else {
// Try clicking avatar/user menu first
const userMenu = page.locator('button[aria-label*="user"], button[aria-label*="menu"], [data-testid="user-menu"]').first();
if (await userMenu.isVisible({ timeout: 2000 }).catch(() => false)) {
await userMenu.click();
const profileLinkInMenu = page.locator('a[href*="/profile"], a[href*="/user"]').first();
await expect(profileLinkInMenu).toBeVisible({ timeout: 5000 });
await profileLinkInMenu.click();
await expect(page).toHaveURL(new RegExp(`${TEST_CONFIG.FRONTEND_URL}/(profile|user)`));
}
}
});
test('should highlight active route in sidebar', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/library`);
await page.waitForLoadState('networkidle');
// Check if library link has active state
const libraryLink = page.locator('nav a[href="/library"]').first();
const isActive = await libraryLink.evaluate((el) => {
return el.classList.contains('active') ||
el.getAttribute('aria-current') === 'page' ||
el.closest('[aria-current="page"]') !== null;
});
// Some apps use different active indicators, so we just check it's visible
await expect(libraryLink).toBeVisible();
});
test('should support browser back navigation', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
await page.waitForLoadState('networkidle');
// Navigate to library
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/library`);
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(new RegExp(`${TEST_CONFIG.FRONTEND_URL}/library`));
// Go back
await page.goBack();
await page.waitForLoadState('networkidle');
// Should be back on dashboard (or previous page)
const currentUrl = page.url();
expect(currentUrl).toMatch(new RegExp(`${TEST_CONFIG.FRONTEND_URL}/(dashboard|library)?`));
});
test('should support browser forward navigation', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
await page.waitForLoadState('networkidle');
// Navigate to library
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/library`);
await page.waitForLoadState('networkidle');
// Go back
await page.goBack();
await page.waitForLoadState('networkidle');
// Go forward
await page.goForward();
await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(new RegExp(`${TEST_CONFIG.FRONTEND_URL}/library`));
});
test('should support deep linking to protected routes', async ({ page }) => {
// Direct navigation to a protected route
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/library`);
await page.waitForLoadState('networkidle');
// Should be able to access the route (already authenticated)
await expect(page).toHaveURL(new RegExp(`${TEST_CONFIG.FRONTEND_URL}/library`));
// Page should be loaded (not showing login)
const loginForm = page.locator('form[action*="login"], input[type="email"]');
await expect(loginForm).not.toBeVisible({ timeout: 2000 });
});
});
test.describe('Unauthenticated Navigation', () => {
// Reset storage state to ensure we're not authenticated
test.use({ storageState: { cookies: [], origins: [] } });
test('should redirect to login when accessing protected route', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/library`);
await page.waitForLoadState('networkidle');
// Should redirect to login
await expect(page).toHaveURL(new RegExp(`${TEST_CONFIG.FRONTEND_URL}/(login|auth/login)`));
});
test('should allow access to public routes', async ({ page }) => {
// Try to access login page
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/login`);
await page.waitForLoadState('networkidle');
// Should be on login page
const loginForm = page.locator('form[action*="login"], input[type="email"]').first();
await expect(loginForm).toBeVisible({ timeout: 5000 });
});
test('should allow access to register page', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/register`);
await page.waitForLoadState('networkidle');
// Should be on register page
const registerForm = page.locator('form[action*="register"], input[name*="email"]').first();
await expect(registerForm).toBeVisible({ timeout: 5000 });
});
});
test.describe('Mobile Navigation', () => {
test.beforeEach(async ({ page }) => {
await loginAsUser(page);
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
});
test('should show mobile menu when hamburger is clicked', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
await page.waitForLoadState('networkidle');
// Look for hamburger menu button
const hamburgerButton = page.locator('button[aria-label*="menu"], button[aria-label*="navigation"], [data-testid="mobile-menu-button"]').first();
if (await hamburgerButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await hamburgerButton.click();
// Menu should be visible
const mobileMenu = page.locator('nav[aria-label*="mobile"], nav[data-testid="mobile-nav"]').first();
await expect(mobileMenu).toBeVisible({ timeout: 3000 });
} else {
// Mobile menu might not be implemented, skip test
test.skip();
}
});
test('should navigate from mobile menu', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
await page.waitForLoadState('networkidle');
const hamburgerButton = page.locator('button[aria-label*="menu"], button[aria-label*="navigation"]').first();
if (await hamburgerButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await hamburgerButton.click();
// Click library link in mobile menu
const libraryLink = page.locator('nav a[href="/library"]').first();
await expect(libraryLink).toBeVisible({ timeout: 3000 });
await libraryLink.click();
await expect(page).toHaveURL(new RegExp(`${TEST_CONFIG.FRONTEND_URL}/library`));
} else {
test.skip();
}
});
});
test.describe('Error Handling', () => {
test.beforeEach(async ({ page }) => {
await loginAsUser(page);
});
test('should handle 404 pages gracefully', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/non-existent-page-12345`);
await page.waitForLoadState('networkidle');
// Should show 404 page or redirect to dashboard
const currentUrl = page.url();
const has404Content = await page.locator('text=404, text=Not Found, text=Page not found').first().isVisible({ timeout: 2000 }).catch(() => false);
const redirectedToDashboard = currentUrl.includes('/dashboard') || currentUrl === `${TEST_CONFIG.FRONTEND_URL }/`;
expect(has404Content || redirectedToDashboard).toBeTruthy();
});
test('should handle navigation errors gracefully', async ({ page }) => {
// Intercept navigation and simulate error
await page.route('**/api/**', (route) => {
if (route.request().url().includes('/library')) {
route.abort('failed');
} else {
route.continue();
}
});
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/dashboard`);
await page.waitForLoadState('networkidle');
// Try to navigate to library (should handle error)
const libraryLink = page.locator('nav a[href="/library"]').first();
if (await libraryLink.isVisible({ timeout: 2000 }).catch(() => false)) {
await libraryLink.click();
// Should show error message or stay on current page
await page.waitForTimeout(2000);
const errorToast = page.locator('text=error, text=Error, text=failed').first();
const stillOnDashboard = page.url().includes('/dashboard');
// Either error is shown or we're still on dashboard
expect(await errorToast.isVisible({ timeout: 2000 }).catch(() => false) || stillOnDashboard).toBeTruthy();
}
});
});
});