Skip to content

Commit

Permalink
test: add test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
Prithpal-Sooriya committed Feb 6, 2025
1 parent 92ff208 commit b3461c1
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 157 deletions.
196 changes: 75 additions & 121 deletions app/components/UI/Notification/List/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,31 @@
import React from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import {
INotification,
TRIGGER_TYPES,
} from '@metamask/notification-services-controller/notification-services';
import { Provider } from 'react-redux';
import createMockStore from 'redux-mock-store';
import NotificationsList, { NotificationsListItem } from './';
import { processNotification } from '@metamask/notification-services-controller/notification-services';
import { createMockNotificationEthSent } from '@metamask/notification-services-controller/notification-services/mocks';
import NotificationsList, {
NotificationsListItem,
useNotificationOnClick,
} from './';
import NotificationsService from '../../../../util/notifications/services/NotificationService';
import renderWithProvider, {
DeepPartial,
} from '../../../../util/test/renderWithProvider';
import renderWithProvider from '../../../../util/test/renderWithProvider';
import MOCK_NOTIFICATIONS from '../__mocks__/mock_notifications';
import initialRootState, {
backgroundState,
} from '../../../../util/test/initial-root-state';
import { RootState } from '../../../../reducers';
import { createNavigationProps } from '../../../../util/testUtils';
import {
hasNotificationModal,
hasNotificationComponents,
NotificationComponentState,
} from '../../../../util/notifications/notification-states';
import { useMarkNotificationAsRead } from '../../../../util/notifications/hooks/useNotifications';
// eslint-disable-next-line import/no-namespace
import * as Actions from '../../../../actions/notification/helpers';
import { NotificationState } from '../../../../util/notifications/notification-states/types/NotificationState';
import { NavigationProp, ParamListBase } from '@react-navigation/native';

const mockNavigation = createNavigationProps({});

const mockTrackEvent = jest.fn();
const mockCreateEventBuilder = jest.fn(() => ({
addProperties: jest.fn(() => ({
build: jest.fn(),
})),
}));

