Show Stats
jacobwicks
Posted on January 17, 2020
We are now going to make the Stats
component so that the user can see the stats for each card that they look at.
User Story
- The user sees a card. They hover their mouse over an icon and a popup appears. The popup shows the user how many times they have seen the card, and how many times they have gotten the answer right or wrong.
Features
- an
Icon
that shows up on the screen - a
Popup
that appears when the user mouses over theIcon
- stats are shown to the user in the
Popup
Choosing Components
Now that we have the StatsContext
we can track the stats for each card. We could put the stats on the screen all the time. But the user probably doesn't want to see them all the time. So we only want to show the stats sometimes. And instead of showing all zeros for a new question, let's make a special display that says the user hasn't seen the question before.
Popup: We'll use a Popup
to show the stats to the user.
Icon: We'll show an Icon
that the user can mouseover to trigger the Popup.
What to test
Test that the icon shows up. Test that the popup is triggered when the user mouses over the icon. Test that the correct stats are shown in the popup.
Tests
File: src/scenes/Answering/components/Stats/index.test.tsx
Will Match: src/scenes/Answering/components/Stats/index.test-1.tsx
Write your comments:
//has an icon
//there's a popup
//popup appears when mouseover icon
//if there are no stats for the current question, popup tells you that you haven't seen the question before
//if there are stats for the current question, popup shows you the correct stats
Write your Imports at the top of the file. Notice that we are importing the initialState from CardContext, but we are renaming it to cardState. So when we refer to cardState in the tests, we are talking about the initialState object exported by CardContext.
import React from 'react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Stats from './index';
import { StatsContext } from '../../../../services/StatsContext';
import { StatsState } from '../../../../types';
import { CardContext } from '../../../../services/CardContext';
import { initialState as cardState } from '../../../../services/CardContext';
Call afterEach.
afterEach(cleanup);
Test 1: Has Icon
Write the test for the icon. We'll get the icon using a testId.
//has an icon
it('has an icon', () => {
// We'll get the icon by using a testId.
const { getByTestId } = render(<Stats/>);
const icon = getByTestId('icon')
expect(icon).toBeInTheDocument();
});
Pass Test 1: Has Icon
File: src/scenes/Answering/components/Stats/index.tsx
Will Match: src/scenes/Answering/components/Stats/index-1.tsx
We'll pass the first test by rendering an Icon with a testId. Semantic UI React has a large set of icons that are built in. Pass the name prop to choose which one. We are using 'question circle,' which is a question mark in a circle.
Imports:
import React, { useContext } from 'react';
import { Icon, Popup } from 'semantic-ui-react';
import { CardContext } from '../../../../services/CardContext';
import { StatsContext } from '../../../../services/StatsContext';
Give the icon a testId.
const Stats = () => <Icon data-testid='icon' name='question circle'/>
export default Stats;
Test 2: Popup Appears
File: src/scenes/Answering/components/Stats/index.test.tsx
Will Match: src/scenes/Answering/components/Stats/index.test-2.tsx
The Icon
is always shown on the screen. The Popup
does not always show up on the screen. The Popup
is triggered when the user puts their mouse cursor over the icon. So how do we simulate putting the mouse over the Icon
to get the Popup
to show up for our tests?
We will use fireEvent
. We can use fireEvent
to simulate many events, not just clicking or entering text. So let's write a test where we simulate mouseover with fireEvent.mouseOver()
.
Make a describe block named 'theres a popup.' Inside the describe block, write the test for the Popup
. The Popup
will appear when the user moves their mouse over the Icon
.
Use getByTestId
to get a reference to the Icon
. Then use fireEvent.mouseOver
to simulate the mouseover event. After firing mouseover, use getByText
to find the textContents of the Popup
.
//there's a popup
describe('theres a popup', () => {
//popup appears when mouseover icon
it('popup exists and opens', () => {
const { getByText, getByTestId } = render(<Stats/>);
const icon = getByTestId('icon');
expect(icon).toBeInTheDocument();
//mouseOver the icon
fireEvent.mouseOver(icon);
const popup = getByText(/you haven't seen this question before/i);
expect(popup).toBeInTheDocument();
});
//if there are no stats for the current question, popup tells you that you haven't seen the question before
//if there are stats for the current question, popup shows you the correct stats
});
Looks good, right? Yes. But I have bad news. This test won't work even after we add the Popup to the Stats component. The reason it will fail is because the simulated mouseOver event just doesn't work to trigger the Semantic UI React Popup component. So the popup will never show up in our test render! Let's go add the Popup to the Stats component, watch it fail, then come back and fix this test.
Fail to Pass Test 2: Add the Popup
File: src/scenes/Answering/components/Stats/index.tsx
Will Match: src/scenes/Answering/components/Stats/index-2.tsx
Change the Stats component. Declare a const icon
reference to the JSX call to the Icon. Instead of returning the Icon
, return a Popup
. The Popup
takes a content prop. The content is the text (or anything else) that will appear inside the Popup
. The prop 'trigger' takes the element that will appear on screen and trigger the Popup
when the user mouses over it. Pass icon
to the trigger prop.
const Stats = () => {
//declare icon as a variable
const icon = <Icon data-testid='icon' name='question circle'/>
return <Popup
content="You haven't seen this question before"
trigger={icon}
/>
};
Now save it. The popup test should pass. But it doesn't.
The simulated mouseOver doesn't open the popup. We'll end up solving this by using fireEvent.click() to simulate a click on the Icon, which does trigger the popup.
When Testing Doesn't Work How You Think It Should
To be honest, this happens a lot. You're getting used to the testing mindset, you chose your components, you know what you're trying to test, you are using commands and methods that you used before... but the test fails. Sometimes it's a typo, or you're using the wrong method. But sometimes it's just that the method you thought would work won't work with the component you're using. This happens a lot with components from third party libraries.
Dealing with this is just one of the many logic puzzles you work through as a programmer. The first step is to add in a call to debug() to see what is rendered. Check the documentation of each method that you're using and see if you are calling it correctly, giving it the right parameters. Try something else and see if that works. Do an internet search for your situation and check through StackOverflow, GitHub issues, Reddit, and other internet resources. Think about if you can design the test differently using a different command.
You can get frustrated, but don't worry if it takes hours. That's just the nature of the process. Eventually you will come up with a solution that does work to test what you were doing. And if your search for an answer didn't come up with any results written by anyone else, maybe you should write a post with your solution here on dev.to!
Pass Test 2: The Working Popup Test Using fireEvent.click()
File: src/scenes/Answering/components/Stats/index.test.tsx
Will Match: src/scenes/Answering/components/Stats/index.test-3.tsx
Here's the final, working test of the Popup
. We have to use fireEvent.click()
because the simulated mouseover does not trigger the Popup
for some reason.
//popup appears when mouseover icon
it('popup exists and opens', () => {
const { getByText, getByTestId } = render(<Stats/>);
const icon = getByTestId('icon');
expect(icon).toBeInTheDocument();
//can't effectively simulate hover
//mouseOver and mouseEnter don't trigger it
//but click does, so... go with it
fireEvent.click(icon);
const popup = getByText(/you haven't seen this question before/i);
expect(popup).toBeInTheDocument();
});
Test 3: Popup Message for No Stats
File: src/scenes/Answering/components/Stats/index.test.tsx
Will Match: src/scenes/Answering/components/Stats/index.test-4.tsx
This test renders Stats outside of any context. When Stats doesn't see stats for the current question, it should render a popup that says "You haven't seen this question before." This test will pass when you run it.
//if there are no stats for the current question, popup tells you that you haven't seen the question before
it('without stats, you havent seen it', () => {
const { getByText, getByTestId } = render(<Stats/>);
const icon = getByTestId('icon');
fireEvent.click(icon);
const unSeen = getByText("You haven't seen this question before");
expect(unSeen).toBeInTheDocument();
});
That's a clue that this test is not telling us something new about the component. Let's give the Stats
component access to StatsContext
and CardContext
and make sure it still passes.
Access StatsContext and CardContext
File: src/scenes/Answering/components/Stats/index.tsx
Will Match: src/scenes/Answering/components/Stats/index-3.tsx
We want the Stats
component to show the stats for the current card. To do that we need to get data from CardContext
and StatsContext
. CardContext
will let us find the current card and get its question. Once we have the question we can look it up in StatsContext
.
If there are no stats for the current card we will return a Popup that says the user hasn't seen this question before.
Change the Stats component to this:
const Stats = () => {
//get cards and current index from CardContext
const { cards, current } = useContext(CardContext);
//get the current question
const { question } = cards[current];
//this is the entire stats context
const allStats = useContext(StatsContext);
//stats for the current question
const stats = allStats[question];
//declare icon as a variable
const icon = <Icon data-testid='icon' name='question circle'/>
if (!stats) return (
<Popup
content="You haven't seen this question before"
trigger={icon}
/>);
return <Popup
content="There are stats"
trigger={icon}
/>
};
It still passes! Good, we haven't broken anything.
Test 4: When There Are Stats for the Current Question, Popup Shows the Stats
File: src/scenes/Answering/components/Stats/index.test.tsx
Will Match: src/scenes/Answering/components/Stats/index.test-4.tsx
Make a describe block named 'with Stats.' Make a stats
variable, statsState
to pass to the StatsProvider
, and testState
for the CardProvider
.
describe('with Stats', () => {
//some stats
const stats = {
right: 3,
wrong: 2,
skip: 5
};
//a StatsState to pass to StatsProvider
//using the question from cards index 0
const statsState = {
[cardState.cards[0].question] : stats
} as StatsState;
//a CardState with current set to 0
const testState = {
...cardState,
current: 0
};
Make a helper function to render Stats
inside the CardProvider
and StatsProvider
. Rendering a component inside multiple providers is how you let the component access multiple contexts. This helper function will allow Stats
to access the CardContext
and StatsContext
during testing.
//helper function to render stats inside CardProvider, StatsProvider
const renderStats = () => render(
<CardProvider testState={testState}>
<StatsProvider testState={statsState}>
<Stats/>
</StatsProvider>
</CardProvider>);
Write the test. After we trigger the Popup
with a simulated click event, we use getByText to look for text that says 'you have seen this question.'
//if there are stats for the current question, popup shows you the correct stats
it('with stats, shows stats for that question', () => {
const { getByText, getByTestId } = renderStats();
const icon = getByTestId('icon');
fireEvent.click(icon);
const seen = getByText(/you have seen this question/i);
expect(seen).toBeInTheDocument();
});
})
Pass Test 4: When There Are Stats for the Current Question, Popup Shows the Stats
File: src/scenes/Answering/components/Stats/index.tsx
Will Match: src/scenes/Answering/components/Stats/index-4.tsx
Change the return values to this:
if (!stats) return (
<Popup
content="You haven't seen this question before"
trigger={icon}
/>);
return <Popup
content="You have seen this question"
trigger={icon}
/>
};
Test 5: Popup Should Show the Total Number of Times User Has Seen the Question
File: src/scenes/Answering/components/Stats/index.test.tsx
Will Match: src/scenes/Answering/components/Stats/index.test-5.tsx
The popup should calculate the total number of times the user has seen the question. Let's test for a question they have seen 10 times.
it('calculates total times seen', () => {
const { getByTestId, getByText } = renderStats();
const icon = getByTestId('icon');
fireEvent.click(icon);
const seen = getByText(/you have seen this question/i);
expect(seen).toBeInTheDocument();
expect(seen).toHaveTextContent('You have seen this question 10 times.')
});
Pass Test 5: Popup Should Show the Total Number of Times User Has Seen the Question
File: src/scenes/Answering/components/Stats/index.tsx
Will Match: src/scenes/Answering/components/Stats/index-5.tsx
We already get the stats for the current card in the Stats
component. Recall that the stats are an object with three properties: right, skip, and wrong. We need to add the values of these properties together to get a total number.
Adding Up Total Times Seen
Use Object.keys
to get an array of the keys from the stats for the current card. Use Array.reduce to iterate through the keys, add the value of that key to the total, and get the total of times the user has seen it.
Object.keys(stats)
will give us an array of three strings, ['right','skip','wrong']
.
Array.reduce
can look more complicated than it actually is. It takes two arguments. The first argument is a function, and the second argument is the starting value. We're adding up numbers, so we'll give a starting value of 0.
Array.reduce passes two arguments to the function. The first argument is the accumulator. I named it 'acc' in this code. The first time the function runs the accumulator is the starting value. So acc will start at 0, the starting value that we passed in. Then every time the function runs the accumulator is the value that was returned by the function the last time it ran.
The second argument is the current item in the array that is being iterated over. I named it 'cur' in this code. The array that we are iterating over is ['right','skip','wrong']. So the first time through, cur
will be the item at array 0, the string 'right.' We use bracket notation to look in the object stats for the value corresponding to the key 'right.' Then we add that value to the total, acc
, and return the total. In the next iteration, the function will run with acc
equal to the updated total, and cur
will be the next item in the array- the string 'skip'.
Added complexity from TypeScript
Before we can use bracket notation and cur
to look in stats
and get a value, we have to cast cur
to a key of the type of stats. Basically, we are convincing TypeScript that the variable key
is one of the object properties of stats
. If we tried to look at stats[cur]
, TypeScript would throw an error even though we got the value cur
from the array of Object.keys
of stats
. This is the type (haha) of thing you have to deal with fairly often when using TypeScript
. You will be faced with a situation where you know that the code you wrote will work, but then you need to find out the right way to tell TypeScript that the code you wrote will work. It's just part of the learning curve.
When to Calculate the Total
Notice that we calculate the total after the first return statement. If we don't have stats, we'll return the Popup that says 'You haven't seen this question before.' If we do have stats, then we'll calculate the total before returning a Popup that reports the total.
if (!stats) return (
<Popup
content="You haven't seen this question before"
trigger={icon}
/>);
//stats is truthy, so we can calculate the total
const total = Object.keys(stats)
.reduce((acc, cur) => {
//cast cur to key from the typeof stats
//which is really the keys of Stats as defined in our src/types.ts file
const key = cur as keyof typeof stats;
//stats[key] is a number
//set acc equal to the prior value of acc plus the value of stats[key]
//to get the new total
acc = acc + stats[key];
//return the new total for the next iteration to use
return acc;
//starting value of 0
}, 0);
return <Popup
data-testid='popup'
content={
<div>
<div>You have seen this question {total} time{total !== 1 && 's'}.</div>
</div>}
trigger={icon}
/>
Test 6: Correct Value for each Stat
File: src/scenes/Answering/components/Stats/index.test.tsx
Will Match: src/scenes/Answering/components/Stats/index.test-6.tsx
Let's use test.each to test for each type of stat- 'right', 'skip', and 'wrong.' Declare questionZero
equal to the question of the card at index 0 in cards. Declare expectedStats
to access the stats for the question at index 0 in our stats testState
.
Then set up the literal and the tests. We'll pass in three arguments for each test. stat
is just a string that we use to generate the title. regEx
is a regular expression that we will pass to getByText to find the element. expected
is the expected number from stats. We cast the number to a string using toString() because we are comparing it to textContent, which is a string. A string will not equal a number in expect().toHaveTextContent().
//remember, current index in our testState is set to 0
const questionZero = cardState.cards[0].question;
const expectedStats = statsState[questionZero];
//use test each to test for each type of stat
test.each`
stat | regEx | expected
${'right'} | ${/You got it right/i}| ${expectedStats.right.toString()}
${'wrong'} | ${/Wrong/i} | ${expectedStats.wrong.toString()}
${'skip'} | ${/You skipped it/i} | ${expectedStats.skip.toString()}
`('Popup returns correct value of $stat, $expected',
({stat, regEx, expected}) => {
const { getByTestId, getByText } = renderStats();
//open the popup
const icon = getByTestId('icon');
fireEvent.click(icon);
//make find the element by regular expression
const result = getByText(regEx);
expect(result).toHaveTextContent(expected);
});
Show the Value for Each Stat
File: src/scenes/Answering/components/Stats/index.tsx
Will Match: src/scenes/Answering/components/Stats/index-6.tsx
Add divs to show each stat. The total div uses the total that we calculated using Array.reduce. When total does not equal 1, we'll add 's' so it says 'times' instead of 'time.'
return <Popup
data-testid='popup'
content={
<div>
<div>You have seen this question {total} time{total !== 1 && 's'}.</div>
<div>You got it right {stats.right}</div>
<div>Wrong {stats.wrong}</div>
<div>You skipped it {stats.skip}</div>
</div>}
trigger={icon}
/>
Great! All the tests pass.
Add Stats into Answering
Now to make Stats
available to the user, we'll add it to Answering
.
Decide what to test for
We don't need to re-do all the tests for Stats
in the tests for the Answering
component. We are already testing Stats
in the tests for Stats
. Let's just make sure that Answering
has the stats Icon
.
Answering Test 1: Has a Stats Icon
File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-16.tsx
Add a new test to look for the Icon
from the Stats
component.
it('has the stats icon', () => {
const { getByTestId } = renderAnswering();
const stats = getByTestId('icon');
expect(stats).toBeInTheDocument();
});
Pass Answering Test 1: Has a Stats Icon
File: src/scenes/Answering/index.tsx
Will Match: src/scenes/Answering/complete/index-12.tsx
Import the Stats component.
import Stats from './components/Stats';
Change the question Header to this:
<Header data-testid='question'><Stats/>{question}</Header>
The whole return value of the Answering
component will look like this.
<Container data-testid='container' style={{position: 'absolute', left: 200}}>
<Header data-testid='question'><Stats/>{question}</Header>
<Button onClick={() => dispatch({type: CardActionTypes.next})}>Skip</Button>
<Form>
<TextArea data-testid='textarea'/>
</Form>
<Buttons answered={showAnswer} submit={() => setShowAnswer(true)}/>
<Answer visible={showAnswer}/>
</Container>
Update the snapshot.
Run the app. The stats Icon will show up!
Make the stats change
We know the Stats
component works because it passes the tests. We know the Stats
component shows up because we test for that, too. But if you run the app you will see that the stats don't actually update when you skip or submit questions. That is because we are not dispatching any actions to StatsContext
. So StatsContext
doesn't receive an action and doesn't make any changes to the state. We need to dispatch an action to StatsContext
when the user skips a question, records a right answer, or records a wrong answer.
There are three times that we need to dispatch an action to the Stats context:
- When the user clicks the
Skip
card button - When the user clicks the
Right
answer button - When the user clicks the
Wrong
answer button
Answering Test 2: The Skip Button Updates Stats
File: src/scenes/Answering/index.test.tsx
Will Match: src/scenes/Answering/complete/test-17.tsx
Import useContext
. We'll need it to make a helper component that displays stats.
import React, { useContext } from 'react';
Import StatsState
, StatsContext
and StatsProvider
.
import { CardState, StatsState } from '../../types';
import { StatsContext, StatsProvider } from '../../services/StatsContext';
Add a new test above the snapshot. We'll create a cardState
, blankStats
, question
and a statsState
for this test. Then we'll make a helper component SkipDisplay
to display the value of 'skip' for the question. We'll render Answering
and SkipDisplay
inside of the CardProvider
and StatsProvider
. Then we'll click the Skip
button and see what happens.
//when the user clicks the skip button, the skip is recorded in the stats
it('clicking skip records stats', () => {
//create a CardState with current set to 0
const cardState = {
...initialState,
current: 0
};
//a blank stats object
const blankStats = {
right: 0,
wrong: 0,
skip: 0
};
//get the question from cards index 0
const { question } = cardState.cards[0];
//create statsState with stats for the question
const statsState: StatsState = {
[question]: blankStats
};
//helper component displays the value of skip for the question
const SkipDisplay = () => {
const stats = useContext(StatsContext)
const { skip } = stats[question];
return <div data-testid='skipDisplay'>{skip}</div>
};
//render Answering and SkipDisplay inside the providers
//pass the providers the cardState and StatsState values that we defined
const { getByTestId, getByText } = render(
<CardProvider testState={cardState}>
<StatsProvider testState={statsState}>
<Answering />
<SkipDisplay/>
</StatsProvider>
</CardProvider>
);
//find the skip button
const skipButton = getByText(/skip/i);
//find the skip display
const skipDisplay = getByTestId('skipDisplay');
//skip display should start at 0
expect(skipDisplay).toHaveTextContent('0');
//click the skip button
fireEvent.click(skipButton);
expect(skipDisplay).toHaveTextContent('1');
});
Pass Answering Test 2: The Skip Button Updates Stats
File: src/scenes/Answering/index.tsx
Will Match: src/scenes/Answering/complete/index-13.tsx
Import StatsActionType
.
//The types of action that CardContext can handle
import { CardActionTypes, StatsActionType } from '../../types';
Import StatsContext
.
import { StatsContext } from '../../services/StatsContext';
Use object destructuring to get the dispatch method out of useContext(StatsContext)
. Watch out! We already have a variable called dispatch. The variable called dispatch we already have is the function that dispatches actions to the CardContext
. So we can't call the dispatch function for the StatsContext
'dispatch.' We have to call the dispatch function for the StatsContext
something else. Let's call it statsDispatch
.
To rename variables that you get from object destructuring you type the original variable name, a colon, and then the new name. So const { originalName : newName } = objectToBeDestructured
. In this case, we write dispatch: statsDispatch
to rename dispatch to statsDispatch.
const { dispatch: statsDispatch } = useContext(StatsContext);
Change the onClick function for the Skip
button.
From
<Button onClick={() => dispatch({type: CardActionTypes.next})}>Skip</Button>
To
<Button onClick={() => {
dispatch({type: CardActionTypes.next});
statsDispatch({type: StatsActionType.skip, question});
}}>Skip</Button>
Notice that the anonymous function now contains two expressions. Because there is more than one expression, we have to enclose the expressions in curly brackets. We switch from a concise function body without brackets, to a block body with brackets.
Run your app and click the Skip
button twice. Clicking it twice will get you back to the first question. Mouse over the stats icon. The stats popup will now show correct totals for each question.
Right And Wrong Buttons
Now let's make the Right
and Wrong
buttons update StatsContext
.
What to Test
- Clicking the
Right
button updates stats - Clicking the
Wrong
button updates stats
We'll use the same techniques that we used to test the Skip
Button. We'll make a helper component StatsDisplay
to show the stats, render Buttons
and StatsDisplay
inside of Providers, and check StatsDisplay
to make sure Buttons
successfully dispatches actions.
Buttons Test 1: the Right Button Updates Stats
File: src/scenes/Answering/components/Buttons/index.test.tsx
Will Match: src/scenes/Answering/components/Buttons/complete/test-7.tsx
Import StatsState
.
import { CardState, StatsState } from '../../../../types';
Import StatsContext
and StatsProvider
.
import { StatsContext, StatsProvider } from '../../../../services/StatsContext';
Make a describe block named 'clicking buttons records stats.' Declare cardState
, blankStats
, and the question
from the card at index 0. Make a StatsDisplay
helper component to display right and wrong from the StatsContext
.
Make a renderWithDisplay
helper function to render Buttons
and StatsDisplay
inside the CardProvider
and StatsProvider
with the cardState
and statsState
.
//when the user clicks the skip button, the skip is recorded in the stats
describe('clicking buttons records stats', () => {
//create a CardState with current set to 0
const cardState = {
...initialState,
current: 0
};
//a blank stats object
const blankStats = {
right: 0,
wrong: 0,
skip: 0
};
//get the question from cards index 0
const { question } = cardState.cards[0];
//create statsState with stats for the question
const statsState: StatsState = {
[question]: blankStats
};
//helper component displays the value of skip for the question
const StatsDisplay = () => {
const stats = useContext(StatsContext)
const { right, wrong } = stats[question];
return <div>
<div data-testid='rightDisplay'>{right}</div>
<div data-testid='wrongDisplay'>{wrong}</div>
</div>
};
const renderWithDisplay = () => render(
<CardProvider testState={cardState}>
<StatsProvider testState={statsState}>
<Buttons answered={true} submit={jest.fn()} />
<StatsDisplay/>
</StatsProvider>
</CardProvider>
);
//clicking the right button updates stats
//clicking the wrong button updates stats
});
Write the test for the right button inside the describe block.
//clicking the right button updates stats
it('clicking the right button updates stats', () => {
//render Answering and StatsDisplay inside the providers
//pass the providers the cardState and StatsState values that we defined
const { getByTestId, getByText } = renderWithDisplay();
//find the right button
const rightButton = getByText(/right/i);
//find the right display
const rightDisplay = getByTestId('rightDisplay');
//right display should start at 0
expect(rightDisplay).toHaveTextContent('0');
//click the right button
fireEvent.click(rightButton);
expect(rightDisplay).toHaveTextContent('1');
});
Pass Buttons Test 1: the Right Button Updates Stats
File: src/scenes/Answering/components/Buttons/index.tsx
Will Match: src/scenes/Answering/components/Buttons/index-6.tsx
Import StatsActionType
.
import { CardActionTypes, StatsActionType } from '../../../../types';
Import StatsContext
.
import { StatsContext } from '../../../../services/StatsContext';
Change the Buttons
component. Get cards and current from CardContext
so that you can then get the question from the current card. Get dispatch from StatsContext
and rename it to statsDispatch
so it won't conflict with the CardContext
dispatch. Change the onClick function for the Right
button to statsDispatch
an action with a type StatActionType.right
.
const Buttons = ({
answered,
submit
}:{
answered: boolean,
submit: () => void
}) => {
//get cards and current so that we can get the question
const { cards, current, dispatch } = useContext(CardContext);
//get the question so we can track stats
const { question } = cards[current];
//to dispatch actions to the StatsContext
const { dispatch: statsDispatch } = useContext(StatsContext);
return answered
? <Button.Group>
<Button content='Right' positive
onClick={() => {
statsDispatch({ type: StatsActionType.right, question })
dispatch({ type: CardActionTypes.next })
}}/>
<Button.Or/>
<Button content='Wrong' negative
onClick={() => dispatch({ type: CardActionTypes.next })}
/>
</Button.Group>
: <Button content='Submit' onClick={() => submit()}/>
};
Buttons Test 2: the Wrong Button Updates Stats
File: src/scenes/Answering/components/Buttons/index.test.tsx
Will Match: src/scenes/Answering/components/Buttons/complete/test-8.tsx
Add the test inside the describe block.
//clicking the wrong button updates Stats
it('clicking the wrong button updates stats', () => {
//render Answering and StatsDisplay inside the providers
//pass the providers the cardState and StatsState values that we defined
const { getByTestId, getByText } = renderWithDisplay();
//find the wrong button
const wrongButton = getByText(/wrong/i);
//find the wrong display
const wrongDisplay = getByTestId('wrongDisplay');
//wrong display should start at 0
expect(wrongDisplay).toHaveTextContent('0');
//click the wrong button
fireEvent.click(wrongButton);
expect(wrongDisplay).toHaveTextContent('1');
});
Pass Buttons Test 2: the Wrong Button Updates Stats
File: src/scenes/Answering/components/Buttons/index.tsx
Will Match: src/scenes/Answering/components/Buttons/complete/index-7.tsx
<Button content='Wrong' negative
onClick={() => {
statsDispatch({ type: StatsActionType.wrong, question })
dispatch({ type: CardActionTypes.next })
}}/>
Load up the app and try out the buttons. You'll see the stats in the popup update correctly.
Posted on January 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.