From 8659b99059d2282bc2131000ae693d67a44ff2f2 Mon Sep 17 00:00:00 2001 From: Barcode Betty Date: Tue, 31 Mar 2026 16:49:36 +0000 Subject: [PATCH] feat(e2e): add J1 and J8 journey tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(e2e): add J1 and J8 journey tests - J1: Registration and Login — register flow, validation errors, sign-in with existing account, nav between pages - J8: Unauthenticated Access — /, /purchases, /products, /coupons all redirect to /login when no session - Enable VITE_MOCK_AUTH in playwright webServer so registration tests work without a live Better-Auth instance - Add playwright to devDependencies to ensure CI has the package Co-Authored-By: Paperclip --- e2e/journeys/j1-registration-login.spec.ts | 65 ++++++++++++++++++++++ e2e/journeys/j8-unauth-access.spec.ts | 49 ++++++++++++++++ package-lock.json | 3 +- package.json | 5 +- playwright.config.ts | 2 +- 5 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 e2e/journeys/j1-registration-login.spec.ts create mode 100644 e2e/journeys/j8-unauth-access.spec.ts diff --git a/e2e/journeys/j1-registration-login.spec.ts b/e2e/journeys/j1-registration-login.spec.ts new file mode 100644 index 0000000..0b22637 --- /dev/null +++ b/e2e/journeys/j1-registration-login.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@playwright/test'; + +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 }) => { + 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"]'); + + // With VITE_MOCK_AUTH=true the app navigates to "/" on success + await expect(page).toHaveURL('http://localhost:5173/'); + await expect(page.getByRole('heading', { name: /cart/i })).toBeVisible(); + }); + + test('shows validation error when registration fields are empty', async ({ page }) => { + await page.goto('/register'); + await page.click('button[type="submit"]'); + + await expect(page.locator('.bg-red-50')).toContainText('Please fill in all fields'); + }); + + test('can navigate from register to login', async ({ page }) => { + await page.goto('/register'); + await page.getByRole('link', { name: /sign in/i }).click(); + + await expect(page).toHaveURL(/\/login/); + await expect(page.getByRole('heading', { name: /cartsnitch/i })).toBeVisible(); + }); + + 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 page.goto('/login'); + await page.fill('[placeholder="Email"]', email); + await page.fill('[placeholder="Password"]', 'TestPass123!'); + await page.click('button[type="submit"]'); + + await expect(page).toHaveURL('http://localhost:5173/'); + }); + + test('shows error on login with wrong password', async ({ page }) => { + await page.goto('/login'); + await page.fill('[placeholder="Email"]', 'nobody@cartsnitch.test'); + await page.fill('[placeholder="Password"]', 'WrongPassword1!'); + await page.click('button[type="submit"]'); + + // With VITE_MOCK_AUTH=false the catch block shows error + await expect(page.locator('.bg-red-50')).toBeVisible(); + }); +}); diff --git a/e2e/journeys/j8-unauth-access.spec.ts b/e2e/journeys/j8-unauth-access.spec.ts new file mode 100644 index 0000000..79e8ee3 --- /dev/null +++ b/e2e/journeys/j8-unauth-access.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@playwright/test'; + +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 page.goto('/'); + + await expect(page).toHaveURL(/\/login/); + await expect(page.getByRole('heading', { name: /cartsnitch/i })).toBeVisible(); + }); + + test('redirects /purchases to /login when not authenticated', async ({ page }) => { + await page.context().clearCookies(); + await page.goto('/purchases'); + + await expect(page).toHaveURL(/\/login/); + await expect(page.getByRole('heading', { name: /cartsnitch/i })).toBeVisible(); + }); + + test('redirects /products to /login when not authenticated', async ({ page }) => { + await page.context().clearCookies(); + await page.goto('/products'); + + await expect(page).toHaveURL(/\/login/); + await expect(page.getByRole('heading', { name: /cartsnitch/i })).toBeVisible(); + }); + + test('redirects /coupons to /login when not authenticated', async ({ page }) => { + await page.context().clearCookies(); + await page.goto('/coupons'); + + await expect(page).toHaveURL(/\/login/); + await expect(page.getByRole('heading', { name: /cartsnitch/i })).toBeVisible(); + }); + + test('shows loading spinner while auth session is pending', async ({ page }) => { + // Intercept but don't respond — session stays pending + await page.context().clearCookies(); + const response = await page.request.fetch('/api/auth/session', { + method: 'GET', + }); + + // Just navigate to a protected route — ProtectedRoute will show spinner while session is pending + await page.goto('/purchases'); + // Spinner is visible briefly; once resolved, should redirect to login + await expect(page).toHaveURL(/\/login/, { timeout: 10_000 }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 3083888..6921d4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.4", - "@playwright/test": "^1.49.0", + "@playwright/test": "^1.58.2", "@tailwindcss/vite": "^4.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.2", @@ -33,6 +33,7 @@ "globals": "^17.4.0", "jsdom": "^25.0.1", "msw": "^2.12.14", + "playwright": "^1.58.2", "tailwindcss": "^4.0.0", "typescript": "^5.7.3", "typescript-eslint": "^8.56.1", diff --git a/package.json b/package.json index bc63f25..3981847 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.4", - "@playwright/test": "^1.49.0", + "@playwright/test": "^1.58.2", "@tailwindcss/vite": "^4.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.2", @@ -38,6 +38,7 @@ "globals": "^17.4.0", "jsdom": "^25.0.1", "msw": "^2.12.14", + "playwright": "^1.58.2", "tailwindcss": "^4.0.0", "typescript": "^5.7.3", "typescript-eslint": "^8.56.1", @@ -50,4 +51,4 @@ "flatted": "^3.4.2", "serialize-javascript": "7.0.5" } -} \ No newline at end of file +} diff --git a/playwright.config.ts b/playwright.config.ts index a2d7b0b..b22d74a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ }, ], webServer: { - command: 'npm run dev', + command: 'VITE_MOCK_AUTH=true npm run dev', url: 'http://localhost:5173', reuseExistingServer: !process.env.CI, },