Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(login): Login form unit test #64

Merged
merged 38 commits into from
Apr 8, 2021
Merged

Conversation

fluturecode
Copy link
Contributor

@fluturecode fluturecode commented Mar 23, 2021

Tied to Feature Set #70

Changes

  1. Created unit test for Login Form (LoginForm.tsx) with React-Testing-Library.
  2. Created Login Form component (LoginForm.test.tsx) with React-Hook-Form.
  3. Added a Home Page component to pull in the Login Form.
  4. Added form validation with Yup.
  • Build unit test for Login form.
  • Build login form.
  • Add form validation.
  • Check that validation refactor passes all unit tests.

Purpose

To add unit testing to the login form component.
To create the basic login form through TDD.

Approach

There was no login form previously.

Learning

Researched libraries for forms as well as validation. Chose to use React-Hook-Form and Yup. For more information '(https://react-hook-form.com/)' '(https://github.com/jquense/yup)'

Testing

  1. Pull in the changes to your local copy of this branch.
  2. Run [yarn test]
  3. Check that all tests have passed.
  4. Run [yarn start]
  5. Check that the Login form renders in the browser.

Screenshots

Screen Shot 2021-04-05 at 2 39 56 PM

Closes #70

@fluturecode fluturecode added Feature This is a feature from the scope of work feature sets. test Code tests only labels Mar 23, 2021
@fluturecode fluturecode self-assigned this Mar 23, 2021
@fluturecode fluturecode changed the title Ee 63 login form test feat(login): Login form unit test Mar 23, 2021
userEvent.click(submitButton)
})

expect(mockOnSubmit).not.toHaveBeenCalled()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be worth ensuring that the validation messages are correct as well.

Copy link
Contributor Author

@fluturecode fluturecode Apr 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thomascking Agreed. This is being refactored, and will be pushed back up, moving the validators outside this component, as well as clarifying the messages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thomascking Great suggestion. I refactored the code to move schema validation into a separate test file and added the following tests for the LoginForm component:

  • check that validation messages are not displayed when inputs are valid
  • check that validation messages are displayed when inputs are invalid
  • check that only email validation message is displayed when email is valid but password is valid
  • check that only password validation message is displayed when password is invalid but email is valid}

Comment on lines 55 to 59
<label htmlFor="username">
<span>Username</span>
<input id="username" type="text" name={LoginKey.username} ref={register} />
</label>
{errors.username && <p role="alert">{errors.username.message}</p>}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is very similar to the below code, maybe a FormControl component would make sense here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thomascking I'm new to working with React-hook-form, but it does offer a 'Controller' class that I'll try to implement as well which should address your comment.

@@ -0,0 +1,9 @@
import React from 'react'

