Passing a variable between plugins in Rollup - javascript

How can I pass a variable between plugins in Rollup?
What I've tried:
// plugin-a.js
const pluginA = () => {
return {
name: 'pluginA',
async options(options) {
options.define = options.define || {};
options.define['foo'] = 'bar';
}
}
}
// plugin-b.js
const pluginB = (options = {}) => {
return {
name: 'pluginB',
buildStart: async (options) => {
console.log(options)
}
}
}
I'm getting a warning:
(!) You have passed an unrecognized option
Unknown input options: define. Allowed options: acorn, acornInjectPlugins, cache, context, experimentalCacheExpiry, external, inlineDynamicImports, input, makeAbsoluteExternalsRelative, manualChunks, maxParallelFileOps, maxParallelFileReads, moduleContext, onwarn, perf, plugins, preserveEntrySignatures, preserveModules, preserveSymlinks, shimMissingExports, strictDeprecations, treeshake, watch

It seems passing data should be done by what Rollup refers to as Direct plugin communication. This is working for me. I feel this is very hard coupled though.
function parentPlugin() {
return {
name: 'parent',
api: {
//...methods and properties exposed for other plugins
doSomething(...args) {
// do something interesting
}
}
// ...plugin hooks
};
}
function dependentPlugin() {
let parentApi;
return {
name: 'dependent',
buildStart({ plugins }) {
const parentName = 'parent';
const parentPlugin = plugins.find(plugin => plugin.name === parentName);
if (!parentPlugin) {
// or handle this silently if it is optional
throw new Error(`This plugin depends on the "${parentName}" plugin.`);
}
// now you can access the API methods in subsequent hooks
parentApi = parentPlugin.api;
},
transform(code, id) {
if (thereIsAReasonToDoSomething(id)) {
parentApi.doSomething(id);
}
}
};
}
There's also Custom module meta-data, however when I read the meta I always get null.

Related

Sending some but not all args to a function without defining nulls

I'm using vue 3 and composable files for sharing some functions through my whole app.
My usePluck.js composable file looks like
import { api } from 'boot/axios'
export default function usePlucks() {
const pluckUsers = ({val = null, excludeIds = null}) => api.get('/users/pluck', { params: { search: val, exclude_ids: excludeIds }})
return {
pluckUsers
}
}
In order to make use of this function in my component I do
<script>
import usePlucks from 'composables/usePlucks.js'
export default {
name: 'Test',
setup() {
const { pluckUsers } = usePlucks()
onBeforeMount(() => {
pluckUsers({excludeIds: [props.id]})
})
return {
}
}
}
</script>
So far so good, but now I'd like to even be able to not send any args to the function
onBeforeMount(() => {
pluckUsers()
})
But when I do that, I get
Uncaught TypeError: Cannot read properties of undefined (reading 'val')
I assume it's because I'm not sending an object as argument to the function, therefore I'm trying to read val from a null value: null.val
What I'm looking for is a way to send, none, only one, or all arguments to the function with no need to set null values:
// I don't want this
pluckUsers({})
// Nor this
pluckUsers({val: null, excludeIds: [props.id]})
I just want to send only needed args.
Any advice about any other approach will be appreciated.
import { api } from 'boot/axios'
export default function usePlucks() {
const pluckUsers = ({val = null, excludeIds = null} = {}) => api.get('/users/pluck', { params: { search: val, exclude_ids: excludeIds }})
return {
pluckUsers
}
}
I believe this is what you're looking for. The { ... } = {}
EDIT: It didn't work without this because with no argument the destructuring failed because it can't match an object. That's why you also need a default value on the parameter object, also called simulated named parameter.

How to use an async function as the second parameter to jest.mock?

