Skip to content

Commit 7ecb38d

Browse files
authored
test: E2E tests for login redirect flow (#1564)
* test: add E2E tests for login redirect flow Verify the complete login lifecycle: unauthenticated redirect to /login, login page rendering, post-login dashboard loading, auth state persistence across navigation, and version endpoint stability. * fix: show dev login buttons in VITE_E2E_MODE builds The dev login buttons were only rendered when `import.meta.env.DEV` is true, but E2E CI builds are compiled with VITE_E2E_MODE=true (not in dev mode). This caused the 'login page shows dev login buttons in dev mode' E2E test to fail because the buttons were absent in the preview build. Also merges origin/develop to pick up lint fixes in test files (unused vi import, import() type annotation). --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent bdc28e6 commit 7ecb38d

2 files changed

Lines changed: 109 additions & 2 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { test, expect } from '@playwright/test'
2+
import { test as authTest, expect as authExpect, navigateTo } from './fixtures'
3+
4+
/**
5+
* Login redirect flow E2E tests.
6+
*
7+
* Verifies the complete login lifecycle:
8+
* - Unauthenticated users are redirected to /login
9+
* - Login page renders correctly
10+
* - After authentication, dashboard loads successfully
11+
* - API calls succeed post-login (stat cards resolve, version endpoint)
12+
* - Auth state persists across client-side navigation
13+
*
14+
* These tests run against the Vite dev server. Tenant subdomain redirect
15+
* (TenantSubdomainEnforcer) is skipped on localhost by design, so
16+
* subdomain-specific redirect logic is not exercised here.
17+
*/
18+
19+
test.describe('Login redirect - unauthenticated', () => {
20+
test('bare domain redirects to /login', async ({ page }) => {
21+
await page.goto('/')
22+
await expect(page).toHaveURL('/login')
23+
})
24+
25+
test('protected route redirects to /login', async ({ page }) => {
26+
await page.goto('/accounts')
27+
// ProtectedRoute redirects unauthenticated requests to /login
28+
await expect(page).toHaveURL('/login')
29+
})
30+
31+
test('login page renders sign-in heading', async ({ page }) => {
32+
await page.goto('/login')
33+
await expect(page.getByRole('heading', { name: 'Meridian Operations Console' })).toBeVisible()
34+
await expect(page.getByText('Please sign in to continue.')).toBeVisible()
35+
})
36+
37+
test('login page shows dev login buttons in dev mode', async ({ page }) => {
38+
await page.goto('/login')
39+
await expect(page.getByRole('button', { name: /platform.admin/i })).toBeVisible()
40+
await expect(page.getByRole('button', { name: /tenant.user/i })).toBeVisible()
41+
})
42+
})
43+
44+
test.describe('Post-login flow - tenant user', () => {
45+
authTest('dashboard heading renders after login', async ({ authenticatedPage: page }) => {
46+
await authExpect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
47+
})
48+
49+
authTest('dashboard shows tenant context', async ({ authenticatedPage: page }) => {
50+
await authExpect(page.getByText(/Overview for dev-tenant/)).toBeVisible({ timeout: 15_000 })
51+
})
52+
53+
authTest('stat cards resolve from loading state', async ({ authenticatedPage: page }) => {
54+
// Stat card skeletons disappear once API calls complete (success or error)
55+
await authExpect(page.getByTestId('stat-card-skeleton')).toHaveCount(0, { timeout: 15_000 })
56+
})
57+
58+
authTest('navigation preserves auth after login', async ({ authenticatedPage: page }) => {
59+
// Navigate away from dashboard
60+
await navigateTo(page, '/accounts')
61+
await authExpect(page.getByRole('heading', { name: 'Accounts' })).toBeVisible({
62+
timeout: 10_000,
63+
})
64+
65+
// Navigate back to dashboard
66+
await navigateTo(page, '/')
67+
await authExpect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
68+
// Tenant context must still be present
69+
await authExpect(page.getByText(/Overview for dev-tenant/)).toBeVisible({ timeout: 15_000 })
70+
})
71+
})
72+
73+
test.describe('Post-login flow - platform admin', () => {
74+
authTest('dashboard renders for platform admin', async ({ platformAdminPage: page }) => {
75+
await authExpect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
76+
})
77+
78+
authTest('platform admin can access tenant list', async ({ platformAdminPage: page }) => {
79+
await navigateTo(page, '/tenants')
80+
await authExpect(
81+
page.getByRole('heading', { level: 1 }).filter({ hasText: /Tenant/i }),
82+
).toBeVisible({ timeout: 10_000 })
83+
})
84+
})
85+
86+
test.describe('Version check', () => {
87+
authTest('version endpoint is reachable after login', async ({ authenticatedPage: page }) => {
88+
// BuildInfo component fetches /version on mount. In dev mode without a backend,
89+
// the fetch will fail gracefully. We verify the fetch attempt doesn't cause errors
90+
// by checking the app shell remains stable.
91+
await authExpect(page.locator('main')).toBeVisible()
92+
// No error boundary should be shown
93+
await authExpect(page.getByText(/Something went wrong/i)).not.toBeVisible()
94+
})
95+
})
96+
97+
test.describe('Login page does not render for authenticated users', () => {
98+
authTest('authenticated user at / sees dashboard, not login', async ({
99+
authenticatedPage: page,
100+
}) => {
101+
// The fixture injects auth and navigates to /. Verify we see dashboard, not login.
102+
await authExpect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
103+
await authExpect(
104+
page.getByRole('heading', { name: 'Meridian Operations Console' }),
105+
).not.toBeVisible()
106+
})
107+
})

frontend/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,8 @@ function LoginPage() {
232232
</>
233233
)}
234234

235-
{/* Dev-only fake JWT buttons */}
236-
{import.meta.env.DEV && (
235+
{/* Dev-only fake JWT buttons (also shown in E2E mode) */}
236+
{(import.meta.env.DEV || import.meta.env.VITE_E2E_MODE === 'true') && (
237237
<div className="space-y-2">
238238
<p className="text-xs text-muted-foreground uppercase tracking-wider text-center">
239239
Development Login

0 commit comments

Comments
 (0)