Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

localstorage doesn't work within jsdom #1137

Closed
alvarorahul opened this issue May 28, 2015 · 30 comments · Fixed by #2076
Closed

localstorage doesn't work within jsdom #1137

alvarorahul opened this issue May 28, 2015 · 30 comments · Fixed by #2076

Comments

@alvarorahul
Copy link
Contributor

Hey folks,

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

Regards,
Alvaro

@domenic
Copy link
Member

domenic commented May 28, 2015

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.

@alvarorahul
Copy link
Contributor Author

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?

@Sebmaster
Copy link
Member

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.

@mnahkies
Copy link

mnahkies commented Sep 2, 2015

@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.

@Sebmaster
Copy link
Member

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.

@mnahkies
Copy link

mnahkies commented Sep 3, 2015

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.

@Joris-van-der-Wel
Copy link
Member

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

@justinmchase
Copy link
Contributor

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?

@mnahkies
Copy link

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

@justinmchase
Copy link
Contributor

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

@mnahkies
Copy link

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

@avesus
Copy link

avesus commented Dec 23, 2015

+1

1 similar comment
@eudisd
Copy link

eudisd commented Jan 9, 2016

+1

@alvarorahul
Copy link
Contributor Author

perhaps something like this would work https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage ?

@mnahkies
Copy link

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

@justinmchase
Copy link
Contributor

@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.

@just-boris
Copy link

+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)

@adjavaherian
Copy link

@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.

@mnahkies
Copy link

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.

@domenic
Copy link
Member

domenic commented Apr 30, 2016

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.

@alexedev
Copy link

+1

@ianva
Copy link

ianva commented Jul 20, 2016

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;

@FullStackForger
Copy link

FullStackForger commented Oct 28, 2016

@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.

@domenic
Copy link
Member

domenic commented Aug 21, 2017

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?)

@Zirro
Copy link
Member

Zirro commented Dec 7, 2017

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

@simoami
Copy link

simoami commented Apr 2, 2018

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')) || [];

@davidjb
Copy link

davidjb commented Apr 3, 2018

@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.

@rkurbatov
Copy link

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

@kaansoral
Copy link

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

@Spixmaster
Copy link

For me, I needed:

const dom: JSDOM = new JSDOM("...");
global.localStorage = dom.window.localStorage;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.