Skip to content
Open
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
7 changes: 6 additions & 1 deletion src/hooks/useReportActionsVisibility.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {reportVisibleActionsSelector} from '@selectors/ReportAction';
import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils';
import {isCreatedAction, isDeletedParentAction, isIOUActionMatchingTransactionList, isReportActionVisible} from '@libs/ReportActionsUtils';
import {isConciergeChatReport} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReportAction} from '@src/types/onyx';
import type {VisibleReportActionsDerivedValue} from '@src/types/onyx/DerivedValues';
import useConciergeSidePanelReportActions from './useConciergeSidePanelReportActions';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
import useIsInSidePanel from './useIsInSidePanel';
Expand Down Expand Up @@ -58,7 +60,10 @@ function useReportActionsVisibility({

const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS);
const [reportVisibleActions] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {
selector: reportVisibleActionsSelector(reportID),
});
const visibleReportActionsData: VisibleReportActionsDerivedValue | undefined = reportID && reportVisibleActions ? {[reportID]: reportVisibleActions} : undefined;

const isInSidePanel = useIsInSidePanel();
const isConciergeSidePanel = isInSidePanel && isConciergeChatReport(report, conciergeReportID);
Expand Down
8 changes: 7 additions & 1 deletion src/selectors/ReportAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import type {OnyxEntry} from 'react-native-onyx';
import {filterOutDeprecatedReportActions, getLinkedTransactionID, getSortedReportActions, isActionOfType} from '@libs/ReportActionsUtils';
import CONST from '@src/CONST';
import type {ReportAction, ReportActions} from '@src/types/onyx';
import type {VisibleReportActionsDerivedValue} from '@src/types/onyx/DerivedValues';

/**
* Module-level selector factory that scopes the VISIBLE_REPORT_ACTIONS derived value to a single report
*/
const reportVisibleActionsSelector = (reportID: string | undefined) => (data: VisibleReportActionsDerivedValue | undefined) => (reportID ? data?.[reportID] : undefined);

function getParentReportActionSelector(parentReportActions: OnyxEntry<ReportActions>, parentReportActionID?: string): OnyxEntry<ReportAction> {
if (!parentReportActions || !parentReportActionID) {
Expand Down Expand Up @@ -83,4 +89,4 @@ function getReceiptScanFailedIOUActionDataSelector(
};
}

export {getParentReportActionSelector, getLastClosedReportAction, getReportActionByIDSelector, getReceiptScanFailedIOUActionDataSelector};
export {getParentReportActionSelector, getLastClosedReportAction, getReportActionByIDSelector, getReceiptScanFailedIOUActionDataSelector, reportVisibleActionsSelector};
124 changes: 124 additions & 0 deletions tests/unit/useReportActionsVisibilityTest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {act, renderHook} from '@testing-library/react-native';
import type {ReactNode} from 'react';
import Onyx from 'react-native-onyx';
import OnyxListItemProvider from '@components/OnyxListItemProvider';
import useReportActionsVisibility from '@hooks/useReportActionsVisibility';
import initOnyxDerivedValues from '@userActions/OnyxDerived';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReportActions} from '@src/types/onyx';
import {getFakeReportAction} from '../utils/ReportTestUtils';
import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';

/**
* Guards that `useReportActionsVisibility` subscribes to VISIBLE_REPORT_ACTIONS scoped to its own
* report via `reportVisibleActionsSelector`. The test renders the real hook and counts how often a
* consumer re-renders: with the per-report selector, an unrelated report's activity must not
* re-render this report's consumer.
*/

const REPORT_A = 'reportA';
const REPORT_B = 'reportB';

/** Build a deterministic, visible ADD_COMMENT action keyed by its reportActionID. */
function buildVisibleComment(reportActionID: string) {
return getFakeReportAction(1, {reportActionID, actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT});
}

function buildActions(...reportActionIDs: string[]): ReportActions {
return Object.fromEntries(reportActionIDs.map((id) => [id, buildVisibleComment(id)]));
}

function setReportActions(reportID: string, actions: ReportActions) {
return Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, actions);
}

type HookProps = Parameters<typeof useReportActionsVisibility>[0];

function buildHookProps(reportID: string, actions: ReportActions): HookProps {
const actionsArray = Object.values(actions);
return {
reportID,
reportActions: actionsArray,
allReportActions: actionsArray,
canPerformWriteAction: true,
hasOlderActions: false,
loadOlderChats: jest.fn(),
};
}

const wrapper = ({children}: {children: ReactNode}) => <OnyxListItemProvider>{children}</OnyxListItemProvider>;

describe('useReportActionsVisibility scopes VISIBLE_REPORT_ACTIONS per report', () => {
beforeAll(() => {
Onyx.init({keys: ONYXKEYS});
initOnyxDerivedValues();
});

beforeEach(async () => {
await Onyx.clear();
TestHelper.signInWithTestUser(1, 'test@test.com');
await waitForBatchedUpdates();
});

it('does not re-render the open report (A) when an unrelated report (B) actions change', async () => {
// Given reports A and B both have visible actions in the derived value, and the hook is mounted for A
await setReportActions(REPORT_A, buildActions('a1', 'a2'));
await setReportActions(REPORT_B, buildActions('b1'));
await waitForBatchedUpdates();

let renders = 0;
const {unmount} = renderHook(
() => {
renders++;
return useReportActionsVisibility(buildHookProps(REPORT_A, buildActions('a1', 'a2')));
},
{wrapper},
);
await waitForBatchedUpdates();

const rendersBefore = renders;

// When a new action is added to the unrelated report B
await act(async () => {
await setReportActions(REPORT_B, buildActions('b2'));
await waitForBatchedUpdates();
});

// Then the hook for report A does not re-render.
// (With a whole-collection subscription, report B's write would re-render A here.)
expect(renders).toBe(rendersBefore);

unmount();
});

it('re-renders the open report (A) when its own actions change (guards against over-scoping)', async () => {
// Given the hook is mounted for report A
await setReportActions(REPORT_A, buildActions('a1'));
await waitForBatchedUpdates();

let renders = 0;
const {unmount} = renderHook(
() => {
renders++;
return useReportActionsVisibility(buildHookProps(REPORT_A, buildActions('a1')));
},
{wrapper},
);
await waitForBatchedUpdates();

const rendersBefore = renders;

// When a new action is added to report A itself
await act(async () => {
await setReportActions(REPORT_A, buildActions('a2'));
await waitForBatchedUpdates();
});

// Then the hook reacted to its own report's visibility change
expect(renders).toBeGreaterThan(rendersBefore);

unmount();
});
});
Loading