diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5cb613735035..d41f0ce3fabc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -441,11 +441,11 @@ PODS: - react-native-config/App (= 1.4.5) - react-native-config/App (1.4.5): - React-Core - - react-native-document-picker (5.1.0): + - react-native-document-picker (7.1.1): - React-Core - - react-native-flipper (0.103.0): + - react-native-flipper (0.117.0): - React-Core - - react-native-image-picker (4.0.3): + - react-native-image-picker (4.1.2): - React-Core - react-native-netinfo (7.1.3): - React-Core diff --git a/package-lock.json b/package-lock.json index 7b56009f69f5..1eb3f7bd1d58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26887,6 +26887,11 @@ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz", "integrity": "sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA==" }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "immer": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", @@ -31572,6 +31577,14 @@ "type-check": "~0.4.0" } }, + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "requires": { + "immediate": "~3.0.5" + } + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -31635,6 +31648,14 @@ "json5": "^2.1.2" } }, + "localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "requires": { + "lie": "3.1.1" + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -38520,9 +38541,12 @@ "integrity": "sha512-5oiAsoW88SOYDg/0cleJ2vJDqv98FJUbFQYEnH4sdMtEn3AAT3lb7BkTGW8HO/t3Vk9VOruwxUUnO4tzuxzCsw==" }, "react-native-document-picker": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-5.1.0.tgz", - "integrity": "sha512-XMSDibp1GX0UMlVdsmAgjmf4/FJ+TCvVLWdKjV4QkTIO3TbDKsWSAS1+9jgUYcqIwQpO87SFBkvJ5cjOwx9vNA==" + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-7.1.1.tgz", + "integrity": "sha512-lEgyfl+JbU/UCDu8UdagBrC5CC71WTfqCzWyGeALLZiXGHrCJo/5BMqWbAc9PSCeiqNMnFkjcybO/kws5gMkxA==", + "requires": { + "invariant": "^2.2.4" + } }, "react-native-fast-image": { "version": "8.5.11", @@ -38530,9 +38554,9 @@ "integrity": "sha512-cNW4bIJg3nvKaheG8vGMfqCt5LMWX9MS5+wMudgKIHbGO51spRr4sgnlhVgwHLcZ5aeNOVJ8CPRxDIWKRq/0QA==" }, "react-native-flipper": { - "version": "0.103.0", - "resolved": "https://registry.npmjs.org/react-native-flipper/-/react-native-flipper-0.103.0.tgz", - "integrity": "sha512-F49O8FAPLt9BsuKQYre3ATJ8oaghc9xJu/S4WFhKqAboesFoUyB3i8EAkBFbXEgA1+Foolct3QTWQLc7KTI/tA==", + "version": "0.117.0", + "resolved": "https://registry.npmjs.org/react-native-flipper/-/react-native-flipper-0.117.0.tgz", + "integrity": "sha512-r5IdubsO1rdDhRSDIL2Xk3DSZQSwWcNgjxgz7NcD+NuBv8y7mw48LbiW0779/MTHM2K9URshGnfLT0mrI0ZBKA==", "dev": true }, "react-native-gesture-handler": { @@ -38599,9 +38623,9 @@ "integrity": "sha512-BF66XeP6dzuANsPmmFsJshM2Jyh/Mo1t8FsGc1L9Q9/sVP8MJULDabB1hms+eAoqgtyhMr5BuXV3E1hJ5U5H6Q==" }, "react-native-image-picker": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-4.0.3.tgz", - "integrity": "sha512-S4a1jE4fAPDzmah/7OVTEAXGz1/wlGyClU+spygmek5rVLERR5BgwnkX3tLP/UvMQbfdPZNUbnH0hEe7su2AZg==" + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-4.1.2.tgz", + "integrity": "sha512-e4frAekSJkOgEmL9UOLukGhjtPwHSD7qSf3Rmwk+850ofxKqZFfAIfWc9MO3UmCb6G7oB6PkckyTOGOXybrN5A==" }, "react-native-image-size": { "version": "1.1.3", @@ -38623,8 +38647,8 @@ } }, "react-native-onyx": { - "version": "git+https://github.com/Expensify/react-native-onyx.git#00bcc1520cf6cf7846d8aa40b5161bd1f407341f", - "from": "git+https://github.com/Expensify/react-native-onyx.git#00bcc1520cf6cf7846d8aa40b5161bd1f407341f", + "version": "git+https://github.com/Expensify/react-native-onyx.git#40c5b14dcc77e1193d027ecc9dd5f2563516e148", + "from": "git+https://github.com/Expensify/react-native-onyx.git#40c5b14dcc77e1193d027ecc9dd5f2563516e148", "requires": { "ascii-table": "0.0.9", "expensify-common": "git+https://github.com/Expensify/expensify-common.git#2e5cff552cf132da90a3fb9756e6b4fb6ae7b40c", diff --git a/package.json b/package.json index 4cb523f3c8a0..0afd8d735176 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "file-loader": "^6.0.0", "html-entities": "^1.3.1", "htmlparser2": "^7.2.0", + "localforage": "^1.10.0", "lodash": "4.17.21", "metro-config": "^0.64.0", "moment": "^2.27.0", @@ -83,17 +84,17 @@ "react-native-bootsplash": "^3.2.7", "react-native-collapsible": "^1.6.0", "react-native-config": "^1.4.5", - "react-native-document-picker": "^5.1.0", + "react-native-document-picker": "^7.1.1", "react-native-fast-image": "^8.5.11", "react-native-gesture-handler": "1.9.0", "react-native-google-places-autocomplete": "^2.4.1", "react-native-haptic-feedback": "^1.13.0", "react-native-image-pan-zoom": "^2.1.12", - "react-native-image-picker": "^4.0.3", + "react-native-image-picker": "^4.1.2", "react-native-image-size": "^1.1.3", "react-native-keyboard-spacer": "^0.4.1", "react-native-modal": "^11.10.0", - "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#00bcc1520cf6cf7846d8aa40b5161bd1f407341f", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#40c5b14dcc77e1193d027ecc9dd5f2563516e148", "react-native-pdf": "^6.2.2", "react-native-performance": "^2.0.0", "react-native-permissions": "^3.0.1", @@ -170,7 +171,7 @@ "pusher-js-mock": "^0.3.3", "react-hot-loader": "^4.12.21", "react-native-clean-project": "^4.0.0-alpha4.0", - "react-native-flipper": "^0.103.0", + "react-native-flipper": "^0.117.0", "react-native-performance-flipper-reporter": "^2.0.0", "react-native-svg-transformer": "^0.14.3", "react-test-renderer": "16.13.1", diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 4c7ea591f3ff..7f583d4051e3 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -145,9 +145,6 @@ export default { // Stores Workspace ID that will be tied to reimbursement account during setup REIMBURSEMENT_ACCOUNT_WORKSPACE_ID: 'reimbursementAccountWorkspaceID', - // Notifies all tabs that they should sign out and clear storage. - SHOULD_SIGN_OUT: 'shouldSignOut', - // Set when we are loading payment methods IS_LOADING_PAYMENT_METHODS: 'isLoadingPaymentMethods', diff --git a/src/components/AttachmentPicker/index.native.js b/src/components/AttachmentPicker/index.native.js index 883aa70a2ff3..0b56964d4eea 100644 --- a/src/components/AttachmentPicker/index.native.js +++ b/src/components/AttachmentPicker/index.native.js @@ -53,6 +53,7 @@ function getImagePickerOptions(type) { */ const documentPickerOptions = { type: [RNDocumentPicker.types.allFiles], + copyTo: 'cachesDirectory', }; /** @@ -66,7 +67,7 @@ function getDataForUpload(fileData) { const fileResult = { name: fileData.fileName || fileData.name || 'chat_attachment', type: fileData.type, - uri: fileData.uri, + uri: fileData.fileCopyUri || fileData.uri, size: fileData.fileSize || fileData.size, }; @@ -127,20 +128,22 @@ class AttachmentPicker extends Component { * Handles the image/document picker result and * sends the selected attachment to the caller (parent component) * - * @param {ImagePickerResponse|DocumentPickerResponse} attachment + * @param {Array} attachments * @returns {Promise} */ - pickAttachment(attachment) { - if (!attachment) { + pickAttachment(attachments = []) { + if (attachments.length === 0) { return; } - if (attachment.width === -1 || attachment.height === -1) { + const fileData = _.first(attachments); + + if (fileData.width === -1 || fileData.height === -1) { this.showImageCorruptionAlert(); return; } - return getDataForUpload(attachment).then((result) => { + return getDataForUpload(fileData).then((result) => { this.completeAttachmentSelection(result); }).catch((error) => { this.showGeneralAlert(error.message); @@ -196,7 +199,7 @@ class AttachmentPicker extends Component { } // Resolve with the first (and only) selected file - return resolve(response.assets[0]); + return resolve(response.assets); }); }); } @@ -225,7 +228,7 @@ class AttachmentPicker extends Component { /** * Launch the DocumentPicker. Results are in the same format as ImagePicker * - * @returns {Promise} + * @returns {Promise} */ showDocumentPicker() { return RNDocumentPicker.pick(documentPickerOptions).catch((error) => { diff --git a/src/libs/Network.js b/src/libs/Network.js index d7d74b889bf6..98b9c2dd037e 100644 --- a/src/libs/Network.js +++ b/src/libs/Network.js @@ -179,7 +179,9 @@ function processNetworkRequestQueue() { networkRequestQueue = _.reject(networkRequestQueue, (request) => { const shouldRetry = lodashGet(request, 'data.shouldRetry'); if (shouldRetry && request.data.persist) { - retryableRequests.push(request); + // exclude functions as they cannot be persisted + const requestToPersist = _.omit(request, val => _.isFunction(val)); + retryableRequests.push(requestToPersist); return true; } }); diff --git a/src/libs/SignoutManager.js b/src/libs/SignoutManager.js deleted file mode 100644 index 65c07e07c29a..000000000000 --- a/src/libs/SignoutManager.js +++ /dev/null @@ -1,34 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '../ONYXKEYS'; -import createCallback from './createCallback'; -import setShouldSignOut from './actions/Session/setShouldSignOut'; - -const [signoutCallback, registerSignoutCallback] = createCallback(); - -let errorMessage = ''; -let shouldSignOut = false; -Onyx.connect({ - key: ONYXKEYS.SHOULD_SIGN_OUT, - callback: (val) => { - if (!shouldSignOut && val) { - signoutCallback(errorMessage); - errorMessage = ''; - setShouldSignOut(false); - } - - shouldSignOut = val; - }, -}); - -/** - * @param {String} message - */ -function signOut(message) { - errorMessage = message; - setShouldSignOut(true); -} - -export default { - signOut, - registerSignoutCallback, -}; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 47d0ba7291a5..570aedf26e4d 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1079,7 +1079,7 @@ function fetchAllReports( * * @param {Number} reportID * @param {String} text - * @param {Object} [file] + * @param {File} [file] */ function addAction(reportID, text, file) { // Convert the comment from MD into HTML because that's how it is stored in the database @@ -1158,8 +1158,8 @@ function addAction(reportID, text, file) { API.Report_AddComment({ reportID, - reportComment: commentText, file, + reportComment: commentText, clientID: optimisticReportActionID, // The persist flag enables this request to be retried if we are offline and the app is completely killed. We do diff --git a/src/libs/actions/Session/setShouldSignOut.js b/src/libs/actions/Session/setShouldSignOut.js deleted file mode 100644 index 75acc853e873..000000000000 --- a/src/libs/actions/Session/setShouldSignOut.js +++ /dev/null @@ -1,9 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '../../../ONYXKEYS'; - -/** - * @param {Boolean} shouldSignOut - */ -export default function setShouldSignOut(shouldSignOut) { - Onyx.set(ONYXKEYS.SHOULD_SIGN_OUT, shouldSignOut); -} diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 95e8b862d8bc..be838e817111 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -1,5 +1,4 @@ import Onyx from 'react-native-onyx'; -import SignoutManager from '../SignoutManager'; import ONYXKEYS from '../../ONYXKEYS'; let currentActiveClients; @@ -38,8 +37,6 @@ function clearStorageAndRedirect(errorMessage) { }); } -SignoutManager.registerSignoutCallback(clearStorageAndRedirect); - /** * Clears the Onyx store and redirects to the sign in page. * Normally this method would live in Session.js, but that would cause a circular dependency with Network.js. @@ -47,7 +44,7 @@ SignoutManager.registerSignoutCallback(clearStorageAndRedirect); * @param {String} [errorMessage] error message to be displayed on the sign in page */ function redirectToSignIn(errorMessage) { - SignoutManager.signOut(errorMessage); + clearStorageAndRedirect(errorMessage); } export default redirectToSignIn; diff --git a/src/libs/listenToStorageEvents/index.js b/src/libs/listenToStorageEvents/index.js deleted file mode 100644 index bf061aa847fa..000000000000 --- a/src/libs/listenToStorageEvents/index.js +++ /dev/null @@ -1,23 +0,0 @@ -import Log from '../Log'; - -/** - * Listens for storage events so that multiple tabs can keep track of what - * other tabs are doing - * - * @param {Function} callback - */ -function listenToStorageEvents(callback) { - window.addEventListener('storage', (e) => { - let newValue; - try { - newValue = JSON.parse(e.newValue); - } catch (err) { - Log.hmmm('Could not parse the newValue of the storage event', {event: e.key, error: err}); - } - if (newValue !== undefined) { - callback(e.key, newValue); - } - }); -} - -export default listenToStorageEvents; diff --git a/src/libs/listenToStorageEvents/index.native.js b/src/libs/listenToStorageEvents/index.native.js deleted file mode 100644 index c1221db97d64..000000000000 --- a/src/libs/listenToStorageEvents/index.native.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Native clients don't have storage events and that's OK because - * you can't have multiple native clients open at the same time on the same - * device - */ -function listenToStorageEvents() {} - -export default listenToStorageEvents; diff --git a/src/libs/migrateOnyx.js b/src/libs/migrateOnyx.js index 72629ae833e1..726ad34abe7f 100644 --- a/src/libs/migrateOnyx.js +++ b/src/libs/migrateOnyx.js @@ -3,6 +3,7 @@ import Log from './Log'; import AddEncryptedAuthToken from './migrations/AddEncryptedAuthToken'; import RenameActiveClientsKey from './migrations/RenameActiveClientsKey'; import RenamePriorityModeKey from './migrations/RenamePriorityModeKey'; +import MoveToIndexedDB from './migrations/MoveToIndexedDB'; export default function () { const startTime = Date.now(); @@ -11,6 +12,7 @@ export default function () { return new Promise((resolve) => { // Add all migrations to an array so they are executed in order const migrationPromises = [ + MoveToIndexedDB, RenameActiveClientsKey, RenamePriorityModeKey, AddEncryptedAuthToken, diff --git a/src/libs/migrations/MoveToIndexedDB.js b/src/libs/migrations/MoveToIndexedDB.js new file mode 100644 index 000000000000..995be3e09936 --- /dev/null +++ b/src/libs/migrations/MoveToIndexedDB.js @@ -0,0 +1,76 @@ +import _ from 'underscore'; +import Onyx from 'react-native-onyx'; + +import Log from '../Log'; +import ONYXKEYS from '../../ONYXKEYS'; +import getPlatform from '../getPlatform'; +import CONST from '../../CONST'; + +/** + * Test whether the current platform is eligible for migration + * This migration is only applicable for desktop/web + * We're also skipping logged-out users as there would be nothing to migrate + * + * @returns {Boolean} + */ +function shouldMigrate() { + const isTargetPlatform = _.contains([CONST.PLATFORM.WEB, CONST.PLATFORM.DESKTOP], getPlatform()); + if (!isTargetPlatform) { + Log.info('[Migrate Onyx] Skipped migration MoveToIndexedDB (Not applicable to current platform)'); + return false; + } + + const session = window.localStorage.getItem(ONYXKEYS.SESSION); + if (!session || !session.includes('authToken')) { + Log.info('[Migrate Onyx] Skipped migration MoveToIndexedDB (Not applicable to logged out users)'); + return false; + } + + return true; +} + +/** + * Find Onyx data and move it from local storage to IndexedDB + * + * @returns {Promise} + */ +function applyMigration() { + const onyxKeys = new Set(_.values(ONYXKEYS)); + const onyxCollections = _.values(ONYXKEYS.COLLECTION); + + // Prepare a key-value dictionary of keys eligible for migration + // Targeting existing Onyx keys in local storage or any key prefixed by a collection name + const dataToMigrate = _.chain(window.localStorage) + .keys() + .filter(key => onyxKeys.has(key) || _.some(onyxCollections, collectionKey => key.startsWith(collectionKey))) + .map(key => [key, JSON.parse(window.localStorage.getItem(key))]) + .object() + .value(); + + // Move the data in Onyx and only then delete it from local storage + return Onyx.multiSet(dataToMigrate) + .then(() => _.each(dataToMigrate, (value, key) => window.localStorage.removeItem(key))); +} + +/** + * Migrate Web/Desktop storage to IndexedDB + * + * @returns {Promise} + */ +export default function () { + if (!shouldMigrate()) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + applyMigration() + .then(() => { + Log.info('[Migrate Onyx] Ran migration MoveToIndexedDB'); + resolve(); + }) + .catch((e) => { + Log.alert('[Migrate Onyx] MoveToIndexedDB failed', {error: e.message, stack: e.stack}, false); + reject(e); + }); + }); +} diff --git a/src/setup/index.js b/src/setup/index.js index 09901fcf65fe..f2b9ed0e4503 100644 --- a/src/setup/index.js +++ b/src/setup/index.js @@ -1,7 +1,6 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; -import listenToStorageEvents from '../libs/listenToStorageEvents'; import platformSetup from './platformSetup'; import * as Metrics from '../libs/Metrics'; @@ -34,9 +33,6 @@ export default function () { }, [ONYXKEYS.IS_SIDEBAR_LOADED]: false, }, - registerStorageEventListener: (onStorageEvent) => { - listenToStorageEvents(onStorageEvent); - }, }); // Perform any other platform-specific setup diff --git a/tests/unit/MigrationTest.js b/tests/unit/MigrationTest.js new file mode 100644 index 000000000000..5e5f17c6a7a3 --- /dev/null +++ b/tests/unit/MigrationTest.js @@ -0,0 +1,76 @@ +import Onyx from 'react-native-onyx'; +import _ from 'underscore'; +import CONST from '../../src/CONST'; +import getPlatform from '../../src/libs/getPlatform'; +import MoveToIndexedDB from '../../src/libs/migrations/MoveToIndexedDB'; +import ONYXKEYS from '../../src/ONYXKEYS'; + +jest.mock('../../src/libs/getPlatform'); + +// Using fake timers is causing problems with promises getting timed out +// This seems related: https://github.com/facebook/jest/issues/11876 +jest.useRealTimers(); + +describe('MoveToIndexedDb', () => { + beforeEach(() => { + jest.resetAllMocks(); + getPlatform.mockImplementation(() => CONST.PLATFORM.WEB); + jest.spyOn(Onyx, 'multiSet').mockImplementation(() => Promise.resolve()); + localStorage.clear(); + }); + + it('Should do nothing for non web/desktop platforms', () => { + // Given the migration is not running on web or desktop + getPlatform.mockImplementation(() => CONST.PLATFORM.ANDROID); + + // When the migration runs + return MoveToIndexedDB() + .then(() => { + // Then we don't expect any storage calls + expect(Onyx.multiSet).not.toHaveBeenCalled(); + }); + }); + + it('Should do nothing when there is no old session data', () => { + // Given no session in old storage medium (localStorage) + localStorage.removeItem(ONYXKEYS.SESSION); + + // When the migration runs + return MoveToIndexedDB() + .then(() => { + // Then we don't expect any storage calls + expect(Onyx.multiSet).not.toHaveBeenCalled(); + }); + }); + + it('Should migrate Onyx keys in localStorage to (new) Onyx', () => { + // Given some old data exists in storage + const data = { + [ONYXKEYS.SESSION]: {authToken: 'mock-token', loading: false}, + [ONYXKEYS.ACCOUNT]: {email: 'test@mock.com'}, + [ONYXKEYS.NETWORK]: {isOffline: true}, + }; + + _.forEach(data, (value, key) => localStorage.setItem(key, JSON.stringify(value))); + + // When the migration runs + return MoveToIndexedDB() + .then(() => { + // Then multiset should be called with all the data available in localStorage + expect(Onyx.multiSet).toHaveBeenCalledWith(data); + }); + }); + + it('Should not clear non Onyx keys from localStorage', () => { + // Given some Onyx and non-Onyx data exists in localStorage + localStorage.setItem(ONYXKEYS.SESSION, JSON.stringify({authToken: 'mock-token', loading: false})); + localStorage.setItem('non-onyx-item', 'MOCK'); + + // When the migration runs + return MoveToIndexedDB() + .then(() => { + // Then non-Onyx data should remain in localStorage + expect(localStorage.getItem('non-onyx-item')).toEqual('MOCK'); + }); + }); +}); diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index 5e4923f8517c..68a595bc299d 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -23,7 +23,6 @@ jest.useFakeTimers(); Onyx.init({ keys: ONYXKEYS, - registerStorageEventListener: () => {}, }); beforeEach(() => Onyx.clear().then(waitForPromisesToResolve)); diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index df981f3631d5..b479a3c99052 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -227,7 +227,6 @@ describe('OptionsListUtils', () => { total: '1000', }, }, - registerStorageEventListener: () => {}, }); Onyx.registerLogger(() => {}); return waitForPromisesToResolve();