From 0cda315cc6880650529827c16d81121d53b8a2f0 Mon Sep 17 00:00:00 2001 From: Philipp Kursawe Date: Tue, 29 Mar 2022 17:31:53 +0200 Subject: [PATCH] fix(useAsyncStorage): make more robust --- .gitignore | 1 + __tests__/useAsyncStorage.test.ts | 84 ++++++++++++++++++++++++ package.json | 6 +- src/hooks.ts | 36 +++++++++-- yarn.lock | 103 ++++++++++++++++++++++++++++++ 5 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 __tests__/useAsyncStorage.test.ts diff --git a/.gitignore b/.gitignore index c28b3c0e..d2d87ea8 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,4 @@ buck-out/ # builds by bob lib/ +coverage \ No newline at end of file diff --git a/__tests__/useAsyncStorage.test.ts b/__tests__/useAsyncStorage.test.ts new file mode 100644 index 00000000..5e3c5f9d --- /dev/null +++ b/__tests__/useAsyncStorage.test.ts @@ -0,0 +1,84 @@ +/** + * @format + */ +/* eslint-disable no-shadow */ +import { renderHook, act } from '@testing-library/react-hooks'; + +import AsyncStorage, { useAsyncStorage } from '../src'; + +afterEach(AsyncStorage.clear); + +function expectStableCallbacks(result: any, rerender: () => void) { + const previousFunctions = Object.values(result.current).filter( + (item) => typeof item === 'function' + ); + rerender(); + const nowFunctions = Object.values(result.current).filter( + (item) => typeof item === 'function' + ); + expect(nowFunctions).toHaveLength(previousFunctions.length); + expect(nowFunctions).toEqual(previousFunctions); +} + +describe('useAsyncStorage', () => { + it('should export only stable output', async () => { + const { result, rerender } = renderHook(() => useAsyncStorage('key')); + expect(result.error).toBeUndefined(); + expectStableCallbacks(result, rerender); + }); + + it('can read/write data to/from storage', async () => { + const newData = Math.floor(Math.random() * 1000).toString(); + const { result } = renderHook(() => useAsyncStorage('key')); + + await result.current.setItem(newData); + + const data = await result.current.getItem(); + + expect(data).toBe(newData); + }); + + it('can remove from storage', async () => { + await AsyncStorage.setItem('key', 'value'); + const { result } = renderHook(() => useAsyncStorage('key')); + await act(() => result.current.removeItem()); + expect(await AsyncStorage.getItem('key')).toBeNull(); + }); + + it.skip('should throw when mergeItem is not supported', async () => { + const { result } = renderHook(() => useAsyncStorage('key')); + const mergeItem = AsyncStorage.mergeItem; + delete AsyncStorage.mergeItem; + expect(result.current.mergeItem({})).rejects.toThrow(); + AsyncStorage.mergeItem = mergeItem; + }); + + it('can use merge with current data in storage', async () => { + let originalPerson = { + name: 'Jerry', + age: 21, + characteristics: { + hair: 'black', + eyes: 'green', + }, + }; + + const { result } = renderHook(() => useAsyncStorage('person')); + + await result.current.setItem(JSON.stringify(originalPerson)); + + originalPerson.name = 'Harry'; + originalPerson.characteristics.hair = 'red'; + // @ts-expect-error + originalPerson.characteristics.shoeSize = 40; + + await result.current.mergeItem(JSON.stringify(originalPerson)); + + const currentPerson = await result.current.getItem(); + const person = JSON.parse(currentPerson); + + expect(person).toHaveProperty('name', 'Harry'); + expect(person.characteristics).toHaveProperty('hair', 'red'); + expect(person.characteristics).toHaveProperty('shoeSize', 40); + }); +}); diff --git a/package.json b/package.json index f2cfabf9..9118e02c 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,8 @@ "@react-native-community/eslint-config": "^3.0.0", "@semantic-release/changelog": "^6.0.0", "@semantic-release/git": "^10.0.0", + "@testing-library/react-hooks": "^7.0.2", + "@types/jest": "^27.4.1", "@types/react": "^17.0.0", "@types/react-native": "^0.64.0", "concurrently": "^6.4.0", @@ -98,10 +100,10 @@ "npm/chalk": "^4.1.2" }, "jest": { - "preset": "react-native", "setupFiles": [ "./example/jest.setup.js" - ] + ], + "restoreMocks": true }, "detox": { "test-runner": "jest", diff --git a/src/hooks.ts b/src/hooks.ts index 3098a418..e3f3fc40 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,13 +1,35 @@ +import React from 'react'; import AsyncStorage from './AsyncStorage'; -import type { AsyncStorageHook } from './types'; -export function useAsyncStorage(key: string): AsyncStorageHook { - return { - getItem: (...args) => AsyncStorage.getItem(key, ...args), - setItem: (...args) => AsyncStorage.setItem(key, ...args), - mergeItem: (...args) => +export function useAsyncStorage(key: string) { + const getItem = React.useCallback( + (...args) => AsyncStorage.getItem(key, ...args), + [key] + ); + + const setItem = React.useCallback( + //@ts-ignore + (...args) => AsyncStorage.setItem(key, ...args), + [key] + ); + + const mergeItem = React.useCallback( + (...args) => + //@ts-ignore AsyncStorage.mergeItem?.(key, ...args) ?? Promise.reject('Not implemented'), - removeItem: (...args) => AsyncStorage.removeItem(key, ...args), + [key] + ); + + const removeItem = React.useCallback( + (...args) => AsyncStorage.removeItem(key, ...args), + [key] + ); + + return { + getItem, + setItem, + mergeItem, + removeItem, }; } diff --git a/yarn.lock b/yarn.lock index a4bb9e4e..9fd92896 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1084,6 +1084,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.5": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" + integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.0.0", "@babel/template@^7.16.0", "@babel/template@^7.3.3", "@babel/template@^7.8.6": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6" @@ -1860,6 +1867,9 @@ dependencies: "@octokit/openapi-types" "^9.4.0" +"@react-native-async-storage/async-storage@./src": + version "0.0.0" + "@react-native-community/cli-debugger-ui@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-5.0.1.tgz#6b1f3367b8e5211e899983065ea2e72c1901d75f" @@ -2209,6 +2219,17 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@testing-library/react-hooks@^7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz#3388d07f562d91e7f2431a4a21b5186062ecfee0" + integrity sha512-dYxpz8u9m4q1TuzfcUApqi8iFfR6R0FaMbr2hjZJy1uC8z+bO/K4v8Gs9eogGKYQop7QsrBTFkv/BCF7MzD2Cg== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/react" ">=16.9.0" + "@types/react-dom" ">=16.9.0" + "@types/react-test-renderer" ">=16.9.0" + react-error-boundary "^3.1.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -2291,6 +2312,14 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/jest@^27.4.1": + version "27.4.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d" + integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw== + dependencies: + jest-matcher-utils "^27.0.0" + pretty-format "^27.0.0" + "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -2336,6 +2365,13 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== +"@types/react-dom@>=16.9.0": + version "17.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466" + integrity sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q== + dependencies: + "@types/react" "*" + "@types/react-native@^0.64.0": version "0.64.19" resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.64.19.tgz#2b888c082ad293fa0fa6ae34c5e9457cfb38e50a" @@ -2343,6 +2379,13 @@ dependencies: "@types/react" "*" +"@types/react-test-renderer@>=16.9.0": + version "17.0.1" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b" + integrity sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^17.0.0": version "17.0.37" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959" @@ -2352,6 +2395,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@>=16.9.0": + version "17.0.39" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.39.tgz#d0f4cde092502a6db00a1cded6e6bf2abb7633ce" + integrity sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/retry@^0.12.0": version "0.12.1" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" @@ -2854,6 +2906,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0, ansi-styles@^4.3.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + ansicolors@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" @@ -4941,6 +4998,11 @@ diff-sequences@^26.6.2: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== + diff@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" @@ -7533,6 +7595,16 @@ jest-diff@^26.6.2: jest-get-type "^26.3.0" pretty-format "^26.6.2" +jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== + dependencies: + chalk "^4.0.0" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + jest-docblock@^26.0.0: version "26.0.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" @@ -7581,6 +7653,11 @@ jest-get-type@^26.3.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== + jest-haste-map@^26.5.2, jest-haste-map@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" @@ -7644,6 +7721,16 @@ jest-matcher-utils@^26.6.2: jest-get-type "^26.3.0" pretty-format "^26.6.2" +jest-matcher-utils@^27.0.0: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== + dependencies: + chalk "^4.0.0" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + jest-message-util@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" @@ -10777,6 +10864,15 @@ pretty-format@^26.4.0, pretty-format@^26.5.2, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" +pretty-format@^27.0.0, pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + proc-log@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-1.0.0.tgz#0d927307401f69ed79341e83a0b2c9a13395eb77" @@ -11058,6 +11154,13 @@ react-dom@17.0.1: object-assign "^4.1.1" scheduler "^0.20.1" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.9: version "6.0.9" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"