Skip to content

Commit 97f2fed

Browse files
committed
add unit tests
1 parent 5d3f582 commit 97f2fed

4 files changed

Lines changed: 341 additions & 0 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {renderHook} from '@testing-library/react-native';
2+
import useCreateReportRestrictionCheck from '@pages/iou/request/step/IOURequestStepReport/hooks/useCreateReportRestrictionCheck';
3+
import type * as OnyxTypes from '@src/types/onyx';
4+
5+
const mockShouldRestrict = jest.fn();
6+
7+
jest.mock('@libs/SubscriptionUtils', () => ({
8+
shouldRestrictUserBillableActions: (...args: unknown[]) => mockShouldRestrict(...args),
9+
}));
10+
11+
jest.mock('@hooks/useOnyx', () => ({
12+
__esModule: true,
13+
default: (key: string) => {
14+
if (key === 'sharedNVP_private_billingGracePeriodEnd_') {
15+
return [{end: 123}];
16+
}
17+
if (key === 'nvp_private_billingGracePeriodEnd') {
18+
return [456];
19+
}
20+
if (key === 'nvp_private_amountOwed') {
21+
return [789];
22+
}
23+
return [undefined];
24+
},
25+
}));
26+
27+
const session = {accountID: 42} as unknown as OnyxTypes.Session;
28+
const restrictedPolicy = {id: 'p1'} as unknown as OnyxTypes.Policy;
29+
30+
describe('useCreateReportRestrictionCheck', () => {
31+
beforeEach(() => {
32+
mockShouldRestrict.mockReset();
33+
});
34+
35+
it('returns false when no restriction policy is supplied (skip the subscription check)', () => {
36+
mockShouldRestrict.mockReturnValue(true);
37+
const {result} = renderHook(() => useCreateReportRestrictionCheck(session));
38+
39+
expect(result.current(undefined)).toBe(false);
40+
expect(mockShouldRestrict).not.toHaveBeenCalled();
41+
});
42+
43+
it('forwards billing/grace-period state and accountID to shouldRestrictUserBillableActions', () => {
44+
mockShouldRestrict.mockReturnValue(true);
45+
const {result} = renderHook(() => useCreateReportRestrictionCheck(session));
46+
47+
expect(result.current(restrictedPolicy)).toBe(true);
48+
expect(mockShouldRestrict).toHaveBeenCalledWith(restrictedPolicy, expect.anything(), expect.anything(), expect.anything(), 42);
49+
});
50+
51+
it('returns whatever shouldRestrictUserBillableActions returns', () => {
52+
mockShouldRestrict.mockReturnValue(false);
53+
const {result} = renderHook(() => useCreateReportRestrictionCheck(session));
54+
55+
expect(result.current(restrictedPolicy)).toBe(false);
56+
});
57+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import {renderHook} from '@testing-library/react-native';
2+
import useDistanceRequestData from '@pages/iou/request/step/IOURequestStepDistance/hooks/useDistanceRequestData';
3+
import type * as OnyxTypes from '@src/types/onyx';
4+
import type {Participant} from '@src/types/onyx/IOU';
5+
6+
const mockSetMoneyRequestAmount = jest.fn();
7+
const mockSetSplitShares = jest.fn();
8+
9+
jest.mock('@libs/actions/IOU', () => ({
10+
setMoneyRequestAmount: (...args: unknown[]) => mockSetMoneyRequestAmount(...args),
11+
}));
12+
13+
jest.mock('@libs/actions/IOU/Split', () => ({
14+
setSplitShares: (...args: unknown[]) => mockSetSplitShares(...args),
15+
}));
16+
17+
jest.mock('@libs/DistanceRequestUtils', () => ({
18+
__esModule: true,
19+
default: {
20+
getMileageRates: () => ({rate1: {currency: 'USD', rate: 60, unit: 'mi'}}),
21+
getDefaultMileageRate: () => ({currency: 'USD', rate: 60, unit: 'mi'}),
22+
getRateForP2P: () => ({currency: 'USD', rate: 100, unit: 'mi'}),
23+
getDistanceRequestAmount: (distance: number, _unit: string, rate: number) => distance * rate,
24+
},
25+
}));
26+
27+
jest.mock('@libs/TransactionUtils', () => ({
28+
getDistanceInMeters: () => 5,
29+
isCustomUnitRateIDForP2P: () => false,
30+
}));
31+
32+
type Params = Parameters<typeof useDistanceRequestData>[0];
33+
34+
const baseParams: Params = {
35+
policy: {outputCurrency: 'USD'} as unknown as OnyxTypes.Policy,
36+
personalPolicy: {outputCurrency: 'USD'},
37+
transaction: {transactionID: 'txn1'} as unknown as OnyxTypes.Transaction,
38+
customUnitRateID: 'rate1',
39+
transactionID: 'txn1',
40+
isSplitRequest: false,
41+
};
42+
43+
const personalParticipant: Participant = {accountID: 1, isPolicyExpenseChat: false};
44+
const otherParticipant: Participant = {accountID: 2, isPolicyExpenseChat: false};
45+
const policyParticipant: Participant = {accountID: 3, isPolicyExpenseChat: true};
46+
47+
describe('useDistanceRequestData', () => {
48+
beforeEach(() => {
49+
mockSetMoneyRequestAmount.mockClear();
50+
mockSetSplitShares.mockClear();
51+
});
52+
53+
it('primes setMoneyRequestAmount with the policy mileage rate × distance', () => {
54+
const {result} = renderHook(() => useDistanceRequestData(baseParams));
55+
result.current([personalParticipant]);
56+
57+
// distance(5) × rate(60) = 300
58+
expect(mockSetMoneyRequestAmount).toHaveBeenCalledWith('txn1', 300, 'USD');
59+
});
60+
61+
it('does not call setSplitShares when not a split request', () => {
62+
const {result} = renderHook(() => useDistanceRequestData(baseParams));
63+
result.current([personalParticipant, otherParticipant]);
64+
65+
expect(mockSetSplitShares).not.toHaveBeenCalled();
66+
});
67+
68+
it('calls setSplitShares for split requests against non-policy chats', () => {
69+
const {result} = renderHook(() => useDistanceRequestData({...baseParams, isSplitRequest: true}));
70+
result.current([personalParticipant, otherParticipant]);
71+
72+
expect(mockSetSplitShares).toHaveBeenCalledTimes(1);
73+
expect(mockSetSplitShares).toHaveBeenCalledWith(baseParams.transaction, 300, 'USD', [1, 2]);
74+
});
75+
76+
it('skips setSplitShares for split requests against a policy expense chat', () => {
77+
const {result} = renderHook(() => useDistanceRequestData({...baseParams, isSplitRequest: true}));
78+
result.current([personalParticipant, policyParticipant]);
79+
80+
expect(mockSetSplitShares).not.toHaveBeenCalled();
81+
});
82+
});
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import {act, renderHook} from '@testing-library/react-native';
2+
import useOdometerReadingsState from '@pages/iou/request/step/IOURequestStepDistance/hooks/useOdometerReadingsState';
3+
import CONST from '@src/CONST';
4+
import type * as OnyxTypes from '@src/types/onyx';
5+
6+
jest.mock('@libs/actions/OdometerTransactionUtils', () => ({
7+
isOdometerDraftPendingHydration: jest.fn(() => false),
8+
}));
9+
10+
const {isOdometerDraftPendingHydration} = jest.requireMock('@libs/actions/OdometerTransactionUtils') as {
11+
isOdometerDraftPendingHydration: jest.Mock;
12+
};
13+
14+
type Params = Parameters<typeof useOdometerReadingsState>[0];
15+
16+
const buildOdometerTransaction = (overrides: Partial<OnyxTypes.Transaction['comment']> = {}): OnyxTypes.Transaction =>
17+
({
18+
transactionID: 't1',
19+
iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER,
20+
comment: {
21+
odometerStart: 100,
22+
odometerEnd: 250,
23+
...overrides,
24+
},
25+
}) as unknown as OnyxTypes.Transaction;
26+
27+
const baseParams: Params = {
28+
currentTransaction: buildOdometerTransaction(),
29+
isEditing: false,
30+
selectedTab: CONST.TAB_REQUEST.DISTANCE_ODOMETER,
31+
isLoadingSelectedTab: false,
32+
hasVerifiedBlobs: true,
33+
odometerDraft: undefined,
34+
};
35+
36+
describe('useOdometerReadingsState', () => {
37+
beforeEach(() => {
38+
isOdometerDraftPendingHydration.mockReturnValue(false);
39+
});
40+
41+
it('starts with empty form state, then hydrates startReading/endReading from the transaction', () => {
42+
const {result} = renderHook(() => useOdometerReadingsState(baseParams));
43+
44+
// The sync-from-transaction effect runs synchronously after mount.
45+
expect(result.current.startReading).toBe('100');
46+
expect(result.current.endReading).toBe('250');
47+
expect(result.current.formError).toBe('');
48+
expect(result.current.startReadingRef.current).toBe('100');
49+
expect(result.current.endReadingRef.current).toBe('250');
50+
});
51+
52+
it('captures initial baseline refs once blobs are verified and no draft is pending', () => {
53+
const {result} = renderHook(() => useOdometerReadingsState(baseParams));
54+
55+
expect(result.current.hasInitializedRefs.current).toBe(true);
56+
expect(result.current.initialStartReadingRef.current).toBe('100');
57+
expect(result.current.initialEndReadingRef.current).toBe('250');
58+
});
59+
60+
it('does not snapshot the baseline while blobs are still being verified', () => {
61+
const {result} = renderHook(() => useOdometerReadingsState({...baseParams, hasVerifiedBlobs: false}));
62+
63+
expect(result.current.hasInitializedRefs.current).toBe(false);
64+
expect(result.current.initialStartReadingRef.current).toBe('');
65+
});
66+
67+
it('does not snapshot the baseline while a save-for-later draft is still pending hydration', () => {
68+
isOdometerDraftPendingHydration.mockReturnValue(true);
69+
const {result} = renderHook(() =>
70+
useOdometerReadingsState({
71+
...baseParams,
72+
odometerDraft: {odometerStartReading: 999} as unknown as OnyxTypes.OdometerDraft,
73+
}),
74+
);
75+
76+
expect(result.current.hasInitializedRefs.current).toBe(false);
77+
});
78+
79+
it('skips initialization on a non-odometer transaction unless we are editing', () => {
80+
const transaction = {
81+
transactionID: 't1',
82+
iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE,
83+
comment: {odometerStart: 100, odometerEnd: 250},
84+
} as unknown as OnyxTypes.Transaction;
85+
86+
const {result} = renderHook(() => useOdometerReadingsState({...baseParams, currentTransaction: transaction, isEditing: false}));
87+
88+
expect(result.current.hasInitializedRefs.current).toBe(false);
89+
});
90+
91+
it('resets local state and the initial-refs flag via resetOdometerLocalState', () => {
92+
const {result} = renderHook(() => useOdometerReadingsState(baseParams));
93+
94+
expect(result.current.hasInitializedRefs.current).toBe(true);
95+
96+
act(() => {
97+
result.current.resetOdometerLocalState();
98+
});
99+
100+
expect(result.current.startReading).toBe('');
101+
expect(result.current.endReading).toBe('');
102+
expect(result.current.startReadingRef.current).toBe('');
103+
expect(result.current.endReadingRef.current).toBe('');
104+
expect(result.current.initialStartReadingRef.current).toBe('');
105+
expect(result.current.initialEndReadingRef.current).toBe('');
106+
expect(result.current.hasInitializedRefs.current).toBe(false);
107+
});
108+
109+
it('clears form state and bumps inputKey when user switches away from the odometer tab', () => {
110+
const {result, rerender} = renderHook((params: Params) => useOdometerReadingsState(params), {initialProps: baseParams});
111+
112+
const initialKey = result.current.inputKey;
113+
expect(result.current.startReading).toBe('100');
114+
115+
rerender({...baseParams, selectedTab: CONST.TAB_REQUEST.DISTANCE});
116+
117+
expect(result.current.startReading).toBe('');
118+
expect(result.current.endReading).toBe('');
119+
expect(result.current.formError).toBe('');
120+
expect(result.current.inputKey).toBe(initialKey + 1);
121+
});
122+
123+
it('does not run the tab-reset effect while the selected-tab Onyx key is still loading', () => {
124+
const {result, rerender} = renderHook((params: Params) => useOdometerReadingsState(params), {
125+
initialProps: {...baseParams, isLoadingSelectedTab: true, selectedTab: undefined},
126+
});
127+
128+
const initialKey = result.current.inputKey;
129+
rerender({...baseParams, isLoadingSelectedTab: true, selectedTab: CONST.TAB_REQUEST.DISTANCE});
130+
131+
expect(result.current.inputKey).toBe(initialKey);
132+
});
133+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {renderHook} from '@testing-library/react-native';
2+
import useWaypointValidation, {isWaypointEmpty} from '@pages/iou/request/step/IOURequestStepDistance/hooks/useWaypointValidation';
3+
import type {Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
4+
5+
jest.mock('@libs/TransactionUtils', () => ({
6+
isWaypointNullIsland: (waypoint: Waypoint | undefined) => waypoint?.lat === 0 && waypoint?.lng === 0,
7+
}));
8+
9+
const startWaypoint: Waypoint = {keyForList: 'start', address: '1 Main St', lat: 40.7128, lng: -74.006};
10+
const stopWaypoint: Waypoint = {keyForList: 'stop', address: '500 Broadway', lat: 40.722, lng: -73.997};
11+
const nullIslandWaypoint: Waypoint = {keyForList: 'null', address: 'Atlantic Ocean', lat: 0, lng: 0};
12+
const emptyWaypoint: Waypoint = {keyForList: 'empty'};
13+
14+
describe('useWaypointValidation', () => {
15+
it('reports atLeastTwoDifferentWaypointsError when only the empty start/stop placeholders are present', () => {
16+
const waypoints: WaypointCollection = {waypoint0: emptyWaypoint, waypoint1: emptyWaypoint};
17+
const validatedWaypoints: WaypointCollection = {};
18+
const {result} = renderHook(() => useWaypointValidation({waypoints, validatedWaypoints}));
19+
20+
expect(result.current.nonEmptyWaypointsCount).toBe(0);
21+
expect(result.current.atLeastTwoDifferentWaypointsError).toBe(true);
22+
expect(result.current.duplicateWaypointsError).toBe(false);
23+
expect(result.current.isWaypointsNullIslandError).toBe(false);
24+
});
25+
26+
it('clears all error flags when two distinct waypoints validate successfully', () => {
27+
const waypoints: WaypointCollection = {waypoint0: startWaypoint, waypoint1: stopWaypoint};
28+
const validatedWaypoints: WaypointCollection = {waypoint0: startWaypoint, waypoint1: stopWaypoint};
29+
const {result} = renderHook(() => useWaypointValidation({waypoints, validatedWaypoints}));
30+
31+
expect(result.current.nonEmptyWaypointsCount).toBe(2);
32+
expect(result.current.atLeastTwoDifferentWaypointsError).toBe(false);
33+
expect(result.current.duplicateWaypointsError).toBe(false);
34+
expect(result.current.isWaypointsNullIslandError).toBe(false);
35+
});
36+
37+
it('reports duplicateWaypointsError when two non-empty waypoints geocode to the same location', () => {
38+
const waypoints: WaypointCollection = {waypoint0: startWaypoint, waypoint1: startWaypoint};
39+
// Geocoding deduped them down to a single validated entry.
40+
const validatedWaypoints: WaypointCollection = {waypoint0: startWaypoint};
41+
const {result} = renderHook(() => useWaypointValidation({waypoints, validatedWaypoints}));
42+
43+
expect(result.current.nonEmptyWaypointsCount).toBe(2);
44+
expect(result.current.duplicateWaypointsError).toBe(true);
45+
expect(result.current.atLeastTwoDifferentWaypointsError).toBe(true);
46+
});
47+
48+
it('reports isWaypointsNullIslandError when any waypoint sits at coordinates (0, 0)', () => {
49+
const waypoints: WaypointCollection = {waypoint0: startWaypoint, waypoint1: nullIslandWaypoint};
50+
const validatedWaypoints: WaypointCollection = {waypoint0: startWaypoint, waypoint1: nullIslandWaypoint};
51+
const {result} = renderHook(() => useWaypointValidation({waypoints, validatedWaypoints}));
52+
53+
expect(result.current.isWaypointsNullIslandError).toBe(true);
54+
});
55+
});
56+
57+
describe('isWaypointEmpty', () => {
58+
it('treats undefined as empty', () => {
59+
expect(isWaypointEmpty(undefined)).toBe(true);
60+
});
61+
62+
it('treats a waypoint with only keyForList as empty', () => {
63+
expect(isWaypointEmpty({keyForList: 'foo'})).toBe(true);
64+
});
65+
66+
it('treats a waypoint with an address as non-empty', () => {
67+
expect(isWaypointEmpty(startWaypoint)).toBe(false);
68+
});
69+
});

0 commit comments

Comments
 (0)