I need to use jest.mock together with async, await and import() so that I can use a function from another file inside the mocked module. Otherwise I must copy and paste a few hundreds of slocs or over 1000 slocs, or probably it is not even possible.
An example
This does work well:
jest.mock('./myLin.jsx', () => {
return {
abc: 967,
}
});
Everywhere I use abc later it has 967 as its value, which is different than the original one.
This does not work:
jest.mock('./myLin.jsx', async () => {
return {
abc: 967,
}
});
abc seems to not be available.
Actual issue
I need async to be able to do this:
jest.mock('~/config', async () => {
const { blockTagDeserializer } = await import(
'../editor/deserialize' // or 'volto-slate/editor/deserialize'
);
// … here return an object which contains a call to
// blockTagDeserializer declared above; if I can't do this
// I cannot use blockTagDeserializer since it is outside of
// the scope of this function
}
Actual results
I get errors like:
TypeError: Cannot destructure property 'slate' of '((cov_1viq84mfum.s[13]++) , _config.settings)' as it is undefined.
where _config, I think, is the ~/config module object and slate is a property that should be available on _config.settings.
Expected results
No error, blockTagDeserializer works in the mocked module and the unit test is passed.
The unit test code
The code below is a newer not-working code based on this file on GitHub.
import React from 'react';
import renderer from 'react-test-renderer';
import WysiwygWidget from './WysiwygWidget';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-intl-redux';
const mockStore = configureStore();
global.__SERVER__ = true; // eslint-disable-line no-underscore-dangle
global.__CLIENT__ = false; // eslint-disable-line no-underscore-dangle
jest.mock('~/config', async () => {
const { blockTagDeserializer } = await import(
'../editor/deserialize' // or 'volto-slate/editor/deserialize'
);
const createEmptyParagraph = () => {
return {
type: 'p',
children: [{ text: '' }],
};
};
return {
settings: {
supportedLanguages: [],
slate: {
elements: {
default: ({ attributes, children }) => (
<p {...attributes}>{children}</p>
),
strong: ({ children }) => {
return <strong>{children}</strong>;
},
},
leafs: {},
defaultBlockType: 'p',
textblockExtensions: [],
extensions: [
(editor) => {
editor.htmlTagsToSlate = {
STRONG: blockTagDeserializer('strong'),
};
return editor;
},
],
defaultValue: () => {
return [createEmptyParagraph()];
},
},
},
};
});
window.getSelection = () => ({});
test('renders a WysiwygWidget component', () => {
const store = mockStore({
intl: {
locale: 'en',
messages: {},
},
});
const component = renderer.create(
<Provider store={store}>
<WysiwygWidget
id="qwertyu"
title="My Widget"
description="My little description."
required={true}
value={{ data: 'abc <strong>def</strong>' }}
onChange={(id, data) => {
// console.log('changed', data.data);
// setHtml(data.data);
}}
/>
</Provider>,
);
const json = component.toJSON();
expect(json).toMatchSnapshot();
});
What I've tried
The code snippets above show partially what I have tried.
I searched the web for 'jest mock async await import' and did not found something relevant.
The question
If jest.mock is not made to work with async, what else can I do to make my unit test work?
Update 1
In the last snippet of code above, the line
STRONG: blockTagDeserializer('strong'),
uses blockTagDeserializer defined here which uses deserializeChildren, createEmptyParagraph (which is imported from another module), normalizeBlockNodes (which is imported from another module) and jsx (which is imported from another module) functions, which use deserialize which uses isWhitespace which is imported from another module and typeDeserialize which uses jsx and deserializeChildren.
Without using await import(...) syntax how can I fully mock the module so that my unit test works?
If you want to dig into our code, please note that the volto-slate/ prefix in the import statements is for the src/ folder in the repo.
Thank you.
I'd advise not doing any "heavy" stuff (whatever that means) in a callback of jest.mock, – it is designed only for mocking values.
Given your specific example, I'd just put whatever the output of blockTagDeserializer('strong') right inside the config:
jest.mock('~/config', () => {
// ...
extensions: [
(editor) => {
editor.htmlTagsToSlate = {
STRONG: ({ children }) => <strong>{children}</strong>, // or whatever this function actually returns for 'strong'
};
return editor;
},
],
// ...
});
This doesn't require anything asynchronous to be done.
If you need this setup to be present in a lot of files, extracting it in a setup file seems to be the next best thing.
I found a solution. I have a ref callback that sets the htmlTagsToSlate property of the editor in the actual code of the module, conditioned by global.__JEST__ which is defined as true in Jest command line usage:
import { htmlTagsToSlate } from 'volto-slate/editor/config';
[...]
testingEditorRef={(val) => {
ref.current = val;
if (val && global.__JEST__) {
val.htmlTagsToSlate = { ...htmlTagsToSlate };
}
}}
Now the jest.mock call for ~/config is simple, there is no need to do an import in it.
I also use this function:
const handleEditorRef = (editor, ref) => {
if (typeof ref === 'function') {
ref(editor);
} else if (typeof ref === 'object') {
ref.current = editor;
}
return editor;
};

How can I evaluate Python code in the document context from JavaScript in JupyterLab?

With Jupyter Notebooks, I could have a cell
%%javascript IPython.notebook.kernel.execute('x = 42')
Then, elsewhere in the document a Python code cell with x would show it bound to 42 as expected.
I'm trying to produce something similar with JupyterLab. I understand I'm supposed to write a plugin rather than using ad-hoc JS, and that's fine, but I'm not finding an interface to the kernel similar to the global IPython from notebooks:
import { JupyerLab, JupyterLabPlugin } from '#jupyterlab/application';
const extension: JupyterLabPlugin<void> = {
// ...
requires: [],
activate: (app: JupyterLab) => {
// can I get to Python evaluation through app?
// by adding another class to `requires` above?
}
}
export default extension;
Here's a hacky attempt that "works". Could still use advice if anyone knows where there is a public promise for the kernel being ready, how to avoid the intermediate class, or any other general improvements:
import { JupyterLab, JupyterLabPlugin } from '#jupyterlab/application';
import { DocumentRegistry } from '#jupyterlab/docregistry';
import { INotebookModel, NotebookPanel } from '#jupyterlab/notebook';
import { IDisposable, DisposableDelegate } from '#phosphor/disposable';
declare global {
interface Window {
'execPython': {
'readyState': string,
'exec': (code: string) => any,
'ready': Promise<void>
} | null
}
}
class ExecWidgetExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
createNew(nb: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {
if (window.execPython) {
return;
}
window.execPython = {
'readyState': 'waiting',
'exec': null,
'ready': new Promise((resolve) => {
const wait = setInterval(() => {
if (!context.session.kernel || window.execPython.readyState === 'ready') {
return;
}
clearInterval(wait);
window.execPython.readyState = 'ready';
window.execPython.exec = (code: string) =>
context.session.kernel.requestExecute({ code }, true);
resolve();
}, 50);
})
};
// Usage elsewhere: execPython.ready.then(() => execPython.exec('x = 42').done.then(console.log, console.error))
return new DisposableDelegate(() => {});
}
}
const extension: JupyterLabPlugin<void> = {
'id': 'jupyterlab_foo',
'autoStart': true,
'activate': (app: JupyterLab) => {
app.docRegistry.addWidgetExtension('Notebook', new ExecWidgetExtension())
}
};
export default extension;

Vue 2.0, where to place local functions

Where do you correctly place local functions in vue 2.x?
I could just place them in the "methods" object, but I'd like them to be completely local to the instance if thats possible.
Sort of like this in Plain JS :
window._global = (function () {
function _secretInsideFunct(){
return "FooBar";
}
var __localObject = {
outsideFunct : function () {
return _secretInsideFunct();
}
}
return __localObject;
}());
..where _global._secretInsideFunct() wouldnt be accessible anywhere else but from inside the _global object.
In this specific case I want to make a function that creates an array object if it doesn't exist.. Something like:
function CreateOrSet (workArray, itemName, itemValue ){
var salaryRow = self.Status.Rows.find(r => r.recordID == itemName);
if (!salaryRow) {
salaryRow = { recordID: itemName, recordAmount: 0, recordName: "Løn" };
self.Status.Rows.push(salaryRow);
}
salaryRow.recordAmount = itemValue ;
}
..but a general approach for these cases is better :)
Now this function doesn't looks like a utility or helper function, but relate to a state, Status.Rows. If I were you, I will define it as close as to the state or the module that the state being used.
If the state will be used across the app, maybe I will define it in entry file, index.js or app.vue.
Or If you are using vuex, you can define it as an vuex action. So you may do something like this:
const store = new Vuex.Store({
state: {
status: {
rows: []
},
mutations: {
pushRow (state, salaryRow) {
state.status.rows.push(salaryRow)
},
changeAmount (state, id, amount) {
const salaryRow = state.status.Rows.find(r => r.recordID === id)
salaryRow.recordAmount = amount
}
},
actions: {
createRow (context, itemName) {
const salaryRow = { recordID: itemName, recordAmount: 0, recordName: "Løn" };
context.commit('pushRow', salaryRow)
}
}
})
You can put all of the code to a single action, it is just an idea, how you organize your code depend on your needs.

How to mock localStorage in JavaScript unit tests?

Are there any libraries out there to mock localStorage?
I've been using Sinon.JS for most of my other javascript mocking and have found it is really great.
My initial testing shows that localStorage refuses to be assignable in firefox (sadface) so I'll probably need some sort of hack around this :/
My options as of now (as I see) are as follows:
Create wrapping functions that all my code uses and mock those
Create some sort of (might be complicated) state management (snapshot localStorage before test, in cleanup restore snapshot) for localStorage.
??????
What do you think of these approaches and do you think there are any other better ways to go about this? Either way I'll put the resulting "library" that I end up making on github for open source goodness.
Here is a simple way to mock it with Jasmine:
let localStore;
beforeEach(() => {
localStore = {};
spyOn(window.localStorage, 'getItem').and.callFake((key) =>
key in localStore ? localStore[key] : null
);
spyOn(window.localStorage, 'setItem').and.callFake(
(key, value) => (localStore[key] = value + '')
);
spyOn(window.localStorage, 'clear').and.callFake(() => (localStore = {}));
});
If you want to mock the local storage in all your tests, declare the beforeEach() function shown above in the global scope of your tests (the usual place is a specHelper.js script).
just mock the global localStorage / sessionStorage (they have the same API) for your needs.
For example:
// Storage Mock
function storageMock() {
let storage = {};
return {
setItem: function(key, value) {
storage[key] = value || '';
},
getItem: function(key) {
return key in storage ? storage[key] : null;
},
removeItem: function(key) {
delete storage[key];
},
get length() {
return Object.keys(storage).length;
},
key: function(i) {
const keys = Object.keys(storage);
return keys[i] || null;
}
};
}
And then what you actually do, is something like that:
// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();
The current solutions will not work in Firefox. This is because localStorage is defined by the html spec as being not modifiable. You can however get around this by accessing localStorage's prototype directly.
The cross browser solution is to mock the objects on Storage.prototype e.g.
instead of spyOn(localStorage, 'setItem') use
spyOn(Storage.prototype, 'setItem')
spyOn(Storage.prototype, 'getItem')
taken from bzbarsky and teogeos's replies here https://github.com/jasmine/jasmine/issues/299
Also consider the option to inject dependencies in an object's constructor function.
var SomeObject(storage) {
this.storge = storage || window.localStorage;
// ...
}
SomeObject.prototype.doSomeStorageRelatedStuff = function() {
var myValue = this.storage.getItem('myKey');
// ...
}
// In src
var myObj = new SomeObject();
// In test
var myObj = new SomeObject(mockStorage)
In line with mocking and unit testing, I like to avoid testing the storage implementation. For instance no point in checking if length of storage increased after you set an item, etc.
Since it is obviously unreliable to replace methods on the real localStorage object, use a "dumb" mockStorage and stub the individual methods as desired, such as:
var mockStorage = {
setItem: function() {},
removeItem: function() {},
key: function() {},
getItem: function() {},
removeItem: function() {},
length: 0
};
// Then in test that needs to know if and how setItem was called
sinon.stub(mockStorage, 'setItem');
var myObj = new SomeObject(mockStorage);
myObj.doSomeStorageRelatedStuff();
expect(mockStorage.setItem).toHaveBeenCalledWith('myKey');
This is what I do...
var mock = (function() {
var store = {};
return {
getItem: function(key) {
return store[key];
},
setItem: function(key, value) {
store[key] = value.toString();
},
clear: function() {
store = {};
}
};
})();
Object.defineProperty(window, 'localStorage', {
value: mock,
});
Are there any libraries out there to mock localStorage?
I just wrote one:
(function () {
var localStorage = {};
localStorage.setItem = function (key, val) {
this[key] = val + '';
}
localStorage.getItem = function (key) {
return this[key];
}
Object.defineProperty(localStorage, 'length', {
get: function () { return Object.keys(this).length - 2; }
});
// Your tests here
})();
My initial testing shows that localStorage refuses to be assignable in firefox
Only in global context. With a wrapper function as above, it works just fine.
Overwriting the localStorage property of the global window object as suggested in some of the answers won't work in most JS engines, because they declare the localStorage data property as not writable and not configurable.
However I found out that at least with PhantomJS's (version 1.9.8) WebKit version you could use the legacy API __defineGetter__ to control what happens if localStorage is accessed. Still it would be interesting if this works in other browsers as well.
var tmpStorage = window.localStorage;
// replace local storage
window.__defineGetter__('localStorage', function () {
throw new Error("localStorage not available");
// you could also return some other object here as a mock
});
// do your tests here
// restore old getter to actual local storage
window.__defineGetter__('localStorage',
function () { return tmpStorage });
The benefit of this approach is that you would not have to modify the code you're about to test.
You don't have to pass the storage object to each method that uses it. Instead, you can use a configuration parameter for any module that touches the storage adapter.
Your old module
// hard to test !
export const someFunction (x) {
window.localStorage.setItem('foo', x)
}
// hard to test !
export const anotherFunction () {
return window.localStorage.getItem('foo')
}
Your new module with config "wrapper" function
export default function (storage) {
return {
someFunction (x) {
storage.setItem('foo', x)
}
anotherFunction () {
storage.getItem('foo')
}
}
}
When you use the module in testing code
// import mock storage adapater
const MockStorage = require('./mock-storage')
// create a new mock storage instance
const mock = new MockStorage()
// pass mock storage instance as configuration argument to your module
const myModule = require('./my-module')(mock)
// reset before each test
beforeEach(function() {
mock.clear()
})
// your tests
it('should set foo', function() {
myModule.someFunction('bar')
assert.equal(mock.getItem('foo'), 'bar')
})
it('should get foo', function() {
mock.setItem('foo', 'bar')
assert.equal(myModule.anotherFunction(), 'bar')
})
The MockStorage class might look like this
export default class MockStorage {
constructor () {
this.storage = new Map()
}
setItem (key, value) {
this.storage.set(key, value)
}
getItem (key) {
return this.storage.get(key)
}
removeItem (key) {
this.storage.delete(key)
}
clear () {
this.constructor()
}
}
When using your module in production code, instead pass the real localStorage adapter
const myModule = require('./my-module')(window.localStorage)
Here is an exemple using sinon spy and mock:
// window.localStorage.setItem
var spy = sinon.spy(window.localStorage, "setItem");
// You can use this in your assertions
spy.calledWith(aKey, aValue)
// Reset localStorage.setItem method
spy.reset();
// window.localStorage.getItem
var stub = sinon.stub(window.localStorage, "getItem");
stub.returns(aValue);
// You can use this in your assertions
stub.calledWith(aKey)
// Reset localStorage.getItem method
stub.reset();
credits to
https://medium.com/#armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87
Make a fake localstorage, and spy on localstorage, when it is caleld
beforeAll( () => {
let store = {};
const mockLocalStorage = {
getItem: (key: string): string => {
return key in store ? store[key] : null;
},
setItem: (key: string, value: string) => {
store[key] = `${value}`;
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
}
};
spyOn(localStorage, 'getItem')
.and.callFake(mockLocalStorage.getItem);
spyOn(localStorage, 'setItem')
.and.callFake(mockLocalStorage.setItem);
spyOn(localStorage, 'removeItem')
.and.callFake(mockLocalStorage.removeItem);
spyOn(localStorage, 'clear')
.and.callFake(mockLocalStorage.clear);
})
And here we use it
it('providing search value should return matched item', () => {
localStorage.setItem('defaultLanguage', 'en-US');
expect(...
});
I found that I did not need to mock it. I could change the actual local storage to the state I wanted it via setItem, then just query the values to see if it changed via getItem. It's not quite as powerful as mocking as you can't see how many times something was changed, but it worked for my purposes.
I decided to reiterate my comment to Pumbaa80's answer as separate answer so that it'll be easier to reuse it as a library.
I took Pumbaa80's code, refined it a bit, added tests and published it as an npm module here:
https://www.npmjs.com/package/mock-local-storage.
Here is a source code:
https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js
Some tests:
https://github.com/letsrock-today/mock-local-storage/blob/master/test/mock-localstorage.js
Module creates mock localStorage and sessionStorage on the global object (window or global, which of them is defined).
In my other project's tests I required it with mocha as this: mocha -r mock-local-storage to make global definitions available for all code under test.
Basically, code looks like follows:
(function (glob) {
function createStorage() {
let s = {},
noopCallback = () => {},
_itemInsertionCallback = noopCallback;
Object.defineProperty(s, 'setItem', {
get: () => {
return (k, v) => {
k = k + '';
_itemInsertionCallback(s.length);
s[k] = v + '';
};
}
});
Object.defineProperty(s, 'getItem', {
// ...
});
Object.defineProperty(s, 'removeItem', {
// ...
});
Object.defineProperty(s, 'clear', {
// ...
});
Object.defineProperty(s, 'length', {
get: () => {
return Object.keys(s).length;
}
});
Object.defineProperty(s, "key", {
// ...
});
Object.defineProperty(s, 'itemInsertionCallback', {
get: () => {
return _itemInsertionCallback;
},
set: v => {
if (!v || typeof v != 'function') {
v = noopCallback;
}
_itemInsertionCallback = v;
}
});
return s;
}
glob.localStorage = createStorage();
glob.sessionStorage = createStorage();
}(typeof window !== 'undefined' ? window : global));
Note that all methods added via Object.defineProperty so that them won't be iterated, accessed or removed as regular items and won't count in length. Also I added a way to register callback which is called when an item is about to be put into object. This callback may be used to emulate quota exceeded error in tests.
Unfortunately, the only way we can mock the localStorage object in a test scenario is to change the code we're testing. You have to wrap your code in an anonymous function (which you should be doing anyway) and use "dependency injection" to pass in a reference to the window object. Something like:
(function (window) {
// Your code
}(window.mockWindow || window));
Then, inside of your test, you can specify:
window.mockWindow = { localStorage: { ... } };
This is how I like to do it. Keeps it simple.
let localStoreMock: any = {};
beforeEach(() => {
angular.mock.module('yourApp');
angular.mock.module(function ($provide: any) {
$provide.service('localStorageService', function () {
this.get = (key: any) => localStoreMock[key];
this.set = (key: any, value: any) => localStoreMock[key] = value;
});
});
});
Need to interact with stored data
A quite short approach
const store = {};
Object.defineProperty(window, 'localStorage', {
value: {
getItem:(key) => store[key]},
setItem:(key, value) => {
store[key] = value.toString();
},
clear: () => {
store = {};
}
},
});
Spy with Jasmine
If you just need these functions to spy on them using jasmine it will be even shorter and easier to read.
Object.defineProperty(window, 'localStorage', {
value: {
getItem:(key) => {},
setItem:(key, value) => {},
clear: () => {},
...
},
});
const spy = spyOn(localStorage, 'getItem')
Now you don't need a store at all.
For those wanting to mock localstorage and not simply spy on it, this worked for me:
Storage.prototype.getItem = jest.fn(() => 'bla');
Source:
https://github.com/facebook/jest/issues/6858
I know OP specifically asked about mocking, but arguably it's better to spy rather than mock. And what if you use Object.keys(localStorage) to iterate over all available keys? You can test it like this:
const someFunction = () => {
const localStorageKeys = Object.keys(localStorage)
console.log('localStorageKeys', localStorageKeys)
localStorage.removeItem('whatever')
}
and the test code will be like follows:
describe('someFunction', () => {
it('should remove some item from the local storage', () => {
const _localStorage = {
foo: 'bar', fizz: 'buzz'
}
Object.setPrototypeOf(_localStorage, {
removeItem: jest.fn()
})
jest.spyOn(global, 'localStorage', 'get').mockReturnValue(_localStorage)
someFunction()
expect(global.localStorage.removeItem).toHaveBeenCalledTimes(1)
expect(global.localStorage.removeItem).toHaveBeenCalledWith('whatever')
})
})
No need for mocks or constructors. Relatively few lines, too.
None of these answers are completely accurate or safe to use. Neither is this one but it is as accurate as I wanted without figuring out how to manipulate getters and setters.
TypeScript
const mockStorage = () => {
for (const storage of [window.localStorage, window.sessionStorage]) {
let store = {};
spyOn(storage, 'getItem').and.callFake((key) =>
key in store ? store[key] : null
);
spyOn(storage, 'setItem').and.callFake(
(key, value) => (store[key] = value + '')
);
spyOn(storage, 'removeItem').and.callFake((key: string) => {
delete store[key];
});
spyOn(storage, 'clear').and.callFake(() => (store = {}));
spyOn(storage, 'key').and.callFake((i: number) => {
throw new Error(`Method 'key' not implemented`);
});
// Storage.length is not supported
// Property accessors are not supported
}
};
Usage
describe('Local storage', () => {
beforeEach(() => {
mockStorage();
});
it('should cache a unit in session', () => {
LocalStorageService.cacheUnit(testUnit);
expect(window.sessionStorage.setItem).toHaveBeenCalledTimes(1);
expect(window.sessionStorage.getItem(StorageKeys.units)).toContain(
testUnit.id
);
});
});
Caveats
With localStorage you can do window.localStorage['color'] = 'red';
this will bypass the mock.
window.localStorage.length will bypass this mock.
window.localStorage.key throws in this mock as code relying on this can not be tested by this mock.
Mock correctly separates local and session storage.
Please also see: MDN: Web Storage API

Categories

Resources