From 360b8962858b33b9eb0341ef39f1c8d73576d821 Mon Sep 17 00:00:00 2001 From: Alvin Lindstam Date: Wed, 16 Nov 2016 15:02:58 +0100 Subject: [PATCH 1/2] Add `listen` and `unlisten` for storage events --- README.md | 34 ++++++++++++++- lib/client.js | 47 ++++++++++++++++++++ lib/hub.js | 58 +++++++++++++++++++++++-- test/hub.html | 2 +- test/test.js | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 252 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6d16d7d..42c4525 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Features an API using ES6 promises. * [CrossStorageClient.prototype.getKeys()](#crossstorageclientprototypegetkeys) * [CrossStorageClient.prototype.clear()](#crossstorageclientprototypeclear) * [CrossStorageClient.prototype.close()](#crossstorageclientprototypeclose) + * [CrossStorageClient.prototype.listen(callback)](#crossstorageclientprototypelisten) + * [CrossStorageClient.prototype.unlisten(key)](#crossstorageclientprototypeunlisten) * [Compatibility](#compatibility) * [Compression](#compression) * [Building](#building) @@ -125,12 +127,12 @@ Accepts an array of objects with two keys: origin and allow. The value of origin is expected to be a RegExp, and allow, an array of strings. The cross storage hub is then initialized to accept requests from any of the matching origins, allowing access to the associated lists of methods. -Methods may include any of: get, set, del, getKeys and clear. A 'ready' +Methods may include any of: get, set, del, getKeys, clear and listen. A 'ready' message is sent to the parent window once complete. ``` javascript CrossStorageHub.init([ - {origin: /localhost:3000$/, allow: ['get', 'set', 'del', 'getKeys', 'clear']} + {origin: /localhost:3000$/, allow: ['get', 'set', 'del', 'getKeys', 'clear', 'listen']} ]); ``` @@ -246,6 +248,34 @@ storage.onConnect().then(function() { }); ``` +#### CrossStorageClient.prototype.listen(callback) + +Adds an event listener to the `storage` event in the hub. All `storage` events +will be sent to the client and used to call the given callback. + +The callback will be called on each `storage` event, with an object with the +keys `key`, `newValue`, `oldValue` and `url` taken from the original event. + +``` javascript +var storageEventListenerKey; +storage.onConnect().then(function() { + return storage.listen(console.log); +}).then(function(key) { + storageEventListenerKey = key +}); +``` + +#### CrossStorageClient.prototype.unlisten(eventKey) + +Removes the storage event listener. + +The client will ignore any events as soon as this is called. Returns a promise +that is settled on successful event listener removal from the hub. + +``` javascript +storage.unlisten(storageEventListenerKey); +``` + ## Compatibility For compatibility with older browsers, simply load a Promise polyfill such as diff --git a/lib/client.js b/lib/client.js index 2a78d0f..247148e 100644 --- a/lib/client.js +++ b/lib/client.js @@ -48,6 +48,8 @@ this._count = 0; this._timeout = opts.timeout || 5000; this._listener = null; + this._storageEventListeners = {}; + this._storageEventListenerCount = 0; this._installListener(); @@ -193,6 +195,44 @@ return this._request('get', {keys: args}); }; + /** + * Accepts a callback which will be called on `storage` events from the hub. + * + * The callback will be called on changes to the hub's storage (trigger from + * other documents than the hub). It will be called with an object with + * the keys `key`, `newValue`, `oldValue` and `url`, as defined by the `storage` + * event in the hub. + * + * Returns a promise that is settled on success (in adding the event listener), + * in which case it is fullfilled with a key that can be used to remove the + * listener. On failure, it is rejected with the corresponding error message. + * + * @param {function} callback Function to be called on storage changes + * @returns {Promise} A promise that is settled on hub response or timeout + */ + CrossStorageClient.prototype.listen = function(callback) { + this._storageEventListenerCount++; + var eventKey = this._id + ":" + this._storageEventListenerCount; + this._storageEventListeners[eventKey] = callback; + return this._request('listen', {eventKey: eventKey}).then(function () { + return eventKey + }); + }; + + /** + * Removes the storage event listener. + * + * The client will ignore any events as soon as this is called. Returns a promise + * that is settled on successful event listener removal from the hub. + * + * @param {string} eventKey The key returned initiating the listener with `listen` + * @returns {Promise} A promise that is settled on hub response or timeout + */ + CrossStorageClient.prototype.unlisten = function(eventKey) { + delete this._storageEventListeners[eventKey]; + return this._request('unlisten', {eventKey: eventKey}); + }; + /** * Accepts one or more keys for deletion. Returns a promise that is settled on * hub response or timeout. @@ -307,6 +347,13 @@ return; } + if(response.type === 'event') { + if (response.eventKey in client._storageEventListeners) { + client._storageEventListeners[response.eventKey](response.eventData); + } + return; + } + if (!response.id) return; if (client._requests[response.id]) { diff --git a/lib/hub.js b/lib/hub.js index 85f395a..d067e9c 100644 --- a/lib/hub.js +++ b/lib/hub.js @@ -38,6 +38,7 @@ } CrossStorageHub._permissions = permissions || []; + CrossStorageHub._eventListeners = {}; CrossStorageHub._installListener(); window.parent.postMessage('cross-storage:ready', '*'); }; @@ -120,7 +121,7 @@ /** * Returns a boolean indicating whether or not the requested method is * permitted for the given origin. The argument passed to method is expected - * to be one of 'get', 'set', 'del' or 'getKeys'. + * to be one of 'get', 'set', 'del', 'clear', 'listen' or 'getKeys'. * * @param {string} origin The origin for which to determine permissions * @param {string} method Requested action @@ -128,8 +129,8 @@ */ CrossStorageHub._permitted = function(origin, method) { var available, i, entry, match; - - available = ['get', 'set', 'del', 'clear', 'getKeys']; + if (method==='unlisten') method = 'listen'; + available = ['get', 'set', 'listen', 'del', 'clear', 'getKeys']; if (!CrossStorageHub._inArray(method, available)) { return false; } @@ -185,6 +186,57 @@ return (result.length > 1) ? result : result[0]; }; + /** + * Adds an event listener to `storage` events which sends all events to the client with the given eventKey + * + * @param {object} params An object with an eventKey + */ + CrossStorageHub._listen = function(params) { + if (params.eventKey in CrossStorageHub._eventListeners) { + throw new Error("Can't reuse eventKeys") + } + var handler = function(event) { + if (event.storageArea != window.localStorage) return; + var data = { + type: 'event', + eventKey: params.eventKey, + eventData: { + key: event.key, + newValue: event.newValue, + oldValue: event.oldValue, + url: event.url + // storageArea, ignored because we only use localStorage + } + }; + window.parent.postMessage(JSON.stringify(data), '*'); + }; + + // Support IE8 with attachEvent + if (window.addEventListener) { + window.addEventListener('storage', handler, false); + } else { + window.attachEvent('onstorage', handler); + } + CrossStorageHub._eventListeners[params.eventKey] = handler + }; + + /** + * Removes an event listener with the given eventKey + * + * @param {object} params An object with an eventKey + */ + CrossStorageHub._unlisten = function(params) { + var handler = CrossStorageHub._eventListeners[params.eventKey]; + + // Support IE8 with attachEvent + if (window.removeEventListener) { + window.removeEventListener('storage', handler, false); + } else { + window.detachEvent('onstorage', handler); + } + CrossStorageHub._eventListeners[params.eventKey] = null + }; + /** * Deletes all keys specified in the array found at params.keys. * diff --git a/test/hub.html b/test/hub.html index 0895165..bbfc8d6 100644 --- a/test/hub.html +++ b/test/hub.html @@ -6,7 +6,7 @@ diff --git a/test/test.js b/test/test.js index fd7cbc9..03c6bcf 100644 --- a/test/test.js +++ b/test/test.js @@ -53,6 +53,18 @@ describe('CrossStorageClient', function() { }; }; + var timeoutPromise = function(timeout) { + return function() { + return new Promise(function (resolve) { + window.setTimeout(function () { + resolve() + }, timeout + ); + }); + }; + }; + + // Used to delete keys before each test var cleanup = function(fn) { storage.onConnect().then(function() { @@ -333,5 +345,110 @@ describe('CrossStorageClient', function() { done(); })['catch'](done); }); + + it('can listen to updates', function(done) { + var keys = ['key1', 'key2']; + var values = ['foo', 'bar']; + var storageEvents1 = []; + var storageEvents2 = []; + var otherStorage = new CrossStorageClient(url, {timeout: 10000}); + + storage.onConnect() + .then(function(){return otherStorage.onConnect()}) + .then(function(){ + storage.listen(function(evt){storageEvents1.push(evt)}); + otherStorage.listen(function(evt){storageEvents2.push(evt)}); + }) + .then(setGet(keys[0], values[0])) + .then(timeoutPromise(100)) + .then(function(){ + expect(storageEvents1).to.have.length(0); + expect(storageEvents2).to.eql([{ + key: keys[0], + newValue: 'foo', + oldValue: null, + url: url + }]); + storageEvents2.pop(); + }) + .then(setGet(keys[0], values[1])) + .then(timeoutPromise(100)) + .then(function(){ + expect(storageEvents1).to.have.length(0); + expect(storageEvents2).to.eql([{ + key: keys[0], + newValue: 'bar', + oldValue: 'foo', + url: url + }]); + storageEvents2.pop(); + }) + .then(function() { + otherStorage.del(keys[0]); + }) + .then(timeoutPromise(100)) + .then(function(){ + expect(storageEvents2).to.have.length(0); + expect(storageEvents1).to.eql([{ + key: keys[0], + newValue: null, + oldValue: "bar", + url: url + }]); + done() + })['catch'](done); + }); + + it('can unlisten to updates', function(done) { + var keys = ['key1', 'key2']; + var values = ['foo', 'bar']; + var storageEvents1 = []; + var storageEvents2 = []; + var otherStorage = new CrossStorageClient(url, {timeout: 10000}); + var eventListenerKey; + + storage.onConnect() + .then(function(){return otherStorage.onConnect()}) + .then(function(){ + return Promise.all([ + storage.listen(function(evt){storageEvents1.push(evt)}), + otherStorage.listen(function(evt){storageEvents2.push(evt)}).then(function(key){eventListenerKey = key}) + ]); + }) + .then(setGet(keys[0], values[0])) + .then(timeoutPromise(100)) + .then(function(){ + expect(storageEvents1).to.have.length(0); + expect(storageEvents2).to.eql([{ + key: keys[0], + newValue: 'foo', + oldValue: null, + url: url + }]); + storageEvents2.pop(); + return otherStorage.unlisten(eventListenerKey) + }) + .then(setGet(keys[0], values[1])) + .then(timeoutPromise(100)) + .then(function(){ + expect(storageEvents1).to.have.length(0); + expect(storageEvents2).to.have.length(0); + storageEvents2.pop(); + }) + .then(function() { + otherStorage.del(keys[0]); + }) + .then(timeoutPromise(100)) + .then(function(){ + expect(storageEvents2).to.have.length(0); + expect(storageEvents1).to.eql([{ + key: keys[0], + newValue: null, + oldValue: "bar", + url: url + }]); + done() + })['catch'](done); + }); }); }); From e69f8b37061cde04ec4b9b105fc5acd5e145ef8b Mon Sep 17 00:00:00 2001 From: "Daniel St. Jules" Date: Mon, 12 Jun 2017 12:37:44 -0400 Subject: [PATCH 2/2] Update listen/unlisten code for merge --- README.md | 54 ++++++++++----------- lib/client.js | 82 +++++++++++++++---------------- lib/hub.js | 45 +++++++++-------- test/test.js | 130 ++++++++++++++++++-------------------------------- 4 files changed, 138 insertions(+), 173 deletions(-) diff --git a/README.md b/README.md index 42c4525..dc4c09f 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ Features an API using ES6 promises. * [CrossStorageClient.prototype.del(key1, \[key2\], \[...\])](#crossstorageclientprototypedelkey1-key2-) * [CrossStorageClient.prototype.getKeys()](#crossstorageclientprototypegetkeys) * [CrossStorageClient.prototype.clear()](#crossstorageclientprototypeclear) - * [CrossStorageClient.prototype.close()](#crossstorageclientprototypeclose) * [CrossStorageClient.prototype.listen(callback)](#crossstorageclientprototypelisten) * [CrossStorageClient.prototype.unlisten(key)](#crossstorageclientprototypeunlisten) + * [CrossStorageClient.prototype.close()](#crossstorageclientprototypeclose) * [Compatibility](#compatibility) * [Compression](#compression) * [Building](#building) @@ -233,47 +233,47 @@ storage.onConnect().then(function() { }); ``` -#### CrossStorageClient.prototype.close() +#### CrossStorageClient.prototype.listen(fn) -Deletes the iframe and sets the connected state to false. The client can -no longer be used after being invoked. +Adds an event listener to the storage event in the hub. The callback will +be invoked on any storage event not originating from that client. The +callback will be invoked with an object containing the following keys taken +from the original event: `key`, `newValue`, `oldValue` and `url`. Returns a +promise that resolves to a listener id that can be used to unregister the +listener. ``` javascript storage.onConnect().then(function() { - return storage.set('key1', 'key2'); -}).catch(function(err) { - // Handle error -}).then(function() { - storage.close(); + return storage.listen(function(event) { + console.log(event); + }); +}).then(function(id) { + // id can be passed to storage.unlisten }); ``` -#### CrossStorageClient.prototype.listen(callback) - -Adds an event listener to the `storage` event in the hub. All `storage` events -will be sent to the client and used to call the given callback. +#### CrossStorageClient.prototype.unlisten(id) -The callback will be called on each `storage` event, with an object with the -keys `key`, `newValue`, `oldValue` and `url` taken from the original event. +Removes the registered listener with the supplied id. Returns a promise +that resolves on completion. ``` javascript -var storageEventListenerKey; -storage.onConnect().then(function() { - return storage.listen(console.log); -}).then(function(key) { - storageEventListenerKey = key -}); +storage.unlisten(id); ``` -#### CrossStorageClient.prototype.unlisten(eventKey) +#### CrossStorageClient.prototype.close() -Removes the storage event listener. - -The client will ignore any events as soon as this is called. Returns a promise -that is settled on successful event listener removal from the hub. +Deletes the iframe and sets the connected state to false. The client can +no longer be used after being invoked. ``` javascript -storage.unlisten(storageEventListenerKey); +storage.onConnect().then(function() { + return storage.set('key1', 'key2'); +}).catch(function(err) { + // Handle error +}).then(function() { + storage.close(); +}); ``` ## Compatibility diff --git a/lib/client.js b/lib/client.js index 247148e..eaa7bf2 100644 --- a/lib/client.js +++ b/lib/client.js @@ -48,8 +48,9 @@ this._count = 0; this._timeout = opts.timeout || 5000; this._listener = null; - this._storageEventListeners = {}; - this._storageEventListenerCount = 0; + + this._storageListeners = {}; + this._storageListenerCount = 0; this._installListener(); @@ -195,44 +196,6 @@ return this._request('get', {keys: args}); }; - /** - * Accepts a callback which will be called on `storage` events from the hub. - * - * The callback will be called on changes to the hub's storage (trigger from - * other documents than the hub). It will be called with an object with - * the keys `key`, `newValue`, `oldValue` and `url`, as defined by the `storage` - * event in the hub. - * - * Returns a promise that is settled on success (in adding the event listener), - * in which case it is fullfilled with a key that can be used to remove the - * listener. On failure, it is rejected with the corresponding error message. - * - * @param {function} callback Function to be called on storage changes - * @returns {Promise} A promise that is settled on hub response or timeout - */ - CrossStorageClient.prototype.listen = function(callback) { - this._storageEventListenerCount++; - var eventKey = this._id + ":" + this._storageEventListenerCount; - this._storageEventListeners[eventKey] = callback; - return this._request('listen', {eventKey: eventKey}).then(function () { - return eventKey - }); - }; - - /** - * Removes the storage event listener. - * - * The client will ignore any events as soon as this is called. Returns a promise - * that is settled on successful event listener removal from the hub. - * - * @param {string} eventKey The key returned initiating the listener with `listen` - * @returns {Promise} A promise that is settled on hub response or timeout - */ - CrossStorageClient.prototype.unlisten = function(eventKey) { - delete this._storageEventListeners[eventKey]; - return this._request('unlisten', {eventKey: eventKey}); - }; - /** * Accepts one or more keys for deletion. Returns a promise that is settled on * hub response or timeout. @@ -266,6 +229,39 @@ return this._request('getKeys'); }; + /** + * Adds an event listener to the storage event in the hub. The callback will + * be invoked on any storage event not originating from that client. The + * callback will be invoked with an object containing the following keys taken + * from the original event: `key`, `newValue`, `oldValue` and `url`. Returns a + * promise that resolves to a listener id that can be used to unregister the + * listener. + * + * @param {function} fn Callback to invoke on storage event + * @returns {Promise} A promise that is settled on hub response or timeout + */ + CrossStorageClient.prototype.listen = function(fn) { + this._storageListenerCount++; + var id = this._id + ":" + this._storageListenerCount; + this._storageListeners[id] = fn; + return this._request('listen', {listenerId: id}).then(function() { + return id; + }); + }; + + /** + * Removes the registered listener with the supplied id. Returns a promise + * that resolves on completion. + * + * @param {string} id The id of the listener to unregister + * @returns {Promise} A promise that is settled on hub response or timeout + */ + CrossStorageClient.prototype.unlisten = function(id) { + delete this._storageListeners[id]; + return this._request('unlisten', {listenerId: id}); + }; + + /** * Deletes the iframe and sets the connected state to false. The client can * no longer be used after being invoked. @@ -347,9 +343,9 @@ return; } - if(response.type === 'event') { - if (response.eventKey in client._storageEventListeners) { - client._storageEventListeners[response.eventKey](response.eventData); + if (response.event) { + if (client._storageListeners[response.listenerId]) { + client._storageListeners[response.listenerId](response.event); } return; } diff --git a/lib/hub.js b/lib/hub.js index d067e9c..bf06963 100644 --- a/lib/hub.js +++ b/lib/hub.js @@ -38,7 +38,7 @@ } CrossStorageHub._permissions = permissions || []; - CrossStorageHub._eventListeners = {}; + CrossStorageHub._storageListeners = {}; CrossStorageHub._installListener(); window.parent.postMessage('cross-storage:ready', '*'); }; @@ -121,7 +121,8 @@ /** * Returns a boolean indicating whether or not the requested method is * permitted for the given origin. The argument passed to method is expected - * to be one of 'get', 'set', 'del', 'clear', 'listen' or 'getKeys'. + * to be one of 'get', 'set', 'del', 'clear', 'getKeys', 'listen', or + * 'unlisten'. * * @param {string} origin The origin for which to determine permissions * @param {string} method Requested action @@ -129,8 +130,12 @@ */ CrossStorageHub._permitted = function(origin, method) { var available, i, entry, match; - if (method==='unlisten') method = 'listen'; - available = ['get', 'set', 'listen', 'del', 'clear', 'getKeys']; + available = ['get', 'set', 'listen', 'del', 'clear', 'getKeys', 'listen']; + + if (method === 'unlisten') { + method = 'listen'; + } + if (!CrossStorageHub._inArray(method, available)) { return false; } @@ -187,54 +192,54 @@ }; /** - * Adds an event listener to `storage` events which sends all events to the client with the given eventKey + * Listens to storage events, sending them to the client. * - * @param {object} params An object with an eventKey + * @param {object} params An object with a listener id */ CrossStorageHub._listen = function(params) { - if (params.eventKey in CrossStorageHub._eventListeners) { - throw new Error("Can't reuse eventKeys") + if (params.listenerId in CrossStorageHub._storageListeners) { + return; } + var handler = function(event) { - if (event.storageArea != window.localStorage) return; + if (event.storageArea !== window.localStorage) return; + var data = { - type: 'event', - eventKey: params.eventKey, - eventData: { + listenerId: params.listenerId, + event: { key: event.key, newValue: event.newValue, oldValue: event.oldValue, url: event.url - // storageArea, ignored because we only use localStorage } }; + window.parent.postMessage(JSON.stringify(data), '*'); }; - // Support IE8 with attachEvent + CrossStorageHub._storageListeners[params.listenerId] = handler; + if (window.addEventListener) { window.addEventListener('storage', handler, false); } else { window.attachEvent('onstorage', handler); } - CrossStorageHub._eventListeners[params.eventKey] = handler }; /** - * Removes an event listener with the given eventKey + * Removes an event listener with the given id * - * @param {object} params An object with an eventKey + * @param {object} params An object with an id */ CrossStorageHub._unlisten = function(params) { - var handler = CrossStorageHub._eventListeners[params.eventKey]; + var handler = CrossStorageHub._storageListeners[params.listenerId]; + CrossStorageHub._storageListeners[params.listenerId] = null; - // Support IE8 with attachEvent if (window.removeEventListener) { window.removeEventListener('storage', handler, false); } else { window.detachEvent('onstorage', handler); } - CrossStorageHub._eventListeners[params.eventKey] = null }; /** diff --git a/test/test.js b/test/test.js index 03c6bcf..0a4b37a 100644 --- a/test/test.js +++ b/test/test.js @@ -55,16 +55,12 @@ describe('CrossStorageClient', function() { var timeoutPromise = function(timeout) { return function() { - return new Promise(function (resolve) { - window.setTimeout(function () { - resolve() - }, timeout - ); + return new Promise(function(resolve) { + window.setTimeout(resolve, timeout); }); }; }; - // Used to delete keys before each test var cleanup = function(fn) { storage.onConnect().then(function() { @@ -347,107 +343,75 @@ describe('CrossStorageClient', function() { }); it('can listen to updates', function(done) { - var keys = ['key1', 'key2']; - var values = ['foo', 'bar']; + var key = 'foo'; + var value = 'bar'; var storageEvents1 = []; var storageEvents2 = []; var otherStorage = new CrossStorageClient(url, {timeout: 10000}); - storage.onConnect() - .then(function(){return otherStorage.onConnect()}) - .then(function(){ - storage.listen(function(evt){storageEvents1.push(evt)}); - otherStorage.listen(function(evt){storageEvents2.push(evt)}); - }) - .then(setGet(keys[0], values[0])) + var listen = function() { + return Promise.all([ + storage.listen(function(evt) { + storageEvents1.push(evt) + }), + otherStorage.listen(function(evt) { + storageEvents2.push(evt) + }) + ]); + }; + + Promise.all([ + storage.onConnect(), + otherStorage.onConnect() + ]) + .then(listen) + .then(setGet(key, value)) .then(timeoutPromise(100)) - .then(function(){ + .then(function() { expect(storageEvents1).to.have.length(0); expect(storageEvents2).to.eql([{ - key: keys[0], - newValue: 'foo', + key: key, + newValue: value, oldValue: null, url: url }]); - storageEvents2.pop(); - }) - .then(setGet(keys[0], values[1])) - .then(timeoutPromise(100)) - .then(function(){ - expect(storageEvents1).to.have.length(0); - expect(storageEvents2).to.eql([{ - key: keys[0], - newValue: 'bar', - oldValue: 'foo', - url: url - }]); - storageEvents2.pop(); - }) - .then(function() { - otherStorage.del(keys[0]); - }) - .then(timeoutPromise(100)) - .then(function(){ - expect(storageEvents2).to.have.length(0); - expect(storageEvents1).to.eql([{ - key: keys[0], - newValue: null, - oldValue: "bar", - url: url - }]); - done() + done(); })['catch'](done); }); it('can unlisten to updates', function(done) { - var keys = ['key1', 'key2']; - var values = ['foo', 'bar']; var storageEvents1 = []; var storageEvents2 = []; var otherStorage = new CrossStorageClient(url, {timeout: 10000}); - var eventListenerKey; + var listenerId; - storage.onConnect() - .then(function(){return otherStorage.onConnect()}) - .then(function(){ + var listen = function() { return Promise.all([ - storage.listen(function(evt){storageEvents1.push(evt)}), - otherStorage.listen(function(evt){storageEvents2.push(evt)}).then(function(key){eventListenerKey = key}) + storage.listen(function(evt) { + storageEvents1.push(evt) + }), + otherStorage.listen(function(evt) { + storageEvents2.push(evt) + }).then(function(key){ + listenerId = key + }) ]); - }) - .then(setGet(keys[0], values[0])) - .then(timeoutPromise(100)) - .then(function(){ - expect(storageEvents1).to.have.length(0); - expect(storageEvents2).to.eql([{ - key: keys[0], - newValue: 'foo', - oldValue: null, - url: url - }]); - storageEvents2.pop(); - return otherStorage.unlisten(eventListenerKey) - }) - .then(setGet(keys[0], values[1])) - .then(timeoutPromise(100)) - .then(function(){ - expect(storageEvents1).to.have.length(0); - expect(storageEvents2).to.have.length(0); - storageEvents2.pop(); - }) + }; + + Promise.all([ + storage.onConnect(), + otherStorage.onConnect() + ]) + .then(listen) .then(function() { - otherStorage.del(keys[0]); + return otherStorage.unlisten(listenerId); }) + .then(setGet('foo', 'bar')) .then(timeoutPromise(100)) - .then(function(){ + .then(function() { + expect(storageEvents1).to.have.length(0); expect(storageEvents2).to.have.length(0); - expect(storageEvents1).to.eql([{ - key: keys[0], - newValue: null, - oldValue: "bar", - url: url - }]); - done() + done(); })['catch'](done); }); });