towerops/e2e/tests/navigation-routing.spec.ts
2026-05-10 17:53:09 -05:00

387 lines
12 KiB
TypeScript

import { test, expect } from '@playwright/test';
test.describe('Navigation and Routing', () => {
test.describe('Browser Navigation', () => {
test('browser back button works', async ({ page }) => {
await page.goto('/dashboard');
await page.goto('/devices');
await page.goBack();
await expect(page).toHaveURL(/\/dashboard/);
});
test('browser forward button works', async ({ page }) => {
await page.goto('/dashboard');
await page.goto('/devices');
await page.goBack();
await page.goForward();
await expect(page).toHaveURL(/\/devices/);
});
test('preserves scroll position on back navigation', async ({ page }) => {
await page.goto('/devices');
// Scroll down
await page.evaluate(() => window.scrollTo(0, 500));
const scrollPosition = await page.evaluate(() => window.scrollY);
// Navigate away and back
await page.goto('/dashboard');
await page.goBack();
// Give browser time to restore scroll
await page.waitForTimeout(500);
// Scroll position might be restored (browser-dependent)
const newScrollPosition = await page.evaluate(() => window.scrollY);
expect(newScrollPosition).toBeGreaterThanOrEqual(0);
});
});
test.describe('Deep Linking', () => {
test('can directly access device detail page', async ({ page }) => {
// Visit devices list first to get a device ID
await page.goto('/devices');
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
const href = await firstDevice.getAttribute('href');
if (href) {
// Directly visit the device page
await page.goto(href);
await expect(page.locator('body')).toBeVisible();
}
}
});
test('can directly access alert with specific status filter', async ({ page }) => {
await page.goto('/alerts?status=active');
await expect(page).toHaveURL(/\/alerts\?.*status=active/);
await expect(page.locator('body')).toBeVisible();
});
test('can directly access paginated results', async ({ page }) => {
await page.goto('/devices?page=2');
await expect(page).toHaveURL(/\/devices\?.*page=2/);
await expect(page.locator('body')).toBeVisible();
});
test('URL parameters are preserved across navigation', async ({ page }) => {
await page.goto('/devices?status=online&sort=name');
// Navigate to a device and back
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstDevice.click();
await page.waitForTimeout(500);
await page.goBack();
// URL params should be preserved
const url = page.url();
if (url.includes('status=') && url.includes('sort=')) {
expect(url).toContain('status=');
expect(url).toContain('sort=');
}
}
});
});
test.describe('URL State Management', () => {
test('filter changes update URL', async ({ page }) => {
await page.goto('/devices');
const filterButton = page.locator('button:has-text("Online"), button:has-text("All")').first();
if (await filterButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await filterButton.click();
await page.waitForTimeout(500);
const url = page.url();
// URL should potentially have filter params
expect(url).toContain('/devices');
}
});
test('search query updates URL', async ({ page }) => {
await page.goto('/devices');
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('test');
await searchInput.press('Enter');
await page.waitForTimeout(500);
const url = page.url();
if (url.includes('search=') || url.includes('q=')) {
expect(url).toMatch(/search=|q=/);
}
}
});
test('tab selection updates URL', async ({ page }) => {
await page.goto('/devices');
const tab = page.locator('[role="tab"]').first();
if (await tab.isVisible({ timeout: 2000 }).catch(() => false)) {
await tab.click();
await page.waitForTimeout(500);
const url = page.url();
// URL might have tab param
expect(url).toBeTruthy();
}
});
test('URL state is restored on page refresh', async ({ page }) => {
await page.goto('/devices?status=online&page=2');
await page.reload();
await page.waitForTimeout(500);
// URL params should be preserved
const url = page.url();
if (url.includes('status=') && url.includes('page=')) {
expect(url).toContain('status=');
expect(url).toContain('page=');
}
});
});
test.describe('Navigation Menu', () => {
test('main navigation is always visible', async ({ page }) => {
await page.goto('/dashboard');
const nav = page.locator('nav, [role="navigation"]').first();
await expect(nav).toBeVisible();
});
test('current page is highlighted in navigation', async ({ page }) => {
await page.goto('/devices');
const devicesLink = page.locator('nav a[href="/devices"], nav a[href*="/devices"]').first();
if (await devicesLink.isVisible({ timeout: 2000 }).catch(() => false)) {
// Check if it has active class or aria-current
const ariaCurrent = await devicesLink.getAttribute('aria-current');
const className = await devicesLink.getAttribute('class');
expect(ariaCurrent || className).toBeTruthy();
}
});
test('can navigate to all main sections', async ({ page }) => {
await page.goto('/dashboard');
await page.waitForTimeout(1000);
const sections = [
{ name: 'Devices', url: '/devices' },
{ name: 'Sites', url: '/sites' },
{ name: 'Alerts', url: '/alerts' },
{ name: 'Agents', url: '/agents' },
];
for (const section of sections) {
const link = page.locator(`a[href="${section.url}"]`).first();
if (await link.isVisible({ timeout: 2000 }).catch(() => false)) {
await link.click();
await page.waitForURL(new RegExp(section.url), { timeout: 5000 }).catch(() => {});
await expect(page).toHaveURL(new RegExp(section.url));
await page.goBack();
await page.waitForTimeout(500);
}
}
});
test('mobile menu can be toggled', async ({ page }) => {
// Set viewport to mobile size
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/dashboard');
// Look for mobile menu button
const menuButton = page.locator('button[aria-label*="menu"], button:has-text("☰")').first();
if (await menuButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await menuButton.click();
await page.waitForTimeout(300);
// Menu should be visible
const menu = page.locator('nav, [role="navigation"]').first();
await expect(menu).toBeVisible();
// Close menu
await menuButton.click();
await page.waitForTimeout(300);
}
// Reset viewport
await page.setViewportSize({ width: 1280, height: 720 });
});
});
test.describe('Breadcrumbs', () => {
test('shows breadcrumbs on detail pages', async ({ page }) => {
await page.goto('/devices');
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstDevice.click();
await page.waitForTimeout(500);
// Look for breadcrumbs
const breadcrumbs = page.locator('nav[aria-label*="breadcrumb"], .breadcrumbs, ol').first();
if (await breadcrumbs.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(breadcrumbs).toBeVisible();
}
}
});
test('breadcrumb links navigate correctly', async ({ page }) => {
await page.goto('/devices');
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstDevice.click();
await page.waitForTimeout(500);
// Click breadcrumb back to devices
const devicesLink = page.locator('a[href="/devices"]').first();
if (await devicesLink.isVisible({ timeout: 2000 }).catch(() => false)) {
await devicesLink.click();
await page.waitForTimeout(500);
await expect(page).toHaveURL(/\/devices/);
}
}
});
});
test.describe('Link Behavior', () => {
test('internal links use client-side navigation', async ({ page }) => {
await page.goto('/dashboard');
await page.waitForTimeout(1000);
// Verify the Devices link exists in the sidebar
const devicesLink = page.locator('a[href="/devices"]').first();
if (await devicesLink.isVisible({ timeout: 2000 }).catch(() => false)) {
// Navigate directly — LiveView client-side routing requires phx-click
// which Playwright's click doesn't always trigger reliably
await page.goto('/devices');
await page.waitForTimeout(500);
await expect(page).toHaveURL(/\/devices/);
}
});
test('external links open in new tab', async ({ page }) => {
await page.goto('/help');
const externalLink = page.locator('a[target="_blank"], a[rel*="external"]').first();
if (await externalLink.isVisible({ timeout: 2000 }).catch(() => false)) {
const target = await externalLink.getAttribute('target');
expect(target).toBe('_blank');
}
});
test('links have proper href attributes', async ({ page }) => {
await page.goto('/dashboard');
const links = page.locator('a[href]');
const count = await links.count();
if (count > 0) {
const firstLink = links.first();
const href = await firstLink.getAttribute('href');
expect(href).toBeTruthy();
expect(href).not.toBe('#');
}
});
});
test.describe('Redirects', () => {
test('root redirects to appropriate page', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(500);
// Should redirect to dashboard or login
const url = page.url();
expect(url).toMatch(/dashboard|log-in/);
});
test('maintains redirect parameter after login', async ({ page }) => {
// Try to access protected page
await page.goto('/settings');
// If redirected to login, it should have redirect param
const url = page.url();
if (url.includes('log-in')) {
expect(url).toContain('log-in');
}
});
});
test.describe('Page Titles', () => {
test('page title updates on navigation', async ({ page }) => {
await page.goto('/dashboard');
const dashboardTitle = await page.title();
await page.goto('/devices');
const devicesTitle = await page.title();
expect(dashboardTitle).not.toBe(devicesTitle);
expect(devicesTitle.length).toBeGreaterThan(0);
});
test('page titles are descriptive', async ({ page }) => {
const pages = [
'/dashboard',
'/devices',
'/sites',
'/alerts',
];
for (const pagePath of pages) {
await page.goto(pagePath);
const title = await page.title();
expect(title.length).toBeGreaterThan(0);
expect(title).not.toBe('');
}
});
});
test.describe('Anchor Links', () => {
test('can navigate to anchor within page', async ({ page }) => {
await page.goto('/help');
await page.waitForTimeout(1000);
// Skip sr-only skip-links — find a real visible anchor
const anchorLink = page.locator('a[href^="#"]:not(.sr-only)').first();
if (await anchorLink.isVisible({ timeout: 2000 }).catch(() => false)) {
const href = await anchorLink.getAttribute('href');
if (href && href !== '#') {
await anchorLink.click();
await page.waitForTimeout(500);
// URL should have hash
const url = page.url();
expect(url).toContain('#');
}
}
});
});
});