dawarich/e2e/navigation.spec.ts
2025-07-12 15:24:10 +02:00

472 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect } from '@playwright/test';
import { TestHelpers } from './fixtures/test-helpers';
test.describe('Navigation', () => {
let helpers: TestHelpers;
test.beforeEach(async ({ page }) => {
helpers = new TestHelpers(page);
await helpers.loginAsDemo();
});
test.describe('Main Navigation', () => {
test('should display main navigation elements', async ({ page }) => {
await helpers.navigateTo('Map');
// Check for main navigation items - note Trips has α symbol, Settings is in user dropdown
await expect(page.getByRole('link', { name: 'Map', exact: true })).toBeVisible();
await expect(page.getByRole('link', { name: /Trips/ })).toBeVisible(); // Match with α symbol
await expect(page.getByRole('link', { name: 'Stats' })).toBeVisible();
// Settings is in user dropdown, not main nav - check user dropdown instead
const userDropdown = page.locator('details').filter({ hasText: /@/ }).first();
await expect(userDropdown).toBeVisible();
// Check for "My data" dropdown - select the visible one (not hidden mobile version)
await expect(page.getByText('My data').and(page.locator(':visible'))).toBeVisible();
});
test('should navigate to Map section', async ({ page }) => {
await helpers.navigateTo('Map');
await expect(page).toHaveURL(/\/map/);
// No h1 heading on map page - check for map interface instead
await expect(page.locator('#map')).toBeVisible();
});
test('should navigate to Trips section', async ({ page }) => {
await helpers.navigateTo('Trips');
await expect(page).toHaveURL(/\/trips/);
// No h1 heading on trips page - check for trips interface instead (visible elements only)
await expect(page.getByText(/trip|distance|duration/i).and(page.locator(':visible')).first()).toBeVisible();
});
test('should navigate to Stats section', async ({ page }) => {
await helpers.navigateTo('Stats');
await expect(page).toHaveURL(/\/stats/);
// No h1 heading on stats page - check for stats interface instead (visible elements only)
await expect(page.getByText(/total.*distance|points.*tracked/i).and(page.locator(':visible')).first()).toBeVisible();
});
test('should navigate to Settings section', async ({ page }) => {
await helpers.navigateTo('Settings');
await expect(page).toHaveURL(/\/settings/);
// No h1 heading on settings page - check for settings interface instead
await expect(page.getByText(/integration|map.*configuration/i).first()).toBeVisible();
});
});
test.describe('My Data Dropdown', () => {
test('should expand My data dropdown', async ({ page }) => {
await helpers.navigateTo('Map');
// Click on "My data" dropdown - select the visible one (not hidden mobile version)
await page.getByText('My data').and(page.locator(':visible')).click();
// Should show dropdown items
await expect(page.getByRole('link', { name: 'Points' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Visits' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Imports' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Exports' })).toBeVisible();
});
test('should navigate to Points', async ({ page }) => {
await helpers.navigateTo('Points');
await expect(page).toHaveURL(/\/points/);
// No h1 heading on points page - check for points interface instead (visible elements only)
await expect(page.getByText(/point|location|coordinate/i).and(page.locator(':visible')).first()).toBeVisible();
});
test('should navigate to Visits', async ({ page }) => {
await helpers.navigateTo('Visits');
await expect(page).toHaveURL(/\/visits/);
// No h1 heading on visits page - check for visits interface instead (visible elements only)
await expect(page.getByText(/visit|place|duration/i).and(page.locator(':visible')).first()).toBeVisible();
});
test('should navigate to Imports', async ({ page }) => {
await helpers.navigateTo('Imports');
await expect(page).toHaveURL(/\/imports/);
// No h1 heading on imports page - check for imports interface instead (visible elements only)
await expect(page.getByText(/import|file|source/i).and(page.locator(':visible')).first()).toBeVisible();
});
test('should navigate to Exports', async ({ page }) => {
await helpers.navigateTo('Exports');
await expect(page).toHaveURL(/\/exports/);
// No h1 heading on exports page - check for exports interface instead (visible elements only)
await expect(page.getByText(/export|download|format/i).and(page.locator(':visible')).first()).toBeVisible();
});
});
test.describe('User Navigation', () => {
test('should display user menu', async ({ page }) => {
await helpers.navigateTo('Map');
// Click on user dropdown using the details/summary structure
const userDropdown = page.locator('details').filter({ hasText: /@/ }).first();
await userDropdown.locator('summary').click();
// Should show user menu items
await expect(page.getByRole('link', { name: 'Account' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Logout' })).toBeVisible();
});
test('should navigate to Account settings', async ({ page }) => {
await helpers.navigateTo('Map');
const userDropdown = page.locator('details').filter({ hasText: /@/ }).first();
await userDropdown.locator('summary').click();
await page.getByRole('link', { name: 'Account' }).click();
await expect(page).toHaveURL(/\/users\/edit/);
await expect(page.getByLabel('Email')).toBeVisible();
});
test('should show logout functionality', async ({ page }) => {
await helpers.navigateTo('Map');
const userDropdown = page.locator('details').filter({ hasText: /@/ }).first();
await userDropdown.locator('summary').click();
await page.getByRole('link', { name: 'Logout' }).click();
// Should redirect to home/login
await expect(page).toHaveURL('/');
await expect(page.getByRole('link', { name: 'Sign in' })).toBeVisible();
});
});
test.describe('Breadcrumb Navigation', () => {
test('should show breadcrumbs on detail pages', async ({ page }) => {
await helpers.navigateTo('Trips');
// Look for trip links
const tripLinks = page.getByRole('link').filter({ hasText: /trip|km|miles/i });
const linkCount = await tripLinks.count();
if (linkCount > 0) {
// Click on first trip
await tripLinks.first().click();
await page.waitForLoadState('networkidle');
// Should show breadcrumb navigation
const breadcrumbs = page.locator('.breadcrumb, .breadcrumbs, nav').filter({ hasText: /trip/i });
if (await breadcrumbs.isVisible()) {
await expect(breadcrumbs).toBeVisible();
}
}
});
test('should navigate back using breadcrumbs', async ({ page }) => {
await helpers.navigateTo('Imports');
// Look for import detail links
const importLinks = page.getByRole('link').filter({ hasText: /\.json|\.gpx|\.rec/i });
const linkCount = await importLinks.count();
if (linkCount > 0) {
await importLinks.first().click();
await page.waitForLoadState('networkidle');
// Look for back navigation
const backLink = page.getByRole('link', { name: /back|imports/i });
if (await backLink.isVisible()) {
await backLink.click();
await expect(page).toHaveURL(/\/imports/);
}
}
});
});
test.describe('URL Navigation', () => {
test('should handle direct URL navigation', async ({ page }) => {
// Navigate directly to different sections - no h1 headings on pages
await page.goto('/map');
await expect(page.locator('#map')).toBeVisible();
await page.goto('/trips');
await expect(page.getByText(/trip|distance|duration/i).and(page.locator(':visible')).first()).toBeVisible();
await page.goto('/stats');
await expect(page.getByText(/total.*distance|points.*tracked/i).and(page.locator(':visible')).first()).toBeVisible();
await page.goto('/settings');
await expect(page.getByText(/integration|map.*configuration/i).first()).toBeVisible();
});
test('should handle browser back/forward navigation', async ({ page }) => {
// Navigate to different pages
await helpers.navigateTo('Map');
await helpers.navigateTo('Trips');
await helpers.navigateTo('Stats');
// Use browser back
await page.goBack();
await expect(page).toHaveURL(/\/trips/);
await page.goBack();
await expect(page).toHaveURL(/\/map/);
// Use browser forward
await page.goForward();
await expect(page).toHaveURL(/\/trips/);
});
test('should handle URL parameters', async ({ page }) => {
// Navigate to map with date parameters
await page.goto('/map?start_at=2024-01-01T00:00&end_at=2024-01-02T23:59');
// Should preserve URL parameters
await expect(page).toHaveURL(/start_at=2024-01-01/);
await expect(page).toHaveURL(/end_at=2024-01-02/);
// Form should be populated with URL parameters - use display labels
await expect(page.getByLabel('Start at')).toHaveValue(/2024-01-01/);
await expect(page.getByLabel('End at')).toHaveValue(/2024-01-02/);
});
});
test.describe('Mobile Navigation', () => {
test('should show mobile navigation menu', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await helpers.navigateTo('Map');
// Look for mobile menu button (hamburger)
const mobileMenuButton = page.locator('button').filter({ hasText: /menu|☰|≡/ }).first();
if (await mobileMenuButton.isVisible()) {
await mobileMenuButton.click();
// Should show mobile navigation
await expect(page.getByRole('link', { name: 'Map' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Trips' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Stats' })).toBeVisible();
}
});
test('should handle mobile navigation interactions', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await helpers.navigateTo('Map');
// Open mobile navigation
await helpers.openMobileNavigation();
// Navigate to different section
await page.getByRole('link', { name: 'Stats' }).click();
// Should navigate successfully - no h1 heading on stats page
await expect(page).toHaveURL(/\/stats/);
await expect(page.getByText(/total.*distance|points.*tracked/i).and(page.locator(':visible')).first()).toBeVisible();
});
test('should handle mobile dropdown menus', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await helpers.navigateTo('Map');
// Open mobile navigation
await helpers.openMobileNavigation();
// Look for "My data" in mobile menu - select the visible one
const myDataMobile = page.getByText('My data').and(page.locator(':visible'));
if (await myDataMobile.isVisible()) {
await myDataMobile.click();
// Should show mobile dropdown
await expect(page.getByRole('link', { name: 'Points' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Imports' })).toBeVisible();
}
});
});
test.describe('Active Navigation State', () => {
test('should highlight active navigation item', async ({ page }) => {
await helpers.navigateTo('Map');
// Map should be active - use exact match to avoid attribution links
const mapLink = page.getByRole('link', { name: 'Map', exact: true });
await expect(mapLink).toHaveClass(/active|current/);
// Navigate to different section
await helpers.navigateTo('Trips');
// Trips should now be active
const tripsLink = page.getByRole('link', { name: 'Trips' });
await expect(tripsLink).toHaveClass(/active|current/);
});
test('should update active state on URL change', async ({ page }) => {
// Navigate directly via URL
await page.goto('/stats');
// Stats should be active - use exact match to avoid "Update stats" button
const statsLink = page.getByRole('link', { name: 'Stats', exact: true });
await expect(statsLink).toHaveClass(/active|current/);
// Navigate via URL again
await page.goto('/settings');
// Settings link is in user dropdown, not main nav - check URL instead
await expect(page).toHaveURL(/\/settings/);
});
});
test.describe('Navigation Performance', () => {
test('should navigate between sections quickly', async ({ page }) => {
const startTime = Date.now();
// Navigate through multiple sections (Settings uses different navigation)
await helpers.navigateTo('Map');
await helpers.navigateTo('Trips');
await helpers.navigateTo('Stats');
await helpers.navigateTo('Points'); // Navigate to Points instead of Settings
const endTime = Date.now();
const totalTime = endTime - startTime;
// Should complete navigation within reasonable time
expect(totalTime).toBeLessThan(10000); // 10 seconds
});
test('should handle rapid navigation clicks', async ({ page }) => {
await helpers.navigateTo('Map');
// Rapidly click different navigation items (Settings is not in main nav)
await page.getByRole('link', { name: /Trips/ }).click(); // Match with α symbol
await page.getByRole('link', { name: 'Stats' }).click();
await page.getByRole('link', { name: 'Map', exact: true }).click();
// Should end up on the last clicked item
await expect(page).toHaveURL(/\/map/);
await expect(page.locator('#map')).toBeVisible();
});
});
test.describe('Error Handling', () => {
test('should handle non-existent routes', async ({ page }) => {
// Navigate to a non-existent route
await page.goto('/non-existent-page');
// Should show 404 or redirect to valid page
const currentUrl = page.url();
// Either shows 404 page or redirects to valid page
if (currentUrl.includes('non-existent-page')) {
// Should show 404 page
await expect(page.getByText(/404|not found/i)).toBeVisible();
} else {
// Should redirect to valid page
expect(currentUrl).toMatch(/\/(map|home|$)/);
}
});
test('should handle network errors gracefully', async ({ page }) => {
await helpers.navigateTo('Map');
// Mock network error for navigation
await page.route('**/trips', route => route.abort());
// Try to navigate
await page.getByRole('link', { name: 'Trips' }).click();
// Should handle gracefully (stay on current page or show error)
await page.waitForTimeout(2000);
// Should not crash - page should still be functional - use exact match
await expect(page.getByRole('link', { name: 'Map', exact: true })).toBeVisible();
});
});
test.describe('Keyboard Navigation', () => {
test('should support keyboard navigation', async ({ page }) => {
await helpers.navigateTo('Map');
// Press Tab to navigate to links
await page.keyboard.press('Tab');
// Should focus on navigation elements
const focusedElement = page.locator(':focus');
await expect(focusedElement).toBeVisible();
// Should be able to navigate with keyboard
await page.keyboard.press('Enter');
await page.waitForTimeout(500);
// Should navigate to focused element - use exact match to avoid attribution links
await expect(page.getByRole('link', { name: 'Map', exact: true })).toBeVisible();
});
test('should handle keyboard shortcuts', async ({ page }) => {
await helpers.navigateTo('Map');
// Test common keyboard shortcuts if they exist
// This depends on the application implementing keyboard shortcuts
// For example, if there's a keyboard shortcut for settings
await page.keyboard.press('Alt+S');
await page.waitForTimeout(500);
// May or may not navigate (depends on implementation)
const currentUrl = page.url();
// Just verify the page is still functional - use exact match
await expect(page.getByRole('link', { name: 'Map', exact: true })).toBeVisible();
});
});
test.describe('Accessibility', () => {
test('should have proper ARIA labels', async ({ page }) => {
await helpers.navigateTo('Map');
// Check for main navigation landmark
const mainNav = page.locator('nav[role="navigation"]').or(page.locator('nav'));
await expect(mainNav.first()).toBeVisible();
// Check for accessible navigation items
const navItems = page.getByRole('link');
const navCount = await navItems.count();
expect(navCount).toBeGreaterThan(0);
// Navigation items should have proper text content
for (let i = 0; i < Math.min(navCount, 5); i++) {
const navItem = navItems.nth(i);
const text = await navItem.textContent();
expect(text).toBeTruthy();
}
});
test('should support screen reader navigation', async ({ page }) => {
await helpers.navigateTo('Map');
// No h1 headings exist - check for navigation landmark instead
const nav = page.locator('nav').first();
await expect(nav).toBeVisible();
// Check for proper link labels
const links = page.getByRole('link');
const linkCount = await links.count();
// Most links should have text content (skip icon-only links)
let linksWithText = 0;
for (let i = 0; i < Math.min(linkCount, 10); i++) {
const link = links.nth(i);
const text = await link.textContent();
if (text?.trim()) {
linksWithText++;
}
}
// At least half of the links should have text content
expect(linksWithText).toBeGreaterThan(Math.min(linkCount, 10) / 2);
});
});
});