Jsdom: localstorage doesn't work within jsdom

Created on 28 May 2015  ·  29Comments  ·  Source: jsdom/jsdom

Hey folks,

Is somebody working on implementing localStorage/sessionStorage APIs in jsdom?

Regards,
Alvaro

best-fixed-by-webidl2js feature html living standard

Most helpful comment

@justinmchase 's solution is great for testing. Might want to add sessionStorage too.

    var jsdom = require('jsdom').jsdom;
    document = jsdom('hello world');
    window = document.defaultView;
    navigator = window.navigator;
    window.localStorage = window.sessionStorage = {
        getItem: function (key) {
            return this[key];
        },
        setItem: function (key, value) {
            this[key] = value;
        }
    };

FAIK this mocks well. Fixes some of my tests. Thanks.

All 29 comments

These are unfortunately quite hard without ES2015 proxies :(. We could get getItem/setItem/etc. working, but having property modifications apply correctly is not really possible.

It would be nice to have at least an in-memory experience i.e. localStorage having behavior similar to sessionStorage
that should be less complicated no?

No, because we can't emulate the API well enough. For simple use-cases (i.e. only using getItem/setItem) it might work, but as soon as properties are accessed directly, our implementation will break down.

@Sebmaster: I wrote a simple shim for the storage interface for use in my unit tests for a separate package.

https://github.com/mnahkies/node-storage-shim

It's currently a transient solution, and forbids the setting of keys that clash with the method names of the storage interface. It also doesn't currently implement the event firing portions of the spec.

Real localStorage does allow the setting of these keys using setItem, but using property access does blow away the function definitions, and once set you can't access those keys using property access either.

However aside from these limitations I think it's a pretty faithful implementation:
https://github.com/mnahkies/node-storage-shim/blob/master/test.js

Could you elaborate on what aspects of the API can't be emulated well enough?

As far as I can tell the main thing that isn't currently possible to emulate is the storage event firing in response to property setting.

I would be happy to have a go at integrating it with jsdom if you thought it was complete enough with the mentioned caveat's.

Could you elaborate on what aspects of the API can't be emulated well enough?

localStorage supports setting any property name directly on the interface as in

localStorage.myKey = "myVal";
localStorage.getItem('myKey') // 'myVal'

localStorage.setItem('otherKey', 'val')
localStorage.otherKey // 'val'

which we could emulate somewhat by always creating a getter if setItem is called, but we'll not be able to support the other way around (setting arbitary properties on the object) without proxies.

Right, I guess the main issue with regards to setting keys by property access is that we can't coerce the values to strings correctly, as the real interface does.

Yes that is a very important aspect of local storage. We need ES6 Proxy for this, v8 has not implemented it yet (microsoft and mozilla already have it!)

We are hitting this same road block in multiple locations in jsdom

I don't understand why this wouldn't work:

var localStorage = {
  getItem: function (key) {
    return this[key];
  },
  setItem: function (key, value) {
    this[key] = value;
  }
};

localStorage.setItem('foo', 'bar');
localStorage.bar = 'foo'
assert(localStorage.foo === 'bar')
assert(localStorage.getItem('bar') === 'foo')

What am I missing?

When you set items using property access, the real localStorage always coerces the value to a string.

Eg:

localStorage.foo = 35
assert(typeof localStorage.foo === "string")

localStorage.foo = {my: 'object'}
assert(localStorage.foo === "[object Object]")

This is not possible without ES6 proxy, and is an important charactoristic of localStorage

I see. Though if you're doing that shame on you ;)

I think more importantly if you are doing that with objects it is probably a bug that would be masked by jsdom and then occur in the browser

+1

+1

I think you'll find that you are still missing some of the behavior that the real local storage has as explained above.

The linked solution makes an good polyfill but I don't think it would be a faithful enough implementation for jsdom

@mnahkies Honestly, the polyfill is better than nothing (which is what you have now). You should just put it in and add some documentation on the limitations which 99% of people will never hit anyway.

+1 to the last comment, example on MDN uses setItem and getItem as accessors to storage. So it can be considered as a common use case that we are really missing.

For now, I solve it with jasmine.createSpy (because I work with Jasmine, other spy libs also can do it)

@justinmchase 's solution is great for testing. Might want to add sessionStorage too.

    var jsdom = require('jsdom').jsdom;
    document = jsdom('hello world');
    window = document.defaultView;
    navigator = window.navigator;
    window.localStorage = window.sessionStorage = {
        getItem: function (key) {
            return this[key];
        },
        setItem: function (key, value) {
            this[key] = value;
        }
    };

FAIK this mocks well. Fixes some of my tests. Thanks.

It fixs your tests until someone accidentally writes an object to local storage using property access and creates a subtle bug.

In my opinion you'd be better off having an object that abstracts storage with a swappable storage backend that you can operate with an in memory solution in your tests.

This also makes it easier to do more complex things like encryption, compression, write through caching etc if you require it at a later date.

It's time!!!

Node.js v6 is out, and with it... PROXIES.

@Sebmaster, would you be up for updating webidl2js to be able to generate proxies when named getters/setters/deleters are present? I think we should target this request first, before worrying about other stuff like NamedNodeMap or NodeList or whatever. This is nice and self-contained and won't impact performance of core primitives.

https://html.spec.whatwg.org/multipage/webstorage.html#storage-2 has the IDL that we need to support. I think the way to go would be to simply make the proxy's get behavior delegate to the impl's getItem, etc.

+1

You can use node-localstorage

var LocalStorage = require('node-localstorage').LocalStorage;
global.localStorage = new LocalStorage('./build/localStorageTemp');

global.document = jsdom('');

global.window = document.defaultView;
global.window.localStorage = global.localStorage;

@adjavaherian @justinmchase I was playing around with jsdom which i use for testing in both react-jwt-auth and react-jwt-auth-redux and it works great. However, I have been also working on enverse - environment checks for isomorphic development. One of the checks is localStorage and sessionStorage. I have run exactly to the same problem of how to test it properly and your polyfil works great. Thank you guys! It would be great if it comes with jsdom by default.

webidl2js support for this is now in place. If people want to take this on, I'd suggest studying the DOMStringList implementation (for dataset) that recently landed.

For now we can keep everything in-memory, although eventually we should provide a way of storing to disk if people want that. (Do people want that?)

I'd like to work on implementing this. You can assign the issue to me.

this mock works better because when a key isn't stored, localStorage.getItem() is supposed to return null, not undefined:

const jsdom = require('jsdom');
// setup the simplest document possible
const doc = jsdom.jsdom('<!doctype html><html><body></body></html>');;

// get the window object out of the document
const win = doc.defaultView;

win.localStorage = win.sessionStorage = {
  getItem: function(key) {
    const value = this[key];
    return typeof value === 'undefined' ? null : value;
  },
  setItem: function (key, value) {
    this[key] = value;
  },
  removeItem: function(key) {
    return delete this[key]
  }
};

// set globals for mocha that make access to document and window feel
// natural in the test environment
global.document = doc;
global.window = win;

Why is it relevant? The line below works fine in a browser environment. json.parse() fails if passed undefined as argument, but works fine with null as parameter:

let users = JSON.parse(localStorage.getItem('users')) || [];

@simoami Just watch out with your assignment chaining with win.localStorage = win.sessionStorage = { ... }. It's the same object reference assigned to both variables so calling either's get/set functions will access the same underlying object. Eg calling localStorage.set('foo', 'bar') means that sessionStorage.get('foo') will work -- this might be okay for simple tests, but will mess up anything that requires separate storage.

https://gist.github.com/rkurbatov/17468b2ade459a7498c8209800287a03 - we use this polyfill for both local/session storages. It's based on https://github.com/capaj/localstorage-polyfill by @capaj

Those who stumble onto this thread later on, after the recent jsdom improvements you need to set window._localStorage to your own mocking storage

As a feedback, I couldn't find the native localStorage events mentioned as a solution to this problem

And as a heads up to anyone who uses jsdom in parallel and use localStorage heavily, the node-localstorage etc. are pretty much useless, you might as well re-invent the wheel, they are not designed for parallel usage, also basic things like for(var key in localStorage) or Object.keys(localStorage) etc. doesn't work either

Was this page helpful?
0 / 5 - 0 ratings