From c503b78badd18f4c8b4188580b3c5b74b6a246bf Mon Sep 17 00:00:00 2001 From: Eduardo Date: Tue, 15 Oct 2024 16:54:49 +0200 Subject: [PATCH] Adding conflict resolver for delete comment --- src/libs/Network/SequentialQueue.ts | 11 +- src/libs/actions/PersistedRequests.ts | 17 +- src/libs/actions/Report.ts | 61 +++- src/types/onyx/Request.ts | 22 +- tests/actions/ReportTest.ts | 471 ++++++++++++++++++++++++++ tests/unit/PersistedRequests.ts | 4 +- 6 files changed, 577 insertions(+), 9 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 35c7b2bf779a..a7cb948a1242 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -96,7 +96,7 @@ function process(): Promise { pause(); } - PersistedRequests.remove(requestToProcess); + PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); RequestThrottle.clear(); return process(); }) @@ -104,7 +104,7 @@ function process(): Promise { // On sign out we cancel any in flight requests from the user. Since that user is no longer signed in their requests should not be retried. // Duplicate records don't need to be retried as they just mean the record already exists on the server if (error.name === CONST.ERROR.REQUEST_CANCELLED || error.message === CONST.ERROR.DUPLICATE_RECORD) { - PersistedRequests.remove(requestToProcess); + PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); RequestThrottle.clear(); return process(); } @@ -113,7 +113,7 @@ function process(): Promise { .then(process) .catch(() => { Onyx.update(requestToProcess.failureData ?? []); - PersistedRequests.remove(requestToProcess); + PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); RequestThrottle.clear(); return process(); }); @@ -220,6 +220,11 @@ function push(newRequest: OnyxRequest) { PersistedRequests.save(newRequest); } else if (conflictAction.type === 'replace') { PersistedRequests.update(conflictAction.index, newRequest); + } else if (conflictAction.type === 'delete') { + PersistedRequests.deleteRequestsByIndices(conflictAction.indices); + if (conflictAction.pushNewRequest) { + PersistedRequests.save(newRequest); + } } else { Log.info(`[SequentialQueue] No action performed to command ${newRequest.command} and it will be ignored.`); } diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index fc14e8c2303b..10003b8b4b5e 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -53,7 +53,7 @@ function save(requestToPersist: Request) { }); } -function remove(requestToRemove: Request) { +function endRequestAndRemoveFromQueue(requestToRemove: Request) { ongoingRequest = null; /** * We only remove the first matching request because the order of requests matters. @@ -76,6 +76,19 @@ function remove(requestToRemove: Request) { }); } +function deleteRequestsByIndices(indices: number[]) { + // Create a Set from the indices array for efficient lookup + const indicesSet = new Set(indices); + + // Create a new array excluding elements at the specified indices + persistedRequests = persistedRequests.filter((_, index) => !indicesSet.has(index)); + + // Update the persisted requests in storage or state as necessary + Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests).then(() => { + Log.info(`Multiple (${indices.length}) requests removed from the queue. Queue length is ${persistedRequests.length}`); + }); +} + function update(oldRequestIndex: number, newRequest: Request) { const requests = [...persistedRequests]; requests.splice(oldRequestIndex, 1, newRequest); @@ -131,4 +144,4 @@ function getOngoingRequest(): Request | null { return ongoingRequest; } -export {clear, save, getAll, remove, update, getLength, getOngoingRequest, processNextRequest, updateOngoingRequest, rollbackOngoingRequest}; +export {clear, save, getAll, endRequestAndRemoveFromQueue, update, getLength, getOngoingRequest, processNextRequest, updateOngoingRequest, rollbackOngoingRequest, deleteRequestsByIndices}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 95bd2aa0b834..b5e1825a4496 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1414,6 +1414,16 @@ function handleReportChanged(report: OnyxEntry) { } } } +const addNewMessage = new Set([WRITE_COMMANDS.ADD_COMMENT, WRITE_COMMANDS.ADD_ATTACHMENT, WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT]); + +const commentsToBeDeleted = new Set([ + WRITE_COMMANDS.ADD_COMMENT, + WRITE_COMMANDS.ADD_ATTACHMENT, + WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT, + WRITE_COMMANDS.UPDATE_COMMENT, + WRITE_COMMANDS.ADD_EMOJI_REACTION, + WRITE_COMMANDS.REMOVE_EMOJI_REACTION, +]); /** Deletes a comment from the report, basically sets it as empty string */ function deleteReportComment(reportID: string, reportAction: ReportAction) { @@ -1538,7 +1548,56 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { CachedPDFPaths.clearByKey(reportActionID); - API.write(WRITE_COMMANDS.DELETE_COMMENT, parameters, {optimisticData, successData, failureData}); + API.write( + WRITE_COMMANDS.DELETE_COMMENT, + parameters, + {optimisticData, successData, failureData}, + { + checkAndFixConflictingRequest: (persistedRequests) => { + const indices: number[] = []; + let addCommentFound = false; + + persistedRequests.forEach((request, index) => { + if (!commentsToBeDeleted.has(request.command) || request.data?.reportActionID !== reportActionID) { + return; + } + if (addNewMessage.has(request.command)) { + addCommentFound = true; + } + indices.push(index); + }); + + if (indices.length === 0) { + return { + conflictAction: { + type: 'push', + }, + }; + } + + if (addCommentFound) { + const rollbackData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, + value: { + [reportActionID]: null, + }, + }, + ]; + Onyx.update(rollbackData); + } + + return { + conflictAction: { + type: 'delete', + indices, + pushNewRequest: !addCommentFound, + }, + }; + }, + }, + ); // if we are linking to the report action, and we are deleting it, and it's not a deleted parent action, // we should navigate to its report in order to not show not found page diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 238e3a8c6a81..085100870943 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -70,6 +70,26 @@ type ConflictRequestReplace = { index: number; }; +/** + * Model of a conflict request that needs to be deleted from the request queue. + */ +type ConflictRequestDelete = { + /** + * The action to take in case of a conflict. + */ + type: 'delete'; + + /** + * The indices of the requests in the queue that are to be deleted. + */ + indices: number[]; + + /** + * A flag to mark if the new request should be pushed into the queue after deleting the conflicting requests. + */ + pushNewRequest: boolean; +}; + /** * Model of a conflict request that has to be enqueued at the end of request queue. */ @@ -97,7 +117,7 @@ type ConflictActionData = { /** * The action to take in case of a conflict. */ - conflictAction: ConflictRequestReplace | ConflictRequestPush | ConflictRequestNoAction; + conflictAction: ConflictRequestReplace | ConflictRequestDelete | ConflictRequestPush | ConflictRequestNoAction; }; /** diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 0ffb0ee9bc08..dc54c3730ad8 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -1,8 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {afterEach, beforeAll, beforeEach, describe, expect, it} from '@jest/globals'; +import {addSeconds, format, subMinutes} from 'date-fns'; import {toZonedTime} from 'date-fns-tz'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import {WRITE_COMMANDS} from '@libs/API/types'; +import * as EmojiUtils from '@libs/EmojiUtils'; import CONST from '@src/CONST'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import * as PersistedRequests from '@src/libs/actions/PersistedRequests'; @@ -757,4 +760,472 @@ describe('actions/Report', () => { expect(reportActionReaction?.[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); }); }); + + it('should remove AddComment and UpdateComment without sending any request when DeleteComment is set', async () => { + global.fetch = TestHelper.getGlobalFetchMock(); + + const TEST_USER_ACCOUNT_ID = 1; + const REPORT_ID = '1'; + const TEN_MINUTES_AGO = subMinutes(new Date(), 10); + const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); + + Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + + Report.addComment(REPORT_ID, 'Testing a comment'); + // Need the reportActionID to delete the comments + const newComment = PersistedRequests.getAll().at(0); + const reportActionID = (newComment?.data?.reportActionID as string) ?? '-1'; + const reportAction = TestHelper.buildTestReportComment(created, TEST_USER_ACCOUNT_ID, reportActionID); + Report.editReportComment(REPORT_ID, reportAction, 'Testing an edited comment'); + + // wait for Onyx.connect execute the callback and start processing the queue + await Promise.resolve(); + + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.PERSISTED_REQUESTS, + callback: (persistedRequests) => { + Onyx.disconnect(connection); + + expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_COMMENT); + expect(persistedRequests?.at(1)?.command).toBe(WRITE_COMMANDS.UPDATE_COMMENT); + + resolve(); + }, + }); + }); + + // Checking the Report Action exists before dleting it + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + callback: (reportActions) => { + Onyx.disconnect(connection); + expect(reportActions?.[reportActionID]).not.toBeNull(); + expect(reportActions?.[reportActionID].reportActionID).toBe(reportActionID); + resolve(); + }, + }); + }); + + Report.deleteReportComment(REPORT_ID, reportAction); + + await waitForBatchedUpdates(); + expect(PersistedRequests.getAll().length).toBe(0); + + // Checking the Report Action doesn't exist after deleting it + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + callback: (reportActions) => { + Onyx.disconnect(connection); + expect(reportActions?.[reportActionID]).toBeUndefined(); + }, + }); + + Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + await waitForBatchedUpdates(); + + // Checking no requests were or will be made + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_COMMENT, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.UPDATE_COMMENT, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.DELETE_COMMENT, 0); + }); + + it('should send DeleteComment request and remove UpdateComment accordingly', async () => { + global.fetch = TestHelper.getGlobalFetchMock(); + + const TEST_USER_ACCOUNT_ID = 1; + const REPORT_ID = '1'; + const TEN_MINUTES_AGO = subMinutes(new Date(), 10); + const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); + + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + + Report.addComment(REPORT_ID, 'Testing a comment'); + + // Need the reportActionID to delete the comments + const newComment = PersistedRequests.getAll().at(1); + const reportActionID = (newComment?.data?.reportActionID as string) ?? '-1'; + const reportAction = TestHelper.buildTestReportComment(created, TEST_USER_ACCOUNT_ID, reportActionID); + + // wait for Onyx.connect execute the callback and start processing the queue + await Promise.resolve(); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + + Report.editReportComment(REPORT_ID, reportAction, 'Testing an edited comment'); + + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.PERSISTED_REQUESTS, + callback: (persistedRequests) => { + Onyx.disconnect(connection); + + expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.UPDATE_COMMENT); + + resolve(); + }, + }); + }); + + Report.deleteReportComment(REPORT_ID, reportAction); + + await waitForBatchedUpdates(); + expect(PersistedRequests.getAll().length).toBe(1); + + Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + await waitForBatchedUpdates(); + + // Checking no requests were or will be made + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_COMMENT, 1); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.UPDATE_COMMENT, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.DELETE_COMMENT, 1); + }); + + it('should send DeleteComment request and remove UpdateComment accordingly', async () => { + global.fetch = TestHelper.getGlobalFetchMock(); + + const TEST_USER_ACCOUNT_ID = 1; + const REPORT_ID = '1'; + const TEN_MINUTES_AGO = subMinutes(new Date(), 10); + const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); + + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + + Report.addComment(REPORT_ID, 'Testing a comment'); + + // Need the reportActionID to delete the comments + const newComment = PersistedRequests.getAll().at(1); + const reportActionID = (newComment?.data?.reportActionID as string) ?? '-1'; + const reportAction = TestHelper.buildTestReportComment(created, TEST_USER_ACCOUNT_ID, reportActionID); + + // wait for Onyx.connect execute the callback and start processing the queue + await Promise.resolve(); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + + Report.editReportComment(REPORT_ID, reportAction, 'Testing an edited comment'); + + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.PERSISTED_REQUESTS, + callback: (persistedRequests) => { + Onyx.disconnect(connection); + + expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.UPDATE_COMMENT); + + resolve(); + }, + }); + }); + + Report.deleteReportComment(REPORT_ID, reportAction); + + await waitForBatchedUpdates(); + expect(PersistedRequests.getAll().length).toBe(1); + + Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + await waitForBatchedUpdates(); + + // Checking no requests were or will be made + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_COMMENT, 1); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.UPDATE_COMMENT, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.DELETE_COMMENT, 1); + }); + + it('should send not DeleteComment request and remove AddAttachment accordingly', async () => { + global.fetch = TestHelper.getGlobalFetchMock(); + + const TEST_USER_ACCOUNT_ID = 1; + const REPORT_ID = '1'; + const TEN_MINUTES_AGO = subMinutes(new Date(), 10); + const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); + + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + + const file = new File([''], 'test.txt', {type: 'text/plain'}); + Report.addAttachment(REPORT_ID, file); + + // Need the reportActionID to delete the comments + const newComment = PersistedRequests.getAll().at(0); + const reportActionID = (newComment?.data?.reportActionID as string) ?? '-1'; + const reportAction = TestHelper.buildTestReportComment(created, TEST_USER_ACCOUNT_ID, reportActionID); + + // wait for Onyx.connect execute the callback and start processing the queue + await Promise.resolve(); + + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.PERSISTED_REQUESTS, + callback: (persistedRequests) => { + Onyx.disconnect(connection); + + expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_ATTACHMENT); + resolve(); + }, + }); + }); + + // Checking the Report Action exists before dleting it + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + callback: (reportActions) => { + Onyx.disconnect(connection); + expect(reportActions?.[reportActionID]).not.toBeNull(); + expect(reportActions?.[reportActionID].reportActionID).toBe(reportActionID); + resolve(); + }, + }); + }); + + Report.deleteReportComment(REPORT_ID, reportAction); + + await waitForBatchedUpdates(); + expect(PersistedRequests.getAll().length).toBe(0); + + // Checking the Report Action doesn't exist after deleting it + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + callback: (reportActions) => { + Onyx.disconnect(connection); + expect(reportActions?.[reportActionID]).toBeUndefined(); + }, + }); + + Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + await waitForBatchedUpdates(); + + // Checking no requests were or will be made + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_ATTACHMENT, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.DELETE_COMMENT, 0); + }, 2000); + + it('should send not DeleteComment request and remove AddTextAndAttachment accordingly', async () => { + global.fetch = TestHelper.getGlobalFetchMock(); + + const TEST_USER_ACCOUNT_ID = 1; + const REPORT_ID = '1'; + const TEN_MINUTES_AGO = subMinutes(new Date(), 10); + const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); + const file = new File([''], 'test.txt', {type: 'text/plain'}); + + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + + Report.addAttachment(REPORT_ID, file, 'Attachment with comment'); + + // Need the reportActionID to delete the comments + const newComment = PersistedRequests.getAll().at(0); + const reportActionID = (newComment?.data?.reportActionID as string) ?? '-1'; + const reportAction = TestHelper.buildTestReportComment(created, TEST_USER_ACCOUNT_ID, reportActionID); + + // wait for Onyx.connect execute the callback and start processing the queue + await Promise.resolve(); + + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.PERSISTED_REQUESTS, + callback: (persistedRequests) => { + Onyx.disconnect(connection); + + expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT); + resolve(); + }, + }); + }); + + // Checking the Report Action exists before dleting it + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + callback: (reportActions) => { + Onyx.disconnect(connection); + expect(reportActions?.[reportActionID]).not.toBeNull(); + expect(reportActions?.[reportActionID].reportActionID).toBe(reportActionID); + resolve(); + }, + }); + }); + + Report.deleteReportComment(REPORT_ID, reportAction); + + await waitForBatchedUpdates(); + expect(PersistedRequests.getAll().length).toBe(0); + + // Checking the Report Action doesn't exist after deleting it + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + callback: (reportActions) => { + Onyx.disconnect(connection); + expect(reportActions?.[reportActionID]).toBeUndefined(); + }, + }); + + Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + await waitForBatchedUpdates(); + + // Checking no requests were or will be made + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.DELETE_COMMENT, 0); + }); + + it('should not send DeleteComment request and remove any Reactions accordingly', async () => { + global.fetch = TestHelper.getGlobalFetchMock(); + jest.spyOn(EmojiUtils, 'hasAccountIDEmojiReacted').mockImplementation(() => true); + const TEST_USER_ACCOUNT_ID = 1; + const REPORT_ID = '1'; + const TEN_MINUTES_AGO = subMinutes(new Date(), 10); + const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); + + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + await Promise.resolve(); + + Report.addComment(REPORT_ID, 'reactions with comment'); + // Need the reportActionID to delete the comments + const newComment = PersistedRequests.getAll().at(0); + const reportActionID = (newComment?.data?.reportActionID as string) ?? '-1'; + const reportAction = TestHelper.buildTestReportComment(created, TEST_USER_ACCOUNT_ID, reportActionID); + + await waitForBatchedUpdates(); + + Report.toggleEmojiReaction(REPORT_ID, reportAction, {name: 'smile', code: '😄'}, {}); + Report.toggleEmojiReaction( + REPORT_ID, + reportAction, + {name: 'smile', code: '😄'}, + { + smile: { + createdAt: '2024-10-14 14:58:12', + oldestTimestamp: '2024-10-14 14:58:12', + users: { + [`${TEST_USER_ACCOUNT_ID}`]: { + id: `${TEST_USER_ACCOUNT_ID}`, + oldestTimestamp: '2024-10-14 14:58:12', + skinTones: { + '-1': '2024-10-14 14:58:12', + }, + }, + }, + }, + }, + ); + + await waitForBatchedUpdates(); + + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.PERSISTED_REQUESTS, + callback: (persistedRequests) => { + Onyx.disconnect(connection); + expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_COMMENT); + expect(persistedRequests?.at(1)?.command).toBe(WRITE_COMMANDS.ADD_EMOJI_REACTION); + expect(persistedRequests?.at(2)?.command).toBe(WRITE_COMMANDS.REMOVE_EMOJI_REACTION); + resolve(); + }, + }); + }); + + // Checking the Report Action exists before deleting it + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + callback: (reportActions) => { + Onyx.disconnect(connection); + expect(reportActions?.[reportActionID]).not.toBeNull(); + expect(reportActions?.[reportActionID].reportActionID).toBe(reportActionID); + resolve(); + }, + }); + }); + + Report.deleteReportComment(REPORT_ID, reportAction); + + await waitForBatchedUpdates(); + expect(PersistedRequests.getAll().length).toBe(0); + + // Checking the Report Action doesn't exist after deleting it + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + callback: (reportActions) => { + Onyx.disconnect(connection); + expect(reportActions?.[reportActionID]).toBeUndefined(); + }, + }); + + Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + await waitForBatchedUpdates(); + + // Checking no requests were or will be made + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_COMMENT, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_EMOJI_REACTION, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.REMOVE_EMOJI_REACTION, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.DELETE_COMMENT, 0); + }); + + it('should send DeleteComment request and remove any Reactions accordingly', async () => { + global.fetch = TestHelper.getGlobalFetchMock(); + jest.spyOn(EmojiUtils, 'hasAccountIDEmojiReacted').mockImplementation(() => true); + const TEST_USER_ACCOUNT_ID = 1; + const REPORT_ID = '1'; + const TEN_MINUTES_AGO = subMinutes(new Date(), 10); + const created = format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING); + + Report.addComment(REPORT_ID, 'Attachment with comment'); + + // Need the reportActionID to delete the comments + const newComment = PersistedRequests.getAll().at(0); + const reportActionID = (newComment?.data?.reportActionID as string) ?? '-1'; + const reportAction = TestHelper.buildTestReportComment(created, TEST_USER_ACCOUNT_ID, reportActionID); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + + // wait for Onyx.connect execute the callback and start processing the queue + await Promise.resolve(); + + Report.toggleEmojiReaction(REPORT_ID, reportAction, {name: 'smile', code: '😄'}, {}); + Report.toggleEmojiReaction( + REPORT_ID, + reportAction, + {name: 'smile', code: '😄'}, + { + smile: { + createdAt: '2024-10-14 14:58:12', + oldestTimestamp: '2024-10-14 14:58:12', + users: { + [`${TEST_USER_ACCOUNT_ID}`]: { + id: `${TEST_USER_ACCOUNT_ID}`, + oldestTimestamp: '2024-10-14 14:58:12', + skinTones: { + '-1': '2024-10-14 14:58:12', + }, + }, + }, + }, + }, + ); + + await waitForBatchedUpdates(); + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.PERSISTED_REQUESTS, + callback: (persistedRequests) => { + Onyx.disconnect(connection); + expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_EMOJI_REACTION); + expect(persistedRequests?.at(1)?.command).toBe(WRITE_COMMANDS.REMOVE_EMOJI_REACTION); + resolve(); + }, + }); + }); + + Report.deleteReportComment(REPORT_ID, reportAction); + + await waitForBatchedUpdates(); + expect(PersistedRequests.getAll().length).toBe(1); + + Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + await waitForBatchedUpdates(); + + // Checking no requests were or will be made + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_COMMENT, 1); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_EMOJI_REACTION, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.REMOVE_EMOJI_REACTION, 0); + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.DELETE_COMMENT, 1); + }); }); diff --git a/tests/unit/PersistedRequests.ts b/tests/unit/PersistedRequests.ts index 7d3a7288ed90..c488b36013ad 100644 --- a/tests/unit/PersistedRequests.ts +++ b/tests/unit/PersistedRequests.ts @@ -36,7 +36,7 @@ describe('PersistedRequests', () => { }); it('remove a request from the PersistedRequests array', () => { - PersistedRequests.remove(request); + PersistedRequests.endRequestAndRemoveFromQueue(request); expect(PersistedRequests.getAll().length).toBe(0); }); @@ -84,7 +84,7 @@ describe('PersistedRequests', () => { it('when removing a request should update the persistedRequests queue and clear the ongoing request', () => { PersistedRequests.processNextRequest(); expect(PersistedRequests.getOngoingRequest()).toEqual(request); - PersistedRequests.remove(request); + PersistedRequests.endRequestAndRemoveFromQueue(request); expect(PersistedRequests.getOngoingRequest()).toBeNull(); expect(PersistedRequests.getAll().length).toBe(0); });