How to use Jest for testing a react component with localStorage?
Asked Answered
W

2

9

I have a component that calls to local storage and want to test it with jestJS. As far as I can tell jest does not support calls to localStorage.

This is the component that I need to have tests for:

const NavBar: React.FC = () => {
  const history = useHistory();

  const handleCLick = () => {
    localStorage.clear();
    history.push('/login');
  };

  return (
    <div>
      <header>
        <div className="banner">
          <div className="container">
            <img
              className="icon "
              alt="icon"
              title="icon"
              src={favicon57}
            />
            <p>Official website of the Stuff</p>
          </div>
        </div>
        <nav className="navbar navbar-expand-md navbar-dark fixed-top">
          <div className="container">
            <div className="navbar-header">
              <img
                className="logo "
                alt="logo"
                title="Logo"
                src={Blah}
              />
            </div>
            <button
              className="navbar-toggler"
              type="button"
              data-toggle="collapse"
              data-target="#navbarCollapse"
              aria-controls="navbarCollapse"
              aria-expanded="false"
              aria-label="Toggle navigation"
            >
              <span className="navbar-toggler-icon" />
            </button>
            <div className="collapse navbar-collapse" id="navbarCollapse">
              <ul className="navbar-nav ml-auto">
                {isTokenAdmin() ? (
                  <li className="nav-item">
                    <a id="nav-users" className="nav-link" href={ADMIN_URL}>
                      View Users
                    </a>
                  </li>
                ) : (
                  <div> </div>
                )}
                {isTokenActive() ? (
                  <li className="nav-item">
                    <a id="nav-log-out" className="nav-link" href={APP_URL}>
                      Locations
                    </a>
                  </li>
                ) : (
                  <div> </div>
                )}
                {isTokenActive() ? (
                  <li className="nav-item">
                    <a
                      id="nav-log-out"
                      className="nav-link"
                      href={LOGIN_URL}
                      onClick={() => {
                        handleCLick();
                      }}
                    >
                      Logout
                    </a>
                  </li>
                ) : (
                  <div> </div>
                )}
              </ul>
            </div>
          </div>
        </nav>
      </header>
    </div>
  );
};

export default NavBar;

As you can see I am rendering the buttons based off of the token that I have stored in localStorage. How would you get this to 100% test coverage?

EDIT:

The code for the functions to get the token are:

export const isTokenActive = (): boolean => {
  const userToken: string | null = localStorage.getItem('exp');
  if (typeof userToken === 'string') {
    return new Date().getTime() < Number.parseInt(userToken, 10);
  }
  return false;
};

export const isTokenAdmin = (): boolean => {
  const userToken: string | null = localStorage.getItem('access_token');
  if (typeof userToken === 'string') {
    const decodedToken: TokenDetails = jwt_decode(userToken);
    return decodedToken.authorities[0] === 'ROLE_Administrator';
  }
  return false;
};
Wilcox answered 14/12, 2020 at 0:26 Comment(3)
Please provide the code of isTokenAdmin, isTokenActive functions.Buoy
It's because you are trying to access localStorage directly from the browser as a global variable. You need to either declare it in your globals or mock it (some also use jsdom to mock the window object)Phlogistic
Just edited the question to include the code for isTokenAdmin, isTokenActiveWilcox
N
15

You are correct that Jest does not support calls to localStorage. It is not a browser and doesn't implement localStorage.

The solution is to mock your own fake localStorage support like below:

browserMocks.js

const localStorageMock = (function() {
  let store = {}

  return {
    getItem: function(key) {
      return store[key] || null
    },
    setItem: function(key, value) {
      store[key] = value.toString()
    },
    removeItem: function(key) {
      delete store[key]
    },
    clear: function() {
      store = {}
    }
  }
})()

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

Jest config (can be inside your package.json)

  "jest": {
    "setupFiles": [
      "<rootDir>/__jest__/browserMocks.js",

Then to see if localstorage has been called you can spy on it like this:

describe('signOutUser', () => {
  it('should sign out a user', async () => {
    const spyLoStoRemove = jest.spyOn(localStorage, 'removeItem')

    await signOutUser()
    
    expect(spyLoStoRemove).toHaveBeenCalled()
    expect(spyLoStoRemove).toHaveBeenCalledTimes(2)
  })
})
Notum answered 14/12, 2020 at 9:22 Comment(0)
P
5

Mocking in the way @Jonathan Irwin does work in most cases but not always as it doesn't truly imitate how local storage is structured.

It won't work e.g when you use in operator e.g.

if (!('MY_KEY' in localStorage)) {
  return 'something'
}

It won't work as the original local storage instance stores keys on itself and methods come from its Storage prototype, this behaviour simulates classical inheritance, but it is really more of delegation than inheritance, we can verify that in e.g Chrome console: enter image description here prototype part: enter image description here

In @Jonathan Irwin mock we don't store value-key pairs on local storage instance but rather in separate, inner closure object hence we can't use it in a similar way as the original local storage

Improved version:

const storagePrototype = {
  getItem: function (key) {
    return localStorageMock[key] || null;
  },
  setItem: function (key, value) {
    if (!localStorageMock[key]) {
        this.length++;
    }
    localStorageMock[key] = value.toString();
  },
  removeItem: function (key) {
    if (localStorageMock[key]) {
        this.length--;
    }
    delete localStorageMock[key];
  },
  clear: function () {
    Object.keys(localStorageMock).forEach(
      (key) => delete localStorageMock[key]
    );
    this.length = 0;
  },
  length: 0,
};

export const localStorageMock = Object.create(storagePrototype);

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

Class-based version:

class Storage {
  constructor() {
    this.length = 0;
  }
  getItem(key) {
    return this[key] || null;
  }
  setItem(key, value) {
    if (!this[key]) {
      this.length++;
    }
    this[key] = value.toString();
  }
  removeItem(key) {
    if (this[key]) {
      this.length--;
    }
    delete this[key];
  }
  clear() {
    Object.keys(this).forEach((key) => delete this[key]);
    this.length = 0;
  }
}

export const localStorageMock = Object.create(new Storage());
Pickpocket answered 14/3, 2023 at 19:55 Comment(2)
Nice! This looks goodNotum
This works for me in a create-react-app app after importing the localStorageMock implementation above into the file "src/setupTests.js", and then adding this line to "src/setupTests.js": Object.defineProperty(window, 'localStorage', { value: localStorageMock});Clientele

© 2022 - 2024 — McMap. All rights reserved.