Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit7e48df8

Browse files
authored
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
1 parentf4ac7a3 commit7e48df8

File tree

7 files changed

+164
-57
lines changed

7 files changed

+164
-57
lines changed

‎site/e2e/pom/SignInPage.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ export class SignInPage extends BasePom {
77
}
88

99
asyncsubmitBuiltInAuthentication(email:string,password:string):Promise<void>{
10-
awaitthis.page.fill("id=signin-form-inpt-email",email)
11-
awaitthis.page.fill("id=signin-form-inpt-password",password)
12-
awaitthis.page.click("id=signin-form-submit")
10+
awaitthis.page.fill("text=Email",email)
11+
awaitthis.page.fill("text=Password",password)
12+
awaitthis.page.click("text=Sign In")
1313
}
1414
}

‎site/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"@storybook/addon-links":"6.4.19",
4949
"@storybook/react":"6.4.19",
5050
"@testing-library/react":"12.1.4",
51+
"@testing-library/user-event":"^13.5.0",
5152
"@types/express":"4.17.13",
5253
"@types/jest":"27.4.1",
5354
"@types/node":"14.18.12",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import{FormikContextType}from"formik/dist/types"
2+
import{getFormHelpers,onChangeTrimmed}from"./index"
3+
4+
interfaceTestType{
5+
untouchedGoodField:string
6+
untouchedBadField:string
7+
touchedGoodField:string
8+
touchedBadField:string
9+
}
10+
11+
constmockHandleChange=jest.fn()
12+
13+
constform={
14+
errors:{
15+
untouchedGoodField:undefined,
16+
untouchedBadField:"oops!",
17+
touchedGoodField:undefined,
18+
touchedBadField:"oops!",
19+
},
20+
touched:{
21+
untouchedGoodField:false,
22+
untouchedBadField:false,
23+
touchedGoodField:true,
24+
touchedBadField:true,
25+
},
26+
handleChange:mockHandleChange,
27+
handleBlur:jest.fn(),
28+
getFieldProps:(name:string)=>{
29+
return{
30+
name,
31+
onBlur:jest.fn(),
32+
onChange:jest.fn(),
33+
value:"",
34+
}
35+
},
36+
}asunknownasFormikContextType<TestType>
37+
38+
describe("form util functions",()=>{
39+
describe("getFormHelpers",()=>{
40+
constuntouchedGoodResult=getFormHelpers<TestType>(form,"untouchedGoodField")
41+
constuntouchedBadResult=getFormHelpers<TestType>(form,"untouchedBadField")
42+
consttouchedGoodResult=getFormHelpers<TestType>(form,"touchedGoodField")
43+
consttouchedBadResult=getFormHelpers<TestType>(form,"touchedBadField")
44+
it("populates the 'field props'",()=>{
45+
expect(untouchedGoodResult.name).toEqual("untouchedGoodField")
46+
expect(untouchedGoodResult.onBlur).toBeDefined()
47+
expect(untouchedGoodResult.onChange).toBeDefined()
48+
expect(untouchedGoodResult.value).toBeDefined()
49+
})
50+
it("sets the id to the name",()=>{
51+
expect(untouchedGoodResult.id).toEqual("untouchedGoodField")
52+
})
53+
it("sets error to true if touched and invalid",()=>{
54+
expect(untouchedGoodResult.error).toBeFalsy
55+
expect(untouchedBadResult.error).toBeFalsy
56+
expect(touchedGoodResult.error).toBeFalsy
57+
expect(touchedBadResult.error).toBeTruthy
58+
})
59+
it("sets helperText to the error message if touched and invalid",()=>{
60+
expect(untouchedGoodResult.helperText).toBeUndefined
61+
expect(untouchedBadResult.helperText).toBeUndefined
62+
expect(touchedGoodResult.helperText).toBeUndefined
63+
expect(touchedBadResult.helperText).toEqual("oops!")
64+
})
65+
})
66+
67+
describe("onChangeTrimmed",()=>{
68+
it("calls handleChange with trimmed value",()=>{
69+
constevent={target:{value:" hello "}}asReact.ChangeEvent<HTMLInputElement>
70+
onChangeTrimmed<TestType>(form)(event)
71+
expect(mockHandleChange).toHaveBeenCalledWith({target:{value:"hello"}})
72+
})
73+
})
74+
})

