Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ chrome_profile_notebooklm/
# Playwright / test artifacts
test-results/
playwright-report/
# Playwright auth storage state (contains real JWTs)
e2e/.auth/

# Coverage HTML reports
coverage/
Expand Down
19 changes: 11 additions & 8 deletions e2e/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,21 +173,24 @@ test.describe('Authentication', () => {
});

test.describe('Protected Routes (Frontend)', () => {
// These tests verify the frontend is accessible
// Auth protection on the frontend will be implemented later
// These tests run unauthenticated to verify RequireAuth redirects users
// to /login. Override the global storageState fixture with empty state
// so we start without any tokens.
test.use({ storageState: { cookies: [], origins: [] } });

test('dashboard loads without auth (dev mode)', async ({ page }) => {
test('dashboard redirects to /login when unauthenticated', async ({ page }) => {
await page.goto('/');

// Dashboard should load (no auth gate in current implementation)
await expect(page.locator('main')).toBeVisible({ timeout: 10000 });
// RequireAuth should bounce the user to /login
await page.waitForURL('**/login', { timeout: 10_000 });
await expect(page).toHaveURL(/\/login$/);
});

test('investments page loads without auth (dev mode)', async ({ page }) => {
test('investments redirects to /login when unauthenticated', async ({ page }) => {
await page.goto('/investments');

// Page should load
await expect(page.locator('main')).toBeVisible({ timeout: 10000 });
await page.waitForURL('**/login', { timeout: 10_000 });
await expect(page).toHaveURL(/\/login$/);
});
});
});
29 changes: 29 additions & 0 deletions e2e/global.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test as setup, expect } from '@playwright/test';
import { TEST_CREDENTIALS } from './fixtures/auth';

/**
* Playwright "setup" project: authenticates once and persists the browser
* storage state to disk. Browser-context tests reuse this state via
* `use: { storageState: 'e2e/.auth/admin.json' }` in playwright.config.ts,
* so they start already logged in and skip the /login redirect.
*
* The auth store (src/stores/authStore.ts) persists tokens to localStorage,
* so we drive the UI login flow to ensure the store is properly hydrated.
* `storageState()` captures cookies + localStorage + sessionStorage by default.
*/
const AUTH_FILE = 'e2e/.auth/admin.json';

setup('authenticate as admin', async ({ page }) => {
await page.goto('/login');

await page.getByLabel('Email').fill(TEST_CREDENTIALS.admin.email);
await page.getByLabel('Password').fill(TEST_CREDENTIALS.admin.password);
await page.getByRole('button', { name: /sign in/i }).click();

// RequireAuth replaces /login with / on successful auth, so wait for the
// URL to land on the dashboard root before snapshotting storage.
await page.waitForURL('**/', { timeout: 30_000 });
await expect(page.locator('main')).toBeVisible({ timeout: 15_000 });

await page.context().storageState({ path: AUTH_FILE });
});
15 changes: 14 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,33 @@ export default defineConfig({
},

projects: [
/* Setup project — runs once before browser tests to authenticate and
* persist storage state to e2e/.auth/admin.json. */
{
name: 'setup',
testMatch: /global\.setup\.ts/,
},

/* Local development project — runs by default when no --project is specified */
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
use: {
...devices['Desktop Chrome'],
storageState: 'e2e/.auth/admin.json',
},
dependencies: ['setup'],
},

/* CI project — single browser, stricter timeouts, retries on failure */
{
name: 'ci',
use: {
...devices['Desktop Chrome'],
storageState: 'e2e/.auth/admin.json',
/* CI-specific: capture video on first retry for debugging */
video: 'on-first-retry',
},
dependencies: ['setup'],
retries: 1,
timeout: 30_000,
},
Expand Down
Loading