Add E2E tests for authenticated navigation and theme toggle
- **Authenticated Navigation:** Test header, footer, settings navigation, user menu interactions, and settings tabs for authenticated users. Validate logout and active tab highlighting. - **Theme Toggle:** Add tests for theme persistence and switching on both public and private pages. Verify localStorage integration and DOM updates across scenarios.
This commit is contained in:
212
frontend/e2e/authenticated-navigation.spec.ts
Normal file
212
frontend/e2e/authenticated-navigation.spec.ts
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* E2E Tests for Authenticated Navigation
|
||||||
|
* Tests header, footer, and settings navigation for authenticated users
|
||||||
|
* Requires backend to be running
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Authenticated Navigation', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Login before each test
|
||||||
|
await page.goto('/login');
|
||||||
|
|
||||||
|
// Fill in login credentials (using backend test user)
|
||||||
|
await page.fill('input[name="email"]', 'admin@example.com');
|
||||||
|
await page.fill('input[name="password"]', 'admin123');
|
||||||
|
|
||||||
|
// Submit and wait for redirect to home
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForURL('/'),
|
||||||
|
page.click('button[type="submit"]'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Header Navigation', () => {
|
||||||
|
test('displays logo and navigates to home when clicked', async ({ page }) => {
|
||||||
|
// Navigate to settings first
|
||||||
|
await page.goto('/settings/profile');
|
||||||
|
|
||||||
|
// Click logo
|
||||||
|
const logo = page.getByRole('link', { name: /fastnext/i });
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForURL('/'),
|
||||||
|
logo.click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(page).toHaveURL('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays home link', async ({ page }) => {
|
||||||
|
const homeLink = page.getByRole('link', { name: /^home$/i }).first();
|
||||||
|
await expect(homeLink).toBeVisible();
|
||||||
|
await expect(homeLink).toHaveAttribute('href', '/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays user avatar', async ({ page }) => {
|
||||||
|
// Avatar button should be visible
|
||||||
|
const avatarButton = page.locator('button[class*="rounded-full"]').first();
|
||||||
|
await expect(avatarButton).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays theme toggle button', async ({ page }) => {
|
||||||
|
const themeToggle = page.getByRole('button', { name: /toggle theme/i });
|
||||||
|
await expect(themeToggle).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('User Dropdown Menu', () => {
|
||||||
|
test('opens and displays user menu', async ({ page }) => {
|
||||||
|
// Click avatar
|
||||||
|
const avatarButton = page.locator('button[class*="rounded-full"]').first();
|
||||||
|
await avatarButton.click();
|
||||||
|
|
||||||
|
// Check menu items
|
||||||
|
await expect(page.getByRole('menuitem', { name: /profile/i })).toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: /settings/i })).toBeVisible();
|
||||||
|
await expect(page.getByRole('menuitem', { name: /log out/i })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigates to profile from dropdown', async ({ page }) => {
|
||||||
|
// Open dropdown
|
||||||
|
const avatarButton = page.locator('button[class*="rounded-full"]').first();
|
||||||
|
await avatarButton.click();
|
||||||
|
|
||||||
|
// Click profile
|
||||||
|
const profileLink = page.getByRole('menuitem', { name: /^profile$/i });
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForURL('/settings/profile'),
|
||||||
|
profileLink.click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(page).toHaveURL('/settings/profile');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logs out user', async ({ page }) => {
|
||||||
|
// Open dropdown
|
||||||
|
const avatarButton = page.locator('button[class*="rounded-full"]').first();
|
||||||
|
await avatarButton.click();
|
||||||
|
|
||||||
|
// Click logout
|
||||||
|
const logoutButton = page.getByRole('menuitem', { name: /log out/i });
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForURL('/login'),
|
||||||
|
logoutButton.click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Should redirect to login
|
||||||
|
await expect(page).toHaveURL('/login');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Settings Pages', () => {
|
||||||
|
test('displays settings tabs', async ({ page }) => {
|
||||||
|
await page.goto('/settings/profile');
|
||||||
|
|
||||||
|
// All tabs should be visible
|
||||||
|
await expect(page.getByRole('link', { name: /^profile$/i })).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: /^password$/i })).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: /^sessions$/i })).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: /^preferences$/i })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can navigate to password tab', async ({ page }) => {
|
||||||
|
await page.goto('/settings/profile');
|
||||||
|
|
||||||
|
const passwordTab = page.getByRole('link', { name: /^password$/i });
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForURL('/settings/password'),
|
||||||
|
passwordTab.click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(page).toHaveURL('/settings/password');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can navigate to sessions tab', async ({ page }) => {
|
||||||
|
await page.goto('/settings/profile');
|
||||||
|
|
||||||
|
const sessionsTab = page.getByRole('link', { name: /^sessions$/i });
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForURL('/settings/sessions'),
|
||||||
|
sessionsTab.click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(page).toHaveURL('/settings/sessions');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can navigate to preferences tab', async ({ page }) => {
|
||||||
|
await page.goto('/settings/profile');
|
||||||
|
|
||||||
|
const preferencesTab = page.getByRole('link', { name: /^preferences$/i });
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForURL('/settings/preferences'),
|
||||||
|
preferencesTab.click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(page).toHaveURL('/settings/preferences');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('active tab is highlighted', async ({ page }) => {
|
||||||
|
await page.goto('/settings/password');
|
||||||
|
|
||||||
|
const passwordTab = page.getByRole('link', { name: /^password$/i });
|
||||||
|
const className = await passwordTab.getAttribute('class');
|
||||||
|
expect(className).toContain('border-primary');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Footer', () => {
|
||||||
|
test('displays copyright text', async ({ page }) => {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const copyright = page.getByText(`© ${currentYear} FastNext Template`);
|
||||||
|
await expect(copyright).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays settings link in footer', async ({ page }) => {
|
||||||
|
const settingsLinks = page.getByRole('link', { name: /^settings$/i });
|
||||||
|
// Should have at least one (in footer)
|
||||||
|
await expect(settingsLinks.last()).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays GitHub link with correct attributes', async ({ page }) => {
|
||||||
|
const githubLink = page.getByRole('link', { name: /github/i });
|
||||||
|
await expect(githubLink).toBeVisible();
|
||||||
|
await expect(githubLink).toHaveAttribute('target', '_blank');
|
||||||
|
await expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Theme Toggle Integration', () => {
|
||||||
|
test('can switch to dark theme from header', async ({ page }) => {
|
||||||
|
// Open theme menu
|
||||||
|
const themeButton = page.getByRole('button', { name: /toggle theme/i });
|
||||||
|
await themeButton.click();
|
||||||
|
|
||||||
|
// Select dark theme
|
||||||
|
const darkOption = page.getByRole('menuitem', { name: /^dark$/i });
|
||||||
|
await darkOption.click();
|
||||||
|
|
||||||
|
// Wait for theme to apply
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Verify dark theme is active
|
||||||
|
await expect(page.locator('html')).toHaveClass(/dark/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('theme persists on settings pages', async ({ page }) => {
|
||||||
|
// Set dark theme
|
||||||
|
const themeButton = page.getByRole('button', { name: /toggle theme/i });
|
||||||
|
await themeButton.click();
|
||||||
|
await page.getByRole('menuitem', { name: /^dark$/i }).click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Navigate to settings
|
||||||
|
await page.goto('/settings/profile');
|
||||||
|
await expect(page.locator('html')).toHaveClass(/dark/);
|
||||||
|
|
||||||
|
// Navigate to different settings tab
|
||||||
|
await page.goto('/settings/password');
|
||||||
|
await expect(page.locator('html')).toHaveClass(/dark/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
83
frontend/e2e/theme-toggle.spec.ts
Normal file
83
frontend/e2e/theme-toggle.spec.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* E2E Tests for Theme Toggle
|
||||||
|
* Tests theme switching on public pages (login/register)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Theme Toggle on Public Pages', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Clear localStorage before each test
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.evaluate(() => localStorage.clear());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('theme is applied on login page', async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
|
||||||
|
// Wait for page to load and theme to be applied
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Check that a theme class is applied
|
||||||
|
const htmlElement = page.locator('html');
|
||||||
|
const className = await htmlElement.getAttribute('class');
|
||||||
|
|
||||||
|
// Should have either 'light' or 'dark' class
|
||||||
|
expect(className).toMatch(/light|dark/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('theme persists across page navigation', async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Set theme to dark via localStorage
|
||||||
|
await page.evaluate(() => {
|
||||||
|
localStorage.setItem('theme', 'dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload to apply theme
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Verify dark theme is applied
|
||||||
|
await expect(page.locator('html')).toHaveClass(/dark/);
|
||||||
|
|
||||||
|
// Navigate to register page
|
||||||
|
await page.goto('/register');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Theme should still be dark
|
||||||
|
await expect(page.locator('html')).toHaveClass(/dark/);
|
||||||
|
|
||||||
|
// Navigate to password reset
|
||||||
|
await page.goto('/password-reset');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Theme should still be dark
|
||||||
|
await expect(page.locator('html')).toHaveClass(/dark/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can switch theme programmatically', async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
|
||||||
|
// Set to light theme
|
||||||
|
await page.evaluate(() => {
|
||||||
|
localStorage.setItem('theme', 'light');
|
||||||
|
});
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await expect(page.locator('html')).toHaveClass(/light/);
|
||||||
|
await expect(page.locator('html')).not.toHaveClass(/dark/);
|
||||||
|
|
||||||
|
// Switch to dark theme
|
||||||
|
await page.evaluate(() => {
|
||||||
|
localStorage.setItem('theme', 'dark');
|
||||||
|
});
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await expect(page.locator('html')).toHaveClass(/dark/);
|
||||||
|
await expect(page.locator('html')).not.toHaveClass(/light/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user