Files
hackerspace.sh/e2e/critical-flows.spec.ts
T
Moheeb Zara 7a5d99cf01 test(e2e): tolerate the /login redirect on protected-route load checks
The "should load X page" specs asserted the URL stays on the protected route,
but an unauthenticated run — as in CI, which starts a fresh dev server with no
session — correctly redirects to /login. They passed locally only by racing an
already-warm reused server. Accept either the target route or /login, matching
the tolerance the sibling assertions in this file already use.

Confirmed with the full e2e suite (60 tests, chromium) under CI conditions
(fresh server, single worker, JWT keys).
2026-08-04 23:43:28 -07:00

423 lines
18 KiB
TypeScript

import { test, expect, Page } from '@playwright/test'
// ─── Test Configuration ───────────────────────────────────────────────────────
const BASE_URL = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000'
// ─── Helper Functions ─────────────────────────────────────────────────────────
async function waitForPageLoad(page: Page) {
await page.waitForLoadState('networkidle')
}
async function checkNoConsoleErrors(page: Page): Promise<string[]> {
const errors: string[] = []
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text())
}
})
return errors
}
// ─── Landing & Authentication Tests ───────────────────────────────────────────
test.describe('Landing & Authentication', () => {
test('should load landing page', async ({ page }) => {
await page.goto('/')
await waitForPageLoad(page)
expect(page.url()).toBeDefined()
})
test('should redirect unauthenticated users appropriately', async ({ page }) => {
await page.goto('/dashboard')
await waitForPageLoad(page)
// Should either show login page or dashboard (if already authenticated)
const url = page.url()
expect(url.includes('/login') || url.includes('/dashboard') || url === `${BASE_URL}/`).toBeTruthy()
})
test('should display login form on /login', async ({ page }) => {
await page.goto('/login')
await waitForPageLoad(page)
// Check for form elements
const emailInput = await page.locator('input[type="email"], input[name="email"]').isVisible().catch(() => false)
const passwordInput = await page.locator('input[type="password"]').isVisible().catch(() => false)
// Either login form or already authenticated
expect(emailInput || passwordInput || page.url().includes('/dashboard')).toBeTruthy()
})
test('should show error on invalid login', async ({ page }) => {
await page.goto('/login')
await waitForPageLoad(page)
const emailInput = page.locator('input[type="email"], input[name="email"]')
if (await emailInput.isVisible()) {
await emailInput.fill('[email protected]')
await page.locator('input[type="password"]').fill('wrongpassword')
await page.locator('button[type="submit"]').click()
// Should show error or stay on login page
await page.waitForTimeout(1000)
const url = page.url()
expect(url.includes('/login') || url.includes('/dashboard')).toBeTruthy()
}
})
})
// ─── Dashboard Tests ──────────────────────────────────────────────────────────
test.describe('Dashboard', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/dashboard')
await waitForPageLoad(page)
})
test('should load dashboard without errors', async ({ page }) => {
const errors = await checkNoConsoleErrors(page)
expect(page.url()).toBeDefined()
})
test('should display navigation sidebar or mobile menu', async ({ page }) => {
// Check for desktop sidebar or mobile menu button
const sidebar = await page.locator('aside, nav, [role="navigation"]').first().isVisible().catch(() => false)
const menuButton = await page.locator('[aria-label*="menu"], button:has-text("Menu")').isVisible().catch(() => false)
expect(sidebar || menuButton || page.url().includes('/login')).toBeTruthy()
})
test('should show user info or login prompt', async ({ page }) => {
// Should show user avatar/name or redirect to login
const hasUserInfo = await page.locator('[data-testid="user-avatar"], [class*="avatar"]').isVisible().catch(() => false)
const isLoginPage = page.url().includes('/login')
expect(hasUserInfo || isLoginPage || page.url().includes('/dashboard')).toBeTruthy()
})
})
// ─── Tasks Page Tests ─────────────────────────────────────────────────────────
test.describe('Tasks Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tasks')
await waitForPageLoad(page)
})
test('should load tasks page', async ({ page }) => {
// Unauthenticated (no session in CI) legitimately redirects to /login;
// authenticated stays on /tasks. Either is a healthy load.
expect(page.url().includes('/tasks') || page.url().includes('/login')).toBeTruthy()
})
test('should display task tabs', async ({ page }) => {
// Look for tab-like elements
const hasOpenTab = await page.locator('text=/Open|Chores|Tasks/i').first().isVisible().catch(() => false)
const hasOngoingTab = await page.locator('text=/Ongoing/i').isVisible().catch(() => false)
const hasMineTab = await page.locator('text=/My|Mine/i').first().isVisible().catch(() => false)
const hasDoneTab = await page.locator('text=/Done|Complete/i').first().isVisible().catch(() => false)
expect(hasOpenTab || hasOngoingTab || hasMineTab || hasDoneTab || page.url().includes('/login')).toBeTruthy()
})
test('should allow switching between tabs', async ({ page }) => {
const ongoingTab = page.locator('button:has-text("Ongoing"), [role="tab"]:has-text("Ongoing")').first()
if (await ongoingTab.isVisible()) {
await ongoingTab.click()
await page.waitForTimeout(500)
expect(page.url()).toContain('/tasks')
}
})
test('should show add task button', async ({ page }) => {
const addButton = await page.locator('button:has-text("Add"), button:has-text("New"), [aria-label*="add"]').first().isVisible().catch(() => false)
expect(addButton || page.url().includes('/login')).toBeTruthy()
})
test('should open task creation modal/form', async ({ page }) => {
const addButton = page.locator('button:has-text("Add Task"), button:has-text("New Task")').first()
if (await addButton.isVisible()) {
await addButton.click()
await page.waitForTimeout(500)
// Check for modal or form
const hasModal = await page.locator('[role="dialog"], [class*="modal"], form').isVisible().catch(() => false)
expect(hasModal).toBeTruthy()
}
})
})
// ─── Operations Page Tests ────────────────────────────────────────────────────
test.describe('Operations & Knowledge Base', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/ops')
await waitForPageLoad(page)
})
test('should load ops page', async ({ page }) => {
expect(page.url().includes('/ops') || page.url().includes('/login')).toBeTruthy()
})
test('should display knowledge base section', async ({ page }) => {
const hasKB = await page.locator('text=/Knowledge|KB|Documentation|Guides/i').first().isVisible().catch(() => false)
expect(hasKB || page.url().includes('/login')).toBeTruthy()
})
test('should display secrets section for admins', async ({ page }) => {
const hasSecrets = await page.locator('text=/Secret|Credential|Password/i').first().isVisible().catch(() => false)
// Secrets section visibility depends on user role
expect(hasSecrets || !hasSecrets).toBeTruthy() // Either visible or not
})
test('should display area leads section', async ({ page }) => {
const hasAreas = await page.locator('text=/Area|Lead|Responsibility/i').first().isVisible().catch(() => false)
expect(hasAreas || page.url().includes('/login')).toBeTruthy()
})
})
// ─── Members Page Tests ───────────────────────────────────────────────────────
test.describe('Members Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/members')
await waitForPageLoad(page)
})
test('should load members page', async ({ page }) => {
expect(page.url().includes('/members') || page.url().includes('/login')).toBeTruthy()
})
test('should display members list or table', async ({ page }) => {
const hasList = await page.locator('[role="table"], [role="list"], [class*="grid"]').first().isVisible().catch(() => false)
expect(hasList || page.url().includes('/login')).toBeTruthy()
})
test('should show member status indicators', async ({ page }) => {
const hasStatus = await page.locator('text=/current|unverified|late|inactive/i').first().isVisible().catch(() => false)
// Status may not be visible if no members
expect(true).toBeTruthy()
})
test('should show add member button for admins', async ({ page }) => {
const addButton = await page.locator('button:has-text("Add"), button:has-text("Invite")').first().isVisible().catch(() => false)
// Button visibility depends on user role
expect(addButton || !addButton).toBeTruthy()
})
})
// ─── Payments Page Tests ──────────────────────────────────────────────────────
test.describe('Payments Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/payments')
await waitForPageLoad(page)
})
test('should load payments page', async ({ page }) => {
expect(page.url().includes('/payments') || page.url().includes('/login')).toBeTruthy()
})
test('should display payments list', async ({ page }) => {
const hasList = await page.locator('[role="table"], [class*="grid"], [class*="list"]').first().isVisible().catch(() => false)
expect(hasList || page.url().includes('/login')).toBeTruthy()
})
test('should show payment linking options', async ({ page }) => {
const hasLinkOption = await page.locator('text=/Link|Unlink|Match/i').first().isVisible().catch(() => false)
// Linking options may not be visible if no unlinked payments
expect(true).toBeTruthy()
})
})
// ─── Projects Page Tests ──────────────────────────────────────────────────────
test.describe('Projects Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/projects')
await waitForPageLoad(page)
})
test('should load projects page', async ({ page }) => {
expect(page.url().includes('/projects') || page.url().includes('/login')).toBeTruthy()
})
test('should display project board or list', async ({ page }) => {
const hasBoard = await page.locator('[class*="kanban"], [class*="board"], [role="list"]').first().isVisible().catch(() => false)
expect(hasBoard || page.url().includes('/login')).toBeTruthy()
})
test('should show project status columns', async ({ page }) => {
const hasStatus = await page.locator('text=/Backlog|Active|Paused|Completed/i').first().isVisible().catch(() => false)
expect(hasStatus || page.url().includes('/login')).toBeTruthy()
})
})
// ─── Contacts Page Tests ──────────────────────────────────────────────────────
test.describe('Contacts Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/contacts')
await waitForPageLoad(page)
})
test('should load contacts page', async ({ page }) => {
expect(page.url().includes('/contacts') || page.url().includes('/login')).toBeTruthy()
})
test('should display contacts list', async ({ page }) => {
const hasList = await page.locator('[role="table"], [class*="grid"]').first().isVisible().catch(() => false)
expect(hasList || page.url().includes('/login')).toBeTruthy()
})
})
// ─── Comms Page Tests ─────────────────────────────────────────────────────────
test.describe('Communications', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/comms')
await waitForPageLoad(page)
})
test('should load comms page', async ({ page }) => {
expect(page.url().includes('/comms') || page.url().includes('/login')).toBeTruthy()
})
test('should display announcements or messages', async ({ page }) => {
const hasContent = await page.locator('text=/Announcement|Message|Post/i').first().isVisible().catch(() => false)
expect(hasContent || page.url().includes('/login')).toBeTruthy()
})
})
// ─── Settings Page Tests ──────────────────────────────────────────────────────
test.describe('Settings', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/settings')
await waitForPageLoad(page)
})
test('should load settings page for admins', async ({ page }) => {
// Settings may redirect non-admins
expect(page.url()).toBeDefined()
})
test('should display space settings form', async ({ page }) => {
const hasForm = await page.locator('form, [role="form"]').first().isVisible().catch(() => false)
expect(hasForm || page.url().includes('/login') || page.url().includes('/dashboard')).toBeTruthy()
})
})
// ─── Mobile Navigation Tests ──────────────────────────────────────────────────
test.describe('Mobile Navigation', () => {
test.use({ viewport: { width: 375, height: 667 } })
test('should show mobile menu button', async ({ page }) => {
await page.goto('/dashboard')
await waitForPageLoad(page)
const menuButton = await page.locator('[aria-label*="menu"], [aria-label*="Menu"], button:has(svg)').first().isVisible().catch(() => false)
expect(menuButton || page.url().includes('/login')).toBeTruthy()
})
test('should open mobile drawer on menu click', async ({ page }) => {
await page.goto('/dashboard')
await waitForPageLoad(page)
const menuButton = page.locator('[aria-label*="menu"], [aria-label*="Menu"]').first()
if (await menuButton.isVisible()) {
await menuButton.click()
await page.waitForTimeout(500)
const drawer = await page.locator('[role="dialog"], [class*="drawer"], [class*="sheet"]').isVisible().catch(() => false)
expect(drawer).toBeTruthy()
}
})
test('should navigate via mobile menu', async ({ page }) => {
await page.goto('/dashboard')
await waitForPageLoad(page)
const menuButton = page.locator('[aria-label*="menu"], [aria-label*="Menu"]').first()
if (await menuButton.isVisible()) {
await menuButton.click()
await page.waitForTimeout(300)
const tasksLink = page.locator('a:has-text("Tasks"), button:has-text("Tasks")').first()
if (await tasksLink.isVisible()) {
await tasksLink.click()
await waitForPageLoad(page)
expect(page.url()).toContain('/tasks')
}
}
})
})
// ─── Error Handling Tests ─────────────────────────────────────────────────────
test.describe('Error Handling', () => {
test('should handle 404 pages gracefully', async ({ page }) => {
await page.goto('/nonexistent-page-xyz')
await waitForPageLoad(page)
// Should show 404 page or redirect
expect(page.url()).toBeDefined()
})
test('should not have JavaScript errors on page load', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', err => errors.push(err.message))
await page.goto('/dashboard')
await waitForPageLoad(page)
// Filter out known benign errors
const criticalErrors = errors.filter(e =>
!e.includes('ResizeObserver') &&
!e.includes('hydration')
)
expect(criticalErrors.length).toBe(0)
})
})
// ─── Performance Tests ────────────────────────────────────────────────────────
test.describe('Performance', () => {
test('should load dashboard within acceptable time', async ({ page }) => {
const start = Date.now()
await page.goto('/dashboard')
await waitForPageLoad(page)
const loadTime = Date.now() - start
// Should load within 10 seconds
expect(loadTime).toBeLessThan(10000)
})
test('should load tasks page within acceptable time', async ({ page }) => {
const start = Date.now()
await page.goto('/tasks')
await waitForPageLoad(page)
const loadTime = Date.now() - start
expect(loadTime).toBeLessThan(10000)
})
})
// ─── Accessibility Tests ──────────────────────────────────────────────────────
test.describe('Accessibility', () => {
test('should have proper heading hierarchy', async ({ page }) => {
await page.goto('/dashboard')
await waitForPageLoad(page)
const h1Count = await page.locator('h1').count()
// Should have at least one h1 or be on login page
expect(h1Count >= 0).toBeTruthy()
})
test('should have alt text on images', async ({ page }) => {
await page.goto('/dashboard')
await waitForPageLoad(page)
const imagesWithoutAlt = await page.locator('img:not([alt])').count()
// Decorative images may not have alt text
expect(imagesWithoutAlt >= 0).toBeTruthy()
})
test('should have focusable interactive elements', async ({ page }) => {
await page.goto('/dashboard')
await waitForPageLoad(page)
const focusableElements = await page.locator('button, a, input, select, textarea').count()
expect(focusableElements > 0 || page.url().includes('/login')).toBeTruthy()
})
})