Show the Answer and Submit Button
jacobwicks
Posted on January 17, 2020
In this post we will:
- Make the
Answer
component that gets the current card fromCardContext
, keeps the answer hidden until it is told to show the answer, and shows the answer to the user when they are ready - Make clicking the
Submit
button show the answer
In this post we will make clicking the Submit
button show the answer to the user. In the next post we will move the Submit
button into a new component called Buttons
. Buttons
will show the Submit
button. After the user clicks Submit
Buttons
will show two buttons labeled Right
and Wrong
. Clicking Right
or Wrong
will let the user record if they got the answer right or wrong.
The Answer Component
In the last post we made the Answering
scene. The Answering
scene is where the user answers questions from the cards. It shows the user the question from the current card and gives them a box to type their answer in. The Answer
component will appear on screen as a box that shows the answer after the user is done trying to answer the card.
Now we will make the Answer
component that shows the answer to the current card. Answer
will be hidden until after the user clicks the Submit
button. We will put the Answer
component inside the Answering
scene. That way the user will see it when they need to.
User Story
- The user sees a question displayed on the screen. The user writes an answer to the question. When the user is done with their answer, they click the
Submit
button. The app shows them the answer from the current card. The user compares their answer to the answer from the current card. The user decides they got the question right, and clicks theRight Answer
button. Then the user sees the next question.
Features
- a component that shows the answer to the user
- the answer is hidden and doesn't show up on the screen
- clicking Submit button shows answer
Choose Components
We want to show the answer to the user when they are ready to see it. That means sometimes the answer will be hidden, but sometimes it will be shown. We'll use a Transition component to hide the answer and animate the answer when it appears.
Transition
takes a prop visible
that tells it to show up or not. visible
is boolean. It is either true or false. When visible
is true, the Transition
will show its contents. When visible
is false, the Transition
will hide its contents. When visible
changes from true to false or from false to true, Transition
will run an animation.
Transition
needs its contents to be wrapped with a div
. If the contents are not inside a div
the Transition
won't work right. So we'll put a div inside Transition
. We'll put the the answer from the card inside the div so the user can see it.
We'll put a Header
inside the div
too. A Header
is a component that contains enlarged text that is used to label things. This Header
will say 'Answer' so the user knows that they are seeing the answer.
Decide What to Test
When you decide what to test, ask yourself "What does this component do? What part of that matters to the user?" I decided that there are four things we need to test in this component.
- when visible, it shows the answer
- shows the right answer (the answer from the current card, not some other card)
- has a header with 'Answer' in it so the user knows they are looking at the answer
- if it isn't visible, the answer doesn't show up on the screen
Test 1: Answer
Renders Without Crashing
File: src/scenes/Answering/components/Answer/index.test.tsx
Will Match: src/scenes/Answering/components/Answer/complete/test-1.tsx
A lot of the time, when people do TDD the first test they write for a component is a test to see if it will render without crashing. We won't always start with a test that basic. But for the Answer
component we will start with this basic test.
Make the necessary imports. We are importing CardProvider
and the initialState
object from CardContext
. Answer
will need access to the cards in CardContext
so Answer
can show the answer to the user. To get access to the CardContext
, Answer
must be inside the CardProvider
component that is exported from the CardContext
file.
We are importing the CardProvider
to this test file because in the tests we will render Answer
inside of the CardProvider
. We render Answer
inside of the CardProvider
so that Answer
can access the cards in CardContext
. That way we can be confident that our tests actually show us how Answer
will work when we use it in the app.
import React from 'react';
import { render, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { CardProvider, initialState } from '../../../../services/CardContext';
import Answer from './index';
afterEach(cleanup);
Write the Helper Function renderAnswer
To test Answer
we need to use the render
method from React Testing Library. We need to render Answer
inside of the CardProvider
so that Answer
can access the cards from CardContext
. We will write the helper function renderAnswer
to use the render
method from React Testing Library to render Answer
inside of the CardProvider
. Writing renderAnswer
means that we can call renderAnswer
in each of our tests instead of rewriting the code in each test.
renderAnswer
takes an optional boolean parameter visible
. Optional means that we don't have to pass an argument for visible
. renderAnswer
will work fine without it. But if visible
is defined, it will be passed to Answer
as the value of Answer
's prop named visible.
If the parameter visible
is not defined, we will pass true to Answer
as the value of the prop named visible. So when we call renderAnswer()
without an argument, it will render a visible answer. If we do want to render a hidden answer, we will call renderAnswer(false)
.
//the ?: after visible tells typescript that it is optional
const renderAnswer = (visible?: boolean) => render(
<CardProvider>
<Answer visible={visible !== undefined ? visible : true}/>
</CardProvider>
);
Write the test 'renders without crashing.' To test that Answer
renders without crashing, call renderAnswer
.
it('renders without crashing', () => {
renderAnswer();
});
It does not render without crashing.
Pass Test 1: Answer
Renders Without Crashing
File: src/scenes/Answering/components/Answer/index.tsx
Will Match: src/scenes/Answering/components/Answer/complete/index-1.tsx
We'll render a div to pass the first test. One of the rules of test driven development is that you are only allowed to write the minimum amount of code required to pass the test. We don't always strictly follow that rule in this tutorial. But in this step we do. This is a minimal amount of code for a React component! It is a functional component that returns a div.
import React from 'react';
const Answer = () => <div/>
export default Answer;
Now it renders without crashing!
Test 2: Answer Has a Div that Will Show the Answer
File: src/scenes/Answering/components/Answer/index.test.tsx
Will Match: src/scenes/Answering/components/Answer/complete/test-2.tsx
Answer
will take a boolean prop visible
. Let's test to make sure that when it is visible, it shows the answer. Remember, our helper component passes true as the value of the prop visible
unless we tell it to do something else.
Let's put the three tests of the visible Answer
inside a describe() block. describe() is a method that Jest provides so that you can organize your tests. You will see when you run these tests that Jest shows you the three tests under the name of the describe block.
Make a describe block named 'when visible, it shows the answer.' Write a comment for each of the tests we'll write inside the describe block.
describe('when visible, it shows the answer', () => {
//has the div that will show the answer
// has a header with 'Answer'
// shows the right answer
});
When Answer
is visible, Answer
shows the div that will hold the answer:
describe('when visible, it shows the answer', () => {
//has the div that will show the answer
it('has the answer div', () => {
const { getByTestId } = renderAnswer();
const answerDiv = getByTestId('answer')
expect(answerDiv).toBeInTheDocument();
});
// shows the right answer
// has a header with 'Answer'
});
Pass Test 2: Answer Has Div that Will Show the Answer
File: src/scenes/Answering/components/Answer/index.tsx
Will Match: src/scenes/Answering/components/Answer/complete/index-2.tsx
Add the testId 'answer' to the div.
const Answer = () => <div data-testid='answer'/>
Test 3: Answer Div Shows the Right Answer
File: src/scenes/Answering/components/Answer/index.test.tsx
Will Match: src/scenes/Answering/components/Answer/complete/test-3.tsx
The most important feature of Answer
is that it shows the right answer to the user.
We want to test that the div that has the Header
and the answer from the current card is actually showing the right answer to the user. We find the div by searching for its testId 'answer.' We find the current card by getting the current index from the initialState
object that we imported from CardContext
. Then we look at the current index in the array cards in initialState
. We will compare the contents of the div to the answer from the current card.
// shows the right answer
it('displays the right answer', () => {
const { getByTestId } = renderAnswer();
//find the answer div
const answer = getByTestId('answer');
//get the textContent
const text = answer.textContent;
//this is the answer from the card at index current in cards
const initialAnswer = initialState.cards[initialState.current].answer;
//expect the rendered text in the div
//to equal the answer from initial state,
expect(text).toEqual(initialAnswer);
});
Pass Test 3: Answer Div Shows the Right Answer
File: src/scenes/Answering/components/Answer/index.tsx
Will Match: src/scenes/Answering/components/Answer/complete/index-3.tsx
Import useContext
from React. useContext
is the React Hook that lets you get values from a context. Import CardContext
from the CardContext file. CardContext
is the context that we made. CardContext
has the cards and the current index in it.
import React, { useContext } from 'react';
import { CardContext } from '../../../../services/CardContext';
We call useContext()
and pass it the CardContext
. useContext
will return the current value of the CardState
inside CardContext
.
We use Object Destructuring to get the cards
array and the current
index out of CardContext
.
We use Object Destructuring again to get the answer
out of the card at the current index in cards
.
Return the answer
inside of the answer div.
const Answer = () => {
const { cards, current } = useContext(CardContext);
const { answer } = cards[current];
return <div data-testid='answer'>{answer}</div>
};
Test 4: Header
File: src/scenes/Answering/components/Answer/index.test.tsx
Will Match: src/scenes/Answering/components/Answer/complete/test-4.tsx
We are going to add a Header
with the word 'Answer' in it. Because we know what text will be inside the header, we can use the getByText
query to find it instead of assigning a testId
. See how we have passed '/answer/i' to getByText
? That is a regular expression, or regEx. Regular Expressions are a powerful tool for searching and manipulating text. Regular expressions can get pretty complicated. This one just matches the text 'answer' and the /i means it is case insensitive.
Write Your Tests to Find the Important Things
We use a case insensitive regEx because even though we decided it's important for the text 'Answer' to show up, we don't think that the capitalization is important. So we don't test for capitalization. Using a case insensitive regEx means that no matter how you capitalize the word 'Answer' in the Header
, it will still pass the test. If capitalization was important, you could change the regEx or search for a string instead.
// has a header with 'Answer'
it('has the answer header', () => {
const { getByText } = renderAnswer();
const header = getByText(/answer/i);
expect(header).toBeInTheDocument();
});
Pass Test 4: Header
File: src/scenes/Answering/components/Answer/index.tsx
Will Match: src/scenes/Answering/components/Answer/complete/index-4.tsx
Import the Header
component from Semantic UI React.
import { Header } from 'semantic-ui-react';
Rewrite the returned component. Add the Header
to it. as='h3'
tells the Header
how large it should be. h1 is the biggest header size, h2 is a little smaller, and h3 is smaller than h2. content
is the text that shows up inside the Header
.
return (
<div data-testid='answer'>
<Header as='h3' content ='Answer'/>
{answer}
</div>
)};
Passes the header test. But the test for the right answer fails!
What's Going On?
Look at the error that Jest is showing us. The div still has the answer text in it. But now it also has a Header
component. Inside the Header
is the string 'Answer.' Jest is finding the textContent
of the Header
and the textContent
of the div, not just the textContent
of the div. The result is right but the test is failing. So we need to change the test. To get the test to pass we need to change the way we test for the right answer.
Answer: Change Test Named 'displays the right answer'
File: src/scenes/Answering/components/Answer/index.test.tsx
Will Match: src/scenes/Answering/components/Answer/complete/test-5.tsx
This is an example of the type of problem that comes up a lot when you are testing. You wrote the test the way you thought you needed. Then you wrote the code to do what you want. Now the code does what you want, but the test is failing. Once you look at your code and you are sure that the code is working, then you know that you need to change the test to fit the code.
What's making this test fail is that it is finding the div with the testId 'answer' and looking at all of the textContent inside of that div. The 'answer' div has the Header
in it, so the textContent of the 'answer' div includes the string 'Answer' from the header as well as the answer from the current card.
Here is what gets rendered inside the div with the testId 'answer.' To see this, you can scroll up when a test fails. You can also get the debug
method from the call to render or your helper component, and call debug()
.
You can also use console.log()
to see the textContent
of the answer
div.
console.log(answer.textContent)
So we make a const fullAnswer
by adding the string 'Answer' to the initialAnswer
. Then expect the textContent of the div to match fullAnswer
.
//...rest the test above
const initialAnswer = initialState.cards[initialState.current].answer;
//Answer header is in the div
//add the string 'Answer' to initialAnswer
const fullAnswer = 'Answer' + initialAnswer;
//expect the rendered text in the div
//to equal the answer from initial state,
//plus the 'Answer' string from the header
expect(text).toEqual(fullAnswer);
});
Test 5: Answer is Invisible when Hidden
File: src/scenes/Answering/components/Answer/index.test.tsx
Will Match: src/scenes/Answering/components/Answer/complete/test-6.tsx
This test is to make sure Answer doesn't show up when it is hidden. This test is outside of the describe block 'when visible, it shows the answer.'
We pass false
to the helper function to tell it that we want Answer to be hidden. Then we use a query to search for the answer div by testId. But we aren't using getByTestId
. We are using queryByTestId(), a new query that we haven't seen before.
queryBy vs. getBy
The getBy queries will throw an error and fail the test if they don't find anything. That's normally good. But here we don't expect to find the testId. We expect that we won't find the testId because Answer
shouldn't show up. Answer
is hidden, so we expect that it won't show up on the page. So we use queryByTestId, because the queryBy queries won't throw an error if they don't find anything. When a queryBy query doesn't find anything, it returns null without throwing an error. We set the variable answer
equal to the result of the queryByTestId. We don't expect to find anything, so we expect our variable answer
to be null.
toBeNull() is the assertion you use when you expect something to be null.
it('If not visible, it isnt visible', () => {
const { queryByTestId } = renderAnswer(false);
const answer = queryByTestId('answer');
expect(answer).toBeNull();
});
Pass Test 5: Answer is Invisible When Hidden
File: src/scenes/Answering/components/Answer/index.tsx
Will Match: src/scenes/Answering/components/Answer/complete/index-6.tsx
We import the Transition
component from Semantic UI React. Transition
takes a boolean prop called visible
. Transition
will show or hide its contents based on the value of visible
. Transition
will animate the appearance or disappearance of the contents when visible
changes from true to false or from false to true. I find that Transition only works correctly when the contents are inside of a div. We will use the 'answer' div.
import { Header, Transition } from 'semantic-ui-react';
Add a prop named visible to the Answer component. Use TypeScript to declare visible as type boolean. Wrap the div that Answer returns in the Transition component. Pass the visible prop to Transition.
Transition Props
animation='drop'
tells Transition
what kind of animation to use. Semantic UI React has many types of animations that you can choose from.
duration={500}
tells Transition
how long the animation should take.
unmountOnHide
tells Transition
to unmount the contents from the React component tree when the Transition
is hidden. If you don't tell it to unmount on hide, the contents will stay in the component tree even when it is hidden and the user can't see it. This usually doesn't matter, but one of the tests in a later post won't pass unless we use unmountOnHide
.
const Answer = ({
visible
}:{
visible: boolean
}) => {
const { cards, current } = useContext(CardContext);
const { answer } = cards[current];
return (
<Transition visible={visible} animation='drop' duration={500} unmountOnHide>
<div data-testid='answer'>
<Header as='h3' content ='Answer'/>
{answer}
</div>
</Transition>
)};
export default Answer;
Great! It's passing all the tests. Answer
works how we want it to. Answer
is ready to be added to the Answering
scene.
Read through the test file for Answer
. Do you understand what features you are testing for?
Read through the index file for Answer
. Can you see some things that the component does that you aren't testing for?
Add Answer to Answering
Now it is time to add Answer
into the Answering
scene. Once Answer is added, the answer will show up on screen so the user can see it.
Features
- clicking the
Submit
button makes the answer to the question appear
Choose Components
We will use the Answer
component that we just made.
Choose What to Test
Think about what you are going to need to test. You'll need to test that the Submit
button controls the visibility of the Answer. And you'll want to test that the Answer shows the right answer.
- answer doesn't show up
- when
Submit
is clicked, answer shows up
Answering Tests 1-2:
File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-10.tsx
In the Answering
scene, Answer
won't show up until the user clicks the Submit
button. To test what happens when we click a button, we need to simulate clicking the button. RTL gives us the fireEvent
method. fireEvent
can be used to simulate clicks, mouseover, typing and other events.
Import fireEvent from React Testing Library. You will simulate the click with fireEvent.click().
import { render, cleanup, fireEvent } from '@testing-library/react';
Make a describe block near the bottom of the test file, but above the snapshot test. Name the describe block 'submit button controls display of the answer.' Write a comment for each test we are about to write.
describe('submit button controls display of the answer', () => {
//answer does not show up
//clicking the submit button makes the answer show up
});
Checking if the Answer Shows Up
For both of the tests we are going to write we will need to search for the text of the answer. Remember earlier, when we wrote the Answer
component, we had to change our test to search for the string 'Answer' plus the answer after it? We had to do that because the div that shows the answer also has a header with the string 'Answer' in it.
So now we know we could find the answer by doing the same thing we did in the tests for Answer
. We could find the answer by putting the string 'Answer' in front of it and searching for that. But that is not the best way to do it. Why do you think that is not the best way to do it?
Don't Test Features of Other Components
The reason the answer has extra text added to it is because of how the component Answer
works. When we are testing the Answering
scene, we don't care how the component Answer
works. We don't care if it has a Header
, or what's in the Header
. Our test for Answering
should not also be testing the other components inside it, like Answer
. We don't want to test Answer
. We only want to test Answering
. We only really care about what the user sees and experiences. We only care if the user looking at Answering
can see the answer when they should.
If our test for Answering
looks for the correct answer the same way that the test in Answer
looks for it, with the extra added string, then it will work at first. But what if we change Answer
? What if we take the Header
out of Answer
? Then our tests for Answering
would fail. But should those tests fail? The answer would still show up on the screen. Answering
doesn't test for the Header
. The Header
being there or not shouldn't make Answering
fail tests.
Let's use a different way to make sure that the text of the correct answer is showing up.
Finding Text with a Custom Function
You have seen queryByText
and getByText
. You can use them to find an element by passing a string ('text goes here')
. You can also use them to find an element by passing a regular expression (/text goes here/i)
. There is also a another way to find elements using these queries. You can find elements by writing a custom function and passing the custom function to the queries.
Custom Functions for Queries
The queries look through the rendered component one element at a time. When you pass the query a function, the query will run that function on each element that it looks at. The query passes two arguments to the function. The first argument is the content of the element, which is a string. The second argument is the element itself. The function must return a boolean value, true or false.
A function for an RTL query has to be in this form: Accepts up to two parameters and returns a boolean value.
(content : string, element: HTMLElement) => boolean
When the custom function returns true, the query will add that element to its results. Some queries only look for one element. Those queries will stop looking when they find the first element that returns true. Other queries look for an array of elements. Those queries will go through all the elements and add each one that returns true to the array of results.
The Function to Find the Answer to the Current Question
Let's write the custom function that will find the element that contains the answer. We'll write this code inside the describe block, but before and outside the tests that we will write inside the describe block. That way, each test inside the describe block can use the function.
Get the answer to the current question from the initialState
. Call it initialAnswer
.
//the answer to the current question
const initialAnswer = initialState.cards[initialState.current].answer;
Removing LineBreaks
The initialAnswer
is stored as a string literal. It may contain linebreaks. The linebreaks won't get rendered. So for the comparison to work, we need to remove any linebreaks from the initialAnswer
. Let's create a variable called withoutLineBreaks
, which is the initialAnswer
without linebreaks.
To make withoutLineBreaks
, we'll use the string.replace method. We'll use string.replace
to replace any linebreaks with a space. The first argument passed to the replace
method is a regular expression that identifies linebreaks. This regEx is more complicated than the regExs we have been using to find strings of text. But you should save that for later. Right now, all you need to know is that it will find the linebreaks so that we can replace them with a different value.
The second argument is what we are replacing linebreaks with, which is a space.
//remove linebreaks from initialAnswer for comparison to textContent of elements
const withoutLineBreaks = initialAnswer.replace(/\s{2,}/g, " ");
What is going on in the RegEx?
Short answer:
You don't need to know! You can skip this section and come back later if you are curious.
Long answer:
This function uses a regular expression /\r\n|\r|\n/g
to identify linebreaks. I got it from an answer on StackOverflow. The answer to the StackOverflow question at this link explains that different operating systems will use different characters for linebreaks. Linux uses \n. Windows uses \r\n. Old Macs use \r. So this regEx looks for each of those.
More than you want to know about lineBreaks:
Newlines in JavaScript will always be 'linefeeds', or \n
. So we could get the same effect just looking for \n
instead of also looking for the other types of linebreaks. But the more complex regex will catch all linebreaks. So if we decided to later to store linebreaks in an answer in a different way, it would still work. I also decided to keep it as a good example of a slightly more complex regular expression.
Copying and Pasting RegEx
Getting a regEx from the internet is great. You can often find a regular expression that someone else has written that does what you want. But as with all code that you copy and paste from the internet, if you don't understand it then you might make mistakes, use bad code, or use it incorrectly.
An example of a problem with using copy pasted code without understanding it is that in the StackOverflow link above, the regEx is inside of parentheses: /(\r\n|\r|\n)/g
. The parentheses are a capturing group, a way to group the results of the regEx. But I found out that the regEx inside the capture group split the array differently than I wanted in some of the tests that we use this regEx in, and made those tests fail. So I took the capturing group out.
Full Explanation of This RegEx
For learning regex, I like the website www.rexegg.com and their regEx cheatsheet. The website (https://regexr.com/) is a great tool for writing regular expressions. Here is a link to this regular expression loaded into regexr.
The pieces of this regex are:
/
the regEx is inside of a pair of slashes. That tells the compiler that these are special characters, and it shouldn't read them the normal way.
|
the pipe character means 'or'.
\r
matches a carriage return.
\n
matches a line feed character.
/g
is a 'flag' that means global search. This means that the regEx will find all possible matches in the string.
All together, the line /\r\n|\r|\n/g
tells the compiler: this is a Regular Expression. Return a match when you find a carriage return followed by a linefeed, or a carriage return on its own, or a linefeed on its own. Find every match in the text.
The Custom Function
Write a function that takes a string and compares it to the string withoutLineBreaks
. This function will just look at the textContent
string that it gets from the query. It won't do anything with the whole element, so we are not including a second parameter. That will work fine, the query doesn't need the function to accept both arguments. The query just needs the function to return a boolean value.
Now we can pass this function to queries and find any elements that contain the text of the initialAnswer
.
const compareToInitialAnswer = (content: string) => content === withoutLineBreaks;
The Describe Block So Far
describe('submit button controls display of the answer', () => {
//the answer to the current question
const initialAnswer = initialState.cards[initialState.current].answer;
//remove lineBreaks from initialAnswer for comparison to textContent of elements
const withoutLineBreaks = initialAnswer.replace(/\s{2,}/g, " ");
const compareToInitialAnswer = (content: string) => content === withoutLineBreaks;
//answer does not show up
//clicking the submit button makes the answer show up
});
Answering Test 1: Answer Does Not Show Up Until Submit is Clicked
The first test checks that the answer does not show up before the submit button is clicked. Look how we are passing the compareToInitialAnswer
function to queryByText
. Do you know why we are using queryByText
instead of getByText
?
This test will pass because we haven't added Answer
to Answering
yet, so there is no way the answer will show up on screen. Once we do add Answer
, it will give us confidence that Answer
is working correctly and is not showing up before it should.
//answer does not show up
it('the answer does not show up before the submit button is clicked', () => {
const { queryByText } = renderAnswering();
//use the custom function to search for the initial answer
const answer = queryByText(compareToInitialAnswer);
expect(answer).toBeNull();
});
Answering Test 2: Clicking Submit Makes Answer Show Up
The second test shows that clicking the Submit
button will make the answer show up. We use getByText
to find the Submit
button and fireEvent.click()
to click it. Use the custom function compareToInitialAnswer
to find the answer in the document.
//clicking the submit button makes the answer show up
it('clicks the submit button and shows the answer', () => {
const { getByText } = renderAnswering();
//find the submit button
const submit = getByText(/submit/i);
//simulating a click on the submit button
fireEvent.click(submit);
//use a custom function to find the answer
//the function returns true if content is equal to the initial answer withoutLineBreaks
const answer = getByText(compareToInitialAnswer);
//assertion
expect(answer).toBeInTheDocument();
});
Tests done. Run them and make sure that your last test isn't passing. It should not pass because the answer should not show up yet.
Pass Answering Tests 1-2
File: src/scenes/Answering/index.tsx
Will Match: src/scenes/Answering/complete/index-8.tsx
To pass the tests we just wrote, we'll change Answering so the Submit
button controls the visibility of Answer
.
Import useState
from React.
The useState hook holds a value and gives you a function to set the value to something else. We will use it to hold the value of showAnswer
. showAnswer
will be a boolean variable that determines whether we should show the answer or not.
import React, { useContext, useState } from 'react';
Import the Answer
component you just made.
import Answer from './components/Answer';
Add the useState
hook. useState(startingValue)
returns an array with two values in it.
///the return value of useState
[ value, setValue ]
value
is the value that useState currently holds. It starts as the starting value that was passed in to useState.
setValue
is a function that lets you change the value that useState currently holds.
In the code below, const [showAnswer, setShowAnswer]
is the declaration of two const variables, showAnswer
and setShowAnswer
. Declaring a variable or variables by putting them in brackets with an object on the other side of an equals sign means you are using Array Destructuring. Array Destructuring is like Object Destructuring, except you are getting elements out of an array instead of properties out of an object.
showAnswer
is a boolean variable. So showAnswer
will either be true or false. We pass useState
a starting value of false. Because the starting value is boolean, TypeScript will assume that the value inside this useState
always be boolean and that the function to set it will take a single argument with a boolean value. If we wanted something else, we could explicitly declare the type of the useState
values. But we want it to be boolean, so we are letting TypeScript 'infer' the type. 'Infer' the type means TypeScript will figure out the type from the code. When TypeScript inference works, it is nice. When it doesn't do what you want, then you have to explicitly declare the type.
setShowAnswer
is a function. It takes one argument. The argument that setShowAnswer
takes is boolean. So you can only invoke setShowAnswer
with true or false. After you invoke setShowAnswer
, the value of showAnswer
will be set to the value that you passed to setShowAnswer
.
We will pass the function setShowAnswer
to the Submit
button. When the value of showAnswer
changes, the answer will become visible.
const Answering = () => {
//get cards, current index and dispatch from CardContext
const { cards, current, dispatch } = useContext(CardContext);
//get the question from the current card
const { question } = cards[current];
const [showAnswer, setShowAnswer] = useState(false);
return (
Add an onClick
function to the Submit
button that calls setShowAnswer(true)
. Add the Answer
below the Submit
button. Pass showAnswer
as the value of Answer
's visible
prop.
Now clicking the Submit
button will set the value of showAnswer
to true. We are passing showAnswer
to Answer
as the value of the prop visible.
So when we set showAnswer
to true, we are making Answer
visible.
</Form>
<Button onClick={() => setShowAnswer(true)}>Submit</Button>
<Answer visible={showAnswer}/>
</Container>
Run the app. The answer is not there. Click Submit
and the answer will show up!
Show Linebreaks In the Answer
Ok, the answer shows up. But it's all on one line. Let's change Answer
so that it respects the linebreaks stored in the template literal.
Answer: Change the Test Named 'displays the right answer' so it Looks for Multiple Lines
File: src/scenes/Answering/components/Answer/index.test.tsx
Will Match: src/scenes/Answering/components/Answer/complete/test-7.tsx
We are going to rewrite the test named 'displays the right answer'. To make sure that we are testing for an answer that has linebreaks, we are going to make a new CardState
object called testState
. We'll pass testState
to the CardProvider
instead of the default initialState
.
Import CardState
from types
.
import { CardState } from '../../../../types';
We'll need to pass the testState
to CardProvider
. Make renderAnswer accept a second optional parameter, testState
. Declare testState as a type CardState
. Pass testState
to CardProvider
as a prop.
const renderAnswer = (visible?: boolean, testState?: CardState) => render(
<CardProvider testState={testState}>
<Answer visible={visible !== undefined ? visible : true}/>
</CardProvider>
);
Now we'll rewrite the 'displays the right answer' test.
Declare a const testAnswer
. testAnswer
is a template literal inside of backticks. That sounds complicated, but it just means that we can use linebreaks inside it.
Declare a const cards. Use the spread operator to make a new array from the array initialState.cards
.
Set the answer property of the object at testAnswer.cards[0]
equal to testAnswer
.
Declare a const testState
. Use the spread operator to make a new object from the initialState. Overwrite the existing cards property with the array cards
. Overwrite the existing current property with the number 0.
Then call renderAnswer(true, testState)
. Remember, the first argument tells renderAnswer
that Answer
should be visible. The second argument is the testState
object that we just made.
Use the getByTestId
matcher to find the answer div
.
We expect the answer div to contain a Header, and also to contain one other div
for each line in the answer. The testAnswer
has three lines in it, so we'll expect the answer div
to contain four divs
total.
You can look at the children
property of an element to see how many other elements are inside it. The children
property of an element is an array. So we will make assertions about the length of the children
array and the contents of the children
array.
The first element inside the answer div
is the Header. So answer.children[0]
is the header.
Every other element inside the answer div
will be a div that contains a line of the answer. So answer.children[1]
will be a div with the first line of testAnswer
. answer.children[2]
will be a div with the second line of testAnswer.
testAnswer
is a string. We can't tell what each line is. We need an array with each line of testAnswer
. Use String.split()
to split the string into an array of strings. Pass the regular expression /\n/g
to String.split()
to split the string at every linebreak.
Then we expect that the textContent of each child of the element matches one of the lines in the answer.
// shows the right answer
// shows the right answer
it('displays the right answer', () => {
//testAnswer is a template literal with linebreaks
const testAnswer = `This has linebreaks
Here's the second line
and the third line`;
//create a new array using initialState.cards
const cards = [...initialState.cards];
//set the answer of the card at index 0 = to testAnswer
cards[0].answer = testAnswer;
//create a new CardState with cards, set current to 0
const testState = {
...initialState,
cards,
current: 0
};
//call renderAnswer with visible = true, testState
const { getByTestId } = renderAnswer(true, testState);
//find the answer div
const answer = getByTestId('answer');
//the answer div should have 4 children
//one child is the Header
//plus three more child divs, one for each line in testAnswer
expect(answer.children).toHaveLength(4);
//use Array.split to split testAnswer into an array
//the regular expression /\n/g identifies all the linebreaks
const testAnswerArray = testAnswer.split(/\n/g);
const firstLine = answer.children[1];
const secondLine = answer.children[2];
const thirdLine = answer.children[3];
expect(firstLine.textContent).toEqual(testAnswerArray[0]);
expect(secondLine.textContent).toEqual(testAnswerArray[1]);
expect(thirdLine.textContent).toEqual(testAnswerArray[2]);
});
toEqual instead of toHaveTextContent
Notice that we do not expect firstLine
toHaveTextContent
of the line from the answer. Instead we expect firstLine.textContent
toEqual
the line from the answer. The reason to access the textContent of the element and use toEqual
instead using the whole element and using toHaveTextContent
is because of the way toHaveTextContent
works.
When you pass a string to toHaveTextContent
it will compare that string to the textContent of the element. It looks for a partial match. It doesn't tell you that it is an exact match. So toHaveTextContent('apple')
tells you that the element text contains the string 'apple.' It doesn't tell you that the element text matches the string 'apple.' It would match whether the textContent was 'apple,' 'apples and oranges,' or 'apples, oranges, and pears.'
We want this test to show us that the textContent of each div exactly matches that line of the answer. toEqual
tells us that the text content is actually equal to the the string in the answer array, without any extra text.
Optional Experiment to Compare toEqual and toHaveTextContent
You can use the string.slice method to cut off part of the answer string and see that toHaveTextContent still matches. If you are curious about this, try adding these lines to your test.
The code
testAnswerArray[0].slice(0, testAnswerArray[0].length - 7)
Creates a new string that is the first string in testAnswerArray
with the last seven characters cut off.
This will still pass:
expect(firstLine).toHaveTextContent(testAnswerArray[0].slice(0, testAnswerArray[0].length - 7));
While toEqual
won't:
expect(firstLine.textContent).toEqual(testAnswerArray[0].slice(0, testAnswerArray[0].length - 7));
This test works. But it only tests one answer. It tests an answer with linebreaks. Do you feel like it tests enough that you are certain the app will work? There's no right answer. That's something you'll decide when you develop your own apps.
How would you test to make sure the component correctly displays an answer without any linebreaks? Or an answer with five lines?
Rewrite the Answer Component to Show Multiple Lines
File: src/scenes/Answering/components/Answer/index.tsx
Will Match: src/scenes/Answering/components/Answer/complete/index-7.tsx
We just rewrote the test 'displays the right answer' to expect that multiple lines will be displayed when the stored answer contains lineBreaks. To make the Answer
component display multiple lines, we will first use the String.split
method to make an array of strings from the answer
string. Then we'll use the Array.map
method to make that an array of React elements from the array of strings.
const Answer = ({
visible
}:{
visible: boolean
}) => {
const { cards, current } = useContext(CardContext);
const { answer } = cards[current];
const content = answer
//use string.split and a regEx to split the string into an array
.split(/\n/g)
//use Array.map to make an array of div elements
.map((string, index) => <div key={index}>{string}</div>);
return (
<Transition visible={visible} animation='drop' duration={500} unmountOnHide>
<div data-testid='answer'>
<Header as='h3' content ='Answer'/>
{content}
</div>
</Transition>
)};
When you run all the tests, the test in Answering
named 'clicks the submit button and shows the answer' will fail.
Answering Test: Fix Test 'clicks the submit button and shows the answer'
File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-11.tsx
The test failed and Jest gave us an error message. The error message says:
Unable to find an element with the text: content => content === withoutLineBreaks. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
If you scroll down the screen that displays the failed test, you can see that it failed at the line where we try to use the custom function compareToInitialAnswer
to find the element that contains the answer.
134 | //because the Answer component sticks a header with text in the answer div
135 | //the function returns true if content is equal to the initial answer withoutLineBreaks
> 136 | const answer = getByText(compareToInitialAnswer);
The error message tells us that the function compareToInitialAnswer
did not return true for any of the elements in the document. Here's the code for compareToInitialAnswer
:
const compareToInitialAnswer = (content: string) => content === withoutLineBreaks;
CompareToInitialAnswer No Longer Finds the Answer
Now you know that compareToInitialAnswer
no longer finds the answer. compareToInitialAnswer
no longer finds the answer because the rendered code looks different now that the answer is split up into multiple divs. So the test 'clicks the submit button and shows the answer' fails, and we need to fix it.
But there is something else that you should be concerned about. Take a look at the whole test file for Answering
. Is 'clicks the submit button and shows the answer' the only test that uses compareToInitialAnswer
?
No! The test named 'the answer does not show up before the submit button is clicked' also uses compareToInitialAnswer
. But that test still passes. Why does that test still pass, even though it is using compareToInitialAnswer
and compareToInitialAnswer
doesn't work?
The test named 'the answer does not show up before the submit button is clicked' still passes because it expects to find nothing when it passes compareToInitialAnswer
to a query. Now that compareToInitialAnswer
doesn't work, it will still find nothing. It passes when it finds nothing, and will only fail when the query using compareToInitialAnswer
returns a result.
This is a good example of why it's important to understand how your tests work. You need to know when your tests are actually giving you useful information and when they aren't.
Fix the compareToInitialAnswer Function
Earlier we learned that a custom function for a query can have two parameters:
(content : string, element: HTMLElement) => boolean
compareToInitialAnswer
only has one parameter, content. It just tests if content is equal to the variable withoutLineBreaks
. compareToInitialAnswer
doesn't do anything with the second argument, the element. We can fix the test by changing how compareToInitialAnswer
works.
Instead of looking at the content
string, we'll look at the textContent
of each element. Change compareToInitialAnswer
to this:
const compareToInitialAnswer = (
content: string,
{ textContent } : HTMLElement
) => !!textContent &&
textContent
.replace(/\s{2,}/g, " ")
.slice(6, textContent.length) === withoutLineBreaks;
Here's a line by line explanation of the changes.
{ textContent } : HTMLElement
We add a second parameter. The second parameter is of the type HTMLElement
. HTMLElement
has textContent
that we can look at. We aren't interested in any of the other properties, so we'll use Object Destructuring to pull the textContent
property out of the element that gets passed to the function.
) => !!textContent &&
This anonymous function has an implicit return. It will return either the value of textContent
cast to boolean, or the value of the strict equality comparison of the string that we make from textContent
to withoutLineBreaks
.
!! is the Double Not operator. It casts the value to boolean. The textContent
property of an HTMLElement
will either be a string or null. If the textContent
is null, the function will cast null to boolean, get false, and return false.
&& is the Logical And operator. The first condition in this expression is casting textContent
to boolean. If textContent
is a string, it will be cast to boolean, and evaluate to true. Because the first condition is true, the code after the && operator will then be evaluated.
textContent
We know that the next lines will only be run if textContent
is a string. So we can use the string methods .replace
and .slice
to create a new string that we'll compare to withoutLineBreaks
. We can use those methods on different lines in the code. They do not have to be written all on one line to work.
.replace(/\s{2,}/g, " ")
We use String.replace
to replace any linebreaks and multiple spaces with a single space. You can look at this regEx on regExr if you want to.
.slice(6, textContent.length) === withoutLineBreaks;
We are looking for the element that holds both the Header
with the string 'Answer' in it and also holds a div for each line in the answer. So the textContent
that we want will start with the string 'Answer.' Use the String.slice
method to cut off the first 6 characters and return a new string. This cuts off the 'Answer' from the start of the string and lets us compare to withoutLineBreaks
.
===
is the strict equality operator.
Once you save the changed compareToInitialAnswer
, all tests will pass.
Next Post
In the next post we will make a new component called Buttons
. We will move the Submit
button into Buttons
. Buttons
will also show the Right
and Wrong
buttons after Submit
is clicked. To make the buttons work we will make some changes to the types.ts
file and CardContext
.
Posted on January 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.