DEV Community

loading...
Cover image for Right and Wrong Answer Buttons

Right and Wrong Answer Buttons

jacobwicks
Legal Aid Lawyer turned fullstack developer. Passionate about improving the user experience by developing efficient and beautiful apps.
・24 min read

In this post we will:

  • Make a Buttons component that shows the Submit button and buttons that let the user record if their answer was right or wrong
  • Make clicking the Submit, Right, and Wrong buttons advance to the next card
  • Put the new Buttons component into Answering
  • Hide the answer when we advance to the next card

In the next post we'll make a new context called StatsContext. We'll use StatsContext to track how many times the user answered a question right or wrong. We will also make a component to show the stats for the current question.

Buttons Component

User Story

  • ... When the user is done with their answer, they click the submit button. The app shows them the answer key. The user compares their answer to the answer key. The user decides they got the question right, and clicks the 'right answer' button. Then the user sees the next question.

We don't just want to show the answer. We want the user to be able to say if their answer was right or if their answer was wrong. We need to show the user the Submit button before they are done answering the question. After the user clicks submit we need to show them the Right and Wrong buttons. Let's make the component to do that.

Features

  • after clicking the Submit button, Right and Wrong buttons show up
  • clicking the Right or Wrong button moves to the next question

Choose Components

We have already made the Submit button. We'll move it out of Answering and into our new Buttons component.

We'll use the Button Group from Semantic UI React for the Right and Wrong buttons. Button.Group can show buttons with some nice looking separators, like the word 'or' in a circular graphic.

SUIR Button Group

Decide What to Test

What are the important functions of these buttons?
If the question has not been answered, then the Submit button should show up.
If the question has been answered, then the Right and Wrong buttons should show up.
Clicking the Right button should move to the next card.
Clicking the Wrong button should move to the next card.

Writing the Tests for Buttons

File: src/scenes/Answering/components/Buttons/index.test.tsx
Will Match: src/scenes/Answering/components/Buttons/complete/test-1.tsx

In the test file, write a comment line for each test you are going to write.

//renders without crashing
//Buttons takes a prop answered: boolean 
//if !answered, then it should show a submit button
//if answered, then it should show right and wrong buttons
//clicking right advances to next card
//clicking wrong advances to next card
//clicking submit invokes submit, shows right and wrong buttons

Imports and afterEach go at the top.

import React, { useState, useContext } from 'react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Buttons from './index';
import { CardContext, CardProvider, initialState } from '../../../../services/CardContext';
import { CardState } from '../../../../types';

afterEach(cleanup);

Helper Components

We are going to make Buttons dispatch actions to CardContext that will change the state of CardContext in ways that Buttons won't display. Clicking Right or Wrong will dispatch a CardActionTypes.next action, which should change the current index in CardContext.

In order to make sure that Buttons is dispatching actions correctly, we'll make two helper components to render in our tests. Current will render the current index from CardContext. The other helper component will be ButtonHolder. ButtonHolder will be used instead of the Answering component, when we need a container to hold Buttons and Current.

Helper Component: Current

Write the Current component. Current is a component that returns a div with the current index from CardContext in it. Current lets us see what the current index is, so we can test when the current index in CardContext has changed. This lets us test if other components in the app would show a different card when we click a button, but without the added complexity of actually importing those other components and knowing how they operate.

//displays the current index from cardContext
//allows us to check if buttons can change current
const Current = () => {
    const { current } = useContext(CardContext);
    return <div data-testid='current'>{current}</div>
};

Helper Component: ButtonHolder

Write the ButtonHolder component. This component will let us test Buttons the way our app will use it. Our app will use Buttons inside the CardProvider.

Buttons will take a boolean prop answered. Buttons will use the value of answered to decide whether to show Submit or Right and Wrong.

Write a useState hook in Buttonholder to manage the value of answered. Remember, useState lets you store a value and gives you a function to change that value. This will let us test if clicking the Submit button invokes the submit function. It will also let us test if Buttons shows the Submit button when answered is false, and if Buttons shows the Right and Wrong buttons when answered is true.

