283 lines
11 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|