Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/pages/inbox/report/ReportActionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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]);

Expand Down
80 changes: 79 additions & 1 deletion tests/ui/ReportActionsListTest.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/pages/inbox/ConciergeDraftContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,42 @@ describe('ConciergeDraftContext', () => {
}
});

it('completes a pending draft from a matching persisted action with identical HTML', async () => {
const wrapper = ({children}: PropsWithChildren) => <ConciergeDraftProvider reportID={REPORT_ID}>{children}</ConciergeDraftProvider>;
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) => <ConciergeDraftProvider reportID={REPORT_ID}>{children}</ConciergeDraftProvider>;
const {result, unmount} = renderHook(() => useConciergeDraft(), {wrapper});
Expand Down
Loading