Buttons needs to access the CardContext so it can change to the next card when the user clicks Right or Wrong. Put Buttons inside the CardProvider. Add Current inside the CardProvider. That way we can test if clicking Right and Wrong changes the current index.

ButtonHolder accepts two optional props, answeredStartsAs and testState.

We will pass a value to answeredStartsAs when we want to override the starting value of answered.

We will pass a CardState object to testState when we want to override the default initial state that the CardProvider starts with.

//a container component to hold  Buttons 
//submit() changes answered from false to true
const ButtonHolder = ({
    answeredStartsAs,
    testState
}:{
    answeredStartsAs?: boolean
    testState?: CardState
}) => {
    const [answered, setAnswered] = useState(answeredStartsAs !== undefined ? answeredStartsAs : false);

    return (
        <CardProvider testState={testState}>
            <Buttons answered={answered} submit={() => setAnswered(true)}/>
            <Current/>
        </CardProvider>
    )};

With the helper components written, we are ready to write the first test.

Test 1: Renders Without Crashing

The first test is to make a component that will render without crashing.

//renders without crashing
it('renders without crashing', () => {
    render(<ButtonHolder/>);
});

Pass Test 1: Render Without Crashing

File: src/scenes/Answering/components/Buttons/index.tsx
Will Match: src/scenes/Answering/components/Buttons/complete/index-1.tsx

Let's write a Buttons component that will render without crashing.
We know we want Buttons to accept a boolean prop answered and a function submit. So we will declare those two props. We declare submit's TypeScript type as () => void. The parentheses mean it's a function. This function does not accept any arguments, so the parentheses are empty. This function does not return a value. So the return value is void. We'll invoke the submit function in the Buttons component when the Submit button is clicked.

Return a div.

import React from 'react';

const Buttons = ({
    answered,
    submit
}:{
    answered: boolean,
    submit: () => void
}) => <div/>;

export default Buttons;

Renders Without Crashing

Test 2: When answered is false, Buttons Shows a Submit Button

File: src/scenes/Answering/components/Buttons/index.test.tsx
Will Match: src/scenes/Answering/components/Buttons/complete/test-2.tsx

We'll render Buttons directly, without ButtonHolder. In this test we don't care about what Buttons does with the CardContext. We just want to know that the Submit button is on the screen.

We are using getByText because we expect the text 'Submit' to be found. Notice that we are using a Regular Expression (RegEx) to find the button.

jest.fn() is a method that can replace functions in your tests. It makes a 'Mock Function.' Mock Functions can be a complicated topic. Basically, it's a fake function. We can use it in place of a real function, and if we wanted to, we could find out how many times the component had called it, and what arguments it received. We use a mock function here because Buttons needs a submit prop that is a function and we don't want to have to write a real function to pass to the submit prop.

//Buttons takes a prop answered: boolean 
//if !answered, then it should show a submit button
it('has a submit Button', () => {
    const { getByText } = render(<Buttons answered={false} submit={jest.fn()}/>);
    const submit = getByText(/submit/i);
    expect(submit).toBeInTheDocument();
});

Fails Submit Button

Pass Test 2: When answered is false, Buttons Shows a Submit Button

File: src/scenes/Answering/components/Buttons/index.tsx
Will Match: src/scenes/Answering/components/Buttons/complete/index-2.tsx

Import the Button component from Semantic UI React.

import { Button } from 'semantic-ui-react';

Change the return value from the div to a Button with content = 'Submit'. The content prop of a Button is the label text that shows up on the screen.

}) => <Button content='Submit'/>;

Passes Submit

Test 3: When answered is true, Should Show Right and Wrong Buttons

File: src/scenes/Answering/components/Buttons/index.test.tsx
Will Match: src/scenes/Answering/components/Buttons/complete/test-3.tsx

Add a describe block named 'when answered is true'. This describe block will contain all the tests that cover when answered is true and we are using the Right and Wrong buttons.

describe('when answered is true', () => {
//if answered, then it should show right and wrong buttons

    //clicking right advances to next card

    //clicking wrong advances to next card
});

