Saving to LocalStorage
Tags: react, typescript, testing, tutorial
Published: 2020-01-17 04:49:24

In this post we are going to write the code that saves the cards to the browser's [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). `LocalStorage` is a feature of web browsers that lets you save data to the user's computer in between sessions. Using `localStorage` will make it possible for cards to persist between sessions. When we start the app we can load cards from `localStorage` instead of loading the example cards that we wrote inside the `CardContext` services. 

We are also going to write the code that saves the stats to the browser's `localStorage`. This will let the user's stats persist between sessions.

##User Stories
* The user loads the app. The user sees all the cards they have written. The user selects the subject that they want to study. The program displays the cards in that subject in random order.

* The user thinks of a new card. The user opens the card editor. The user clicks the button to create a new card. The user writes in the card subject, question prompt, and an answer to the question. The user saves their new card.

* The user changes an existing card and saves their changes.

* The user opens the app. The user looks at the stats for a card and sees how many times they have answered it before.

##Features
* Cards save to `localStorage` and load when the app is started
* Stats save to `localStorage` and load when the app is started

##What is localStorage?
[localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) is an object that lets you save data in between browser sessions.

`localStorage.setItem()`: The setItem method allows you set the value of a property of localStorage.

`localStorage.getItem()`: The getItem method lets you retrieve the value of a property of localStorage.

We'll use `JSON.stringify()` on the array cards to turn it into a string before saving it. When we load cards, we'll use `JSON.parse()` to turn it back into an array.

[JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify): Converts a JSON object to a string. 

[JSON.parse()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse): Parses a string to a JSON object.

To test our code that uses `localStorage`, we'll be doing some 'mocking.' 

##What is Mocking?
Mocking is a term that has both a strict, technical meaning, and also a general meaning. Generally, mocking means using any kind of code to make a fake version of other code for use in testing. We'll be making a fake version of `localStorage` so that when our tests call the `localStorage` methods we can see what values they called with and also control what values get returned.
 
