diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 9a0444fbf..c23eab5e9 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -612,10 +612,9 @@ function update(data: OnyxUpdate[]): Promise { return; } - const mergePromises = operations.map((operation) => { - return merge(key, operation); + operations.forEach((operation) => { + promises.push(() => merge(key, operation)); }); - promises.push(() => mergePromises.at(0) ?? Promise.resolve()); }); const snapshotPromises = OnyxUtils.updateSnapshots(data, merge); diff --git a/tests/unit/onyxTest.ts b/tests/unit/onyxTest.ts index 53fd7fc02..1c128054c 100644 --- a/tests/unit/onyxTest.ts +++ b/tests/unit/onyxTest.ts @@ -2089,436 +2089,6 @@ describe('Onyx', () => { }); }); - describe('merge', () => { - it('should replace the old value after a null merge in the top-level object when batching merges', async () => { - let result: unknown; - connection = Onyx.connect({ - key: ONYX_KEYS.COLLECTION.TEST_UPDATE, - waitForCollectionCallback: true, - callback: (value) => { - result = value; - }, - }); - - await Onyx.multiSet({ - [`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: { - id: 'entry1', - someKey: 'someValue', - }, - }); - - // Removing the entire object in this merge. - // Any subsequent changes to this key should completely replace the old value. - Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, null); - - // This change should completely replace `${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1` old value. - Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { - someKey: 'someValueChanged', - }); - - await waitForPromisesToResolve(); - - expect(result).toEqual({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: {someKey: 'someValueChanged'}}); - expect(await StorageMock.getItem(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`)).toEqual({someKey: 'someValueChanged'}); - }); - - describe('should replace the old value after a null merge in a nested property when batching merges', () => { - let result: unknown; - - beforeEach(() => { - connection = Onyx.connect({ - key: ONYX_KEYS.COLLECTION.TEST_UPDATE, - waitForCollectionCallback: true, - callback: (value) => { - result = value; - }, - }); - }); - - it('replacing old object after null merge', async () => { - const entry1: GenericDeepRecord = { - sub_entry1: { - id: 'sub_entry1', - someKey: 'someValue', - }, - }; - await Onyx.multiSet({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: entry1}); - - const entry1ExpectedResult = lodashCloneDeep(entry1); - - Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { - // Removing the "sub_entry1" object in this merge. - // Any subsequent changes to this object should completely replace the existing object in store. - sub_entry1: null, - }); - delete entry1ExpectedResult.sub_entry1; - - Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { - // This change should completely replace "sub_entry1" existing object in store. - sub_entry1: { - newKey: 'newValue', - }, - }); - entry1ExpectedResult.sub_entry1 = {newKey: 'newValue'}; - - await waitForPromisesToResolve(); - - expect(result).toEqual({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: entry1ExpectedResult}); - expect(await StorageMock.getItem(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`)).toEqual(entry1ExpectedResult); - }); - - it('setting new object after null merge', async () => { - const entry1: GenericDeepRecord = { - sub_entry1: { - id: 'sub_entry1', - someKey: 'someValue', - someNestedObject: { - someNestedKey: 'someNestedValue', - anotherNestedObject: { - anotherNestedKey: 'anotherNestedValue', - }, - }, - }, - }; - await Onyx.multiSet({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: entry1}); - - const entry1ExpectedResult = lodashCloneDeep(entry1); - - Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { - sub_entry1: { - someNestedObject: { - // Introducing a new "anotherNestedObject2" object in this merge. - anotherNestedObject2: { - anotherNestedKey2: 'anotherNestedValue2', - }, - }, - }, - }); - entry1ExpectedResult.sub_entry1.someNestedObject.anotherNestedObject2 = {anotherNestedKey2: 'anotherNestedValue2'}; - - Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { - sub_entry1: { - someNestedObject: { - // Removing the "anotherNestedObject2" object in this merge. - // This property was only introduced in a previous merge, so we don't need to care - // about an old existing value because there isn't one. - anotherNestedObject2: null, - }, - }, - }); - delete entry1ExpectedResult.sub_entry1.someNestedObject.anotherNestedObject2; - - Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { - sub_entry1: { - someNestedObject: { - // Introducing the "anotherNestedObject2" object again with this update. - anotherNestedObject2: { - newNestedKey2: 'newNestedValue2', - }, - }, - }, - }); - entry1ExpectedResult.sub_entry1.someNestedObject.anotherNestedObject2 = {newNestedKey2: 'newNestedValue2'}; - - await waitForPromisesToResolve(); - - expect(result).toEqual({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: entry1ExpectedResult}); - expect(await StorageMock.getItem(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`)).toEqual(entry1ExpectedResult); - }); - - it('setting new object after null merge of a primitive property', async () => { - const entry1: GenericDeepRecord = { - sub_entry1: { - id: 'sub_entry1', - someKey: 'someValue', - someNestedObject: { - someNestedKey: 'someNestedValue', - anotherNestedObject: { - anotherNestedKey: 'anotherNestedValue', - }, - }, - }, - }; - await Onyx.multiSet({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: entry1}); - - const entry1ExpectedResult = lodashCloneDeep(entry1); - - Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { - sub_entry1: { - someNestedObject: { - anotherNestedObject: { - // Removing the "anotherNestedKey" property in this merge. - // This property's existing value in store is a primitive value, so we don't need to care - // about it when merging new values in any next merges. - anotherNestedKey: null, - }, - }, - }, - }); - delete entry1ExpectedResult.sub_entry1.someNestedObject.anotherNestedObject.anotherNestedKey; - - Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { - sub_entry1: { - someNestedObject: { - anotherNestedObject: { - // Setting a new object to the "anotherNestedKey" property. - anotherNestedKey: { - newNestedKey: 'newNestedValue', - }, - }, - }, - }, - }); - entry1ExpectedResult.sub_entry1.someNestedObject.anotherNestedObject.anotherNestedKey = {newNestedKey: 'newNestedValue'}; - - await waitForPromisesToResolve(); - - expect(result).toEqual({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: entry1ExpectedResult}); - expect(await StorageMock.getItem(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`)).toEqual(entry1ExpectedResult); - }); - }); - - it('should remove a deeply nested null when merging an existing key', () => { - let result: unknown; - - connection = Onyx.connect({ - key: ONYX_KEYS.TEST_KEY, - initWithStoredValues: false, - callback: (value) => (result = value), - }); - - const initialValue = { - waypoints: { - 1: 'Home', - 2: 'Work', - 3: 'Gym', - }, - }; - - return Onyx.set(ONYX_KEYS.TEST_KEY, initialValue) - .then(() => { - expect(result).toEqual(initialValue); - Onyx.merge(ONYX_KEYS.TEST_KEY, { - waypoints: { - 1: 'Home', - 2: 'Work', - 3: null, - }, - }); - return waitForPromisesToResolve(); - }) - .then(() => { - expect(result).toEqual({ - waypoints: { - 1: 'Home', - 2: 'Work', - }, - }); - }); - }); - }); - - describe('set', () => { - it('should work with skipCacheCheck option', () => { - let testKeyValue: unknown; - - connection = Onyx.connect({ - key: ONYX_KEYS.TEST_KEY, - initWithStoredValues: false, - callback: (value) => { - testKeyValue = value; - }, - }); - - const testData = {id: 1, name: 'test'}; - - return Onyx.set(ONYX_KEYS.TEST_KEY, testData) - .then(() => { - expect(testKeyValue).toEqual(testData); - - return Onyx.set(ONYX_KEYS.TEST_KEY, testData, {skipCacheCheck: true}); - }) - .then(() => { - expect(testKeyValue).toEqual(testData); - }); - }); - - it('should work with skipNullRemoval option', () => { - let testKeyValue: unknown; - - connection = Onyx.connect({ - key: ONYX_KEYS.TEST_KEY, - initWithStoredValues: false, - callback: (value) => { - testKeyValue = value; - }, - }); - - const testDataWithNulls = { - id: 1, - name: 'test', - nested: { - validValue: 'keep', - nullValue: null, - undefinedValue: undefined, - }, - topLevelNull: null, - }; - - return Onyx.set(ONYX_KEYS.TEST_KEY, testDataWithNulls, {skipNullRemoval: true}).then(() => { - // The null values should be preserved - expect(testKeyValue).toEqual(testDataWithNulls); - }); - }); - - it('should remove null values by default when skipNullRemoval is not set', () => { - let testKeyValue: unknown; - - connection = Onyx.connect({ - key: ONYX_KEYS.TEST_KEY, - initWithStoredValues: false, - callback: (value) => { - testKeyValue = value; - }, - }); - - const testDataWithNulls = { - id: 1, - name: 'test', - nested: { - validValue: 'keep', - nullValue: null, - undefinedValue: undefined, - }, - topLevelNull: null, - }; - - // Set value without skipNullRemoval (default behavior) - return Onyx.set(ONYX_KEYS.TEST_KEY, testDataWithNulls).then(() => { - // The null values should be removed - expect(testKeyValue).toEqual({ - id: 1, - name: 'test', - nested: { - validValue: 'keep', - }, - }); - }); - }); - - it('should work with both skipCacheCheck and skipNullRemoval options', () => { - let testKeyValue: unknown; - - connection = Onyx.connect({ - key: ONYX_KEYS.TEST_KEY, - initWithStoredValues: false, - callback: (value) => { - testKeyValue = value; - }, - }); - - const testDataWithNulls = { - id: 1, - name: 'test', - computed: null, - nested: { - result: null, - valid: 'keep', - }, - }; - - Onyx.set(ONYX_KEYS.TEST_KEY, testDataWithNulls, { - skipCacheCheck: true, - skipNullRemoval: true, - }).then(() => { - // The null values should be preserved - expect(testKeyValue).toEqual(testDataWithNulls); - }); - }); - }); - - describe('setCollection', () => { - it('should replace all existing collection members with new values and remove old ones', async () => { - let result: OnyxCollection; - const routeA = `${ONYX_KEYS.COLLECTION.ROUTES}A`; - const routeB = `${ONYX_KEYS.COLLECTION.ROUTES}B`; - const routeB1 = `${ONYX_KEYS.COLLECTION.ROUTES}B1`; - const routeC = `${ONYX_KEYS.COLLECTION.ROUTES}C`; - - connection = Onyx.connect({ - key: ONYX_KEYS.COLLECTION.ROUTES, - initWithStoredValues: false, - callback: (value) => (result = value), - waitForCollectionCallback: true, - }); - - // Set initial collection state - await Onyx.mergeCollection(ONYX_KEYS.COLLECTION.ROUTES, { - [routeA]: {name: 'Route A'}, - [routeB1]: {name: 'Route B1'}, - [routeC]: {name: 'Route C'}, - } as GenericCollection); - - // Replace with new collection data - await Onyx.setCollection(ONYX_KEYS.COLLECTION.ROUTES, { - [routeA]: {name: 'New Route A'}, - [routeB]: {name: 'New Route B'}, - [routeC]: {name: 'New Route C'}, - } as GenericCollection); - - expect(result).toEqual({ - [routeA]: {name: 'New Route A'}, - [routeB]: {name: 'New Route B'}, - [routeC]: {name: 'New Route C'}, - }); - }); - - it('should replace the collection with empty values', async () => { - let result: OnyxCollection; - const routeA = `${ONYX_KEYS.COLLECTION.ROUTES}A`; - - connection = Onyx.connect({ - key: ONYX_KEYS.COLLECTION.ROUTES, - initWithStoredValues: false, - callback: (value) => (result = value), - waitForCollectionCallback: true, - }); - - await Onyx.mergeCollection(ONYX_KEYS.COLLECTION.ROUTES, { - [routeA]: {name: 'Route A'}, - } as GenericCollection); - - await Onyx.setCollection(ONYX_KEYS.COLLECTION.ROUTES, {} as GenericCollection); - - expect(result).toEqual({}); - }); - - it('should reject collection items with invalid keys', async () => { - let result: OnyxCollection; - const routeA = `${ONYX_KEYS.COLLECTION.ROUTES}A`; - const invalidRoute = 'invalid_route'; - - connection = Onyx.connect({ - key: ONYX_KEYS.COLLECTION.ROUTES, - initWithStoredValues: false, - callback: (value) => (result = value), - waitForCollectionCallback: true, - }); - - await Onyx.mergeCollection(ONYX_KEYS.COLLECTION.ROUTES, { - [routeA]: {name: 'Route A'}, - } as GenericCollection); - - await Onyx.setCollection(ONYX_KEYS.COLLECTION.ROUTES, { - [invalidRoute]: {name: 'Invalid Route'}, - } as GenericCollection); - - expect(result).toEqual({ - [routeA]: {name: 'Route A'}, - }); - }); - }); - it('should properly handle setCollection operations in update()', () => { const routeA = `${ONYX_KEYS.COLLECTION.ROUTES}A`; const routeB = `${ONYX_KEYS.COLLECTION.ROUTES}B`; @@ -2618,6 +2188,436 @@ describe('Onyx', () => { }); }); + describe('merge', () => { + it('should replace the old value after a null merge in the top-level object when batching merges', async () => { + let result: unknown; + connection = Onyx.connect({ + key: ONYX_KEYS.COLLECTION.TEST_UPDATE, + waitForCollectionCallback: true, + callback: (value) => { + result = value; + }, + }); + + await Onyx.multiSet({ + [`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: { + id: 'entry1', + someKey: 'someValue', + }, + }); + + // Removing the entire object in this merge. + // Any subsequent changes to this key should completely replace the old value. + Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, null); + + // This change should completely replace `${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1` old value. + Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { + someKey: 'someValueChanged', + }); + + await waitForPromisesToResolve(); + + expect(result).toEqual({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: {someKey: 'someValueChanged'}}); + expect(await StorageMock.getItem(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`)).toEqual({someKey: 'someValueChanged'}); + }); + + describe('should replace the old value after a null merge in a nested property when batching merges', () => { + let result: unknown; + + beforeEach(() => { + connection = Onyx.connect({ + key: ONYX_KEYS.COLLECTION.TEST_UPDATE, + waitForCollectionCallback: true, + callback: (value) => { + result = value; + }, + }); + }); + + it('replacing old object after null merge', async () => { + const entry1: GenericDeepRecord = { + sub_entry1: { + id: 'sub_entry1', + someKey: 'someValue', + }, + }; + await Onyx.multiSet({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: entry1}); + + const entry1ExpectedResult = lodashCloneDeep(entry1); + + Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { + // Removing the "sub_entry1" object in this merge. + // Any subsequent changes to this object should completely replace the existing object in store. + sub_entry1: null, + }); + delete entry1ExpectedResult.sub_entry1; + + Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { + // This change should completely replace "sub_entry1" existing object in store. + sub_entry1: { + newKey: 'newValue', + }, + }); + entry1ExpectedResult.sub_entry1 = {newKey: 'newValue'}; + + await waitForPromisesToResolve(); + + expect(result).toEqual({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: entry1ExpectedResult}); + expect(await StorageMock.getItem(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`)).toEqual(entry1ExpectedResult); + }); + + it('setting new object after null merge', async () => { + const entry1: GenericDeepRecord = { + sub_entry1: { + id: 'sub_entry1', + someKey: 'someValue', + someNestedObject: { + someNestedKey: 'someNestedValue', + anotherNestedObject: { + anotherNestedKey: 'anotherNestedValue', + }, + }, + }, + }; + await Onyx.multiSet({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: entry1}); + + const entry1ExpectedResult = lodashCloneDeep(entry1); + + Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { + sub_entry1: { + someNestedObject: { + // Introducing a new "anotherNestedObject2" object in this merge. + anotherNestedObject2: { + anotherNestedKey2: 'anotherNestedValue2', + }, + }, + }, + }); + entry1ExpectedResult.sub_entry1.someNestedObject.anotherNestedObject2 = {anotherNestedKey2: 'anotherNestedValue2'}; + + Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { + sub_entry1: { + someNestedObject: { + // Removing the "anotherNestedObject2" object in this merge. + // This property was only introduced in a previous merge, so we don't need to care + // about an old existing value because there isn't one. + anotherNestedObject2: null, + }, + }, + }); + delete entry1ExpectedResult.sub_entry1.someNestedObject.anotherNestedObject2; + + Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { + sub_entry1: { + someNestedObject: { + // Introducing the "anotherNestedObject2" object again with this update. + anotherNestedObject2: { + newNestedKey2: 'newNestedValue2', + }, + }, + }, + }); + entry1ExpectedResult.sub_entry1.someNestedObject.anotherNestedObject2 = {newNestedKey2: 'newNestedValue2'}; + + await waitForPromisesToResolve(); + + expect(result).toEqual({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: entry1ExpectedResult}); + expect(await StorageMock.getItem(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`)).toEqual(entry1ExpectedResult); + }); + + it('setting new object after null merge of a primitive property', async () => { + const entry1: GenericDeepRecord = { + sub_entry1: { + id: 'sub_entry1', + someKey: 'someValue', + someNestedObject: { + someNestedKey: 'someNestedValue', + anotherNestedObject: { + anotherNestedKey: 'anotherNestedValue', + }, + }, + }, + }; + await Onyx.multiSet({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: entry1}); + + const entry1ExpectedResult = lodashCloneDeep(entry1); + + Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { + sub_entry1: { + someNestedObject: { + anotherNestedObject: { + // Removing the "anotherNestedKey" property in this merge. + // This property's existing value in store is a primitive value, so we don't need to care + // about it when merging new values in any next merges. + anotherNestedKey: null, + }, + }, + }, + }); + delete entry1ExpectedResult.sub_entry1.someNestedObject.anotherNestedObject.anotherNestedKey; + + Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, { + sub_entry1: { + someNestedObject: { + anotherNestedObject: { + // Setting a new object to the "anotherNestedKey" property. + anotherNestedKey: { + newNestedKey: 'newNestedValue', + }, + }, + }, + }, + }); + entry1ExpectedResult.sub_entry1.someNestedObject.anotherNestedObject.anotherNestedKey = {newNestedKey: 'newNestedValue'}; + + await waitForPromisesToResolve(); + + expect(result).toEqual({[`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`]: entry1ExpectedResult}); + expect(await StorageMock.getItem(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`)).toEqual(entry1ExpectedResult); + }); + }); + + it('should remove a deeply nested null when merging an existing key', () => { + let result: unknown; + + connection = Onyx.connect({ + key: ONYX_KEYS.TEST_KEY, + initWithStoredValues: false, + callback: (value) => (result = value), + }); + + const initialValue = { + waypoints: { + 1: 'Home', + 2: 'Work', + 3: 'Gym', + }, + }; + + return Onyx.set(ONYX_KEYS.TEST_KEY, initialValue) + .then(() => { + expect(result).toEqual(initialValue); + Onyx.merge(ONYX_KEYS.TEST_KEY, { + waypoints: { + 1: 'Home', + 2: 'Work', + 3: null, + }, + }); + return waitForPromisesToResolve(); + }) + .then(() => { + expect(result).toEqual({ + waypoints: { + 1: 'Home', + 2: 'Work', + }, + }); + }); + }); + }); + + describe('set', () => { + it('should work with skipCacheCheck option', () => { + let testKeyValue: unknown; + + connection = Onyx.connect({ + key: ONYX_KEYS.TEST_KEY, + initWithStoredValues: false, + callback: (value) => { + testKeyValue = value; + }, + }); + + const testData = {id: 1, name: 'test'}; + + return Onyx.set(ONYX_KEYS.TEST_KEY, testData) + .then(() => { + expect(testKeyValue).toEqual(testData); + + return Onyx.set(ONYX_KEYS.TEST_KEY, testData, {skipCacheCheck: true}); + }) + .then(() => { + expect(testKeyValue).toEqual(testData); + }); + }); + + it('should work with skipNullRemoval option', () => { + let testKeyValue: unknown; + + connection = Onyx.connect({ + key: ONYX_KEYS.TEST_KEY, + initWithStoredValues: false, + callback: (value) => { + testKeyValue = value; + }, + }); + + const testDataWithNulls = { + id: 1, + name: 'test', + nested: { + validValue: 'keep', + nullValue: null, + undefinedValue: undefined, + }, + topLevelNull: null, + }; + + return Onyx.set(ONYX_KEYS.TEST_KEY, testDataWithNulls, {skipNullRemoval: true}).then(() => { + // The null values should be preserved + expect(testKeyValue).toEqual(testDataWithNulls); + }); + }); + + it('should remove null values by default when skipNullRemoval is not set', () => { + let testKeyValue: unknown; + + connection = Onyx.connect({ + key: ONYX_KEYS.TEST_KEY, + initWithStoredValues: false, + callback: (value) => { + testKeyValue = value; + }, + }); + + const testDataWithNulls = { + id: 1, + name: 'test', + nested: { + validValue: 'keep', + nullValue: null, + undefinedValue: undefined, + }, + topLevelNull: null, + }; + + // Set value without skipNullRemoval (default behavior) + return Onyx.set(ONYX_KEYS.TEST_KEY, testDataWithNulls).then(() => { + // The null values should be removed + expect(testKeyValue).toEqual({ + id: 1, + name: 'test', + nested: { + validValue: 'keep', + }, + }); + }); + }); + + it('should work with both skipCacheCheck and skipNullRemoval options', () => { + let testKeyValue: unknown; + + connection = Onyx.connect({ + key: ONYX_KEYS.TEST_KEY, + initWithStoredValues: false, + callback: (value) => { + testKeyValue = value; + }, + }); + + const testDataWithNulls = { + id: 1, + name: 'test', + computed: null, + nested: { + result: null, + valid: 'keep', + }, + }; + + Onyx.set(ONYX_KEYS.TEST_KEY, testDataWithNulls, { + skipCacheCheck: true, + skipNullRemoval: true, + }).then(() => { + // The null values should be preserved + expect(testKeyValue).toEqual(testDataWithNulls); + }); + }); + }); + + describe('setCollection', () => { + it('should replace all existing collection members with new values and remove old ones', async () => { + let result: OnyxCollection; + const routeA = `${ONYX_KEYS.COLLECTION.ROUTES}A`; + const routeB = `${ONYX_KEYS.COLLECTION.ROUTES}B`; + const routeB1 = `${ONYX_KEYS.COLLECTION.ROUTES}B1`; + const routeC = `${ONYX_KEYS.COLLECTION.ROUTES}C`; + + connection = Onyx.connect({ + key: ONYX_KEYS.COLLECTION.ROUTES, + initWithStoredValues: false, + callback: (value) => (result = value), + waitForCollectionCallback: true, + }); + + // Set initial collection state + await Onyx.mergeCollection(ONYX_KEYS.COLLECTION.ROUTES, { + [routeA]: {name: 'Route A'}, + [routeB1]: {name: 'Route B1'}, + [routeC]: {name: 'Route C'}, + } as GenericCollection); + + // Replace with new collection data + await Onyx.setCollection(ONYX_KEYS.COLLECTION.ROUTES, { + [routeA]: {name: 'New Route A'}, + [routeB]: {name: 'New Route B'}, + [routeC]: {name: 'New Route C'}, + } as GenericCollection); + + expect(result).toEqual({ + [routeA]: {name: 'New Route A'}, + [routeB]: {name: 'New Route B'}, + [routeC]: {name: 'New Route C'}, + }); + }); + + it('should replace the collection with empty values', async () => { + let result: OnyxCollection; + const routeA = `${ONYX_KEYS.COLLECTION.ROUTES}A`; + + connection = Onyx.connect({ + key: ONYX_KEYS.COLLECTION.ROUTES, + initWithStoredValues: false, + callback: (value) => (result = value), + waitForCollectionCallback: true, + }); + + await Onyx.mergeCollection(ONYX_KEYS.COLLECTION.ROUTES, { + [routeA]: {name: 'Route A'}, + } as GenericCollection); + + await Onyx.setCollection(ONYX_KEYS.COLLECTION.ROUTES, {} as GenericCollection); + + expect(result).toEqual({}); + }); + + it('should reject collection items with invalid keys', async () => { + let result: OnyxCollection; + const routeA = `${ONYX_KEYS.COLLECTION.ROUTES}A`; + const invalidRoute = 'invalid_route'; + + connection = Onyx.connect({ + key: ONYX_KEYS.COLLECTION.ROUTES, + initWithStoredValues: false, + callback: (value) => (result = value), + waitForCollectionCallback: true, + }); + + await Onyx.mergeCollection(ONYX_KEYS.COLLECTION.ROUTES, { + [routeA]: {name: 'Route A'}, + } as GenericCollection); + + await Onyx.setCollection(ONYX_KEYS.COLLECTION.ROUTES, { + [invalidRoute]: {name: 'Invalid Route'}, + } as GenericCollection); + + expect(result).toEqual({ + [routeA]: {name: 'Route A'}, + }); + }); + }); + describe('skippable collection member ids', () => { it('should skip the collection member id value when using Onyx.set()', async () => { let testKeyValue: unknown;