jest.mock('../../../../util/notifications/constants', () => ({
...jest.requireActual('../../../../util/notifications/constants'),
Expand All @@ -48,36 +44,16 @@ jest.mock(
}),
);

jest.mock('../../../../util/notifications/notification-states', () => ({
hasNotificationModal: jest.fn(),
hasNotificationComponents: jest.fn(),
NotificationComponentState: {},
}));

jest.mock('../../../hooks/useMetrics', () => ({
useMetrics: () => ({
trackEvent: mockTrackEvent,
createEventBuilder: mockCreateEventBuilder,
}),
MetaMetricsEvents: {
NOTIFICATION_CLICKED: 'NOTIFICATION_CLICKED',
},
}));

const navigation = {
navigate: jest.fn(),
};

const mockInitialState: DeepPartial<RootState> = {
engine: {
backgroundState: {
...backgroundState,
NotificationServicesController: {
metamaskNotificationsList: [],
},
},
},
};

jest.mock('../NotificationMenuItem', () => ({
NotificationMenuItem: {
Root: ({ children }: { children: React.ReactNode }) => (
Expand All @@ -90,26 +66,6 @@ jest.mock('../NotificationMenuItem', () => ({
},
}));

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: (fn: (state: DeepPartial<RootState>) => unknown) =>
fn(mockInitialState),
}));

function arrangeStore() {
const store = createMockStore()(initialRootState);

// Ensure dispatch mocks are handled correctly
store.dispatch = jest.fn().mockImplementation((action) => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return Promise.resolve();
});

return store;
}

function arrangeActions() {
const mockMarkNotificationAsRead = jest
.spyOn(Actions, 'markMetamaskNotificationsAsRead')
Expand All @@ -120,14 +76,6 @@ function arrangeActions() {
};
}

function arrangeHook() {
const store = arrangeStore();
const hook = renderHook(() => useMarkNotificationAsRead(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});

return hook;
}
describe('NotificationsList', () => {
it('renders correctly', () => {
const { toJSON } = renderWithProvider(
Expand Down Expand Up @@ -155,54 +103,14 @@ describe('NotificationsList', () => {
expect(toJSON()).toMatchSnapshot();
});

it('marks notification as read and not navigates if modal does not exist', async () => {
(hasNotificationModal as jest.Mock).mockReturnValue(false);
(NotificationsService.getBadgeCount as jest.Mock).mockResolvedValue(0);
const mockActions = arrangeActions();
const { result } = arrangeHook();
await act(async () => {
await result.current.markNotificationAsRead([
{
id: MOCK_NOTIFICATIONS[2].id,
type: MOCK_NOTIFICATIONS[2].type,
isRead: MOCK_NOTIFICATIONS[2].isRead,
},
]);
});

expect(mockActions.mockMarkNotificationAsRead).toHaveBeenCalledWith([
{
id: MOCK_NOTIFICATIONS[2].id,
type: MOCK_NOTIFICATIONS[2].type,
isRead: MOCK_NOTIFICATIONS[2].isRead,
},
]);
expect(navigation.navigate).not.toHaveBeenCalled();
});

it('derives notificationState correctly based on notification type', () => {
(hasNotificationComponents as unknown as jest.Mock).mockReturnValue(true);
(
NotificationComponentState as Record<
TRIGGER_TYPES,
NotificationState<INotification>
>
)[MOCK_NOTIFICATIONS[2].type] = {
createMenuItem: jest.fn().mockReturnValue({
title: MOCK_NOTIFICATIONS[2].type,
description: {
start: MOCK_NOTIFICATIONS[2].type,
},
image: {
url: MOCK_NOTIFICATIONS[2].type,
variant: 'circle',
},
badgeIcon: MOCK_NOTIFICATIONS[2].type,
createdAt: MOCK_NOTIFICATIONS[2].createdAt,
isRead: MOCK_NOTIFICATIONS[2].isRead,
}),
guardFn: (n): n is INotification => true,
};
const notification = MOCK_NOTIFICATIONS[2];
if (!hasNotificationComponents(notification.type)) {
throw new Error('Test Setup Failure - incorrect mock');
}

const notifState = NotificationComponentState[notification.type];
const mockCreateMenuItem = jest.spyOn(notifState, 'createMenuItem');

renderWithProvider(
<NotificationsListItem
Expand All @@ -211,13 +119,59 @@ describe('NotificationsList', () => {
/>,
);

expect(
(
NotificationComponentState as Record<
TRIGGER_TYPES,
NotificationState<INotification>
>
)[MOCK_NOTIFICATIONS[2].type].createMenuItem,
).toHaveBeenCalledWith(MOCK_NOTIFICATIONS[2]);
expect(mockCreateMenuItem).toHaveBeenCalledWith(MOCK_NOTIFICATIONS[2]);
});
});

describe('useNotificationOnClick', () => {
const arrangeMocks = () => {
const { mockMarkNotificationAsRead } = arrangeActions();
const mockGetBadgeCount = jest
.mocked(NotificationsService.getBadgeCount)
.mockResolvedValue(1);
const mockDecrementBadgeCount = jest.mocked(
NotificationsService.decrementBadgeCount,
);
const mockSetBadgeConut = jest.mocked(NotificationsService.setBadgeCount);

return {
mockMarkNotificationAsRead,
mockGetBadgeCount,
mockDecrementBadgeCount,
mockSetBadgeConut,
mockTrackEvent,
mockNavigation: createNavigationProps({}).navigation as jest.MockedObject<
NavigationProp<ParamListBase>
>,
};
};

beforeEach(() => {
jest.clearAllMocks();
});

it('call correct logic, and invoke navigation + events', async () => {
const mocks = arrangeMocks();
const hook = renderHook(() =>
useNotificationOnClick({ navigation: mocks.mockNavigation }),
);
const notification = processNotification(createMockNotificationEthSent());

await act(() => hook.result.current(notification));

// Assert - Controller Action
expect(mocks.mockMarkNotificationAsRead).toHaveBeenCalledWith([
expect.objectContaining({ id: notification.id }),
]);

// Assert - Page Navigation
expect(mocks.mockNavigation.navigate).toHaveBeenCalled();

// Assert - Badge Update
expect(mocks.mockGetBadgeCount).toHaveBeenCalled();
expect(mocks.mockDecrementBadgeCount).toHaveBeenCalled();

// Assert - Event Fired
expect(mocks.mockTrackEvent).toHaveBeenCalled();
});
});
12 changes: 10 additions & 2 deletions app/components/UI/Notification/List/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ function Loading() {
);
}

export function NotificationsListItem(props: NotificationsListItemProps) {
const { styles } = useStyles();
export function useNotificationOnClick(
props: Pick<NotificationsListItemProps, 'navigation'>,
) {
const { markNotificationAsRead } = useMarkNotificationAsRead();
const { trackEvent, createEventBuilder } = useMetrics();
const onNotificationClick = useCallback(
Expand Down Expand Up @@ -97,6 +98,13 @@ export function NotificationsListItem(props: NotificationsListItemProps) {
[markNotificationAsRead, props.navigation, trackEvent, createEventBuilder],
);

return onNotificationClick;
}

export function NotificationsListItem(props: NotificationsListItemProps) {
const { styles } = useStyles();
const onNotificationClick = useNotificationOnClick(props);

const menuItemState = useMemo(() => {
const notificationState =
props.notification?.type &&
Expand Down
21 changes: 7 additions & 14 deletions app/components/Views/Notifications/Details/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import configureMockStore from 'redux-mock-store';
import NotificationsDetails from './index';
import { backgroundState } from '../../../../util/test/initial-root-state';
import MOCK_NOTIFICATIONS from '../../../../components/UI/Notification/__mocks__/mock_notifications';
import { NotificationComponentState } from '../../../../util/notifications/notification-states';
// eslint-disable-next-line import/no-namespace
import * as NotificationStatesModule from '../../../../util/notifications/notification-states';

const mockInitialState = {
settings: {
Expand Down Expand Up @@ -66,18 +67,10 @@ describe('NotificationsDetails', () => {
expect(toJSON()).toMatchSnapshot();
});

it('derives state correctly based on notification type', () => {
const notificationType = MOCK_NOTIFICATIONS[1].type as keyof typeof NotificationComponentState;

(NotificationComponentState[notificationType] as unknown) = {
createModalDetails: jest.fn().mockReturnValue({
title: 'Test Title',
createdAt: new Date().toISOString(),
header: 'Test Header',
fields: [],
footer: 'Test Footer',
}),
};
it('returns null if unable to create modal state', () => {
jest
.spyOn(NotificationStatesModule, 'hasNotificationComponents')
.mockReturnValue(false);

const result = render(
<Provider store={store}>
Expand All @@ -91,6 +84,6 @@ describe('NotificationsDetails', () => {
/>
</Provider>,
);
expect(result).toBeTruthy();
expect(result.toJSON()).toBe(null);
});
});
Loading

0 comments on commit b3461c1

Please sign in to comment.