Skip to content

Commit c3608ec

Browse files
VGR-GITclaude
andcommitted
test(rewards): add coverage for campaigns reducers and selectors
Add tests for setCampaigns/setCampaignsLoading/setCampaignsError reducers, selectCampaigns/selectCampaignsLoading/selectCampaignsError state selectors, selectCampaignsRewardsEnabledRawFlag/selectCampaignsRewardsEnabledFlag feature flag selectors, and concurrent fetch guard in useRewardCampaigns hook. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2e5a30d commit c3608ec

4 files changed

Lines changed: 360 additions & 0 deletions

File tree

app/components/UI/Rewards/hooks/useRewardCampaigns.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,36 @@ describe('useRewardCampaigns', () => {
273273
expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaignsLoading(false));
274274
expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaignsError(false));
275275
});
276+
277+
it('does not trigger concurrent fetches when already loading', async () => {
278+
setupSelectorMocks();
279+
let resolveFirstFetch: (value: CampaignDto[]) => void;
280+
const firstFetchPromise = new Promise<CampaignDto[]>((resolve) => {
281+
resolveFirstFetch = resolve;
282+
});
283+
mockEngineCall.mockReturnValueOnce(firstFetchPromise);
284+
285+
const { result } = renderHook(() => useRewardCampaigns());
286+
287+
// Start first fetch without awaiting
288+
act(() => {
289+
result.current.fetchCampaigns();
290+
});
291+
292+
// Attempt concurrent fetch — should be skipped
293+
await act(async () => {
294+
await result.current.fetchCampaigns();
295+
});
296+
297+
// Engine should only be called once
298+
expect(mockEngineCall).toHaveBeenCalledTimes(1);
299+
300+
// Resolve the first fetch
301+
await act(async () => {
302+
resolveFirstFetch([]);
303+
await firstFetchPromise;
304+
});
305+
});
276306
});
277307

