In this post we will:
- Make a
Buttons
component that shows theSubmit
button and buttons that let the user record if their answer was right or wrong - Make clicking the
Submit
,Right
, andWrong
buttons advance to the next card - Put the new
Buttons
component intoAnswering
- 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
andWrong
buttons show up - clicking the
Right
orWrong
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.
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;
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();
});
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'/>;
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
});
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'/>;
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');
});
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'/>
};
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');
});
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 })}
/>
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();
});
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()}/>
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.
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
.
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.
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.
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();
});
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.
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?
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));
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('');
});
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)}/>
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.
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.
Top comments (0)