From 5e610a1e5af9f9456025d992852650d9506d4950 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 15:59:40 +0000 Subject: [PATCH] fix(frontend): confirm sign-in and stop showing Login to signed-in users Reported on the deployed demo: - signing in went straight to the dashboard with no feedback; now the button reads "Signing in..." while the request runs and a "Signed in successfully." toast confirms it - /login showed the form to a user who was already signed in; it now redirects to the page they were headed for (state.from) or /dashboard - the public page's header always offered "Login", so a signed-in user landing on / looked logged out; it now shows "Go to dashboard" while a session is stored Tests cover each case (all fail before this change); verified in a browser against the API. Frontend test count now 74. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018oMHDqDASywmwc7ncmxtCY --- CHECKLIST.md | 2 +- frontend/src/pages/PublicLeadPage.test.tsx | 27 ++++++++++++++ frontend/src/pages/PublicLeadPage.tsx | 7 ++-- frontend/src/pages/auth/LoginPage.test.tsx | 41 +++++++++++++++++++++- frontend/src/pages/auth/LoginPage.tsx | 17 ++++++--- 5 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 frontend/src/pages/PublicLeadPage.test.tsx diff --git a/CHECKLIST.md b/CHECKLIST.md index b7369bb..8c8524d 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -177,7 +177,7 @@ - [x] Frontend Build (`pnpm build`) - [x] Frontend Lint (`pnpm lint`) -- [x] Frontend Tests (69/69, `pnpm test`) +- [x] Frontend Tests (74/74, `pnpm test`) - [x] Backend Tests (80/80) - [x] Manual End-to-End Testing - [x] Swagger Verification diff --git a/frontend/src/pages/PublicLeadPage.test.tsx b/frontend/src/pages/PublicLeadPage.test.tsx new file mode 100644 index 0000000..2af50c0 --- /dev/null +++ b/frontend/src/pages/PublicLeadPage.test.tsx @@ -0,0 +1,27 @@ +import { screen } from "@testing-library/react" +import { describe, expect, it } from "vitest" + +import { renderRoute } from "../test/render" +import { setTokens } from "../utils/token" +import PublicLeadPage from "./PublicLeadPage" + +function renderPublic() { + renderRoute(, { path: "/", url: "/" }) +} + +describe("PublicLeadPage header", () => { + it("offers Login to visitors", () => { + renderPublic() + + expect(screen.getByRole("link", { name: "Login" })).toHaveAttribute("href", "/login") + expect(screen.queryByRole("link", { name: "Go to dashboard" })).not.toBeInTheDocument() + }) + + it("offers the dashboard to a signed-in user instead of Login", () => { + setTokens("access-1", "refresh-1") + renderPublic() + + expect(screen.getByRole("link", { name: "Go to dashboard" })).toHaveAttribute("href", "/dashboard") + expect(screen.queryByRole("link", { name: "Login" })).not.toBeInTheDocument() + }) +}) diff --git a/frontend/src/pages/PublicLeadPage.tsx b/frontend/src/pages/PublicLeadPage.tsx index 1e508fb..e52e9d3 100644 --- a/frontend/src/pages/PublicLeadPage.tsx +++ b/frontend/src/pages/PublicLeadPage.tsx @@ -15,6 +15,7 @@ import Button from "../components/ui/Button" import TextField from "../components/ui/TextField" import Footer from "../components/layout/Footer" +import { getAccessToken } from "../utils/token" export default function PublicLeadPage() { const [submitted, setSubmitted] = useState(false) @@ -52,6 +53,8 @@ export default function PublicLeadPage() { const onSubmit = (data: LeadCreateRequest) => { mutation.mutate(data) } + const signedIn = getAccessToken() !== null + return (
@@ -61,10 +64,10 @@ export default function PublicLeadPage() { - Login + {signedIn ? "Go to dashboard" : "Login"}
diff --git a/frontend/src/pages/auth/LoginPage.test.tsx b/frontend/src/pages/auth/LoginPage.test.tsx index 3d6c641..a58a23a 100644 --- a/frontend/src/pages/auth/LoginPage.test.tsx +++ b/frontend/src/pages/auth/LoginPage.test.tsx @@ -2,12 +2,15 @@ import { render, screen } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { AxiosError, type AxiosResponse } from "axios" import { createMemoryRouter, RouterProvider } from "react-router-dom" +import toast from "react-hot-toast" import { describe, expect, it, vi } from "vitest" import api from "../../api/axios" -import { getAccessToken, getRefreshToken } from "../../utils/token" +import { getAccessToken, getRefreshToken, setTokens } from "../../utils/token" import LoginPage from "./LoginPage" +vi.mock("react-hot-toast", () => ({ default: { success: vi.fn(), error: vi.fn() } })) + function renderLogin(from?: string) { const router = createMemoryRouter( [ @@ -118,3 +121,39 @@ describe("LoginPage", () => { expect(screen.queryByRole("alert")).not.toBeInTheDocument() }) }) + +describe("LoginPage feedback and redirects", () => { + it("confirms a successful sign-in and shows progress while signing in", async () => { + let resolve!: (value: unknown) => void + vi.spyOn(api, "post").mockReturnValue(new Promise((r) => (resolve = r)) as never) + + renderLogin() + await submit() + + expect(screen.getByRole("button", { name: "Signing in..." })).toBeDisabled() + + resolve({ data: { access: "access-1", refresh: "refresh-1" } }) + + expect(await screen.findByText("Dashboard")).toBeInTheDocument() + expect(toast.success).toHaveBeenCalledWith("Signed in successfully.") + }) + + it("sends an already signed-in user to the dashboard", async () => { + setTokens("access-1", "refresh-1") + const post = vi.spyOn(api, "post") + + renderLogin() + + expect(await screen.findByText("Dashboard")).toBeInTheDocument() + expect(screen.queryByPlaceholderText("Password")).not.toBeInTheDocument() + expect(post).not.toHaveBeenCalled() + }) + + it("sends an already signed-in user back to the page they were headed for", async () => { + setTokens("access-1", "refresh-1") + + renderLogin("/leads/7") + + expect(await screen.findByText("Lead detail")).toBeInTheDocument() + }) +}) diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx index 0a84154..15c57d7 100644 --- a/frontend/src/pages/auth/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage.tsx @@ -1,10 +1,11 @@ import { useState } from "react" -import { useLocation, useNavigate } from "react-router-dom" +import { Navigate, useLocation, useNavigate } from "react-router-dom" import { isAxiosError } from "axios" +import toast from "react-hot-toast" import { login } from "../../api/auth" -import { setTokens } from "../../utils/token" +import { getAccessToken, setTokens } from "../../utils/token" function loginErrorMessage(error: unknown) { if (isAxiosError(error) && error.response) { @@ -46,6 +47,8 @@ export default function LoginPage() { setTokens(response.data.access, response.data.refresh) + toast.success("Signed in successfully.") + navigate(from, { replace: true }) } catch (err) { setError(loginErrorMessage(err)) @@ -54,6 +57,12 @@ export default function LoginPage() { } } + // Already signed in: skip the form. Expired tokens are refreshed (or the + // session ended) by the API client on the next request. + if (getAccessToken()) { + return + } + return (
- Login + {loading ? "Signing in..." : "Login"}