Alert Component: Walkthrough
Steps
- Write a test for the
<Alert />component for a failed action (error):<Alert message="Error!" />. It should check for a DOM node that contains the "Error!" content and assert the text of the node isError!. - Write the code to make this test pass. Use the error example above as a guideline for how this should look (you might also want to render a dummy
<Alert />inside your<AddComponent />render so you can visualise it). - Write a test for the
<Alert />component for a successful action:<Alert message="Success!!!" success />. It should check for a DOM node with the "Success!!!" content and assert the text isSuccess!!!. - Write the code that makes this test pass.
- Ensure your component is styled so that error messages show up in a red box, and success messages show up in a green box. Test as you go to ensure you haven't broken anything.
- Add some extra initial state to
<AddProperty />:
- In the
handleAddProperty()function, set the above state again (we want to clear any error/success messages before each re-submit of the form so they don't persist.) - When your Axios Promise resolves (
.then()) then set theisSuccessstate to true and set themessageto a helpful string for the user. - When your Axios Promise rejects (
.catch()) keep the isSuccess state asfalseand set themessageto a helpful string for the user. Note that to cause an error so you can test this, just kill your API process - this will result in a network error. - Render the
<Alert />inside your form. It should only show if message is truthy. - Write a test for when
messageis not truthy it does not render the component. - Add a snapshot test to all of the three possible
<Alert />UI scenarios: "error", "success" and "no message".
1 - Write a test for the <Alert /> component for a failed action (error): <Alert message="Error!" />.
It should check for a DOM node that contains the "Error!" content and assert the text of the node is string Error!.
If you haven't already got a __tests__ or tests folder then please go ahead and make one now inside your /src directory. Inside create a components directory, and inside create a file Alert.test.js (so you should have src/tests/components/Alert.test.js).
Import in the necessary dependencies:
Remember, you aren't making the <Alert /> component yet - you expect this test to fail.
Now create a new test:
Use react-testing-library to render the component, passing in a message prop with the message we'll assert against:
Then we are going to search for the DOM node that contains the error. The getByText() method is made available by invoking render() and it is going to come useful in looking for DOM nodes that contain certain text. In our case, we want to look for a DOM node that has the text "Error!"
alertMessageNode now contains markup found on the DOM that has text content which matches the string Error!. We then use the DOM text property textContent to write our expectation that the text of that DOM node is indeed "Error!":
Now run your tests.
Note that this test can be written in a DRYer way, by destructuring getByText from the result of invoking render():
And we can use a regex pattern like /Error/ which matches to any string that contains "Error" (regardless of what comes before or after). This does not change the result of the test in any way, just makes our test easier to read. This test written by using destructuring and a regex will then look like the code below - such a pattern is well used in the react-testing-library documentation!
2 - Write the code to make this test pass.
Use the error example above as a guideline for how this should look (you might also want to render a dummy <Alert /> inside your <AddComponent /> render so you can visualise it).
Ideally what we want to do here is to create the component going solely by the test. When we pass the test, we can modify the aesthetic of our component etc. in knowledge that we have the test to fall back on.
You might feel confident enough to do this step on your own, in which case stop reading and go do that! Otherwise continue.
We know that (for now) this component won't need state so we want to make it a simple function, and that this component will take a prop of message that will be rendered between opening and closing tags of an HTML element with the classname .alert. Let's see how that looks:
In a presentational component, React passes one argument to the function - the object props. Here we destructure props to get the prop message, which we then render inside <div> tags.
3 - Write a test for the <Alert /> component for a successful action: <Alert message="Success!!!" success />.
It should check for a DOM node with the "Success" content and assert the text contains Success string.
What is the difference between this test and our other test? It is a success prop:
When you pass a prop without assigning a value (as with success above), it's the equivalent of assigning prop={true} so the above is really short for:
It's just a nice bit of extra syntactic sugar we get with JSX.
4 - Write the code that makes this test pass.
A couple of things have changed here:
- In addition to the
messageprop, we've also destructured asuccessprop. - We've changed the class name to a JSX expression with a template literal string inside. Inside this template literal we have another JavaScript expression of a ternary. We always have the class name
.alert, but depending ifsuccessis truthy or not we add class.alert-successor.alert-error.
5 - Ensure your component is styled so that error messages show up in a red box, and success messages show up in a green box. Test as you go to ensure you haven't broken anything.
Please attempt this one on your own. Use the CSS guidance above to assist you.
6 - Add some extra initial state to <AddProperty />:
Here's the extra state that needs adding:
Add alert as key to const initialState = { fields: { ... }, alert: { ... } }
And then below the fields state hook, add a second hook to manage the state of alert:
8 - In the handleAddProperty() method, set the above state again
We want to clear any error/success messages before each re-submit of the form so they don't persist.
Right after your event.preventDefault():
9 - When your Axios Promise resolves (.then()) then set the isSuccess state to true and set the message to a helpful string for the user.
10 - When your Axios Promise rejects (.catch()) keep the isSuccess state as false and set the message to a helpful string for the user.
Note that to cause an error so you can test this, just kill your API process - this will result in a network error.
11 - Render the <Alert /> inside your form. It should only show if message is truthy.
We've put this inside the <form> tag for stylistic reasons but position it where you think is best suited for the aesthetic you're going for:
We mention that we should only render <Alert /> if message is truthy, so how can we handle this logic? We have a couple of options available to us, but before we look into them lets remember that:
a) We only want to render <Alert /> if there is a message,
b) message is of type string,
c) Boolean values for strings are:
Option 1
Handle this logic in the parent component <AddProperty /> by using a conditional expression when we render Alert inside the form. Notice how we need to wrap the <Alert /> component in () and the whole expression in {}.
Option 2
Handle this logic inside the child component <Alert />. We do not change <Alert /> inside the form and so leave it like:
Instead we can handle the conditional rendering of <Alert/> by writing an if(){} just before the return statement inside Alert.js
This reads as if not truthy message return null. If the whole if statement evaluates to true, the component execution stops here and renders null to React's virtual DOM. If it evaluates to false, it will be bypassed and the component returns normaly. The component should now look like:
Option 1 vs Option 2
Both approaches produce the same results, with the main differences lying on:
-
who controls the render of the component - their parent (option 1) or itself (option 2),
-
readability - many consider option 2 easier to read
-
testing - when using a per component testing strategy option 2 is easier to test
-
reusability - we can use
<Alert />in other components without having to worry about how to control it. It is enough to pass a "message" prop for it to conditionally render.
12 - Write a test for when message is not truthy it does not render the component
To write this test we are going to use the "Option 2" above and asFragment() instead of getByText(). In react-testing-library, asFragment() gives us a nice DOM slice that corresponds to the React component we're testing
We start by writing a new test and destructuring the asFragment() method from the result of rendering <Alert /> :
Notice how the prop message passed into <Alert /> is an empty string. We are certain that no markup should be rendered when message="", so we are simply going to create a snapshot test based on the result of invoking asFragment()
What do you think the const alert will contain? We expect no markup to be rendered, in this case an empty <Fragment />. The best way to assert this is to use the toMatchSnapshot() from the jest framework. We can set our expectation like:
The first time the test runs, jest will grab a stringified DOM representation of alert and save it under a folder _snapshots_. Snapshots are a great (and quick) way of creating tests that validate markup
Run your tests and head to _snapshots/Alert.test.snap.js to see what we mean.
Snapshots do not capture visual changes made by CSS but they do capture everything else, including markup and CSS classes used in a component. They are a very useful tool whenever you want to make sure your UI does not change unexpectedly but they shouldn't be the only type of tests we use. More on snapshot testing here.
13 - Add a snapshot test to all of the three possible <Alert /> UI scenarios: "error", "success" and "no message".
For "no message" we added this snapshot on the example before. For "error" we can do this by adding asFragment to the properties destructured from rendering <Alert />and then adding an extra expectation:
For "success" you can use the example above and add a snapshot test to the displays a success message test!