-
Notifications
You must be signed in to change notification settings - Fork 10
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
Conversation
userEvent.click(submitButton) | ||
}) | ||
|
||
expect(mockOnSubmit).not.toHaveBeenCalled() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}
src/components/login/LoginForm.tsx
Outdated
<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>} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 => ( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"], |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
beforeEach(() => { | ||
render(<LoginForm onSubmit={mockOnSubmit} />) | ||
usernameField = screen.getByLabelText(/username/i) | ||
passwordField = screen.getByLabelText(/password/i) | ||
submitButton = screen.getByRole('button', { | ||
name: /submit/i, | ||
}) | ||
mockOnSubmit.mockReset() | ||
}) |
There was a problem hiding this comment.
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.
// 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() | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
<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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice null guarding here.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this 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.
There was a problem hiding this 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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) | ||
}) |
There was a problem hiding this comment.
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!
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> | ||
) |
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
389ace8
to
de1e63a
Compare
Closes #64 |
Closes #64 |
Tied to Feature Set #70
Changes
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
[yarn test]
[yarn start]
Screenshots
Closes #70