diff --git a/.circleci/config.yml b/.circleci/config.yml
index dc69d373..dad55b42 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -271,8 +271,7 @@ jobs:
- run:
name: Wake device
command: |
- adb shell input keyevent
- adb shell input keyevent 82 &
+ adb shell input keyevent 82
- run:
name: Run e2e tests
diff --git a/README.md b/README.md
index 4ae1a08d..4834268c 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,10 @@ getData = async () => {
See docs for [api and more examples.](docs/API.md)
+## Writing tests
+
+Using [Jest](https://jestjs.io/) for testing? Make sure to check out [docs on how to integrate it with this module.](./docs/Jest-integration.md)
+
## Contribution
See the [CONTRIBUTING](CONTRIBUTING.md) file for how to help out.
diff --git a/docs/Jest-integration.md b/docs/Jest-integration.md
new file mode 100644
index 00000000..79f60bad
--- /dev/null
+++ b/docs/Jest-integration.md
@@ -0,0 +1,87 @@
+# Jest integration
+
+Async Storage module is tighly coupled with its `NativeModule` part - it needs a running React Native application to work properly. In order to use it in tests, you have to provide its separate implementation. Follow those steps to add a mocked `Async Storage` module.
+
+## Using Async Storage mock
+
+You can use one of two ways to provide mocked version of `AsyncStorage`:
+
+### With __mocks__ directory
+
+1. In your project root directory, create `__mocks__/@react-native-community` directory.
+2. Inside that folder, create `async-storage.js` file.
+3. Inside that file, export `Async Storage` mock.
+
+```javascript
+export default from '@react-native-community/async-storage/jest/async-storage-mock'
+```
+
+### With Jest setup file
+
+1. In your Jest config (probably in `package.json`) add setup files location:
+
+```json
+"jest": {
+ "setupFiles": ["./path/to/jestSetupFile.js"]
+}
+```
+
+2. Inside your setup file, set up Async Storage mocking:
+
+```javascript
+import mockAsyncStorage from '@react-native-community/async-storage/jest/async-storage-mock';
+
+jest.mock('@react-native-community/async-storage', () => mockAsyncStorage);
+```
+## Testing with mock
+
+Each public method available from `Async Storage` is [a mock function](https://jestjs.io/docs/en/mock-functions), that you can test for certain condition, for example, if `.getItem` has been called with a specific arguments:
+
+```javascript
+it('checks if Async Storage is used', async () => {
+ await asyncOperationOnAsyncStorage();
+
+ expect(AsyncStorage.getItem).toBeCalledWith('myKey');
+})
+```
+
+## Overriding Mock logic
+
+You can override mock implementation, by replacing its inner functions:
+
+```javascript
+// somewhere in your configuration files
+import AsyncStorageMock from '@react-native-community/async-storage/jest/async-storage-mock';
+
+AsyncStorageMock.multiGet = jest.fn(([keys], callback) => {
+ // do something here to retrieve data
+ callback([]);
+})
+
+export default AsyncStorageMock;
+```
+
+You can [check its implementation](../jest/async-storage-mock.js) to get more insight into methods signatures.
+
+## Troubleshooting
+
+### **`SyntaxError: Unexpected token export` in async-storage/lib/index.js**
+
+**Note:** In React Native 0.60+, all `@react-native-community` packages are transformed by default.
+
+You need to point Jest to transform this package. You can do so, by adding Async Storage path to `transformIgnorePatterns` setting in Jest's configuration.
+
+
+```json
+"jest": {
+ "transformIgnorePatterns": ["node_modules/(?!(@react-native-community/async-storage/lib))"]
+}
+```
+
+Optionally, you can transform whole scope for `react-native-community` and `react-native`:
+
+```json
+"jest": {
+ "transformIgnorePatterns": ["node_modules/(?!(@react-native-community|react-native))"]
+}
+```
\ No newline at end of file
diff --git a/example/__tests__/App.js b/example/__tests__/App.js
index a79ec3d5..29baabc1 100644
--- a/example/__tests__/App.js
+++ b/example/__tests__/App.js
@@ -1,15 +1,157 @@
/**
* @format
- * @lint-ignore-every XPLATJSCOPYRIGHT1
*/
+/* eslint-disable no-shadow */
import 'react-native';
-import React from 'react';
-import App from '../App';
-// Note: test renderer must be required after react-native.
-import renderer from 'react-test-renderer';
+import AsyncStorage from '@react-native-community/async-storage';
-it('renders correctly', () => {
- renderer.create();
+describe('Async Storage mock functionality', () => {
+ describe('Promise based', () => {
+ it('can read/write data to/from storage', async () => {
+ const newData = Math.floor(Math.random() * 1000);
+
+ await AsyncStorage.setItem('key', newData);
+
+ const data = await AsyncStorage.getItem('key');
+
+ expect(data).toBe(newData);
+ });
+
+ it('can clear storage', async () => {
+ await AsyncStorage.setItem('temp_key', Math.random() * 1000);
+
+ let currentValue = await AsyncStorage.getItem('temp_key');
+
+ expect(currentValue).not.toBeNull();
+
+ await AsyncStorage.clear();
+
+ currentValue = await AsyncStorage.getItem('temp_key');
+
+ expect(currentValue).toBeNull();
+ });
+
+ it('can clear entries in storage', async () => {
+ await AsyncStorage.setItem('random1', Math.random() * 1000);
+ await AsyncStorage.setItem('random2', Math.random() * 1000);
+
+ let data1 = await AsyncStorage.getItem('random1');
+ let data2 = await AsyncStorage.getItem('random2');
+
+ expect(data1).not.toBeNull();
+ expect(data2).not.toBeNull();
+
+ await AsyncStorage.removeItem('random1');
+ await AsyncStorage.removeItem('random2');
+ data1 = await AsyncStorage.getItem('random1');
+ data2 = await AsyncStorage.getItem('random2');
+ expect(data2).toBeNull();
+ expect(data1).toBeNull();
+ });
+
+ it('can use merge with current data in storage', async () => {
+ let originalPerson = {
+ name: 'Jerry',
+ age: 21,
+ characteristics: {
+ hair: 'black',
+ eyes: 'green',
+ },
+ };
+
+ await AsyncStorage.setItem('person', JSON.stringify(originalPerson));
+
+ originalPerson.name = 'Harry';
+ originalPerson.characteristics.hair = 'red';
+ originalPerson.characteristics.shoeSize = 40;
+
+ await AsyncStorage.mergeItem('person', JSON.stringify(originalPerson));
+
+ const currentPerson = await AsyncStorage.getItem('person');
+ const person = JSON.parse(currentPerson);
+
+ expect(person).toHaveProperty('name', 'Harry');
+ expect(person.characteristics).toHaveProperty('hair', 'red');
+ expect(person.characteristics).toHaveProperty('shoeSize', 40);
+ });
+ });
+
+ describe('Callback based', () => {
+ it('can read/write data to/from storage', done => {
+ const newData = Math.floor(Math.random() * 1000);
+
+ AsyncStorage.setItem('key', newData, function() {
+ AsyncStorage.getItem('key', function(_, value) {
+ expect(value).toBe(newData);
+ done();
+ }).catch(e => done.fail(e));
+ });
+ });
+ it('can clear storage', done => {
+ AsyncStorage.setItem('temp_key', Math.random() * 1000, () => {
+ AsyncStorage.getItem('temp_key', (_, currentValue) => {
+ expect(currentValue).not.toBeNull();
+ AsyncStorage.clear(() => {
+ AsyncStorage.getItem('temp_key', (_, value) => {
+ expect(value).toBeNull();
+ done();
+ }).catch(e => done.fail(e));
+ });
+ }).catch(e => done.fail(e));
+ });
+ });
+
+ it('can clear entries in storage', done => {
+ AsyncStorage.setItem('random1', Math.random() * 1000, () => {
+ AsyncStorage.setItem('random2', Math.random() * 1000, () => {
+ AsyncStorage.getItem('random1', (_, data1) => {
+ AsyncStorage.getItem('random2', (_, data2) => {
+ expect(data1).not.toBeNull();
+ expect(data2).not.toBeNull();
+
+ AsyncStorage.removeItem('random1', () => {
+ AsyncStorage.removeItem('random2', () => {
+ AsyncStorage.getItem('random1', (_, value1) => {
+ AsyncStorage.getItem('random2', (_, value2) => {
+ expect(value1).toBeNull();
+ expect(value2).toBeNull();
+ done();
+ }).catch(e => done.fail(e));
+ });
+ });
+ });
+ }).catch(e => done.fail(e));
+ });
+ });
+ });
+ });
+
+ it('can use merge with current data in storage', done => {
+ let originalPerson = {
+ name: 'Jerry',
+ age: 21,
+ characteristics: {
+ hair: 'black',
+ eyes: 'green',
+ },
+ };
+
+ AsyncStorage.setItem('person', JSON.stringify(originalPerson), () => {
+ originalPerson.name = 'Harry';
+ originalPerson.characteristics.hair = 'red';
+ originalPerson.characteristics.shoeSize = 40;
+ AsyncStorage.mergeItem('person', JSON.stringify(originalPerson), () => {
+ AsyncStorage.getItem('person', (_, currentPerson) => {
+ const person = JSON.parse(currentPerson);
+ expect(person).toHaveProperty('name', 'Harry');
+ expect(person.characteristics).toHaveProperty('hair', 'red');
+ expect(person.characteristics).toHaveProperty('shoeSize', 40);
+ done();
+ }).catch(e => done.fail(e));
+ });
+ });
+ });
+ });
});
diff --git a/example/e2e/asyncstorage.spec.js b/example/e2e/asyncstorage.e2e.js
similarity index 100%
rename from example/e2e/asyncstorage.spec.js
rename to example/e2e/asyncstorage.e2e.js
diff --git a/example/e2e/config.json b/example/e2e/config.json
index 38036d30..3491692a 100644
--- a/example/e2e/config.json
+++ b/example/e2e/config.json
@@ -1,4 +1,5 @@
{
"setupFilesAfterEnv": ["./init.js"],
- "testEnvironment": "node"
+ "testEnvironment": "node",
+ "testMatch": [ "**/?(*.)+(e2e).[jt]s?(x)" ]
}
\ No newline at end of file
diff --git a/example/jest.setup.js b/example/jest.setup.js
new file mode 100644
index 00000000..79683a45
--- /dev/null
+++ b/example/jest.setup.js
@@ -0,0 +1,7 @@
+/**
+ * @format
+ */
+
+import mockAsyncStorage from '../jest/async-storage-mock';
+
+jest.mock('@react-native-community/async-storage', () => mockAsyncStorage);
diff --git a/jest/async-storage-mock.js b/jest/async-storage-mock.js
new file mode 100644
index 00000000..9a45dd2e
--- /dev/null
+++ b/jest/async-storage-mock.js
@@ -0,0 +1,130 @@
+/**
+ * @format
+ * @flow
+ */
+
+type KeysType = Array;
+type KeyValueType = Array>;
+type CallbackType = ((?Error) => void) | void;
+type ItemGetCallbackType = (?Error, ?string) => void;
+type ResultCallbackType = ((?Error, ?KeyValueType) => void) | void;
+
+const asMock = {
+ __INTERNAL_MOCK_STORAGE__: {},
+
+ setItem: jest.fn<[string, string, CallbackType], Promise<*>>(
+ async (key: string, value: string, callback: CallbackType) => {
+ const setResult = await asMock.multiSet([[key, value]], undefined);
+
+ callback && callback(setResult);
+ return setResult;
+ },
+ ),
+ getItem: jest.fn<[string, ItemGetCallbackType], Promise<*>>(
+ async (key: string, callback: ItemGetCallbackType) => {
+ const getResult = await asMock.multiGet([key], undefined);
+
+ const result = getResult[0] ? getResult[0][1] : null;
+
+ callback && callback(null, result);
+ return result;
+ },
+ ),
+ removeItem: jest.fn<[string, CallbackType], Promise>(
+ (key: string, callback: CallbackType) =>
+ asMock.multiRemove([key], callback),
+ ),
+ mergeItem: jest.fn<[string, string, CallbackType], Promise<*>>(
+ (key: string, value: string, callback: CallbackType) =>
+ asMock.multiMerge([[key, value]], callback),
+ ),
+
+ clear: jest.fn<[CallbackType], Promise<*>>(_clear),
+ getAllKeys: jest.fn<[], void>(),
+ flushGetRequests: jest.fn<[], void>(),
+
+ multiGet: jest.fn<[KeysType, ResultCallbackType], Promise<*>>(_multiGet),
+ multiSet: jest.fn<[KeyValueType, CallbackType], Promise<*>>(_multiSet),
+ multiRemove: jest.fn<[KeysType, CallbackType], Promise<*>>(_multiRemove),
+ multiMerge: jest.fn<[KeyValueType, CallbackType], Promise<*>>(_multiMerge),
+};
+
+async function _multiSet(keyValuePairs: KeyValueType, callback: CallbackType) {
+ keyValuePairs.forEach(keyValue => {
+ const key = keyValue[0];
+ const value = keyValue[1];
+
+ asMock.__INTERNAL_MOCK_STORAGE__[key] = value;
+ });
+ callback && callback(null);
+ return null;
+}
+
+async function _multiGet(keys: KeysType, callback: ResultCallbackType) {
+ const values = keys.map(key => [
+ key,
+ asMock.__INTERNAL_MOCK_STORAGE__[key] || null,
+ ]);
+ callback && callback(null, values);
+
+ return values;
+}
+
+async function _multiRemove(keys: KeysType, callback: CallbackType) {
+ keys.forEach(key => {
+ if (asMock.__INTERNAL_MOCK_STORAGE__[key]) {
+ delete asMock.__INTERNAL_MOCK_STORAGE__[key];
+ }
+ });
+
+ callback && callback(null);
+ return null;
+}
+
+async function _clear(callback: CallbackType) {
+ asMock.__INTERNAL_MOCK_STORAGE__ = {};
+
+ callback && callback(null);
+
+ return null;
+}
+
+async function _multiMerge(
+ keyValuePairs: KeyValueType,
+ callback: CallbackType,
+) {
+ keyValuePairs.forEach(keyValue => {
+ const key = keyValue[0];
+ const value = JSON.parse(keyValue[1]);
+
+ const oldValue = JSON.parse(asMock.__INTERNAL_MOCK_STORAGE__[key]);
+
+ const processedValue = JSON.stringify(_deepMergeInto(oldValue, value));
+
+ asMock.__INTERNAL_MOCK_STORAGE__[key] = processedValue;
+ });
+
+ callback && callback(null);
+ return null;
+}
+
+const _isObject = obj => typeof obj === 'object' && !Array.isArray(obj);
+const _deepMergeInto = (oldObject, newObject) => {
+ const newKeys = Object.keys(newObject);
+ const mergedObject = oldObject;
+
+ newKeys.forEach(key => {
+ const oldValue = mergedObject[key];
+ const newValue = newObject[key];
+
+ if (_isObject(oldValue) && _isObject(newValue)) {
+ mergedObject[key] = _deepMergeInto(oldValue, newValue);
+ } else {
+ mergedObject[key] = newValue;
+ }
+ });
+
+ return mergedObject;
+};
+
+export default asMock;
diff --git a/lib/AsyncStorage.js b/lib/AsyncStorage.js
index b3298ae7..48e52828 100644
--- a/lib/AsyncStorage.js
+++ b/lib/AsyncStorage.js
@@ -17,14 +17,20 @@ const RCTAsyncStorage =
NativeModules.RNC_AsyncSQLiteDBStorage || NativeModules.RNCAsyncStorage;
if (!RCTAsyncStorage) {
- throw new Error(`@RNCommunity/AsyncStorage: NativeModule.RCTAsyncStorage is null.
+ throw new Error(`[@RNC/AsyncStorage]: NativeModule: AsyncStorage is null.
To fix this issue try these steps:
+
• Run \`react-native link @react-native-community/async-storage\` in the project root.
- • Rebuild and re-run the app.
- • Restart the packager with \`--clearCache\` flag.
+
+ • Rebuild and restart the app.
+
+ • Run the packager with \`--clearCache\` flag.
+
• If you are using CocoaPods on iOS, run \`pod install\` in the \`ios\` directory and then rebuild and re-run the app.
+ • If this happens while testing with Jest, check out docs how to integrate AsyncStorage with it: https://github.com/react-native-community/react-native-async-storage/blob/master/docs/Jest-integration.md
+
If none of these fix the issue, please open an issue on the Github repository: https://github.com/react-native-community/react-native-async-storage/issues
`);
}
diff --git a/lib/hooks.js b/lib/hooks.js
index 3508c8df..adb1d03f 100644
--- a/lib/hooks.js
+++ b/lib/hooks.js
@@ -1,3 +1,7 @@
+/**
+ * @format
+ */
+
import AsyncStorage from './AsyncStorage';
type AsyncStorageHook = {
diff --git a/lib/index.js b/lib/index.js
index b1752582..bd38f085 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,2 +1,8 @@
-export default from './AsyncStorage';
-export { useAsyncStorage } from './hooks';
+/**
+ * @format
+ */
+
+import AsyncStorage from './AsyncStorage';
+
+export default AsyncStorage;
+export {useAsyncStorage} from './hooks';
diff --git a/package.json b/package.json
index 661378b0..ceffac91 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,8 @@
"semantic-release": "15.13.3"
},
"jest": {
- "preset": "react-native"
+ "preset": "react-native",
+ "setupFiles": ["./example/jest.setup.js"]
},
"detox": {
"test-runner": "jest",