diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx
index a733aa6ed3d3..370d9e698def 100644
--- a/src/pages/inbox/report/ReportActionsList.tsx
+++ b/src/pages/inbox/report/ReportActionsList.tsx
@@ -202,7 +202,11 @@ function ReportActionsListContent({reportID, conciergeChat, onLayout}: ReportAct
hasNewerActions,
});
- const persistedDraftReportAction = draftReportAction ? sortedVisibleReportActions.find((action) => action.reportActionID === draftReportAction.reportActionID) : undefined;
+ const persistedDraftReportAction = draftReportAction
+ ? (sortedAllReportActions ?? sortedVisibleReportActions).find(
+ (action) => action.reportActionID === draftReportAction.reportActionID && action.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ )
+ : undefined;
const renderedVisibleReportActions = (() => {
if (!draftReportAction) {
@@ -251,10 +255,12 @@ function ReportActionsListContent({reportID, conciergeChat, onLayout}: ReportAct
}, [clearDraft, draftReportAction, isSyntheticDraftVisible]);
useEffect(() => {
- if (!draftReportAction || !persistedDraftReportAction || getReportActionHtml(draftReportAction) === getReportActionHtml(persistedDraftReportAction)) {
+ if (!draftReportAction || !persistedDraftReportAction) {
return;
}
+ // The persisted action is the durable completion signal when a terminal Pusher event is missed.
+ // Reconcile by action ID even when its HTML is byte-identical to the last streamed draft.
revealDraftFromReportAction(persistedDraftReportAction);
}, [draftReportAction, persistedDraftReportAction, revealDraftFromReportAction]);
diff --git a/tests/ui/ReportActionsListTest.tsx b/tests/ui/ReportActionsListTest.tsx
index 0b1aac4cf61e..7b9134abf5ab 100644
--- a/tests/ui/ReportActionsListTest.tsx
+++ b/tests/ui/ReportActionsListTest.tsx
@@ -1,4 +1,4 @@
-import {render, screen} from '@testing-library/react-native';
+import {render, screen, waitFor} from '@testing-library/react-native';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useIsReportLoadPending} from '@hooks/useInFlightRequests';
@@ -380,6 +380,84 @@ describe('ReportActionsList (body)', () => {
expect(getRenderedReportActionsListItemProps(conciergeDraftReportAction).shouldDisableContextMenuForConciergeDraft).toBe(false);
expect((getCapturedListProps()?.extraData as unknown[]).at(-1)).toBe(false);
});
+
+ it('reconciles a pending draft when the matching persisted action has identical HTML', async () => {
+ const persistedReportAction = mockReportActions.at(-1);
+ const revealDraftFromReportAction = jest.fn();
+ mockUseConciergeDraft.mockReturnValue({
+ draftReportAction: persistedReportAction ?? null,
+ hasActiveDraft: true,
+ isDraftPendingCompletion: true,
+ });
+ mockUseConciergeDraftActions.mockReturnValue({
+ clearDraft: jest.fn(),
+ dispatchLocalDraftEvent: jest.fn(),
+ revealDraftFromReportAction,
+ });
+
+ renderReportActionsList();
+
+ await waitFor(() => {
+ expect(revealDraftFromReportAction).toHaveBeenCalledWith(persistedReportAction);
+ });
+ });
+
+ it('reconciles from all persisted actions when the matching action is outside the visible page', async () => {
+ const persistedReportAction: OnyxTypes.ReportAction = {
+ ...conciergeDraftReportAction,
+ reportActionID: 'persisted-outside-visible-page',
+ };
+ const revealDraftFromReportAction = jest.fn();
+ mockUsePaginatedReportActions.mockReturnValue({
+ ...defaultPaginatedReportActionsResult,
+ reportActions: mockReportActions,
+ sortedAllReportActions: [...mockReportActions, persistedReportAction],
+ });
+ mockUseConciergeDraft.mockReturnValue({
+ draftReportAction: {...persistedReportAction},
+ hasActiveDraft: true,
+ isDraftPendingCompletion: true,
+ });
+ mockUseConciergeDraftActions.mockReturnValue({
+ clearDraft: jest.fn(),
+ dispatchLocalDraftEvent: jest.fn(),
+ revealDraftFromReportAction,
+ });
+
+ renderReportActionsList();
+
+ await waitFor(() => {
+ expect(revealDraftFromReportAction).toHaveBeenCalledWith(persistedReportAction);
+ });
+ });
+
+ it('does not reconcile from a matching optimistic Concierge action', () => {
+ const optimisticReportAction: OnyxTypes.ReportAction = {
+ ...conciergeDraftReportAction,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ isOptimisticAction: true,
+ };
+ const revealDraftFromReportAction = jest.fn();
+ mockUsePaginatedReportActions.mockReturnValue({
+ ...defaultPaginatedReportActionsResult,
+ reportActions: [...mockReportActions, optimisticReportAction],
+ sortedAllReportActions: [...mockReportActions, optimisticReportAction],
+ });
+ mockUseConciergeDraft.mockReturnValue({
+ draftReportAction: optimisticReportAction,
+ hasActiveDraft: true,
+ isDraftPendingCompletion: true,
+ });
+ mockUseConciergeDraftActions.mockReturnValue({
+ clearDraft: jest.fn(),
+ dispatchLocalDraftEvent: jest.fn(),
+ revealDraftFromReportAction,
+ });
+
+ renderReportActionsList();
+
+ expect(revealDraftFromReportAction).not.toHaveBeenCalled();
+ });
});
describe('Skeleton Loading States', () => {
diff --git a/tests/unit/pages/inbox/ConciergeDraftContext.test.tsx b/tests/unit/pages/inbox/ConciergeDraftContext.test.tsx
index 51c4b8d9a3cf..29e732f75cdf 100644
--- a/tests/unit/pages/inbox/ConciergeDraftContext.test.tsx
+++ b/tests/unit/pages/inbox/ConciergeDraftContext.test.tsx
@@ -621,6 +621,42 @@ describe('ConciergeDraftContext', () => {
}
});
+ it('completes a pending draft from a matching persisted action with identical HTML', async () => {
+ const wrapper = ({children}: PropsWithChildren) => {children};
+ const {result, unmount} = renderHook(
+ () => ({
+ actions: useConciergeDraftActions(),
+ state: useConciergeDraft(),
+ }),
+ {wrapper},
+ );
+
+ try {
+ await waitFor(() => {
+ expect(Pusher.subscribe).toHaveBeenCalledTimes(6);
+ });
+ jest.useFakeTimers();
+
+ act(() => {
+ emitPusherEvent(Pusher.TYPE.CONCIERGE_DRAFT_UPDATED, createDraftEvent('OK'));
+ jest.advanceTimersByTime(100);
+ });
+
+ expect(getFirstMessageText(result.current.state.draftReportAction)).toBe('OK');
+ expect(result.current.state.isDraftPendingCompletion).toBe(true);
+
+ act(() => {
+ result.current.actions.revealDraftFromReportAction(createReportAction(SHORT_FINAL_RENDERED_HTML));
+ });
+
+ expect(getFirstMessageText(result.current.state.draftReportAction)).toBe('OK');
+ expect(result.current.state.isDraftPendingCompletion).toBe(false);
+ } finally {
+ unmount();
+ jest.useRealTimers();
+ }
+ });
+
it('applies ordered batched Pusher draft events', async () => {
const wrapper = ({children}: PropsWithChildren) => {children};
const {result, unmount} = renderHook(() => useConciergeDraft(), {wrapper});