feat(login): add LoginView + router
This commit is contained in:
parent
7b7544c78e
commit
5aa82b88af
3 changed files with 187 additions and 0 deletions
|
@ -14,6 +14,7 @@ import { Router, Route } from "./router"
|
|||
|
||||
import FileListView from "./components/FileListView"
|
||||
import RegisterView from "./components/RegisterView"
|
||||
import LoginView from "./components/LoginView"
|
||||
|
||||
const routeLabels = {
|
||||
ITEM_DETAILS: "item-details",
|
||||
|
@ -43,6 +44,9 @@ const App = () => {
|
|||
<Route path="/register">
|
||||
<RegisterView />
|
||||
</Route>
|
||||
<Route path="/login">
|
||||
<LoginView />
|
||||
</Route>
|
||||
</Router>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
82
frontend/src/components/LoginView/LoginView.test.tsx
Normal file
82
frontend/src/components/LoginView/LoginView.test.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { render, screen } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query"
|
||||
import AxiosMockAdapter from "axios-mock-adapter"
|
||||
|
||||
import axios from "../../axios"
|
||||
import LoginView from "."
|
||||
|
||||
function renderComponent() {
|
||||
return {
|
||||
...render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<LoginView />
|
||||
</QueryClientProvider>,
|
||||
),
|
||||
user: userEvent.setup(),
|
||||
}
|
||||
}
|
||||
|
||||
describe("LoginView", () => {
|
||||
it("renders an email and password field", () => {
|
||||
renderComponent()
|
||||
|
||||
expect(screen.getByLabelText("Email")).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByLabelText(/email address login input/i),
|
||||
).toBeInTheDocument()
|
||||
|
||||
expect(screen.getByLabelText("Password")).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/password login input/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders a submit button", () => {
|
||||
renderComponent()
|
||||
|
||||
expect(
|
||||
screen.getByText("Log in", { selector: "button" }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/submit login/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders a registration link", async () => {
|
||||
const mock = jest.fn()
|
||||
const { user } = renderComponent()
|
||||
|
||||
expect(screen.getByText(/don\'t have an account yet?/i)).toBeInTheDocument()
|
||||
|
||||
const registrationLink = screen.getByText(/create one/i)
|
||||
expect(registrationLink).toBeInTheDocument()
|
||||
|
||||
expect(registrationLink.getAttribute("href")).toEqual("/register")
|
||||
})
|
||||
|
||||
it("sends a request to the authentication API on submit", async () => {
|
||||
const axiosMockAdapter = new AxiosMockAdapter(axios)
|
||||
|
||||
axiosMockAdapter.onPost("/auth/session/").reply(201, { token: "testtoken" })
|
||||
|
||||
const { user } = renderComponent()
|
||||
|
||||
const testInput = {
|
||||
username: "test@domain.com",
|
||||
password: "password",
|
||||
}
|
||||
|
||||
const emailInput = screen.getByLabelText(/email address login input/i)
|
||||
await user.type(emailInput, testInput.username)
|
||||
|
||||
const passwordInput = screen.getByLabelText(/password login input/i)
|
||||
await user.type(passwordInput, testInput.password)
|
||||
|
||||
const submitButton = screen.getByText("Log in", { selector: "button" })
|
||||
|
||||
await user.click(submitButton)
|
||||
|
||||
expect(axiosMockAdapter.history.post.length).toEqual(1)
|
||||
|
||||
const requestBody = JSON.parse(axiosMockAdapter.history.post[0].data)
|
||||
|
||||
expect(requestBody).toEqual(testInput)
|
||||
})
|
||||
})
|
101
frontend/src/components/LoginView/index.tsx
Normal file
101
frontend/src/components/LoginView/index.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
import React from "react"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
|
||||
import Typography from "@mui/material/Typography"
|
||||
import Box from "@mui/material/Box"
|
||||
import FormGroup from "@mui/material/FormGroup"
|
||||
import FormControl from "@mui/material/FormControl"
|
||||
import TextField from "@mui/material/TextField"
|
||||
import InputLabel from "@mui/material/InputLabel"
|
||||
import Button from "@mui/material/Button"
|
||||
import Link from "@mui/material/Link"
|
||||
|
||||
import axiosWithDefaults from "../../axios"
|
||||
import TextInput from "../TextInput"
|
||||
|
||||
function LoginView() {
|
||||
const [emailAddress, setEmailAddress] = React.useState<string>("")
|
||||
const [password, setPassword] = React.useState<string>("")
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: async ({
|
||||
email,
|
||||
password,
|
||||
}: { email: string; password: string }) => {
|
||||
const response = await axiosWithDefaults.post("/auth/session/", {
|
||||
username: email,
|
||||
password,
|
||||
})
|
||||
|
||||
return response
|
||||
},
|
||||
})
|
||||
|
||||
const emailField = React.useMemo(
|
||||
() => (
|
||||
<TextInput
|
||||
label="Email"
|
||||
ariaLabel="email address login input"
|
||||
onChange={setEmailAddress}
|
||||
value={emailAddress}
|
||||
inputType="email"
|
||||
/>
|
||||
),
|
||||
[emailAddress, setEmailAddress],
|
||||
)
|
||||
|
||||
const passwordField = React.useMemo(
|
||||
() => (
|
||||
<TextInput
|
||||
label="Password"
|
||||
ariaLabel="password login input"
|
||||
onChange={setPassword}
|
||||
value={password}
|
||||
inputType="password"
|
||||
/>
|
||||
),
|
||||
[setPassword, password],
|
||||
)
|
||||
|
||||
const isFormValid = React.useMemo(() => {
|
||||
return Boolean(emailAddress) && Boolean(password)
|
||||
}, [emailAddress, password])
|
||||
|
||||
const onLoginClick = React.useCallback(() => {
|
||||
if (!isFormValid) return
|
||||
|
||||
mutate({ email: emailAddress, password })
|
||||
}, [mutate, emailAddress, password, isFormValid])
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", justifyContent: "center", width: "100%" }}>
|
||||
<FormGroup
|
||||
sx={{
|
||||
flexGrow: 0.1,
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h1" sx={{ fontSize: "2rem" }}>
|
||||
Log in
|
||||
</Typography>
|
||||
{emailField}
|
||||
{passwordField}
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={onLoginClick}
|
||||
aria-label="submit login"
|
||||
disabled={!isFormValid}
|
||||
>
|
||||
Log in
|
||||
</Button>
|
||||
<Typography>
|
||||
Don't have an account yet? <Link href="/register">Create one!</Link>
|
||||
</Typography>
|
||||
</FormGroup>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginView
|
Reference in a new issue