jest: jsdom update 5 days ago breaks back compatability

🐛 Bug Report

jest.spyOn(localStorage, “setItem”); doesn’t work;

jest-environment-jsdom has caret dependency on jsdom -> 11.5.1, jsdom latest version is 11.12.0 and it includes localStorage and sessionStorage. Previously we could mock localStorage easily but now we don’t have that option and we need to use the default localStorage implementation. So our option is to spy on it which doesn’t work because it throws TypeError: object[methodName].mockImplementation is not a function

What I have discovered is that in ./node_modules/jsdom/lib/jsdom/living/generated/Storage.js the issue is with the set method of the Proxy

Currently the code below is on line 278.

if (ownDesc === undefined) {
          const parent = Reflect.getPrototypeOf(target);
          if (parent !== null) {
            return Reflect.set(parent, P, V, receiver);
          }
          ownDesc = { writable: true, enumerable: true, configurable: true, value: undefined };
        }

if we remove receiver from return Reflect.set(parent, P, V, receiver); we will be able to spy on it. But I guess that’s coming from webidl converter

To Reproduce

Steps to reproduce the behavior:

  1. Install jest.
  2. Write a simple class that leverage localStorage.setItem() or localStorage.getItem()
  3. Try to spy on it -> jest.spyOn(localStorage, “setItem”); and it will throw an error

Expected behavior

The method should be available for spying.

Link to repl or repo (highly encouraged)

https://github.com/vlad0/jest-localstorage-issue

Run npx envinfo --preset jest

  1. npm install
  2. npm test Paste the results here:
> jest

 FAIL  ./service.spec.js
  Service
    ✕ Service set value (7ms)

  ● Service › Service set value

    TypeError: object[methodName].mockImplementation is not a function

       8 |
       9 |     it('Service set value', () => {
    > 10 |         jest.spyOn(localStorage, "setItem");
      11 |         service.setValue("hello", "world")
      12 |
      13 |         expect(localStorage.setItem).toHaveBeenCalled();

      at ModuleMockerClass.spyOn (node_modules/jest-mock/build/index.js:597:26)
      at Object.it (service.spec.js:10:14)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.785s, estimated 1s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 22
  • Comments: 23 (7 by maintainers)

Commits related to this issue

Most upvoted comments

We found out that localStorage from Jest is an instance of a class called Storage provided by jsdom. And using spyOn this way works :

jest.spyOn(Storage.prototype, 'setItem');

jest.spyOn(window.localStorage.__proto__, 'setItem'); should work without any additional imports.

@SimenB , to be honest I was really confused where to raise this issue but since jest-environment-jsdom is part of this repo and it has caret dependency to jsdom which causes the issue I decided to give it a try here.

Let me give another perspective:

Imagine I am a new Jest user and want to spy on localStorage - What are our options to spy on localStorage with the latest version of Jest? - after all it downloads the latest version of jsdom which can’t be spied on or mocked.

There are lots of example with mocking localStorage before jsdom implementation but no solution after the lastest jsdom update.

UPDATE: jsdom team suggests the fix should come from jest 😃 so we seem to be in the limbo 😃 https://github.com/jsdom/jsdom/issues/2318

Thanks @mayank23, you made my day!

An example for those who would like to return a specific value according to the test case:

// file.js
const funcToTest = () => {
  const { localStorage } = window;
  return JSON.parse(localStorage.getItem('toto'));
};

export { funcToTest };
// file.test.js
import mockWindowProperty from './mock-window-property';
import { funcToTest } from './file';

describe('funcToTest', () => {
  let mockGetItem;

  mockWindowProperty('localStorage', {
    getItem: params => mockGetItem(params),
  });

  beforeEach(() => {
    mockGetItem = jest.fn().mockReturnValue(null);
  });

  it('Should do something with id', (done) => {
    mockGetItem = jest.fn().mockReturnValue('{"id": "1"}');
    const res = funcToTest();
    expect(mockGetItem.mock.calls.length).toBe(1);
    expect(mockGetItem.mock.calls[0][0]).toBe('toto');
    expect(res).toStrictEqual({ id: 1 });
    // ...
  });
});

Here’s a utility function for mocking any window property with automatic cleanup:

https://gist.github.com/mayank23/7b994385eb030f1efb7075c4f1f6ac4c

Here’s a solution I adapted from a similar problem mocking window.location https://github.com/facebook/jest/issues/5124

describe("sessionStorage", ()=>{
  let originalSessionStorage;

  beforeAll(()=>{
    originalSessionStorage = window.sessionStorage;
    delete window.sessionStorage;
    Object.defineProperty(window, "sessionStorage", {
      writable: true,
      value: {
        getItem: jest.fn().mockName("getItem"),
        setItem: jest.fn().mockName("setItem")
      }
    });
  });

  beforeEach(()=>{
    sessionStorage.getItem.mockClear();
    sessionStorage.setItem.mockClear();
  });

  afterAll(()=>{
    Object.defineProperty(window, "sessionStorage", {writable: true, value: originalSessionStorage});
  });

  it("calls getItem", ()=>{
    sessionStorage.getItem("abc");

    expect(sessionStorage.getItem).toHaveBeenCalledWith("abc");
  });
});

@mr-wildcard Thanks a lot for the workaround ! How would you distinguish between local and sessionStorage though ? I’d like to check that my data is saved in the right storage area in my tests.