Write the third test. We pass true as the value of answeredStartsAs so that the value of answered starts as true. Buttons should show the Right and Wrong buttons when answered is true. The Right and Wrong buttons have text labels, so we are finding them by searching for the text on them.

describe('when answered is true', () => {
//if answered, then it should show right and wrong buttons
    it('shows right and wrong buttons', () => {
        const { getByText } = render(<ButtonHolder answeredStartsAs={true}/>);

        const right = getByText(/right/i);
        expect(right).toBeInTheDocument();

        const wrong = getByText(/wrong/i);
        expect(wrong).toBeInTheDocument();
    });

    //clicking right advances to next card

    //clicking wrong advances to next card
});

Fails Right and Wrong

Pass Test 3: When answered is true, Should Show Right and Wrong Buttons

File: src/scenes/Answering/components/Buttons/index.tsx
Will Match src/scenes/Answering/components/Buttons/complete/index-3.tsx

Change the return value of Buttons. Write a Button.Group containing the Right and Wrong buttons. Use a ternary operator to return the Button.Group if answered is true and the Submit button if answered is false.

The ternary operator is a short way of writing an if/else statement. The ternary operator returns one value if a condition is true, and a different value if the condition is false. If the condition is true it returns the value after the ?, and if the condition is false it returns the value after the :.

}) => answered
    ?   <Button.Group>
            <Button content='Right' positive />
            <Button.Or/>
            <Button content='Wrong' negative />
        </Button.Group>
    :   <Button content='Submit'/>;

Passes Right and Wrong

Test 4: Clicking Right Changes to Next Card

File: src/scenes/Answering/components/Buttons/index.test.tsx
Will Match: src/scenes/Answering/components/Buttons/complete/test-4.tsx

To test that clicking Right changes to the next card and that clicking Wrong changes to the next card we will pass a CardState object to override the default initialState.

Declare the const zeroState inside the describe block. We'll use the spread operator to make zeroState a copy of the initialState object, but we will explicitly set current to 0. By doing this we make sure that our test starts with current at 0, even if the imported initialState object is changed and has current !== 0. The less you make your tests rely on code that is written outside of your tests, the more you can rely on your tests.