‎site/src/components/Form/index.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,37 @@
1+
import{FormikContextType,getIn}from"formik"
2+
import{ChangeEvent,ChangeEventHandler,FocusEventHandler}from"react"
3+
14
export*from"./FormCloseButton"
25
export*from"./FormSection"
36
export*from"./FormDropdownField"
47
export*from"./FormTextField"
58
export*from"./FormTitle"
9+
10+
interfaceFormHelpers{
11+
name:string
12+
onBlur:FocusEventHandler
13+
onChange:ChangeEventHandler
14+
id:string
15+
value?:string|number
16+
error:boolean
17+
helperText?:string
18+
}
19+
20+
exportconstgetFormHelpers=<T>(form:FormikContextType<T>,name:string):FormHelpers=>{
21+
// getIn is a util function from Formik that gets at any depth of nesting, and is necessary for the types to work
22+
consttouched=getIn(form.touched,name)
23+
consterrors=getIn(form.errors,name)
24+
return{
25+
...form.getFieldProps(name),
26+
id:name,
27+
error:touched&&Boolean(errors),
28+
helperText:touched&&errors,
29+
}
30+
}
31+
32+
exportconstonChangeTrimmed=
33+
<T>(form:FormikContextType<T>)=>
34+
(event:ChangeEvent<HTMLInputElement>):void=>{
35+
event.target.value=event.target.value.trim()
36+
form.handleChange(event)
37+
}

‎site/src/components/SignIn/SignInForm.tsx

Lines changed: 35 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import React from "react"
44
import*asYupfrom"yup"
55

66
import{Welcome}from"./Welcome"
7-
import{FormTextField}from"../Form"
87
importFormHelperTextfrom"@material-ui/core/FormHelperText"
98
import{LoadingButton}from"./../Button"
9+
importTextFieldfrom"@material-ui/core/TextField"
10+
import{getFormHelpers,onChangeTrimmed}from"../Form"
1011

