jacobwicks

jacobwicks

Posted on January 17, 2020

Show Stats

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 the Icon
  • 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
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

Call afterEach.

afterEach(cleanup);
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

Doesn't Run

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';
Enter fullscreen mode Exit fullscreen mode

Give the icon a testId.

const Stats = () => <Icon data-testid='icon' name='question circle'/>    

export default Stats;
Enter fullscreen mode Exit fullscreen mode

Icon Passes

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
});
Enter fullscreen mode Exit fullscreen mode

No Popup

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}
        />
};
Enter fullscreen mode Exit fullscreen mode

Now save it. The popup test should pass. But it doesn't.
No Popup

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();
    });
Enter fullscreen mode Exit fullscreen mode

Popup Pass

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(); 
    });
Enter fullscreen mode Exit fullscreen mode

No Stats Pass

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}
        />
};
Enter fullscreen mode Exit fullscreen mode

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
        };
Enter fullscreen mode Exit fullscreen mode

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>);
Enter fullscreen mode Exit fullscreen mode

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();        
        });
    })
Enter fullscreen mode Exit fullscreen mode

Popup Fail

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}
        />
};
Enter fullscreen mode Exit fullscreen mode

Passes

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.')
        });
Enter fullscreen mode Exit fullscreen mode

Fail to Calculate

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}
    />
Enter fullscreen mode Exit fullscreen mode

Correct Total

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);
        });
Enter fullscreen mode Exit fullscreen mode

Popup Values Fail

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}
            />
Enter fullscreen mode Exit fullscreen mode

All Pass

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();
});
Enter fullscreen mode Exit fullscreen mode

Alt Text

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';
Enter fullscreen mode Exit fullscreen mode

Change the question Header to this:

<Header data-testid='question'><Stats/>{question}</Header>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Snapshot Fail

Update the snapshot.

All pass

Run the app. The stats Icon will show up!

Stats Icon Shows 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:

  1. When the user clicks the Skip card button
  2. When the user clicks the Right answer button
  3. 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';
Enter fullscreen mode Exit fullscreen mode

Import StatsState, StatsContext and StatsProvider.

import { CardState, StatsState } from '../../types';
import { StatsContext, StatsProvider } from '../../services/StatsContext';
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

Skip Fail

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';
Enter fullscreen mode Exit fullscreen mode

Import StatsContext.

import { StatsContext } from '../../services/StatsContext';
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Change the onClick function for the Skip button.
From

         <Button onClick={() => dispatch({type: CardActionTypes.next})}>Skip</Button>
Enter fullscreen mode Exit fullscreen mode

To

<Button onClick={() => {
            dispatch({type: CardActionTypes.next});
            statsDispatch({type: StatsActionType.skip, question});   
         }}>Skip</Button>
Enter fullscreen mode Exit fullscreen mode

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.

Skip Pass

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.

Popup With Stats

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';
Enter fullscreen mode Exit fullscreen mode

Import StatsContext and StatsProvider.

import { StatsContext, StatsProvider } from '../../../../services/StatsContext';
Enter fullscreen mode Exit fullscreen mode

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

});
Enter fullscreen mode Exit fullscreen mode

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');
    });
Enter fullscreen mode Exit fullscreen mode

Right Button Not Update Stats

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';
Enter fullscreen mode Exit fullscreen mode

Import StatsContext.

import { StatsContext } from '../../../../services/StatsContext';
Enter fullscreen mode Exit fullscreen mode

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()}/>
};
Enter fullscreen mode Exit fullscreen mode

Right Button Pass

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');
    });
Enter fullscreen mode Exit fullscreen mode

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 })
                }}/>
Enter fullscreen mode Exit fullscreen mode

Load up the app and try out the buttons. You'll see the stats in the popup update correctly.

Stats

💖 💪 🙅 🚩
jacobwicks
jacobwicks

Posted on January 17, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Introduction, Setup, and Overview
react Introduction, Setup, and Overview

January 17, 2020

Saving to LocalStorage
react Saving to LocalStorage

January 17, 2020

First Component- Answering
react First Component- Answering

January 17, 2020

NavBar
react NavBar

January 17, 2020