From c7b7494151057a776efaa4a7b10130ba23e83ef0 Mon Sep 17 00:00:00 2001 From: "cartsnitch-engineer[bot]" <269717931+cartsnitch-engineer[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:49:55 +0000 Subject: [PATCH 1/2] fix: e2e route mocking and color contrast accessibility (#221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes CAR-673, CAR-676. Replaces VITE_MOCK_AUTH with Playwright route mocking for all e2e tests. Fixes color contrast (text-gray-400 → text-gray-600). --- e2e/fixtures.ts | 90 +++++++++++++++++++++- e2e/journeys/j1-registration-login.spec.ts | 24 ++---- e2e/journeys/j8-unauth-access.spec.ts | 20 ++--- e2e/smoke.spec.ts | 4 +- src/pages/Dashboard.tsx | 6 +- 5 files changed, 107 insertions(+), 37 deletions(-) diff --git a/e2e/fixtures.ts b/e2e/fixtures.ts index 07e12e6..8f61575 100644 --- a/e2e/fixtures.ts +++ b/e2e/fixtures.ts @@ -1,4 +1,4 @@ -import { test as base, expect } from "@playwright/test"; +import { test as base, expect, type Page } from "@playwright/test"; import AxeBuilder from "@axe-core/playwright"; export const test = base.extend<{ axeCheck: void }>({ @@ -10,3 +10,91 @@ export const test = base.extend<{ axeCheck: void }>({ }); export { expect } from "@playwright/test"; + +const MOCK_USER_ID = "mock_user_123"; +const MOCK_SESSION_ID = "mock_session_456"; + +async function mockAuthRoutes(page: Page, authenticated = false) { + await page.route(/.*\/auth\/sign-up\/email.*/, async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + token: null, + user: { + id: MOCK_USER_ID, + email: "mock@cartsnitch.test", + name: "Mock User", + emailVerified: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + }), + }); + }); + + await page.route(/.*\/auth\/sign-in\/email.*/, async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + redirect: false, + token: "mock_token_123", + user: { + id: MOCK_USER_ID, + email: "mock@cartsnitch.test", + name: "Mock User", + emailVerified: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + }), + }); + }); + + await page.route(/.*\/auth\/get-session.*/, async (route) => { + if (authenticated) { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + session: { + id: MOCK_SESSION_ID, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ipAddress: null, + userAgent: null, + }, + user: { + id: MOCK_USER_ID, + email: "mock@cartsnitch.test", + name: "Mock User", + emailVerified: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + }), + }); + } else { + await route.fulfill({ + status: 401, + contentType: "application/json", + body: JSON.stringify({ error: "Unauthorized" }), + }); + } + }); +} + +export async function mockSessionDelayed(page: Page, delayMs = 3000) { + await page.route(/.*\/auth\/get-session.*/, async (route) => { + await new Promise((r) => setTimeout(r, delayMs)); + await route.fulfill({ + status: 401, + contentType: "application/json", + body: JSON.stringify({ error: "Unauthorized" }), + }); + }); +} + +export { mockAuthRoutes }; diff --git a/e2e/journeys/j1-registration-login.spec.ts b/e2e/journeys/j1-registration-login.spec.ts index b1b28a4..a811cc8 100644 --- a/e2e/journeys/j1-registration-login.spec.ts +++ b/e2e/journeys/j1-registration-login.spec.ts @@ -1,17 +1,18 @@ import { test, expect } from '@playwright/test'; +import { mockAuthRoutes } from '../fixtures'; const uniqueEmail = () => `betty+e2e-${Date.now()}@cartsnitch.test`; test.describe('J1: Registration and Login', () => { - test('can register a new account and lands on dashboard', async ({ page }) => { + test('can register a new account and see check your email screen', async ({ page }) => { + await mockAuthRoutes(page, false); await page.goto('/register'); await page.fill('[placeholder="Full Name"]', 'Betty Tester'); await page.fill('[placeholder="Email"]', uniqueEmail()); await page.fill('[placeholder="Password (min. 8 characters)"]', 'TestPass123!'); await page.click('button[type="submit"]'); - await expect(page).toHaveURL('http://localhost:5173/'); - await expect(page.getByRole('heading', { name: /cart/i })).toBeVisible(); + await expect(page.getByRole('heading', { name: /check your email/i })).toBeVisible(); }); test('shows validation error when registration fields are empty', async ({ page }) => { @@ -30,22 +31,9 @@ test.describe('J1: Registration and Login', () => { }); test('can sign in with credentials and land on dashboard', async ({ page }) => { - // Register first so we have a real account - const email = uniqueEmail(); - await page.goto('/register'); - await page.fill('[placeholder="Full Name"]', 'Login Betty'); - await page.fill('[placeholder="Email"]', email); - await page.fill('[placeholder="Password (min. 8 characters)"]', 'TestPass123!'); - await page.click('button[type="submit"]'); - await expect(page).toHaveURL('http://localhost:5173/'); - - // Sign out by clearing the mock session (reload with no session) - await page.goto('/'); - await page.reload(); - - // Now sign in + await mockAuthRoutes(page, true); await page.goto('/login'); - await page.fill('[placeholder="Email"]', email); + await page.fill('[placeholder="Email"]', 'test@cartsnitch.test'); await page.fill('[placeholder="Password"]', 'TestPass123!'); await page.click('button[type="submit"]'); diff --git a/e2e/journeys/j8-unauth-access.spec.ts b/e2e/journeys/j8-unauth-access.spec.ts index 9ed40da..7c9d17d 100644 --- a/e2e/journeys/j8-unauth-access.spec.ts +++ b/e2e/journeys/j8-unauth-access.spec.ts @@ -1,9 +1,9 @@ import { test, expect } from '@playwright/test'; +import { mockAuthRoutes, mockSessionDelayed } from '../fixtures'; test.describe('J8: Unauthenticated Access', () => { test('redirects /dashboard (/) to /login when not authenticated', async ({ page }) => { - // No session cookie — start fresh - await page.context().clearCookies(); + await mockAuthRoutes(page, false); await page.goto('/'); await expect(page).toHaveURL(/\/login/); @@ -11,7 +11,7 @@ test.describe('J8: Unauthenticated Access', () => { }); test('redirects /purchases to /login when not authenticated', async ({ page }) => { - await page.context().clearCookies(); + await mockAuthRoutes(page, false); await page.goto('/purchases'); await expect(page).toHaveURL(/\/login/); @@ -19,7 +19,7 @@ test.describe('J8: Unauthenticated Access', () => { }); test('redirects /products to /login when not authenticated', async ({ page }) => { - await page.context().clearCookies(); + await mockAuthRoutes(page, false); await page.goto('/products'); await expect(page).toHaveURL(/\/login/); @@ -27,7 +27,7 @@ test.describe('J8: Unauthenticated Access', () => { }); test('redirects /coupons to /login when not authenticated', async ({ page }) => { - await page.context().clearCookies(); + await mockAuthRoutes(page, false); await page.goto('/coupons'); await expect(page).toHaveURL(/\/login/); @@ -35,15 +35,9 @@ test.describe('J8: Unauthenticated Access', () => { }); test('shows loading spinner while auth session is pending', async ({ page }) => { - // Intercept but don't respond — session stays pending - await page.context().clearCookies(); - await page.request.fetch('/api/auth/session', { - method: 'GET', - }); - - // Just navigate to a protected route — ProtectedRoute will show spinner while session is pending + await mockSessionDelayed(page, 3000); await page.goto('/purchases'); - // Spinner is visible briefly; once resolved, should redirect to login + await expect(page.locator('.animate-spin')).toBeVisible({ timeout: 2000 }); await expect(page).toHaveURL(/\/login/, { timeout: 10_000 }); }); }); diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index 2819d15..7d7cf3c 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -1,8 +1,8 @@ -import { test, expect } from './fixtures'; +import { test, expect, mockAuthRoutes } from './fixtures'; test('app loads', async ({ page }) => { + await mockAuthRoutes(page, false); await page.goto('/'); - // Unauthenticated users are redirected to /login await expect(page).toHaveURL(/\/login/); await expect(page.getByRole('heading', { name: /CartSnitch/i })).toBeVisible(); }); diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index c3f428a..d7dd8d5 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -79,21 +79,21 @@ function AuthenticatedDashboard({ userName }: { userName: string }) {
Watching
{watchingAlerts.length}
-price alerts
+price alerts
This Month
${recentPurchases.reduce((sum, p) => sum + p.total, 0).toFixed(0)}
-grocery spend
+grocery spend