For a more detailed explanation of mocking, see: [But really, what is a JavaScript mock?](https://kentcdodds.com/blog/but-really-what-is-a-javascript-mock) 
For the different technical meanings of mocking, see [the Little Mocker](https://blog.cleancoder.com/uncle-bob/2014/05/14/TheLittleMocker.html).


##What to Test
* Saving Cards saves Cards to localStorage
* Loading Cards loads Cards from localStorage
* Loading cards returns undefined if nothing found in localStorage
* Saving Stats saves Stats to localStorage
* Loading Stats loads the stats from localstorage
* Loading stats returns empty object if nothing found in localStorage

##Save Test 1: Saving Cards
>File: src/services/Save/index.test.ts
>Will Match: [src/services/Save/complete/test-1.ts]
(https://github.com/jacobwicks/flashcard/blob/master/src/services/Save/complete/test-1.ts)

`Save/index.ts` is a .ts file, not a tsx file. There will not be any JSX in `Save`, so we don't need to use the .tsx extension.

Write a comment for each test.
```javascript
//saving cards saves cards
//loading cards retrieves saved cards
//loading cards returns undefined if nothing found
//saving stats saves stats
//loading stats retrieves saved stats
//loading stats returns empty object if nothing found
```

Imports and `afterEach`.
```javascript
import { cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { saveCards } from './index';
import { initialState } from '../CardContext';

afterEach(cleanup);
```

Make a describe block named 'Saving and Loading Cards.'
```javascript
describe('Saving and Loading Cards', () => {
//saving cards saves cards
//loading cards retrieves saved cards
//loading cards returns undefined if nothing found
});
```

##Setup for Mocking LocalStorage
Inside the describe block, we will get a reference to the original `localStorage` object from the [window](https://developer.mozilla.org/en-US/docs/Web/API/Window). The window is basically the global object for the browser. It contains the document object model (the dom) where all the code that the user sees is. It also contains `localStorage`.

Before each test we get a reference to `localStorage`. During each test, we'll set this reference to a mock `localStorage` that we'll create. That way, we can control what the test sees and interacts with when the test accesses `localStorage`.

```javascript
describe('Saving and Loading Cards', () => {
    let originalLocalStorage: Storage

	beforeEach(() => {
		originalLocalStorage = window.localStorage
	})

	afterEach(() => {
        (window as any).localStorage = originalLocalStorage
    })
    
    const { cards } = initialState;
    const stringCards = JSON.stringify(cards);
    
    //saving cards saves cards
```

Write the first test. We will use [jest.spyOn](https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname) to see if saveCards calls the localStorage setItem method with the right arguments. We are spying on the setItem method of the window.localStorage prototype. When we spy on a method, we replace that method with a jest.fn, and can see what calls get made to the spied on method. jest.spyOn is a type of mocking.

```javascript
it('Saving cards saves cards', () => {

        const setItem = jest.spyOn(window.localStorage.__proto__, 'setItem');
        
        saveCards(cards);

        expect(setItem).toHaveBeenCalledWith("cards", stringCards);
    })
```

![Save Cards Test Fail](https://thepracticaldev.s3.amazonaws.com/i/x4rp2zkyc5be22e65um0.png)

##Pass Save Test 1: Saving Cards
>File: src/services/Save/index.ts
>Will Match: [src/services/complete/index-1.ts]
(https://github.com/jacobwicks/flashcard/blob/master/src/services/Save/complete/index-1.ts)

Using `localStorage` is fairly simple. It's globally available, so you don't need to import it. You access the `setItem` method and pass it two arguments. The first argument is the name of the property you want to set. The name is a string. The second argument is the value of the property. The value is also a string. 

`cards` is an array, so we use `JSON.stringify()` to change it into a string before saving it.


```javascript
export const saveCards = (cards: Card[]) => {
  try {
      localStorage.setItem('cards', JSON.stringify(cards));
    } catch (err) {
      console.error(err);
    }
};
```
When you finish writing the code and run the app, you can check if the cards are getting saved. You can check your `localStorage` in the [dev console](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools) of your web browser. Click application, `localstorage`, then localhost:3000 and you can see the saved cards.

![Save Card Test Pass](https://thepracticaldev.s3.amazonaws.com/i/989e3u7jgcxcpfnwh7mj.png)

##Save Tests 2-3: Loading Cards
>File: src/services/Save/index.test.ts
>Will Match: [src/services/Save/complete/test-2.ts]
(https://github.com/jacobwicks/flashcard/blob/master/src/services/Save/complete/test-2.ts)

Import `loadCards`.
```javascript
import { saveCards, loadCards } from './index';
```

`loadCards` should retrieve the cards from `localStorage` and return them as a JSON object, an array.

We are doing some more complicated mocking in this test. We defined `stringCards` earlier as a `JSON.stringify`'d version of `cards`. Now we are making a jest.fn that will return the value `stringCards` when called. 

```javascript
let mockGetItem = jest.fn().mockReturnValue(stringCards)
```

`localStorageMock` is an object with a property `getItem`. `localStorageMock.getItem` returns a function that accepts any parameters and invokes `mockGetItem`, which returns `stringCards`.
  
```javascript
        let localStorageMock = {
            getItem: (params: any) => mockGetItem(params),
        } 
```
To overwrite localStorage with our localStorageMock we use [Object.defineProperty](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty).

```javascript
        Object.defineProperty(window, 'localStorage', {
            value: localStorageMock,
            writable: true
            });
```

Now when `loadCards` calls `localStorage` it will actually be calling the `localStorageMock` that we just made. Trying to call `localStorage.getItem()` with any parameters will call the mockGetItem jest function.

Because we know `loadCards` will try to call localStorage.getItem('cards'), we know it will receive our mock value. `loadCards` should parse `stringCards` and return an array that matches `cards`.
  
```javascript
    //loading cards retrieves saved cards
    it('Loading cards returns saved cards object', () => {
        let mockGetItem = jest.fn().mockReturnValue(stringCards);
        
        let localStorageMock = {
            getItem: (params: any) => mockGetItem(params),
        }; 

        Object.defineProperty(window, 'localStorage', {
            value: localStorageMock,
            writable: true
           });
            

        const loadedCards = loadCards();
        expect(mockGetItem.mock.calls.length).toBe(1);
        expect(mockGetItem.mock.calls[0][0]).toBe('cards');
        expect(loadedCards).toStrictEqual(cards);
    });
```

We want `loadCards` to return *undefined* if no cards are found in `localStorage`. This time `mockGetItem` returns *undefined*.

```javascript
    //loading cards returns undefined if nothing found
    it('Loading cards when no saved cards returns undefined', () => {
        let mockGetItem = jest.fn().mockReturnValue(undefined);
        let localStorageMock = {
            getItem: (params: any) => mockGetItem(params),
        } 
        Object.defineProperty(window, 'localStorage', {
            value: localStorageMock,
            writable: true
            })
            

        const loadedCards = loadCards();
        expect(mockGetItem.mock.calls.length).toBe(1);
        expect(mockGetItem.mock.calls[0][0]).toBe('cards');
        expect(loadedCards).toStrictEqual(undefined);
    });
```
![Loading Cards Fail](https://thepracticaldev.s3.amazonaws.com/i/u68bdafdsqgmridsz3k7.png)

##Pass Save Tests 2-3: Loading Cards
>File: src/services/Save/index.ts
>Will Match: [src/services/Save/complete/index-2.ts]
(https://github.com/jacobwicks/flashcard/blob/master/src/services/Save/complete/index-2.ts)

Write the `loadCards` function. If we get a value from localStorage, parse it and cast it to an array type Card[]. If we don't get a value, return undefined. 

```javascript
export const loadCards = () => {
  try {
    const stored = localStorage.getItem('cards');
    return stored 
      ? JSON.parse(stored) as Card[]
      : undefined;
  } catch (err) {
      console.error("couldn't get cards from localStorage");
      return undefined;
  }
};
```

![Load Cards Pass](https://thepracticaldev.s3.amazonaws.com/i/6yvwuaudu87vrpa2nart.png)

#Add Saving to `CardContext`
We are going to add saving and loading to `CardContext`. 
* Write the tests
* Import the `saveCards` function into `CardContext`
* Change the `CardContext` Provider so that it saves `cards` to `localStorage` when `cards` changes
* Run the app and use `Writing` and the `Save` button to add another card
* Inside the `CardContext` services file we will make a new `getInitialState` function that will try to load saved cards from `localStorage` 

##CardContext Tests 1-2: Saving the Array 'cards' When it Changes
>File: src/services/CardContext/index.test.tsx
>Will Match: [src/services/CardContext/complete/test-10.tsx](https://github.com/jacobwicks/flashcard/blob/master/src/services/CardContext/complete/test-10.tsx)

Make a describe block named 'saving to localStorage and loading from localStorage.'

```javascript
describe('saving to localStorage and loading from localStorage ', () => {
    it('when a card is added to cards, attempts to save', () => {
        const saveCards = jest.spyOn(localStorage, 'saveCards');

        const newCard = {
            question: 'New Question',
            subject: 'New Subject',
            answer: 'New Answer'
        };

        const newCards = [...initialState.cards, newCard];
        
        const SavesCard = () => {
            const { dispatch } = useContext(CardContext);
            return <Button content='save' onClick={() => dispatch({
                type: CardActionTypes.save,
                ...newCard
            })}/>}

        const { getByText } = render(
            <CardProvider>
                <SavesCard/>
            </CardProvider>);
        
        expect(saveCards).toHaveBeenCalledTimes(1);
        
        const saveCard = getByText(/save/i);
        fireEvent.click(saveCard);
        expect(saveCards).toHaveBeenCalledTimes(2);
        
        expect(saveCards).toHaveBeenCalledWith(newCards);
        saveCards.mockRestore();
    });

    it('when a card is taken out of cards, attempts to save cards', () => {
        const saveCards = jest.spyOn(localStorage, 'saveCards');

        const { current, cards } = initialState;
        const { question }  = cards[current];
        
        const newCards = cards.filter(card => card.question !== question);

        const DeletesCard = () => {
            const { dispatch } = useContext(CardContext);
            return <Button content='delete' onClick={() => dispatch({
                type: CardActionTypes.delete,
                question
            })}/>}

        const { getByText } = render(
            <CardProvider>
                <DeletesCard/>
            </CardProvider>);
        
        expect(saveCards).toHaveBeenCalledTimes(1);
        
        const deleteCard = getByText(/delete/i);
        fireEvent.click(deleteCard);
        expect(saveCards).toHaveBeenCalledTimes(2);
        
        expect(saveCards).toHaveBeenLastCalledWith(newCards);
    });
});
```

![CardContext Fails Tests](https://thepracticaldev.s3.amazonaws.com/i/r5l6rpckpxy4u5uvmrz7.png)

##Pass CardContext Tests 1-2: Saving Cards When Cards Changes
>File: src/services/CardContext/index.tsx
>Will Match: [src/services/CardContext/complete/index-8.tsx](https://github.com/jacobwicks/flashcard/blob/master/src/services/CardContext/complete/index-8.tsx)

So, we want the user to be able to create new cards, change cards, and delete existing cards. That means the app needs to save the changes that the user makes. How would you do it? 

You could give them a `Save All Cards` button, and save to `localStorage` when they click it. You'd probably also want to notify them when they had unsaved changes if you did that. 

You could change the onClick function of the existing `Save` button to save to `localStorage`. You could do the same with the `Delete` button.   

You could change the reducer, and call `saveCards` inside of the `save` case and inside the `delete` case. But you [generally don't want your reducer to to have 'side effects,'](https://redux.js.org/introduction/three-principles/#changes-are-made-with-pure-functions) and saving to `localStorage` is a 'side effect.' 

A side effect is changing anything that's not the state object. Don't worry if you don't fully understand what a side effect is. It's enough to understand that if you use your reducer to change things besides variables that you create inside the reducer, you'll end up writing bugs into your code. In this app that we are writing using the reducer to save to `localStorage` is a side effect that probably wouldn't cause any problems. But we aren't going to do it that way.

The way we are going to make the app save `cards` is to make the `CardContext`  save `cards` to `localStorage` every time the array of `cards` changes. We can do this because the `CardProvider` is a React component like any other. We can use hooks inside of the `CardProvider`. So we can use `useEffect` to trigger a function any time `cards` changes. It's just like how we've used `useEffect` before, to trigger a function that clears inputs when `current` changes. Except this time we are putting it inside the `CardProvider` and the function will call `saveCards` so we can save the `cards` to `localStorage`.

Import `useEffect`.
```javascript
import React, { createContext, useEffect, useReducer } from 'react';
```

Import `saveCards` from Save.
```javascript
import { saveCards } from '../Save';
```

Add a useEffect hook to save cards to localStorage when cards change.
```javascript
    const [state, dispatch] = useReducer(reducer, testState ? testState : initialState);

    useEffect(() => {
        //save cards to localStorage
        saveCards(state.cards);
  }, [state.cards])
```

#Add Loading to CardContext
To make the CardContext load the saved questions we are going to change the way that the CardContext gets the initialState. Right now initialState is an object inside of CardContext/index.js. 

##CardContext Services
We are going to make a function called `getInitialState` that returns the `initialState` object. We are going to put this function into the services subfolder of `CardContext`. This will let us keep the `CardContext` index file organized and easy to read. This is important because later in the project we are going to add some more cases to the reducer, which will make the `CardContext` file bigger.

##CardContext Services Tests
>File: src/services/CardContext/services/index.test.ts
>Will Match: [src/services/CardContext/services/complete/test-1.ts](https://github.com/jacobwicks/flashcard/blob/master/src/services/CardContext/services/complete/test-1.ts)

###What to Test?
We are going to write tests for the `getInitialState` function. Until now, `initialState` was just an object that we had written. We knew what would be in it. But now `initialState` will be the result of the `getInitialState` function. The `getInitialState` function is going to attempt to load saved cards from `localStorage`. And we can't be sure that it's going to get any cards, or that there won't be an error. So we want to test
* `getInitialState` returns a default array of cards when `loadCards` from `localStorage` returns *undefined*
* `getInitialState` returns the saved array of cards when `loadCards` returns a saved array of cards
* `getInitialState` returns a current index of 0

`getInitialState` will always call the `loadCards` function that we wrote in Save. What `loadCards` returns depends on what is in `localStorage`. When we are running tests, we aren't using the localStorage in our web browser. We are using `localStorage` in the test web browser that Jest makes. This test browser `localStorage` starts out empty. And we can put things into it. So one way to test how `getInitialState` works with an empty `localStorage` or with cards in `localStorage` is to actually use the test browser `localStorage`. Don't put anything in and run the first test. Put cards in and run the second test. But then our test of `getInitialState` would also be a a test of the `loadCards` function. And it would depend on how well we understand what is in the test browser `localStorage`.

##We Need to Mock LoadCards
We only want to test `getInitialState`. We don't want to test `loadCards` at the same time. So what we should do is make a fake version of `loadCards`. We will make a fake version of `loadCards`, and declare what the fake version of `loadCards` will return when `getInitialState` calls it. We will then test `getInitialState` in a way that makes `getInitialState` call the fake `loadCards` function instead of the real one. That's how we know what value of `loadCards` `getInitialState` is using. We'll know `getInitialState` is using the value that we want because it is calling the fake version of `loadCards` that we control.

A fake version of a function is called a [mock function](https://jestjs.io/docs/en/mock-functions). The process of setting up mock functions is called mocking. Mocking can be complicated to set up right. I have no doubt that you will someday be very frustrated trying to mock a function while you are testing. But this example should work for you. And I hope it gives you an idea of how to set up mock functions when you are testing your own projects.

##Write a Comment for Each Test.

```javascript
//gets default initialState when it does not get cards from localstorage
//initialState contains saved cards when saved cards returned from localStorage
//current index should start at 0
```

##Use Require Instead of Import
Do we do the imports at the top of this file? No! We aren't using the [import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) command to get the function that we are testing. We are getting the function with the [require](https://nodejs.org/api/modules.html#modules_require_id) command. There are complicated, technical differences between the way that these two commands work. 

The basic reason we are not using `import` is because `import` would do the work to set up `getInitialState` before our mock `loadCards` function was ready. If we got `getInitialState` using `import`, `getInitialState` would be set up to use the real `loadCards` function. After that, our mock `loadCards` function would be set up. Then our tests wouldn't work because when we tested `getInitialState` it would call the real `loadCards` function. That's not what we want!

When we use `require`, `getInitialState` is set up when the `require` code runs. We can call `require` after we set up our mock function. That way, we can force `getInitialState` to call the mock `loadCards` function instead of the real one. When `getInitialState` calls the mock `loadCards`, it will get the return value that we put in the mock function. By controlling the return value of the mock function, we can control the test inputs.

```javascript
//this command will reset the mock values in between tests
beforeEach(() => jest.resetModules());

//gets default initialState when it does not get cards from localstorage
it('gets default initialState when no cards in localstorage', () => {

    //the first argument is the path to the file that has the function you want to mock
    //the second argument is a function that returns an object
    //give the object a property for each function you want to mock
    jest.mock('../../Save', () => ({ 
        //loadCards is the only function we are mocking 
        //the value of loadCards is a function that returns undefined
        loadCards: () => undefined 
    }));

    //get the getInitialState function using require
    //put this AFTER THE MOCK, 
    //so now getInitialState will call the mock loadCards
    //and NOT THE REAL loadCards
    const { cards, getInitialState } = require("./index");
    
    const initialState = getInitialState();

    //because we set loadCards up to return undefined
    //getInitialState should return a CardState where the cards array is the default cards array
    expect(initialState.cards).toEqual(cards);
});

//initialState contains saved cards when saved cards returned from localStorage    
it('returns stored cards', () => {
    const mockCards = ['stored card', 'another stored card'];

    //See how we have a different return value?
    jest.mock('../../Save', () => ({ 
        loadCards: () => mockCards 
    }));

    const { getInitialState } = require("./index");
    
    const initialState = getInitialState();

    //getInitialState().cards should equal the return value we gave it
    expect(initialState.cards).toEqual(mockCards);
});

//current index should start at 0
it('starts current at 0', () => {
    const { getInitialState } = require('./index');
    
    const initialState = getInitialState();

    expect(initialState.current).toEqual(0);
})
```

##Write the CardContext Services Index
>File: src/services/CardContext/services/index.ts
>Will Match: [src/services/CardContext/services/complete/index-1.ts](https://github.com/jacobwicks/flashcard/blob/master/src/services/CardContext/services/complete/index-1.ts)

Start the services file with these imports:
```javascript
import { Card, CardState } from '../../../types';
import { loadCards } from '../../Save';
```
Remember, `loadCards` is the function that we mocked in our tests. We don't need to do anything special with it in this file to mock it in the tests.

Cut and paste `card1`, `card2`, and `cards` from `CardContext/index.tsx` to `CardContext/services/index.ts`.

```javascript
//declare a card object
const card1: Card = {
    question: 'What is a linked list?',
    subject: 'Linked List',
    answer: `A linked list is a sequential list of nodes. 
    The nodes hold data. 
    The nodes hold pointers that point to other nodes containing data.`
};

//declare another card object
const card2: Card = {
    question: 'What is a stack?',
    subject: 'Stack',
    answer: `A stack is a one ended linear data structure.
    The stack models real world situations by having two primary operations: Push and pop.
    Push adds an element to the stack.
    Pop pulls the top element off of the stack.`
}

//make an array with both cards
const cards = [card1, card2];
```

We are going to make a function `getInitialState` that returns the `initialState` object. We will declare a const `loadedCards` and assign it the return value of the `loadCards` function that gets the cards out of localStorage. If `loadedCards` is an array of cards then `getInitialState` will use it. If `loadedCards` is undefined then `getInitialState` will use cards, the array of example cards.

Mocking the `loadCards` function in the tests lets us control the return value of the `loadCards` function. That is how we test our `getInitialState` function.

```javascript
//loadedCards is the result of calling loadCards
//try to get saved cards from localStorage
const loadedCards = loadCards();

//a function that loads the cards from localStorage
//and returns a CardState object
export const getInitialState = () => ({
    //the cards that are displayed to the user
    //if loadedCards is undefined, use cards
    cards: loadedCards ? loadedCards : cards,

    //index of the currently displayed card
    current: 0,
    
    //placeholder for the dispatch function
    dispatch: (action:CardAction) => undefined
} as CardState);
```
##Import getInitialState into CardContext
>File: src/services/CardContext/index.tsx
>Will Match: [src/services/CardContext/complete/index-9.tsx](https://github.com/jacobwicks/flashcard/blob/master/src/services/CardContext/complete/index-9.tsx)

Import the `getInitialState` function from services:
```javascript
import { getInitialState } from './services/';
```
If any of these objects are still in CardContext, delete them:
* card1
* card2
* cards

Change the definition of `initialState` from:
```javascript
export const initialState: CardState = {
    current: 0,
    cards,
    dispatch: ({type}:{type:string}) => undefined,
};
```
to a call to `getInitialState`:
```javascript
export const initialState = getInitialState();
```
Instead of just declaring the `initialState` object in `CardContext`, we call the `getInitialState` function. `getInitialState` will try to load the cards from `localStorage`. If the cards load, `getInitialState` will return the `initialState` object with cards loaded from `localStorage`. If it receives *undefined*, it will return the example cards that we wrote.

![InitialState Pass](https://thepracticaldev.s3.amazonaws.com/i/8jtkfd8o7bp253o66vc1.png)

Those tests we wrote with the mocked `loadCards` function pass now!

Run the app. The cards will now load from `localStorage` when the app starts!

![Cards in LocalStorage](https://thepracticaldev.s3.amazonaws.com/i/7x7ab8ox4b95inxjx9p7.png)

Open the dev console. Click Application. Click localStorage. Click localhost:3000. These commands and menus may be different if you aren't using Chrome, or if you are using a different version of Chrome.

##Save Test 3: Save Stats
>File: src/services/Save/index.test.ts
>Will Match: [src/services/Save/complete/test-3.ts](https://github.com/jacobwicks/flashcard/blob/master/src/services/Save/complete/test-3.ts)

Import `saveStats`. 
```javascript
import { 
    saveCards, 
    loadCards, 
    saveStats
} from './index';
```

Make a describe block 'Saving and Loading Stats.'
```javascript
describe('Saving and Loading Stats', () => {
    let originalLocalStorage: Storage

	beforeEach(() => {
		originalLocalStorage = window.localStorage
	})

	afterEach(() => {
        (window as any).localStorage = originalLocalStorage
    })

//saving stats saves stats
//loading stats retrieves saved stats
//loading stats returns empty object if nothing found
});
```

Make some example stats, and stringify them.
```javascript
    const stats = {
        'Example Question': {
            right: 3,
            wrong: 2,
            skip: 1
        }
    };

    const stringStats = JSON.stringify(stats);

    //saving stats saves stats
```

Make the test for saving stats. Use `jest.spyOn` to mock the localStorage setItem.
```javascript
    //saving stats saves stats
    it('Saving stats saves stats', () => {

        const setItem = jest.spyOn(window.localStorage.__proto__, 'setItem');
        
        saveStats(stats);

        expect(setItem).toHaveBeenCalledWith("cards", stringStats);
    });
```

![Save Stats Fail](https://thepracticaldev.s3.amazonaws.com/i/wnr6a5eqc941hctltwpr.png)

##Pass Save Tests 3: Save Stats
>File: src/services/Save/index.ts
>Will Match: [src/services/Save/complete/index-3.ts](https://github.com/jacobwicks/flashcard/blob/master/src/services/Save/complete/index-3.ts)

Import `StatsType`.
```javascript
import { Card, StatsType } from '../../types';
```

The `saveStats` function is fairly simple.

```javascript
export const saveStats = (stats: StatsType) => {
  try {
    localStorage.setItem('stats', JSON.stringify(stats));
  } catch (err) {
    console.error(err);
  }
};
```

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/ydewr1xbcyy93ilnzo8v.png)

##Save Tests 4-5: Loading Stats
>File: src/services/Save/complete/index.test.ts
>Will Match:[src/services/Save/complete/test-4.ts] (https://github.com/jacobwicks/flashcard/blob/master/src/services/Save/complete/test-4.ts)


Import loadStats.
```javascript
import { 
    saveCards, 
    loadCards, 
    saveStats,
    loadStats
} from './index';
```

If there are stats in localStorage, loadStats should return a stats object.
```javascript
    //loading stats retrieves saved stats
    it('Loading stats returns saved stats object', () => {
        const mockGetItem = jest.fn().mockReturnValue(stringStats);
        
        const localStorageMock = {
            getItem: (params: any) => mockGetItem(params),
        } 
        
        Object.defineProperty(window, 'localStorage', {
            value: localStorageMock,
            writable: true
            })    

        const loadedStats = loadStats();
        
        expect(mockGetItem.mock.calls.length).toBe(1);
        expect(mockGetItem.mock.calls[0][0]).toBe('stats');
        expect(loadedStats).toStrictEqual(stats);
    });
```

`loadStats` should return an empty object (not undefined) if nothing is found in `localStorage`.
```javascript
    //loading stats returns empty object if nothing found
    it('Loading stats when no saved cards returns undefined', () => {
        const mockGetItem = jest.fn().mockReturnValue(undefined);
        
        const localStorageMock = {
            getItem: (params: any) => mockGetItem(params),
        } 

        Object.defineProperty(window, 'localStorage', {
            value: localStorageMock,
            writable: true
            })
            
        const loadedStats = loadStats();
        
        expect(mockGetItem.mock.calls.length).toBe(1);
        expect(mockGetItem.mock.calls[0][0]).toBe('stats');
        expect(loadedStats).toStrictEqual({});
    });
```

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/h031k54wo0uy01unj6db.png)


##Pass Save Tests 4-5: Loading Stats
>File: src/services/Save/complete/index.ts
>Will Match:[src/services/Save/complete/index-4.ts] (https://github.com/jacobwicks/flashcard/blob/master/src/services/Save/complete/index-4.ts)


```javascript
export const loadStats = () => {
  try {
    const stored = localStorage.getItem('stats');
    
    return stored 
      ?  JSON.parse(stored) as StatsType
      : {} as StatsType
  } catch (err) {
      console.error("couldn't get stats from localStorage");
      return {} as StatsType;
  }
};
```

![Load Stats Pass](https://thepracticaldev.s3.amazonaws.com/i/zvpqlmqrnzophhos3len.png)

#Add Saving to StatsContext
We are going to add saving and loading to StatsContext. 
* Write the tests
* Import the `saveStats` function into `StatsContext`
* Change the `StatsContext` provider so that it saves `stats` to `localStorage` when `stats` changes
* Change `getInitialState` to load saved `stats` from `localStorage` 

##StatsContext Tests 1-3: Saves Stats After Each Type of Action
>File: src/services/StatsContext/index.test.tsx
>Will Match: [src/services/StatsContext/complete/test-8.tsx](https://github.com/jacobwicks/flashcard/blob/master/src/services/StatsContext/complete/test-8.tsx)

Import the contents of `Save` as `localStorage`.
```javascript
import * as localStorage from '../Save';
import { Button } from 'semantic-ui-react';
```

###Write a comment for each test.

```javascript
//saves stats when stats changed
//stats is empty object when it does not get stats from localstorage
//initialState contains saved stats when saved stats are returned from localStorage
```

Make a describe block named 'saving to localStorage and loading from localStorage.' Make another describe block inside the first, called 'saving.'

```javascript
describe('saving to localStorage and loading from localStorage ', () => {
    //saves stats when stats changes    
    describe('saves stats when stats changes', () => {
    });

    //stats is empty object when it does not get stats from localstorage
    //initialState contains saved stats when saved stats are returned from localStorage

});
```
Declare a const `question`. This will be the question that we dispatch in stats actions.
Make a helper component `UpdateButtons` with three buttons that dispatch actions to `statsContext`.
Use `Object.values` and `Array.map` to turn the `StatsActionType` into an array of test parameters.

Run the tests with `test.each`.
 
```javascript
    describe('save', () => {        
        const question = 'Is this an example question?';
        
        const UpdateButtons = () => {
            const { dispatch } = useContext(StatsContext);
            const dispatchStat = (type: StatsActionType) => dispatch({type, question});

            return <div>
                <Button content='right' onClick={() => dispatchStat(StatsActionType.right)}/>
                <Button content='wrong' onClick={() => dispatchStat(StatsActionType.wrong)}/>
                <Button content='skip' onClick={() => dispatchStat(StatsActionType.skip)}/>
            </div>
        }

        const eachTest = Object.values(StatsActionType)
        .map(actionType => {
            //an object of type StatsState
            const result = { [question] : {
                ...blankStats,
                [actionType]: 1
            }}

            //return an array of arguments that it.each will turn into a test
            return [
                actionType,
                result
            ];
        });

        //pass the array eachTest to it.each to run tests using arguments
        test.each(eachTest)
        //printing the title from it.each uses 'printf syntax'
        ('%#: %s saves new stats', 
        //name the arguments, same order as in the array we generated
        (
            actionType, 
            result
            ) => {
            //test starts here            
            const saveStats = jest.spyOn(localStorage, 'saveStats');
            saveStats.mockClear();

            const { getByText } = render(
                <StatsProvider testState={{} as StatsState}>
                    <UpdateButtons />
                </StatsProvider>);

            expect(saveStats).toHaveBeenCalledTimes(1);
            expect(saveStats).toHaveBeenCalledWith({});
            
            const regex = new RegExp(actionType as StatsActionType);
            const button = getByText(regex);
            fireEvent.click(button);

            expect(saveStats).toHaveBeenCalledTimes(2);
            expect(saveStats).toHaveBeenLastCalledWith(result);
            
        });
    });
```
![StatsContext Save Fail](https://thepracticaldev.s3.amazonaws.com/i/8448p9jnrf6106yl08zo.png)

##Pass StatsContext Tests 1-3: Saves Stats After Each Type of Action
>File: src/services/StatsContext/index.tsx
>Will Match: [src/services/StatsContext/complete/index-4.tsx](https://github.com/jacobwicks/flashcard/blob/master/src/services/StatsContext/complete/index-5.tsx)

Import `useEffect`.
```javascript
import React, { createContext, useEffect, useReducer } from 'react';
```

Import `saveStats`.
```javascript
import { saveStats } from '../Save';
```

Add the `useEffect` to save `stats` whenever state changes.
```javascript
    const [state, dispatch] = useReducer(reducer, testState ? testState : initialState);
    
    useEffect(() => {
        saveStats(state);
    }, [state])
    
    const value = {...state, dispatch} as StatsState;
```

![StatsContext Save Pass](https://thepracticaldev.s3.amazonaws.com/i/iv9fbqcj0hpt3ssffn4f.png)

##StatsContext Test 4: Loading Stats from LocalStorage
>File: src/services/StatsContext/index.test.tsx
>Will Match: [src/services/StatsContext/complete/test-9.tsx](https://github.com/jacobwicks/flashcard/blob/master/src/services/StatsContext/complete/test-9.tsx)

Change Imports.
 ```javascript
import React, { useContext} from 'react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { Stats, StatsActionType, StatsState } from '../../types';
import { Button } from 'semantic-ui-react';

jest.mock('../Save', () => ({
    saveStats: jest.fn(),
    loadStats: () => ({})
}));

const { 
    blankStats, 
    initialState, 
    reducer, 
    StatsContext,
    StatsProvider 
} = require('./index');
```

Write test. Use `jest.spyOn` to mock `loadStats`.
```javascript
    describe('load', () => {
        //stats is empty object when it does not get stats from localstorage
        it('gets default initialState when no stats in localstorage', () => {        
            expect(initialState).toHaveProperty('dispatch');
            expect(Object.keys(initialState).length).toEqual(1);
        });

        //loading stats retrieves saved stats
        it('loads stats from localStorage when there are stats in localStorage', () => {
            const localStorage = require('../Save'); 
            const loadStats = jest.spyOn(localStorage, 'loadStats');

            loadStats.mockImplementation(() => ({
                'Example Question': {
                    right: 1,
                    wrong: 2,
                    skip: 3
                }
            }));

            const { getInitialState } = require('./index');
            const initialState = getInitialState();

            expect(initialState).toHaveProperty('dispatch');
            expect(initialState).toHaveProperty('Example Question');
            expect(Object.keys(initialState).length).toEqual(2);
        })
    })
```
`initialState` is already the default state, so the first test passes.
![StatsContext Load Fail](https://thepracticaldev.s3.amazonaws.com/i/ymo6m0kpggqoqt8fgdob.png)

##Pass StatsContext Test 4: Loading Stats from LocalStorage
>File: src/services/StatsContext/index.tsx
>Will Match: [src/services/StatsContext/complete/index-6.tsx](https://github.com/jacobwicks/flashcard/blob/master/src/services/StatsContext/complete/index-6.tsx)

Import `loadStats`.
```javascript
import { loadStats, saveStats } from '../Save';
```

Make a `getInitialState` function. Use the spread operator to add the result of `loadStats`. Remember, `loadStats` will just return an empty object if there's an error. 
```javascript
//getInitialState is a function that returns a StatsState object
export const getInitialState = () => ({
    //spread the return value of the loadStats function
    ...loadStats(),
    dispatch: (action: StatsAction) => undefined
//tell TypeScript it is a StatsState object
} as StatsState);

//the object that we use to make the first Context
export const initialState = getInitialState();
```

![StatsContext Pass](https://thepracticaldev.s3.amazonaws.com/i/lf0setdetpr9i6sh4eyb.png)

Ok, now stats will be saved in between sessions!

![Stats in localStorage](https://thepracticaldev.s3.amazonaws.com/i/s7nm5v294hdlona4xsw1.png)

####Next Post: The Selector