mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor(site): SignInForm without wrapper component (#558)
* Remove wrapper from SignInForm * Spruce up tests * Add util for form props * Add back trim * Add unit tests * Lint, type fixes * Pascal case for language * Arrow functions * Target text in e2e
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { FormikContextType } from "formik/dist/types"
|
||||
import { getFormHelpers, onChangeTrimmed } from "./index"
|
||||
|
||||
interface TestType {
|
||||
untouchedGoodField: string
|
||||
untouchedBadField: string
|
||||
touchedGoodField: string
|
||||
touchedBadField: string
|
||||
}
|
||||
|
||||
const mockHandleChange = jest.fn()
|
||||
|
||||
const form = {
|
||||
errors: {
|
||||
untouchedGoodField: undefined,
|
||||
untouchedBadField: "oops!",
|
||||
touchedGoodField: undefined,
|
||||
touchedBadField: "oops!",
|
||||
},
|
||||
touched: {
|
||||
untouchedGoodField: false,
|
||||
untouchedBadField: false,
|
||||
touchedGoodField: true,
|
||||
touchedBadField: true,
|
||||
},
|
||||
handleChange: mockHandleChange,
|
||||
handleBlur: jest.fn(),
|
||||
getFieldProps: (name: string) => {
|
||||
return {
|
||||
name,
|
||||
onBlur: jest.fn(),
|
||||
onChange: jest.fn(),
|
||||
value: "",
|
||||
}
|
||||
},
|
||||
} as unknown as FormikContextType<TestType>
|
||||
|
||||
describe("form util functions", () => {
|
||||
describe("getFormHelpers", () => {
|
||||
const untouchedGoodResult = getFormHelpers<TestType>(form, "untouchedGoodField")
|
||||
const untouchedBadResult = getFormHelpers<TestType>(form, "untouchedBadField")
|
||||
const touchedGoodResult = getFormHelpers<TestType>(form, "touchedGoodField")
|
||||
const touchedBadResult = getFormHelpers<TestType>(form, "touchedBadField")
|
||||
it("populates the 'field props'", () => {
|
||||
expect(untouchedGoodResult.name).toEqual("untouchedGoodField")
|
||||
expect(untouchedGoodResult.onBlur).toBeDefined()
|
||||
expect(untouchedGoodResult.onChange).toBeDefined()
|
||||
expect(untouchedGoodResult.value).toBeDefined()
|
||||
})
|
||||
it("sets the id to the name", () => {
|
||||
expect(untouchedGoodResult.id).toEqual("untouchedGoodField")
|
||||
})
|
||||
it("sets error to true if touched and invalid", () => {
|
||||
expect(untouchedGoodResult.error).toBeFalsy
|
||||
expect(untouchedBadResult.error).toBeFalsy
|
||||
expect(touchedGoodResult.error).toBeFalsy
|
||||
expect(touchedBadResult.error).toBeTruthy
|
||||
})
|
||||
it("sets helperText to the error message if touched and invalid", () => {
|
||||
expect(untouchedGoodResult.helperText).toBeUndefined
|
||||
expect(untouchedBadResult.helperText).toBeUndefined
|
||||
expect(touchedGoodResult.helperText).toBeUndefined
|
||||
expect(touchedBadResult.helperText).toEqual("oops!")
|
||||
})
|
||||
})
|
||||
|
||||
describe("onChangeTrimmed", () => {
|
||||
it("calls handleChange with trimmed value", () => {
|
||||
const event = { target: { value: " hello " } } as React.ChangeEvent<HTMLInputElement>
|
||||
onChangeTrimmed<TestType>(form)(event)
|
||||
expect(mockHandleChange).toHaveBeenCalledWith({ target: { value: "hello" } })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,37 @@
|
||||
import { FormikContextType, getIn } from "formik"
|
||||
import { ChangeEvent, ChangeEventHandler, FocusEventHandler } from "react"
|
||||
|
||||
export * from "./FormCloseButton"
|
||||
export * from "./FormSection"
|
||||
export * from "./FormDropdownField"
|
||||
export * from "./FormTextField"
|
||||
export * from "./FormTitle"
|
||||
|
||||
interface FormHelpers {
|
||||
name: string
|
||||
onBlur: FocusEventHandler
|
||||
onChange: ChangeEventHandler
|
||||
id: string
|
||||
value?: string | number
|
||||
error: boolean
|
||||
helperText?: string
|
||||
}
|
||||
|
||||
export const getFormHelpers = <T>(form: FormikContextType<T>, name: string): FormHelpers => {
|
||||
// getIn is a util function from Formik that gets at any depth of nesting, and is necessary for the types to work
|
||||
const touched = getIn(form.touched, name)
|
||||
const errors = getIn(form.errors, name)
|
||||
return {
|
||||
...form.getFieldProps(name),
|
||||
id: name,
|
||||
error: touched && Boolean(errors),
|
||||
helperText: touched && errors,
|
||||
}
|
||||
}
|
||||
|
||||
export const onChangeTrimmed =
|
||||
<T>(form: FormikContextType<T>) =>
|
||||
(event: ChangeEvent<HTMLInputElement>): void => {
|
||||
event.target.value = event.target.value.trim()
|
||||
form.handleChange(event)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import React from "react"
|
||||
import * as Yup from "yup"
|
||||
|
||||
import { Welcome } from "./Welcome"
|
||||
import { FormTextField } from "../Form"
|
||||
import FormHelperText from "@material-ui/core/FormHelperText"
|
||||
import { LoadingButton } from "./../Button"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import { getFormHelpers, onChangeTrimmed } from "../Form"
|
||||
|
||||
/**
|
||||
* BuiltInAuthFormValues describes a form using built-in (email/password)
|
||||
@@ -18,8 +19,17 @@ interface BuiltInAuthFormValues {
|
||||
password: string
|
||||
}
|
||||
|
||||
export const Language = {
|
||||
emailLabel: "Email",
|
||||
passwordLabel: "Password",
|
||||
emailInvalid: "Please enter a valid email address.",
|
||||
emailRequired: "Please enter an email address.",
|
||||
authErrorMessage: "Incorrect email or password.",
|
||||
signIn: "Sign In",
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
email: Yup.string().required("Email is required."),
|
||||
email: Yup.string().trim().email(Language.emailInvalid).required(Language.emailRequired),
|
||||
password: Yup.string(),
|
||||
})
|
||||
|
||||
@@ -59,50 +69,30 @@ export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMess
|
||||
<>
|
||||
<Welcome />
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<div>
|
||||
<FormTextField
|
||||
label="Email"
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
className={styles.loginTextField}
|
||||
eventTransform={(email: string) => email.trim()}
|
||||
form={form}
|
||||
formFieldName="email"
|
||||
fullWidth
|
||||
inputProps={{
|
||||
id: "signin-form-inpt-email",
|
||||
}}
|
||||
variant="outlined"
|
||||
/>
|
||||
<FormTextField
|
||||
label="Password"
|
||||
autoComplete="current-password"
|
||||
className={styles.loginTextField}
|
||||
form={form}
|
||||
formFieldName="password"
|
||||
fullWidth
|
||||
inputProps={{
|
||||
id: "signin-form-inpt-password",
|
||||
}}
|
||||
isPassword
|
||||
variant="outlined"
|
||||
/>
|
||||
{authErrorMessage && (
|
||||
<FormHelperText data-testid="sign-in-error" error>
|
||||
{authErrorMessage}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</div>
|
||||
<TextField
|
||||
{...getFormHelpers<BuiltInAuthFormValues>(form, "email")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
autoFocus
|
||||
autoComplete="email"
|
||||
className={styles.loginTextField}
|
||||
fullWidth
|
||||
label={Language.emailLabel}
|
||||
variant="outlined"
|
||||
/>
|
||||
<TextField
|
||||
{...getFormHelpers<BuiltInAuthFormValues>(form, "password")}
|
||||
autoComplete="current-password"
|
||||
className={styles.loginTextField}
|
||||
fullWidth
|
||||
id="password"
|
||||
label={Language.passwordLabel}
|
||||
type="password"
|
||||
variant="outlined"
|
||||
/>
|
||||
{authErrorMessage && <FormHelperText error>{Language.authErrorMessage}</FormHelperText>}
|
||||
<div className={styles.submitBtn}>
|
||||
<LoadingButton
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
fullWidth
|
||||
id="signin-form-submit"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
{isLoading ? "" : "Sign In"}
|
||||
<LoadingButton color="primary" loading={isLoading} fullWidth type="submit" variant="contained">
|
||||
{isLoading ? "" : Language.signIn}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user