const SignUpForm = (): JSX.Element => (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, might be useful to type the function React.FC instead of the return type.

Copy link
Contributor Author

@fluturecode fluturecode Apr 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thomascking I'm exploring working with both, as well as seeing how the linting and typescript types work. Still deciding the best way of doing things, but you're right I should stick to one throughout. However, that might be a refactor later on as we are still trying to test different things in the early stage. Thanks for pointing that out.

"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure this is probably prettier or the like, but it is somewhat annoying that it made something multi-line earlier and made this single line.

Copy link
Contributor Author

@fluturecode fluturecode Apr 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thomascking It's definitely annoying. Will figure out how to change that setting.

Comment on lines +16 to +24
beforeEach(() => {
render(<LoginForm onSubmit={mockOnSubmit} />)
usernameField = screen.getByLabelText(/username/i)
passwordField = screen.getByLabelText(/password/i)
submitButton = screen.getByRole('button', {
name: /submit/i,
})
mockOnSubmit.mockReset()
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job keeping the code DRY here.

Comment on lines 51 to 58
// screen.getAllByRole should throw an error if no matching elements are found.
// For some reason, `expect(getAllByRole('alert')).toThrow()` would not work,
// but it seems to work if I manually catch the error.
try {
screen.getAllByRole('alert')
} catch (error) {
expect(error).toBeDefined()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test confused me a little at first. I would clarify that errors have the role of 'alert' so screen.getAllByRole('alert') throws an error because there are no actual errors on the page. Not a big deal at the moment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was trying to verify that no error messages are displayed when the inputs are valid. It's expected behavior for getAllByRole to throw an error because no validation errors are on the page, but usually you assert that by chaining a .toThrow assertion after expect. In this case, that wouldn't work. toThrow wouldn't catch the error and it would cause the test to fail. This is a workaround until I figure out how to clean it up.

Comment on lines +21 to +33
<form onSubmit={onSubmit}>
<label htmlFor="username">
<span>Username</span>
<input id="username" type="text" name="username" ref={register} />
</label>
{errors.username && <span role="alert">{errors.username.message}</span>}
<label htmlFor="password">
<span>Password</span>
<input id="password" type="password" name="password" ref={register} />
</label>
{errors.password && <span role="alert">{errors.password.message}</span>}
<button type="submit">Submit</button>
</form>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice null guarding here.

Copy link
Contributor

@codysmith287 codysmith287 Apr 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another way you could null guard since we're using typescript would be errors?.username?.message so it would just be

<span role="alert">{errors?.username?.message}</span>

What you have is good though so no need to change it.

Copy link
Contributor Author

@fluturecode fluturecode Apr 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codysmith287 That's a great point. I'm going to add that change.

Copy link
Contributor

@codysmith287 codysmith287 Apr 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually never mind in this case, that throws off your tests because the span with role="alert" still appears.

Copy link
Contributor Author

@fluturecode fluturecode Apr 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codysmith287 oh you're right on that!

Copy link
Contributor

@codysmith287 codysmith287 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work getting some solid tests going! Everything looks great, just had one comment on a test that was a little confusing, but definitely something that can be addressed later.

Copy link
Member

@coreyshuman coreyshuman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a super strong start to the project! Typing, unit tests, small components... it's got it all!

try {
screen.getAllByRole('alert')
} catch (error) {
expect(error).toBeDefined()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to be more specific on this check, since lots of things could create an exception to be thrown. We want to make sure we see the right error here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the official React Testing Library docs recommend using queryBy* methods instead to test that elements are not present since they don't throw an error. So we should be testing this condition as

expect(screen.queryAllByRole('alert')).toHaveLength(0)

This makes sense since the error thrown is an internal implementation of the testing library and making our test dependent on that error type can break our tests if they change the implementation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense, good catch!


expect(screen.getAllByRole('alert')).toHaveLength(1)
expect(screen.getByRole('alert').innerHTML).toBe(Constants.validationMessages.username.MUST_BE_VALID_EMAIL)
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice, this is good specificity on this test. And the use of the constant will allow us to change the error message without rewriting the test. I love it!

Comment on lines +12 to +34
const { register, handleSubmit, errors } = useForm<ILoginFormData>({
resolver: yupResolver(loginFormSchema),
})

const onSubmit = handleSubmit((formData: ILoginFormData) => {
props.onSubmit(formData.username, formData.password)
})

return (
<form onSubmit={onSubmit}>
<label htmlFor="username">
<span>Username</span>
<input id="username" type="text" name="username" ref={register} />
</label>
{errors.username && <span role="alert">{errors.username.message}</span>}
<label htmlFor="password">
<span>Password</span>
<input id="password" type="password" name="password" ref={register} />
</label>
{errors.password && <span role="alert">{errors.password.message}</span>}
<button type="submit">Submit</button>
</form>
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a recommendation for a future refactor once we have multiple forms. We're going to have this structure very regularly: useForm, submitHandler, form HTML. If it makes sense, we can create a generic

component to wrap this common functionality, and pass into it the names/types of fields and buttons. It would basically be a form builder.

I think this suggestion will make more sense and bring value once we get to the point of adding locale and translation support. Then we can build out that logic into one reusable form component, instead of needing to refactor every individual component which has a form in it.

const validUsername = '[email protected]'
const validPassword = 'password'

const testValidInput = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking, but I would add the ILoginFormData interface to all of these test case objects. If the implementation (and interface) were to change later, this makes the refactor easier and also lets the developer know the test needs attention before even running it.

patterns: {
DIGIT_REGEX: /[0-9]/,
// eslint-disable-next-line no-useless-escape
EMAIL_REGEX: /^[a-z0-9!#$%&'*+\/=?^_\`{|}~.-]+@[a-z0-9]([a-z0-9-])+(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,
Copy link
Member

@coreyshuman coreyshuman Apr 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend we have a set of passing and failing email formats, and iterate tests across them to verify the Yup format validator behaves in a way that we understand and agree with.

For context, client-side email validation is a touchy subject. Check out this S&P discussion for context (and some email samples to test against):
Shift3/standards-and-practices#39

@fluturecode
Copy link
Contributor Author

Closes #64

@fluturecode fluturecode closed this Apr 8, 2021
@fluturecode fluturecode reopened this Apr 8, 2021
@fluturecode
Copy link
Contributor Author

Closes #64

@fluturecode fluturecode merged commit 2f3d27d into development Apr 8, 2021
@fluturecode fluturecode deleted the ee-63-login-form-test branch April 8, 2021 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature This is a feature from the scope of work feature sets. test Code tests only
Projects
None yet
Development

Successfully merging this pull request may close these issues.

LP1 - Build Login Form with Unit Test
6 participants