From 046e01dfa90b735203c040a44ab3e87a5e622ce8 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Fri, 21 Apr 2017 18:09:48 -0700 Subject: [PATCH] [fix] add AsyncStorage callbacks and tests Add support for the callback interface and add test coverage. Fix #399 Close #400 --- .../__snapshots__/index-test.js.snap | 62 +++- src/apis/AsyncStorage/__tests__/index-test.js | 306 ++++++++++++++---- src/apis/AsyncStorage/index.js | 178 +++++----- 3 files changed, 400 insertions(+), 146 deletions(-) diff --git a/src/apis/AsyncStorage/__tests__/__snapshots__/index-test.js.snap b/src/apis/AsyncStorage/__tests__/__snapshots__/index-test.js.snap index d3428cea..639f40ca 100644 --- a/src/apis/AsyncStorage/__tests__/__snapshots__/index-test.js.snap +++ b/src/apis/AsyncStorage/__tests__/__snapshots__/index-test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`apis/AsyncStorage mergeLocalStorageItem should have same behavior as react-native 1`] = ` +exports[`apis/AsyncStorage mergeItem calls callback after setting item 1`] = ` Object { "age": 31, "name": "Chris", @@ -11,3 +11,63 @@ Object { }, } `; + +exports[`apis/AsyncStorage mergeItem promise after setting item 1`] = ` +Object { + "age": 31, + "name": "Chris", + "traits": Object { + "eyes": "blue", + "hair": "brown", + "shoe_size": 10, + }, +} +`; + +exports[`apis/AsyncStorage multiMerge calls callback after setting items 1`] = ` +Object { + "age": 31, + "name": "Chris", + "traits": Object { + "eyes": "blue", + "hair": "brown", + "shoe_size": 10, + }, +} +`; + +exports[`apis/AsyncStorage multiMerge calls callback after setting items 2`] = ` +Object { + "age": 31, + "name": "Amy", + "traits": Object { + "eyes": "blue", + "hair": "black", + "shoe_size": 10, + }, +} +`; + +exports[`apis/AsyncStorage multiMerge promise after setting items 1`] = ` +Object { + "age": 31, + "name": "Chris", + "traits": Object { + "eyes": "blue", + "hair": "brown", + "shoe_size": 10, + }, +} +`; + +exports[`apis/AsyncStorage multiMerge promise after setting items 2`] = ` +Object { + "age": 31, + "name": "Amy", + "traits": Object { + "eyes": "blue", + "hair": "black", + "shoe_size": 10, + }, +} +`; diff --git a/src/apis/AsyncStorage/__tests__/index-test.js b/src/apis/AsyncStorage/__tests__/index-test.js index 9a2dfef9..9635de5b 100644 --- a/src/apis/AsyncStorage/__tests__/index-test.js +++ b/src/apis/AsyncStorage/__tests__/index-test.js @@ -1,77 +1,275 @@ /* eslint-env jasmine, jest */ import AsyncStorage from '..'; -const waterfall = (fns, cb) => { - const _waterfall = (...args) => { - const fn = (fns || []).shift(); - if (typeof fn === 'function') { - fn(...args, (err, ...nextArgs) => { - if (err) { - return cb(err); - } else { - return _waterfall(...nextArgs); - } - }); - } else { - cb(null, ...args); - } - }; - _waterfall(); -}; +const originalLocalStorage = window.localStorage; -const obj = {}; +let obj = {}; const mockLocalStorage = { + length: 0, + clear() { + obj = {}; + mockLocalStorage.length = 0; + }, getItem(key) { return obj[key]; }, + key(index) { + return Object.keys(obj)[index]; + }, + removeItem(key) { + delete obj[key]; + mockLocalStorage.length -= 1; + }, setItem(key, value) { obj[key] = value; + mockLocalStorage.length += 1; } }; -const originalLocalStorage = window.localStorage; + +const uid123Object = { + name: 'Chris', + age: 30, + traits: { hair: 'brown', eyes: 'green' } +}; +const uid123Delta = { + age: 31, + traits: { eyes: 'blue', shoe_size: 10 } +}; +const uid124Object = { + name: 'Amy', + age: 28, + traits: { hair: 'black', eyes: 'brown' } +}; describe('apis/AsyncStorage', () => { - describe('mergeLocalStorageItem', () => { - test('should have same behavior as react-native', done => { - window.localStorage = mockLocalStorage; - // https://facebook.github.io/react-native/docs/asyncstorage.html - const UID123_object = { - name: 'Chris', - age: 30, - traits: { hair: 'brown', eyes: 'brown' } - }; - const UID123_delta = { - age: 31, - traits: { eyes: 'blue', shoe_size: 10 } - }; + beforeEach(() => { + mockLocalStorage.setItem('UID123', JSON.stringify(uid123Object)); + mockLocalStorage.setItem('UID124', JSON.stringify(uid124Object)); + window.localStorage = mockLocalStorage; + }); - waterfall( - [ - cb => { - AsyncStorage.setItem('UID123', JSON.stringify(UID123_object)) - .then(() => cb(null)) - .catch(cb); - }, - cb => { - AsyncStorage.mergeItem('UID123', JSON.stringify(UID123_delta)) - .then(() => cb(null)) - .catch(cb); - }, - cb => { - AsyncStorage.getItem('UID123') - .then(result => { - cb(null, JSON.parse(result)); - }) - .catch(cb); - } - ], + afterEach(() => { + mockLocalStorage.clear(); + window.localStorage = originalLocalStorage; + }); + + describe('clear', () => { + const assertResult = () => { + expect(mockLocalStorage.length).toEqual(0); + }; + + test('promise of erased keys', () => { + expect(mockLocalStorage.length).toEqual(2); + return AsyncStorage.clear().then(() => { + assertResult(); + }); + }); + + test('calls callback after erasing keys', done => { + expect(mockLocalStorage.length).toEqual(2); + AsyncStorage.clear(err => { + expect(err).toEqual(null); + assertResult(); + done(); + }); + }); + }); + + describe('getAllKeys', () => { + const assertResult = result => { + expect(result).toEqual(['UID123', 'UID124']); + }; + + test('promise of keys', () => { + return AsyncStorage.getAllKeys().then(result => { + assertResult(result); + }); + }); + + test('calls callback with keys', done => { + AsyncStorage.getAllKeys((err, result) => { + expect(err).toEqual(null); + assertResult(result); + done(); + }); + }); + }); + + describe('getItem', () => { + const assertResult = result => { + expect(result).toEqual(JSON.stringify(uid123Object)); + }; + + test('promise of item', () => { + return AsyncStorage.getItem('UID123').then(result => { + assertResult(result); + }); + }); + + test('calls callback with item', done => { + AsyncStorage.getItem('UID123', (err, result) => { + expect(err).toEqual(null); + assertResult(result); + done(); + }); + }); + }); + + describe('multiGet', () => { + const assertResult = result => { + expect(result).toEqual([ + ['UID123', JSON.stringify(uid123Object)], + ['UID124', JSON.stringify(uid124Object)] + ]); + }; + + test('promise of items', () => { + return AsyncStorage.multiGet(['UID123', 'UID124']).then(result => { + assertResult(result); + }); + }); + + test('calls callback with items', done => { + AsyncStorage.multiGet(['UID123', 'UID124'], (err, result) => { + expect(err).toEqual(null); + assertResult(result); + done(); + }); + }); + }); + + describe('setItem', () => { + const assertResult = () => { + expect(mockLocalStorage.getItem('UID123')).toEqual(JSON.stringify(uid123Object)); + }; + + test('promise after setting item', () => { + return AsyncStorage.setItem('UID123', JSON.stringify(uid123Object)).then(() => { + assertResult(); + }); + }); + + test('calls callback after setting item', done => { + AsyncStorage.setItem('UID123', JSON.stringify(uid123Object), (err, result) => { + expect(err).toEqual(null); + assertResult(); + done(); + }); + }); + }); + + describe('multiSet', () => { + const assertResult = result => { + expect(mockLocalStorage.getItem('UID123')).toEqual(JSON.stringify(uid123Object)); + expect(mockLocalStorage.getItem('UID124')).toEqual(JSON.stringify(uid124Object)); + }; + + test('promise after setting items', () => { + return AsyncStorage.multiSet([ + ['UID123', JSON.stringify(uid123Object)], + ['UID124', JSON.stringify(uid124Object)] + ]).then(() => { + assertResult(); + }); + }); + + test('calls callback after setting items', done => { + AsyncStorage.multiSet( + [['UID123', JSON.stringify(uid123Object)], ['UID124', JSON.stringify(uid124Object)]], (err, result) => { expect(err).toEqual(null); - expect(result).toMatchSnapshot(); - window.localStorage = originalLocalStorage; + assertResult(); done(); } ); }); }); + + describe('mergeItem', () => { + const assertResult = () => { + expect(JSON.parse(mockLocalStorage.getItem('UID123'))).toMatchSnapshot(); + }; + + test('promise after setting item', () => { + return AsyncStorage.mergeItem('UID123', JSON.stringify(uid123Delta)).then(() => { + assertResult(); + }); + }); + + test('calls callback after setting item', done => { + AsyncStorage.mergeItem('UID123', JSON.stringify(uid123Delta), (err, result) => { + expect(err).toEqual(null); + assertResult(); + done(); + }); + }); + }); + + describe('multiMerge', () => { + const assertResult = result => { + expect(JSON.parse(mockLocalStorage.getItem('UID123'))).toMatchSnapshot(); + expect(JSON.parse(mockLocalStorage.getItem('UID124'))).toMatchSnapshot(); + }; + + test('promise after setting items', () => { + return AsyncStorage.multiMerge([ + ['UID123', JSON.stringify(uid123Delta)], + ['UID124', JSON.stringify(uid123Delta)] + ]).then(() => { + assertResult(); + }); + }); + + test('calls callback after setting items', done => { + AsyncStorage.multiMerge( + [['UID123', JSON.stringify(uid123Delta)], ['UID124', JSON.stringify(uid123Delta)]], + (err, result) => { + expect(err).toEqual(null); + assertResult(); + done(); + } + ); + }); + }); + + describe('removeItem', () => { + const assertResult = () => { + expect(mockLocalStorage.getItem('UID123')).toBeUndefined(); + }; + + test('promise after setting item', () => { + return AsyncStorage.removeItem('UID123').then(() => { + assertResult(); + }); + }); + + test('calls callback after setting item', done => { + AsyncStorage.removeItem('UID123', (err, result) => { + expect(err).toEqual(null); + assertResult(); + done(); + }); + }); + }); + + describe('multiRemove', () => { + const assertResult = result => { + expect(mockLocalStorage.getItem('UID123')).toBeUndefined(); + expect(mockLocalStorage.getItem('UID124')).toBeUndefined(); + }; + + test('promise after setting items', () => { + return AsyncStorage.multiRemove(['UID123', 'UID124']).then(() => { + assertResult(); + }); + }); + + test('calls callback after setting items', done => { + AsyncStorage.multiRemove(['UID123', 'UID124'], (err, result) => { + expect(err).toEqual(null); + assertResult(); + done(); + }); + }); + }); }); diff --git a/src/apis/AsyncStorage/index.js b/src/apis/AsyncStorage/index.js index 7c9bef35..3ecf7be1 100644 --- a/src/apis/AsyncStorage/index.js +++ b/src/apis/AsyncStorage/index.js @@ -13,66 +13,69 @@ const mergeLocalStorageItem = (key, value) => { window.localStorage.setItem(key, nextValue); }; +const createPromise = (getValue, callback) => { + return new Promise((resolve, reject) => { + try { + const value = getValue(); + if (callback) { + callback(null, value); + } + resolve(value); + } catch (err) { + if (callback) { + callback(err); + } + reject(err); + } + }); +}; + +const createPromiseAll = (promises, callback, processResult) => { + return Promise.all(promises).then( + result => { + const value = processResult ? processResult(result) : null; + callback && callback(null, value); + return Promise.resolve(value); + }, + errors => { + callback && callback(errors); + return Promise.reject(errors); + } + ); +}; + class AsyncStorage { /** * Erases *all* AsyncStorage for the domain. */ - static clear() { - return new Promise((resolve, reject) => { - try { - window.localStorage.clear(); - resolve(null); - } catch (err) { - reject(err); - } - }); + static clear(callback) { + return createPromise(() => { + window.localStorage.clear(); + }, callback); } /** * Gets *all* keys known to the app, for all callers, libraries, etc. */ - static getAllKeys() { - return new Promise((resolve, reject) => { - try { - const numberOfKeys = window.localStorage.length; - const keys = []; - for (let i = 0; i < numberOfKeys; i += 1) { - const key = window.localStorage.key(i); - keys.push(key); - } - resolve(keys); - } catch (err) { - reject(err); + static getAllKeys(callback) { + return createPromise(() => { + const numberOfKeys = window.localStorage.length; + const keys = []; + for (let i = 0; i < numberOfKeys; i += 1) { + const key = window.localStorage.key(i); + keys.push(key); } - }); + return keys; + }, callback); } /** * Fetches `key` value. */ - static getItem(key: string) { - return new Promise((resolve, reject) => { - try { - const value = window.localStorage.getItem(key); - resolve(value); - } catch (err) { - reject(err); - } - }); - } - - /** - * Merges existing value with input value, assuming they are stringified JSON. - */ - static mergeItem(key: string, value: string) { - return new Promise((resolve, reject) => { - try { - mergeLocalStorageItem(key, value); - resolve(null); - } catch (err) { - reject(err); - } - }); + static getItem(key: string, callback) { + return createPromise(() => { + return window.localStorage.getItem(key); + }, callback); } /** @@ -81,13 +84,37 @@ class AsyncStorage { * * multiGet(['k1', 'k2']) -> [['k1', 'val1'], ['k2', 'val2']] */ - static multiGet(keys: Array) { + static multiGet(keys: Array, callback) { const promises = keys.map(key => AsyncStorage.getItem(key)); + const processResult = result => result.map((value, i) => [keys[i], value]); + return createPromiseAll(promises, callback, processResult); + } - return Promise.all(promises).then( - result => Promise.resolve(result.map((value, i) => [keys[i], value])), - error => Promise.reject(error) - ); + /** + * Sets `value` for `key`. + */ + static setItem(key: string, value: string, callback) { + return createPromise(() => { + window.localStorage.setItem(key, value); + }, callback); + } + + /** + * Takes an array of key-value array pairs. + * multiSet([['k1', 'val1'], ['k2', 'val2']]) + */ + static multiSet(keyValuePairs: Array>, callback) { + const promises = keyValuePairs.map(item => AsyncStorage.setItem(item[0], item[1])); + return createPromiseAll(promises, callback); + } + + /** + * Merges existing value with input value, assuming they are stringified JSON. + */ + static mergeItem(key: string, value: string, callback) { + return createPromise(() => { + mergeLocalStorageItem(key, value); + }, callback); } /** @@ -96,57 +123,26 @@ class AsyncStorage { * * multiMerge([['k1', 'val1'], ['k2', 'val2']]) */ - static multiMerge(keyValuePairs: Array>) { + static multiMerge(keyValuePairs: Array>, callback) { const promises = keyValuePairs.map(item => AsyncStorage.mergeItem(item[0], item[1])); - - return Promise.all(promises).then(() => Promise.resolve(null), error => Promise.reject(error)); - } - - /** - * Delete all the keys in the `keys` array. - */ - static multiRemove(keys: Array) { - const promises = keys.map(key => AsyncStorage.removeItem(key)); - - return Promise.all(promises).then(() => Promise.resolve(null), error => Promise.reject(error)); - } - - /** - * Takes an array of key-value array pairs. - * multiSet([['k1', 'val1'], ['k2', 'val2']]) - */ - static multiSet(keyValuePairs: Array>) { - const promises = keyValuePairs.map(item => AsyncStorage.setItem(item[0], item[1])); - - return Promise.all(promises).then(() => Promise.resolve(null), error => Promise.reject(error)); + return createPromiseAll(promises, callback); } /** * Removes a `key` */ - static removeItem(key: string) { - return new Promise((resolve, reject) => { - try { - window.localStorage.removeItem(key); - resolve(null); - } catch (err) { - reject(err); - } - }); + static removeItem(key: string, callback) { + return createPromise(() => { + return window.localStorage.removeItem(key); + }, callback); } /** - * Sets `value` for `key`. + * Delete all the keys in the `keys` array. */ - static setItem(key: string, value: string) { - return new Promise((resolve, reject) => { - try { - window.localStorage.setItem(key, value); - resolve(null); - } catch (err) { - reject(err); - } - }); + static multiRemove(keys: Array, callback) { + const promises = keys.map(key => AsyncStorage.removeItem(key)); + return createPromiseAll(promises, callback); } }