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: 1 addition & 1 deletion CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/pages/PublicLeadPage.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<PublicLeadPage />, { 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()
})
})
7 changes: 5 additions & 2 deletions frontend/src/pages/PublicLeadPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -52,6 +53,8 @@ export default function PublicLeadPage() {
const onSubmit = (data: LeadCreateRequest) => {
mutation.mutate(data)
}
const signedIn = getAccessToken() !== null

return (
<div className="flex h-screen flex-col bg-linear-to-br from-slate-50 via-white to-blue-50">
<header className="border-b bg-white/90 backdrop-blur">
Expand All @@ -61,10 +64,10 @@ export default function PublicLeadPage() {
</h1>

<Link
to="/login"
to={signedIn ? "/dashboard" : "/login"}
className="rounded-lg bg-blue-600 px-5 py-2 font-medium text-white transition hover:bg-blue-700"
>
Login
{signedIn ? "Go to dashboard" : "Login"}
</Link>
</div>
</header>
Expand Down
41 changes: 40 additions & 1 deletion frontend/src/pages/auth/LoginPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
Expand Down Expand Up @@ -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()
})
})
17 changes: 13 additions & 4 deletions frontend/src/pages/auth/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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))
Expand All @@ -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 <Navigate to={from} replace />
}

return (
<div className="flex min-h-screen items-center justify-center">
<form
Expand Down Expand Up @@ -88,9 +97,9 @@ export default function LoginPage() {

<button
disabled={loading}
className="rounded bg-blue-600 p-2 text-white"
className="rounded bg-blue-600 p-2 text-white disabled:opacity-60"
>
Login
{loading ? "Signing in..." : "Login"}
</button>
</form>
</div>
Expand Down