DEV Community

loading...
Cover image for Show the Answer and Submit Button

Show the Answer and Submit Button

jacobwicks profile image jacobwicks ・32 min read

In this post we will:

  • Make the Answer component that gets the current card from CardContext, 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 the Right 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.

Answer fails to render

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!
Renders

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'

 });

Fails, no answer div

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); 
    });  

Does Not Show Correct Answer

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>
};

Passes Showing Correct Answer

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();
    });

Alt Text

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!

Header Pass, 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().

The Answer div

You can also use console.log() to see the textContent of the answer div.

console.log(answer.textContent)

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);  
    });

Header and Answer pass

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();
});

Invisible Fail

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;

Invisible Pass

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.

Button Does Not Control Answer

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>

Tests Pass

Run the app. The answer is not there. Click Submit and the answer will show up!

Click submit and the answer shows 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.

Alt Text

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.

All Tests 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.

Discussion (0)

pic
Editor guide