describe('when answered is true', () => {
    //if answered, then it should show right and wrong buttons
    it('shows right and wrong buttons', () => {
        const { getByText } = render(<ButtonHolder answeredStartsAs={true}/>);

        const right = getByText(/right/i);
        expect(right).toBeInTheDocument();

        const wrong = getByText(/wrong/i);
        expect(wrong).toBeInTheDocument();
    });

    const zeroState = {
        ...initialState,
        current: 0
    };

    //clicking right advances to next card

Now write the test. Pass zeroState to ButtonHolder so that we know current will start as 0.

  //clicking right advances to next card
    it('when the user clicks the Right button, the app changes to the next card', () => { 
        //pass testState with current === 0
        const { getByTestId, getByText } = render(<ButtonHolder answeredStartsAs={true} testState={zeroState}/>);

        //get the helper component Current
        const current = getByTestId('current');
        //current should show text 0
        expect(current).toHaveTextContent('0');

        //get the right button
        const right = getByText(/right/i);
        //click the right button
        fireEvent.click(right);

        expect(current).toHaveTextContent('1');
    });

Alt Text

Pass Test 4: Clicking Right Changes to Next Card

File: src/scenes/Answering/components/Buttons/index.tsx
Will Match: src/Scenes/Answering/components/Buttons/complete/index-4.tsx

Import useContext from React. We'll be dispatching actions to the CardContext when the user clicks buttons. Import CardActionTypes from types.ts. Import CardContext.

import React, { useContext } from 'react';
import { Button } from 'semantic-ui-react';
import { CardActionTypes } from '../../../../types';
import { CardContext } from '../../../../services/CardContext';

Change Buttons to get CardContext's dispatch from useContext. Add an onClick function to the Right Button. The onClick function will dispatch an action with type of CardActionTypes.next.

const Buttons = ({
    answered,
    submit
}:{
    answered: boolean,
    submit: () => void
}) => {
    const { dispatch } = useContext(CardContext);

    return answered
    ?   <Button.Group>
            <Button content='Right' positive 
                onClick={() => dispatch({ type: CardActionTypes.next })}
            />
            <Button.Or/>
            <Button content='Wrong' negative />    
        </Button.Group>
    :   <Button content='Submit'/>
};

Passes Right Card Change

Next we will test that clicking the Wrong button changes the current index. Before you look at the example, try to write the test. Hint: it is based on the test for the Right button.

How do you think we will make the Wrong button pass the test?

Test 5: Clicking Wrong Changes to Next Card

File: src/scenes/Answering/components/Buttons/index.test.tsx
Will Match: src/scenes/Answering/components/Buttons/complete/test-5.tsx

Write the test for clicking the Wrong Button. It is almost the same as the test for clicking the Right Button.

   //clicking wrong advances to next card
    it('when the user clicks the Wrong button, the app changes to the next card', () => { 
        //pass testState with current === 0
        const { getByTestId, getByText } = render(<ButtonHolder answeredStartsAs={true} testState={zeroState}/>);

        //get the helper component Current
        const current = getByTestId('current');
        //current should show text 0
        expect(current).toHaveTextContent('0');

        //get the wrong button
        const wrong = getByText(/wrong/i);
        //click the wrong button
        fireEvent.click(wrong);

        expect(current).toHaveTextContent('1');
    });

Fails Wrong Button

Pass Test 5: Clicking Wrong Changes to Next Card

File: src/scenes/Answering/components/Buttons/index.tsx
Will Match: src/Scenes/Answering/components/Buttons/complete/index-5.tsx

Add an onClick function to the Wrong button. Dispatch an action with type CardActionTypes.next.

            <Button content='Wrong' negative 
                onClick={() => dispatch({ type: CardActionTypes.next })}
            />    

Passes Clicking Wrong Button

Test 6: Clicking Submit shows Right and Wrong Buttons

File: src/scenes/Answering/components/Buttons/index.test.tsx
Will Match: src/Scenes/Answering/components/Buttons/complete/test-6.tsx

The last test we'll do on Buttons is clicking the Submit button should show the Right and Wrong buttons. In the app, and inside the ButtonHolder component, clicking the Submit button will invoke the submit function passed as a prop to Buttons. In Answering, the submit function will set the value of answered to true.

Before we simulate the click event, we use queryByText to look for 'Right' and 'Wrong,' and we expect the results to be null.

After we simulate the click event, we use getByText to look for 'Right' and 'Wrong,' and we expect the results to be elements in the document.

//clicking submit invokes submit, shows right and wrong buttons
it('clicking submit shows right and wrong', () => {
    const { getByText, queryByText } = render(<ButtonHolder />)

    const submit = getByText(/submit/i);
    expect(submit).toBeInTheDocument();

    expect(queryByText(/right/i)).toBeNull()
    expect(queryByText(/wrong/i)).toBeNull()

    fireEvent.click(submit);

    expect(queryByText(/submit/i)).toBeNull();
    expect(getByText(/right/i)).toBeInTheDocument();
    expect(getByText(/wrong/i)).toBeInTheDocument();
});

Clicking Submit Fails

Pass Test 6: Clicking Submit shows Right and Wrong Buttons

File: src/scenes/Answering/components/Buttons/index.tsx
Will Match: src/Scenes/Answering/components/Buttons/complete/index-6.tsx

Add an onClick function to the Submit button. The onClick function invokes the submit function passed as a prop to Buttons.

<Button content='Submit' onClick={() => submit()}/>

Click Submit Passes

Add Buttons to Answering

Now it is time to add Buttons to Answering.

Choose Components

We are adding the Buttons component that we just wrote. We will also remove the old Submit Button from Answering.

Decide What to Test

  • Does clicking the Submit Button still show the answer?
  • Right Button does not show up until the Submit Button is clicked
  • Wrong Button does not show up until the Submit Button is clicked
  • Clicking the Submit Button makes the Right and Wrong Buttons show up

Answering Tests 1-2: Right and Wrong Do Not Show Up Before Submit is Clicked

File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-12.tsx

We already test if clicking the Submit button shows the answer. We'll see if that test still passes when we import the Buttons component or not.

We need to add a test that clicking the Submit button makes the Right and Wrong buttons show up.

Add a describe block near the bottom but above the snapshot test. Name the describe block 'clicking the Submit Button makes the Right and Wrong Buttons show up.' Write a comment inside the describe block for each test we are about to write.

describe('clicking the Submit Button makes the Right and Wrong Buttons show up', () => {
    //the Right button does not show up before Submit is clicked

    //the Wrong button does not show up before Submit is clicked

    //Clicking Submit makes the Right Button show up

    //Clicking Submit makes the Wrong Button show up

});

Inside the describe block write two tests to make sure the Right and Wrong Buttons don't show up before Submit is clicked. We expect these tests to both pass, so we're writing them together.

    //the Right button does not show up before Submit is clicked
    it('the Right button does not show up before Submit is clicked', () => {
        const { queryByText } = renderAnswering();
        const right = queryByText(/right/i);
        expect(right).toBeNull();
    });

    //the Wrong button does not show up before Submit is clicked
    it('the Wrong button does not show up before Submit is clicked', () => {
        const { queryByText } = renderAnswering();
        const wrong = queryByText(/wrong/i);
        expect(wrong).toBeNull();
    });

These tests should pass.

Right and Wrong don't show up

Hopefully they will still pass when we add Buttons to Answering. Look at the queries that we used to search for the Right and Wrong buttons. If you wanted to be sure that these tests worked, what would you do? How would you change the Answering component to make these tests fail?

Answering Tests 3-4: Right and Wrong Show Up After Submit is Clicked

Inside the describe block write two tests to make sure the Right and Wrong Buttons show up after Submit is clicked. Find the submit button and use fireEvent.click() to simulate the click event. Then find the Right or Wrong button using getByText.

    //Clicking Submit makes the Right Button show up
    it('clicks the submit button and shows the Right button', () => {    
        const { getByText } = renderAnswering();

        //find the submit button
        const submit = getByText(/submit/i);
        //simulating a click on the submit button
        fireEvent.click(submit);

        const right = getByText(/right/i);
        expect(right).toBeInTheDocument();
    });

    //Clicking Submit makes the Wrong Button show up
    it('clicks the submit button and shows the Wrong button', () => {    
        const { getByText } = renderAnswering();

        //find the submit button
        const submit = getByText(/submit/i);
        //simulating a click on the submit button
        fireEvent.click(submit);

        const wrong = getByText(/right/i);
        expect(wrong).toBeInTheDocument();
    });

These tests should fail. Right and Wrong won't start showing up until we add Buttons to Answering.

Right and Wrong fail to show up

Answering Pass Tests 1-4: Right and Wrong Show Up After Submit is Clicked

File: src/scenes/Answering/index.tsx
Will Match: src/scenes/Answering/complete/index-9.tsx

Ok, now let's add Buttons to Answering.
Import Buttons.

import Buttons from './components/Buttons';

Change the component. Delete the old Submit Button from the Container:

    <Container data-testid='container' style={{position: 'absolute', left: 200}}>
         <Header data-testid='question' content={question}/>
         <Button>Skip</Button>
         <Form>
            <TextArea data-testid='textarea'/>
        </Form>
        <Button onClick={() => setShowAnswer(true)}>Submit</Button>
        <Answer visible={showAnswer}/>
    </Container>

And replace it with Buttons. Pass showAnswer as the prop answered, and make submit call setShowAnswer(true).

    <Container data-testid='container' style={{position: 'absolute', left: 200}}>
         <Header data-testid='question' content={question}/>
         <Button>Skip</Button>
         <Form>
            <TextArea data-testid='textarea'/>
        </Form>
        <Buttons answered={showAnswer} submit={() => setShowAnswer(true)}/>
        <Answer visible={showAnswer}/>
    </Container>

Save it. Now Answering will pass all the tests.
All Pass

Look at your app

Run the app with the command npm start. Click Submit and the answer shows up. The Right and Wrong buttons show up.
Buttons Are Here

Clicking the Right button will advance to the next card. Clicking the Wrong button will advance to the next card. Clicking the Skip button will also advance to the next card.

Hiding the Answer When the Card Changes

You will notice that when you click Submit, the Answer opens but does not close when you move to the next card. That's not what we want. We need to change Answering so that it will hide the answer when the card changes. To hide the answer, we need to set showAnswer to false when the value of current changes. To do that, we will use the useEffect hook.

The useEffect hook lets us run a function when one of the values that we tell useEffect to watch changes. We'll tell our useEffect to watch current. We'll make the code inside useEffect call setShowAnswer(false) when current changes. Then Answer will become invisible when current changes.

Answering Test 5: Answer is Hidden when Card Changes

File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-13.tsx

Write the test for the answer going away inside of the describe block named 'submit button controls display of the answer.' Find and click the Submit button to show the answer. To check if the answer shows up, use the compareToInitialAnswer function we wrote earlier. Once you know the answer shows up, then find and click the Skip button.

When you click the Skip button, the current index in CardContext will change. We'll search for the first answer again to make sure it isn't still showing up in the document. You should also write a compareToSecondAnswer function so that we can search for the answer from the second card. Make sure the second answer isn't shown either.

  //answer goes away
  it('answer disappears when card changes', () => {
    const { getByText, queryByText } = 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
    const answer = getByText(compareToInitialAnswer);

    //assertion
    expect(answer).toBeInTheDocument();

    //clicking skip changes the current index 
    const skip = getByText(/skip/i);
    fireEvent.click(skip);

    //the answer to the second question
    const secondAnswer = initialState.cards[initialState.current + 1].answer;

    //remove lineBreaks from initialAnswer for comparison to textContent of elements 
    const withoutLineBreaks = secondAnswer.replace(/\s{2,}/g, " ");

    //a function that compares a string to the second answer
        const compareToSecondAnswer = (
            content: string, 
            { textContent } : HTMLElement
        ) => !!textContent && 
            textContent
            .replace(/\s{2,}/g, " ")
            .slice(6, textContent.length) === withoutLineBreaks;

    //look for the first answer
    const gone = queryByText(compareToInitialAnswer);
    //first answer shouldn't show up
    expect(gone).toBeNull();

    //look for the second answer
    const answer2 = queryByText(compareToSecondAnswer);
    //second answer shouldn't show up
    expect(answer2).toBeNull();
  });

Answer Shows Up
Test fails because the answer is still showing up.

Pass Answering Test 5: Answer is Hidden when Card Changes

File: src/scenes/Answering/index.tsx
Will Match: src/scenes/Answering/complete/index-10.tsx

Write the useEffect. useEffect takes two arguments.

The first argument is a function. We will write a function that uses setShowAnswer to set the value of showAnswer to false.

The second argument is an array. This array contains all the values that useEffect 'depends' on. It is called the array of 'dependencies.' Basically it means that when one of those values changes, useEffect will run the function in its first argument.

We'll include current in the dependency array. We put current in there because we want the code to run when current changes.

We will also put setShowAnswer in the dependency array. The function setShowAnswer won't change and trigger the useEffect to run, but it is a value from outside the useEffect that we are using inside the useEffect. So we should put it in there. To find out why, click here, but it is complicated and not something you need to know to get this app working.

This useEffect makes it so when the value of current changes, showAnswer will be set to false. So now when the user switches cards, they won't see the answer anymore.

Import useEffect.

import React, { useState, useContext, useEffect } from 'react';

Write a useEffect hook to setShowAnswer to false when current changes. See how the first argument is a function, and the second argument is the array of dependencies?

    useEffect(() => {
        //hide the answer
        setShowAnswer(false);

        //useEffect triggers when the value of current changes
    }, [current, setShowAnswer]);

return (

Ok! When the value of current changes the code inside the useEffect will trigger and set showAnswer to false. Save and run the tests.

Answer Shows Up

What? Run the app and take a look at it. Click Submit. Click Skip. The answer disappears! So what's the problem?

What's going on here?

Take a look at it through the React Dev Tools in your browser. When you click, you can see that the Answer is still getting rendered for a split second after the card changes. See the header here?
Dev Tools
So we need to test things a bit differently. We need our test to wait for the Header to go away.

React Testing Library gives us waitForElementToBeRemoved. It pretty much does what it says. You don't pass it a reference to an element. You give waitForElementToBeRemoved the query that you want to use, and it will try the query repeatedly for up to 4500 milliseconds. It will stop when the element is removed or it times out.

Change Answering Test 5: Answer is Hidden when Card Changes

File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-14.tsx

Import waitForElementToBeRemoved.

import { render, cleanup, fireEvent, waitForElementToBeRemoved } from '@testing-library/react';

Mark the test callback function as async. Async means we are using asynchronous code. Async code is a complicated topic, and not one that you need to understand to get this app working. But basically, normal JavaScript code all executes one command after the other. If you want to run code a and then code b, you have to run code a, wait for a to finish, before you can then start to run code b. But asynchronous code is code that can be executed while other code is running. So if code a were asynchronous, you could start code a, and while code a is running, you could tell code b to start.

Making it an async function will allow us to use await. Await is one of several ways that javascript has for dealing with async code. Using the await command basically means we are waiting for the async code to get done running before running the next line of code that depends on the results of the async code.

Don't worry if you didn't understand that!

We can use await to wait for waitForElementToBeRemoved to finish running.

 //answer goes away
    it('answer disappears when card changes', async () => {

Change the last lines of the test. Originally, we looked for answer2 and expected it to be null:

//a function that compares a string to the second answer
    const compareToSecondAnswer = (content: string) => content === withoutLineBreaks;

    //look for the first answer
    const gone = queryByText(compareToInitialAnswer);
    //first answer shouldn't show up
    expect(gone).toBeNull();

    //look for the second answer
    const answer2 = queryByText(compareToSecondAnswer);
    //second answer shouldn't show up
    expect(answer2).toBeNull();

We'll change it to awaiting waitForElementToBeRemoved. Pass waitForElementToBeRemoved an anonymous function that uses queryByText() and our custom searching function compareToSecondAnswer.

Remember, queryByText looks at each element and passes the text content to the function compareToSecondAnswer. compareToSecondAnswer compares each string that queryByText passes it to the second answer and returns true if it gets a match.

So what will happen here is waitForElementToBeRemoved will run queryByText(compareToSecondAnswer). It will get an element back, because the second answer starts out in the document. That's why our first version of the test failed, because the search result for the second answer wasn't null. But waitForElementToBeRemoved will keep running that queryByText until it gets a null result.

Once our Answer component finishes animating off, it unmounts its contents. Remember when we put the Transition into Answer and had to pass it the unmountOnHide prop? Passing this test is why we had to do that. When Answer unmounts its contents, queryByText(compareToSecondAnswer) will return null, and waitForElementToBeRemoved will pass.

//a function that compares a string to the second answer
        const compareToSecondAnswer = (
            content: string, 
            { textContent } : HTMLElement
        ) => !!textContent && 
            textContent
            .replace(/\s{2,}/g, " ")
            .slice(6, textContent.length) === withoutLineBreaks;

        //look for the first answer
        const gone = queryByText(compareToInitialAnswer);
        //first answer shouldn't show up
        expect(gone).toBeNull();

        //second answer should go away
        await waitForElementToBeRemoved(() => queryByText(compareToSecondAnswer));

Await Passes

Do you notice anything different about the test 'answer disappears when card changes?' Look at how long it took the tests to pass. Waiting for the Transition component to animate made the test take 1052 ms. That is a lot longer than the rest of the tests.

One Last Thing: Clear the TextArea

If you tried typing an answer in the TextArea, you no doubt noticed that it does not get cleared out when you click Skip, Right, or Wrong. Let's fix that with the React useEffect hook.

Answering Test 6: TextArea Clears When Current Changes

File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-15.tsx

Make a test that puts text in the TextArea, clicks the Skip button, then expects the TextArea to be empty.

We use fireEvent.change() to put text into the TextArea. fireEvent.change() takes two arguments.

The first argument is a reference to the element.

The second argument is an object that describes the properties of the event. This event affects the target, which is the element that we passed as the first argument. The property of the target that is being changed is the value. We are setting the value property of the target element to the placeholder text.

it('clears the answer when card changes', () => {
  const { getByText, getByTestId } =  renderAnswering();
  const textarea = getByTestId('textarea');

  const placeholder = 'placeholder text'
  //put the placeholder text into the textarea
  fireEvent.change(textarea, { target: { value: placeholder } });

//make sure the placeholder text is there
  expect(textarea).toHaveTextContent(placeholder);

//get the skip button
  const skip = getByText(/skip/i);
//click skip, this dispatches a 'next' action to cardcontext
//which should change the value of current 
//and trigger useEffect hook to clear the textarea
  fireEvent.click(skip);

//textarea should be empty
    expect(textarea).toHaveTextContent('');
});

TextArea fails

You might notice that the test isn't failing because textarea didn't clear. The test is failing because textarea doesn't contain the placeholder text. I found that the Semantic UI React TextArea doesn't work with fireEvent unless it is a controlled component. A controlled component is a component where the value is held in state. We need to make TextArea into a controlled component to be able to clear it out when the card changes anyway, so the test will work. But that is the kind of thing that you will encounter when you are trying to test your components.

Pass Answering Test 6: TextArea Clears When Current Changes

File: src/scenes/Answering/index.tsx
Will Match: src/scenes/Answering/complete/index-11.tsx

To make fireEvent.change() work on the Semantic UI React TextArea, we need the TextArea to be a controlled component. Normally, you don't want to change your code to pass your tests. But we need the TextArea to be a controlled component anyway, so there is no problem with doing that now.

Add a useState hook to keep track of the value of input. Set it to an empty string to start with. Notice that because the starting value is a string, TypeScript will infer that input is a string and the function setInput takes a string as an argument.

Inside the useEffect hook that resets showAnswer to false, add a call to setInput. Set the input back to an empty string. Add setInput to the dependency array. Even though the setInput function won't change and trigger the useEffect, it is still a value from outside the useEffect that we are using inside the useEffect.

///the value of the textarea where the user types their input
const [input, setInput] = useState('');

    useEffect(() => {
        //hide the answer
        setShowAnswer(false);

        //clear the TextArea
        setInput('');

        //useEffect triggers when the value of current changes
    }, [current, setShowAnswer, setInput]);

Make the TextArea a controlled component by setting its value prop equal to input. Write the onChange function. You can see the props of the SUIR TextArea here. The onChange function fires with two arguments.

The first argument, that we call e, is the React SyntheticEvent object. We're not using it, so we don't even bother typing it. We just tell TypeScript to designate it as 'any.' If we wanted to type it, its type is SyntheticEvent.

The second argument is all the props and the event value. We use Object Destructuring to pull the value out of the second argument. Notice that we don't have to give the second argument a name or deal with the whole object at all, we can just pull the value out and look at it.

The value can be a few different types. But remember how we let TypeScript infer the type for setInput? Well, we can only pass strings to setInput. So in th onChange function, before we call setInput and pass it the value from the onChange event, we assert that the typeof the value is string. If the type of the value is a string, then we call setInput. If the type is something else, then we don't call setInput.

            <TextArea data-testid='textArea'
            value={input}
            onChange={(e: any, {value}) => typeof(value) === 'string' && setInput(value)}/>

TextArea Passes

Now try writing an answer then clicking Skip, Right, or Wrong. The useEffect will trigger and set the variable input to an empty string. The empty string will be passed to TextArea as its value prop, and the TextArea will clear.

Answer Clears

Next Post: StatsContext

In the next post we will make another Context to track statistics. We will also make a component to show the user the statistics.

Discussion (0)