278308
describe('useFocusEffect integration', () => {

app/reducers/rewards/index.test.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import rewardsReducer, {
2727
setSnapshots,
2828
setSnapshotsLoading,
2929
setSnapshotsError,
30+
setCampaigns,
31+
setCampaignsLoading,
32+
setCampaignsError,
3033
bulkLinkStarted,
3134
bulkLinkAccountResult,
3235
bulkLinkCompleted,
@@ -43,6 +46,8 @@ import {
4346
RewardClaimStatus,
4447
PointsEventDto,
4548
SnapshotDto,
49+
CampaignDto,
50+
CampaignType,
4651
} from '../../core/Engine/controllers/rewards-controller/types';
4752
import { AccountGroupId } from '@metamask/account-api';
4853

@@ -4779,3 +4784,151 @@ describe('setSnapshotsError', () => {
47794784
expect(currentState.snapshotsError).toBe(true);
47804785
});
47814786
});
4787+
4788+
const mockCampaign: CampaignDto = {
4789+
id: 'campaign-1',
4790+
type: 'ONDO_HOLDING' as CampaignType,
4791+
name: 'ONDO Holding Campaign',
4792+
startDate: '2025-01-01T00:00:00.000Z',
4793+
endDate: '2027-01-01T00:00:00.000Z',
4794+
termsAndConditions: null,
4795+
excludedRegions: [],
4796+
statusLabel: 'Active',
4797+
};
4798+
4799+
describe('setCampaigns', () => {
4800+
it('should set campaigns array', () => {
4801+
const action = setCampaigns([mockCampaign]);
4802+
4803+
const state = rewardsReducer(initialState, action);
4804+
4805+
expect(state.campaigns).toEqual([mockCampaign]);
4806+
expect(state.campaignsError).toBe(false);
4807+
});
4808+
4809+
it('should replace existing campaigns with new ones', () => {
4810+
const stateWithCampaigns: RewardsState = {
4811+
...initialState,
4812+
campaigns: [mockCampaign],
4813+
};
4814+
const newCampaign: CampaignDto = { ...mockCampaign, id: 'campaign-2', name: 'New Campaign' };
4815+
const action = setCampaigns([newCampaign]);
4816+
4817+
const state = rewardsReducer(stateWithCampaigns, action);
4818+
4819+
expect(state.campaigns).toHaveLength(1);
4820+
expect(state.campaigns[0].id).toBe('campaign-2');
4821+
});
4822+
4823+
it('should set campaigns to empty array', () => {
4824+
const stateWithCampaigns: RewardsState = {
4825+
...initialState,
4826+
campaigns: [mockCampaign],
4827+
};
4828+
const action = setCampaigns([]);
4829+
4830+
const state = rewardsReducer(stateWithCampaigns, action);
4831+
4832+
expect(state.campaigns).toEqual([]);
4833+
expect(state.campaignsError).toBe(false);
4834+
});
4835+
4836+
it('should reset campaignsError when setting campaigns', () => {
4837+
const stateWithError: RewardsState = {
4838+
...initialState,
4839+
campaignsError: true,
4840+
};
4841+
const action = setCampaigns([mockCampaign]);
4842+
4843+
const state = rewardsReducer(stateWithError, action);
4844+
4845+
expect(state.campaigns).toEqual([mockCampaign]);
4846+
expect(state.campaignsError).toBe(false);
4847+
});
4848+
});
4849+
4850+
describe('setCampaignsLoading', () => {
4851+
it('should set campaignsLoading to true when no campaigns exist', () => {
4852+
const action = setCampaignsLoading(true);
4853+
4854+
const state = rewardsReducer(initialState, action);
4855+
4856+
expect(state.campaignsLoading).toBe(true);
4857+
});
4858+
4859+
it('should not set loading to true when campaigns already exist', () => {
4860+
const stateWithCampaigns: RewardsState = {
4861+
...initialState,
4862+
campaigns: [mockCampaign],
4863+
campaignsLoading: false,
4864+
};
4865+
const action = setCampaignsLoading(true);
4866+
4867+
const state = rewardsReducer(stateWithCampaigns, action);
4868+
4869+
expect(state.campaignsLoading).toBe(false);
4870+
});
4871+
4872+
it('should set campaignsLoading to false when loading is true', () => {
4873+
const stateWithLoading: RewardsState = {
4874+
...initialState,
4875+
campaignsLoading: true,
4876+
};
4877+
const action = setCampaignsLoading(false);
4878+
4879+
const state = rewardsReducer(stateWithLoading, action);
4880+
4881+
expect(state.campaignsLoading).toBe(false);
4882+
});
4883+
4884+
it('should set campaignsLoading to false even when campaigns exist', () => {
4885+
const stateWithCampaigns: RewardsState = {
4886+
...initialState,
4887+
campaigns: [mockCampaign],
4888+
campaignsLoading: true,
4889+
};
4890+
const action = setCampaignsLoading(false);
4891+
4892+
const state = rewardsReducer(stateWithCampaigns, action);
4893+
4894+
expect(state.campaignsLoading).toBe(false);
4895+
});
4896+
});
4897+
4898+
describe('setCampaignsError', () => {
4899+
it('should set campaignsError to true', () => {
4900+
const action = setCampaignsError(true);
4901+
4902+
const state = rewardsReducer(initialState, action);
4903+
4904+
expect(state.campaignsError).toBe(true);
4905+
});
4906+
4907+
it('should set campaignsError to false', () => {
4908+
const stateWithError: RewardsState = {
4909+
...initialState,
4910+
campaignsError: true,
4911+
};
4912+
const action = setCampaignsError(false);
4913+
4914+
const state = rewardsReducer(stateWithError, action);
4915+
4916+
expect(state.campaignsError).toBe(false);
4917+
});
4918+
4919+
it('should toggle error state correctly', () => {
4920+
let currentState = initialState;
4921+
4922+
let action = setCampaignsError(true);
4923+
currentState = rewardsReducer(currentState, action);
4924+
expect(currentState.campaignsError).toBe(true);
4925+
4926+
action = setCampaignsError(false);
4927+
currentState = rewardsReducer(currentState, action);
4928+
expect(currentState.campaignsError).toBe(false);
4929+
4930+
action = setCampaignsError(true);
4931+
currentState = rewardsReducer(currentState, action);
4932+
expect(currentState.campaignsError).toBe(true);
4933+
});
4934+
});

app/reducers/rewards/selectors.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,17 @@ import {
4949
selectBulkLinkAccountProgress,
5050
selectSnapshotsLoading,
5151
selectSnapshotsError,
52+
selectCampaigns,
53+
selectCampaignsLoading,
54+
selectCampaignsError,
5255
} from './selectors';
5356
import { OnboardingStep } from './types';
5457
import {
5558
RewardDto,
5659
SeasonTierDto,
5760
SeasonActivityTypeDto,
61+
CampaignDto,
62+
CampaignType,
5863
SeasonWayToEarnDto,
5964
PointsEventDto,
6065
} from '../../core/Engine/controllers/rewards-controller/types';
@@ -3174,4 +3179,105 @@ describe('Rewards selectors', () => {
31743179
});
31753180
});
31763181
});
3182+
3183+
const mockCampaign: CampaignDto = {
3184+
id: 'campaign-1',
3185+
type: 'ONDO_HOLDING' as CampaignType,
3186+
name: 'ONDO Holding Campaign',
3187+
startDate: '2025-01-01T00:00:00.000Z',
3188+
endDate: '2027-01-01T00:00:00.000Z',
3189+
termsAndConditions: null,
3190+
excludedRegions: [],
3191+
statusLabel: 'Active',
3192+
};
3193+
3194+
describe('selectCampaigns', () => {
3195+
it('returns empty array when campaigns is empty', () => {
3196+
const mockState = { rewards: { campaigns: [] } };
3197+
mockedUseSelector.mockImplementation((selector) => selector(mockState));
3198+
3199+
const { result } = renderHook(() => useSelector(selectCampaigns));
3200+
expect(result.current).toEqual([]);
3201+
});
3202+
3203+
it('returns campaigns array when campaigns exist', () => {
3204+
const mockState = { rewards: { campaigns: [mockCampaign] } };
3205+
mockedUseSelector.mockImplementation((selector) => selector(mockState));
3206+
3207+
const { result } = renderHook(() => useSelector(selectCampaigns));
3208+
expect(result.current).toEqual([mockCampaign]);
3209+
});
3210+
3211+
describe('Direct selector calls', () => {
3212+
it('returns empty array when campaigns is empty', () => {
3213+
const state = createMockRootState({ campaigns: [] });
3214+
expect(selectCampaigns(state)).toEqual([]);
3215+
});
3216+
3217+
it('returns campaigns when they exist', () => {
3218+
const state = createMockRootState({ campaigns: [mockCampaign] });
3219+
expect(selectCampaigns(state)).toEqual([mockCampaign]);
3220+
});
3221+
});
3222+
});
3223+
3224+
describe('selectCampaignsLoading', () => {
3225+
it('returns false when campaigns are not loading', () => {
3226+
const mockState = { rewards: { campaignsLoading: false } };
3227+
mockedUseSelector.mockImplementation((selector) => selector(mockState));
3228+
3229+
const { result } = renderHook(() => useSelector(selectCampaignsLoading));
3230+
expect(result.current).toBe(false);
3231+
});
3232+
3233+
it('returns true when campaigns are loading', () => {
3234+
const mockState = { rewards: { campaignsLoading: true } };
3235+
mockedUseSelector.mockImplementation((selector) => selector(mockState));
3236+
3237+
const { result } = renderHook(() => useSelector(selectCampaignsLoading));
3238+
expect(result.current).toBe(true);
3239+
});
3240+
3241+
describe('Direct selector calls', () => {
3242+
it('returns false when campaignsLoading is false', () => {
3243+
const state = createMockRootState({ campaignsLoading: false });
3244+
expect(selectCampaignsLoading(state)).toBe(false);
3245+
});
3246+
3247+
it('returns true when campaignsLoading is true', () => {
3248+
const state = createMockRootState({ campaignsLoading: true });
3249+
expect(selectCampaignsLoading(state)).toBe(true);
3250+
});
3251+
});
3252+
});
3253+
3254+
describe('selectCampaignsError', () => {
3255+
it('returns false when there is no campaigns error', () => {
3256+
const mockState = { rewards: { campaignsError: false } };
3257+
mockedUseSelector.mockImplementation((selector) => selector(mockState));
3258+
3259+
const { result } = renderHook(() => useSelector(selectCampaignsError));
3260+
expect(result.current).toBe(false);
3261+
});
3262+
3263+
it('returns true when there is a campaigns error', () => {
3264+
const mockState = { rewards: { campaignsError: true } };
3265+
mockedUseSelector.mockImplementation((selector) => selector(mockState));
3266+
3267+
const { result } = renderHook(() => useSelector(selectCampaignsError));
3268+
expect(result.current).toBe(true);
3269+
});
3270+
3271+
describe('Direct selector calls', () => {
3272+
it('returns false when campaignsError is false', () => {
3273+
const state = createMockRootState({ campaignsError: false });
3274+
expect(selectCampaignsError(state)).toBe(false);
3275+
});
3276+
3277+
it('returns true when campaignsError is true', () => {
3278+
const state = createMockRootState({ campaignsError: true });
3279+
expect(selectCampaignsError(state)).toBe(true);
3280+
});
3281+
});
3282+
});
31773283
});

0 commit comments

Comments
 (0)