1112
/**
1213
* BuiltInAuthFormValues describes a form using built-in (email/password)
@@ -18,8 +19,17 @@ interface BuiltInAuthFormValues {
1819
password:string
1920
}
2021

22+
exportconstLanguage={
23+
emailLabel:"Email",
24+
passwordLabel:"Password",
25+
emailInvalid:"Please enter a valid email address.",
26+
emailRequired:"Please enter an email address.",
27+
authErrorMessage:"Incorrect email or password.",
28+
signIn:"Sign In",
29+
}
30+
2131
constvalidationSchema=Yup.object({
22-
email:Yup.string().required("Email isrequired."),
32+
email:Yup.string().trim().email(Language.emailInvalid).required(Language.emailRequired),
2333
password:Yup.string(),
2434
})
2535

@@ -59,50 +69,30 @@ export const SignInForm: React.FC<SignInFormProps> = ({ isLoading, authErrorMess
5969
<>
6070
<Welcome/>
6171
<formonSubmit={form.handleSubmit}>
62-
<div>
63-
<FormTextField
64-
label="Email"
65-
autoComplete="email"
66-
autoFocus
67-
className={styles.loginTextField}
68-
eventTransform={(email:string)=>email.trim()}
69-
form={form}
70-
formFieldName="email"
71-
fullWidth
72-
inputProps={{
73-
id:"signin-form-inpt-email",
74-
}}
75-
variant="outlined"
76-
/>
77-
<FormTextField
78-
label="Password"
79-
autoComplete="current-password"
80-
className={styles.loginTextField}
81-
form={form}
82-
formFieldName="password"
83-
fullWidth
84-
inputProps={{
85-
id:"signin-form-inpt-password",
86-
}}
87-
isPassword
88-
variant="outlined"
89-
/>
90-
{authErrorMessage&&(
91-
<FormHelperTextdata-testid="sign-in-error"error>
92-
{authErrorMessage}
93-
</FormHelperText>
94-
)}
95-
</div>
72+
<TextField
73+
{...getFormHelpers<BuiltInAuthFormValues>(form,"email")}
74+
onChange={onChangeTrimmed(form)}
75+
autoFocus
76+
autoComplete="email"
77+
className={styles.loginTextField}
78+
fullWidth
79+
label={Language.emailLabel}
80+
variant="outlined"
81+
/>
82+
<TextField
83+
{...getFormHelpers<BuiltInAuthFormValues>(form,"password")}
84+
autoComplete="current-password"
85+
className={styles.loginTextField}
86+
fullWidth
87+
id="password"
88+
label={Language.passwordLabel}
89+
type="password"
90+
variant="outlined"
91+
/>
92+
{authErrorMessage&&<FormHelperTexterror>{Language.authErrorMessage}</FormHelperText>}
9693
<divclassName={styles.submitBtn}>
97-
<LoadingButton
98-
color="primary"
99-
loading={isLoading}
100-
fullWidth
101-
id="signin-form-submit"
102-
type="submit"
103-
variant="contained"
104-
>
105-
{isLoading ?"" :"Sign In"}
94+
<LoadingButtoncolor="primary"loading={isLoading}fullWidthtype="submit"variant="contained">
95+
{isLoading ?"" :Language.signIn}
10696
</LoadingButton>
10797
</div>
10898
</form>

‎site/src/pages/login.test.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
importReactfrom"react"
2-
import{act,fireEvent,screen}from"@testing-library/react"
2+
import{act,screen}from"@testing-library/react"
3+
importuserEventfrom"@testing-library/user-event"
34
import{history,render}from"../test_helpers"
45
import{SignInPage}from"./login"
56
import{server}from"../test_helpers/server"
67
import{rest}from"msw"
8+
import{Language}from"../components/SignIn/SignInForm"
79

810
describe("SignInPage",()=>{
911
beforeEach(()=>{
@@ -21,12 +23,12 @@ describe("SignInPage", () => {
2123
render(<SignInPage/>)
2224

2325
// Then
24-
awaitscreen.findByText("Sign In",{exact:false})
26+
awaitscreen.findByText(Language.signIn,{exact:false})
2527
})
2628

2729
it("shows an error message if SignIn fails",async()=>{
2830
// Given
29-
const{ container}=render(<SignInPage/>)
31+
render(<SignInPage/>)
3032
// Make login fail
3133
server.use(
3234
rest.post("/api/v2/users/login",async(req,res,ctx)=>{
@@ -35,17 +37,18 @@ describe("SignInPage", () => {
3537
)
3638

3739
// When
38-
// Set username / password
39-
const[username,password]=container.querySelectorAll("input")
40-
fireEvent.change(username,{target:{value:"test@coder.com"}})
41-
fireEvent.change(password,{target:{value:"password"}})
40+
// Set email / password
41+
constemail=screen.getByLabelText(Language.emailLabel)
42+
constpassword=screen.getByLabelText(Language.passwordLabel)
43+
userEvent.type(email,"test@coder.com")
44+
userEvent.type(password,"password")
4245
// Click sign-in
43-
constsignInButton=awaitscreen.findByText("Sign In")
46+
constsignInButton=awaitscreen.findByText(Language.signIn)
4447
act(()=>signInButton.click())
4548

4649
// Then
4750
// Finding error by test id because it comes from the backend
48-
consterrorMessage=awaitscreen.findByTestId("sign-in-error")
51+
consterrorMessage=awaitscreen.findByText(Language.authErrorMessage)
4952
expect(errorMessage).toBeDefined()
5053
expect(history.location.pathname).toEqual("/login")
5154
})

‎site/yarn.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2731,6 +2731,13 @@
27312731
"@testing-library/dom" "^8.0.0"
27322732
"@types/react-dom" "*"
27332733

2734+
"@testing-library/user-event@^13.5.0":
2735+
version "13.5.0"
2736+
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295"
2737+
integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==
2738+
dependencies:
2739+
"@babel/runtime" "^7.12.5"
2740+
27342741
"@tootallnate/once@1":
27352742
version "1.1.2"
27362743
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp