From 7ae5913b92b2ca637eba1b05fa337accf7f2577f Mon Sep 17 00:00:00 2001 From: zeroday1 Date: Thu, 23 Jul 2026 12:06:37 +0100 Subject: [PATCH 01/16] feat(frontend): wire forgot-password resend form + Playwright E2E Wires the previously-mocked EmailResetPassword.tsx form to actually POST /auth/resend-reset-password-otp via the shared apiClient (with toast.error on failure). Adds a Playwright e2e suite (`page.route` mocks for the three backend endpoints) that drives the canonical /forgot-password route end-to-end. Also adds `.github/workflows/e2e.yml` so every PR that touches `frontend/**` runs the suite against the official `mcr.microsoft.com/playwright:v1.51.1-jammy` container; recent CI run surfaced a strict-mode locator collision in the spec which is fixed in this PR. Backend regression tests for the resend controller live on PR #1; frontend wiring + E2E live here. --- .github/workflows/e2e.yml | 69 +++++++++++++++ frontend/.gitignore | 5 ++ .../components/auth/EmailResetPassword.tsx | 35 +++++--- frontend/e2e/forgot-password.spec.ts | 83 +++++++++++++++++++ frontend/eslint.config.mjs | 5 ++ frontend/package-lock.json | 63 ++++++++++++++ frontend/package.json | 4 +- frontend/playwright.config.ts | 36 ++++++++ frontend/tsconfig.json | 6 +- 9 files changed, 294 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/e2e.yml create mode 100644 frontend/e2e/forgot-password.spec.ts create mode 100644 frontend/playwright.config.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..1355d6d4 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,69 @@ +name: E2E (Playwright) + +on: + pull_request: + branches: [main] + paths: + - 'frontend/**' + - '.github/workflows/e2e.yml' + +# Cancel superseded runs on the same PR so we don't burn CI minutes. +concurrency: + group: e2e-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + e2e-chromium: + name: Playwright (chromium) + runs-on: ubuntu-latest + # The Playwright jammy image ships Node 20, the @playwright/test + # binaries matching v1.51.1, and every system library chromium needs + # (libatk-1.0, libcups, libxkbcommon, libgbm, etc.). The redundant + # `npx playwright install --with-deps chromium` step below is a no-op + # in this image but keeps the workflow resilient if we ever swap to a + # plain Node base image — no surprise breakage on environment drift. + container: + image: mcr.microsoft.com/playwright:v1.51.1-jammy + + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci --no-audit --no-fund + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Run E2E suite + run: npm run test:e2e + + - name: Upload Playwright HTML report on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: frontend/playwright-report/ + retention-days: 7 + + - name: Upload Playwright test logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: frontend/test-results/ + retention-days: 7 diff --git a/frontend/.gitignore b/frontend/.gitignore index 5ef6a520..b749a82d 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -39,3 +39,8 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Playwright (E2E test artifacts and the browser cache) +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/frontend/components/auth/EmailResetPassword.tsx b/frontend/components/auth/EmailResetPassword.tsx index d95422db..6c033790 100644 --- a/frontend/components/auth/EmailResetPassword.tsx +++ b/frontend/components/auth/EmailResetPassword.tsx @@ -6,6 +6,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { ArrowLeft, Info, Mail, Send } from 'lucide-react'; import Link from 'next/link'; import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; import z from 'zod'; import { @@ -19,6 +20,7 @@ import { import { Input } from '@/components/ui/Input'; import { Separator } from '@/components/ui/separator'; import { useState } from 'react'; +import { apiClient } from '@/lib/apiClient'; import ResetPasswordCard from './ResetPasswordCard'; import Alert from '../ui/Alert'; @@ -27,7 +29,7 @@ interface EmailResetPasswordProps { } const EmailResetPassword = ({ onTogglePage }: EmailResetPasswordProps) => { - const [dummyLoadingState, setDummyLoadingState] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); const form = useForm>({ resolver: zodResolver(forgotPasswordSchema), defaultValues: { @@ -35,16 +37,29 @@ const EmailResetPassword = ({ onTogglePage }: EmailResetPasswordProps) => { }, }); - function onSubmit(values: z.infer) { - setDummyLoadingState(true); - - new Promise((resolve) => setTimeout(resolve, 3000)).then(() => { - setDummyLoadingState(false); + // Submits the email to the resend-reset-password-otp endpoint. The resend + // route generates a fresh OTP, persists it, and emails the user — behaviorally + // identical to the initial-request path used by ``. We + // wire to resend intentionally so this form exercises the path that PR #1 + // fixed (previously the email-send inside that controller was commented out + // and the user never received a code). + async function onSubmit(values: z.infer) { + setIsSubmitting(true); + try { + await apiClient.post('/auth/resend-reset-password-otp', { + email: values.email, + }); form.reset(); - console.log(values); onTogglePage('resend'); - }); + } catch (err) { + toast.error( + err instanceof Error ? err.message : 'Failed to send reset code.', + ); + } finally { + setIsSubmitting(false); + } } + return ( <> { )} />