Skip to content

Commit

Permalink
Merge pull request #60 from shaheer-deriv/shaheer/WALL-1489
Browse files Browse the repository at this point in the history
refactor: 🎨 moves notification components to separate files
  • Loading branch information
shaheer-deriv authored Aug 14, 2023
2 parents 73a9d56 + ec8800e commit 09b8b6a
Show file tree
Hide file tree
Showing 16 changed files with 549 additions and 281 deletions.
2 changes: 1 addition & 1 deletion packages/components/src/hooks/use-onclickoutside.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type Handler = (event: MouseEvent) => void;
export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
ref: RefObject<T>,
handler: Handler,
validationFn: (event: MouseEvent) => boolean,
validationFn?: (event: MouseEvent) => boolean,
mouseEvent: 'mousedown' | 'mouseup' = 'mousedown'
): void {
useEventListener(mouseEvent, event => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { EmptyNotification } from '../empty-notification';
import { render, screen } from '@testing-library/react';
import EmptyNotification from '../empty-notification';

describe('EmptyNotification Component', () => {
it('should render EmptyNotification component', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ const EmptyNotification = () => (
</div>
);

export { EmptyNotification };
export default EmptyNotification;
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import { StoreProvider, mockStore } from '@deriv/stores';
import { render, screen } from '@testing-library/react';
import NotificationListWrapper from '../notification-list-wrapper';

jest.mock('App/Components/Routes', () => ({ BinaryLink: 'MockedBinaryLink' }));

describe('NotificationListWrapper', () => {
const mock_store_without_notifications = mockStore({ notifications: { notifications: [] } });
const mock_store_with_notifications = mockStore({
notifications: {
notifications: [
{
key: 'mock_notification_key',
header: 'Mock Notification Header',
message: 'Mock Notification Message',
action: {
route: '/mock/route',
text: 'Mock Notification Action',
},
type: 'mock_notification_type',
},
],
},
});
const renderComponent = (mock_store = mockStore({})) => {
const mock_props = { clearNotifications: jest.fn() };
render(
<StoreProvider store={mock_store}>
<NotificationListWrapper {...mock_props} />
</StoreProvider>
);
};

it('should render the component', () => {
renderComponent(mock_store_without_notifications);
expect(screen.getByTestId('dt_notifications_list_wrapper')).toBeInTheDocument();
});

it('should render the "EmptyNotification" component if notifications list is empty', () => {
renderComponent(mock_store_without_notifications);
expect(screen.getByText('No notifications')).toBeInTheDocument();
expect(screen.getByText('You have yet to receive any notifications')).toBeInTheDocument();
expect(screen.queryByText('Mock Notification Header')).not.toBeInTheDocument();
expect(screen.queryByText('Mock Notification Message')).not.toBeInTheDocument();
expect(screen.queryByText('Mock Notification Action')).not.toBeInTheDocument();
});

it('should render the "NotificationsList" component if notifications list is not empty', () => {
renderComponent(mock_store_with_notifications);
expect(screen.getByText('Mock Notification Header')).toBeInTheDocument();
expect(screen.getByText('Mock Notification Message')).toBeInTheDocument();
expect(screen.getByText('Mock Notification Action')).toBeInTheDocument();
expect(screen.queryByText('No notifications')).not.toBeInTheDocument();
expect(screen.queryByText('You have yet to receive any notifications')).not.toBeInTheDocument();
});

it('should render the "ClearAllFooter" component', () => {
renderComponent(mock_store_without_notifications);
expect(screen.getByText('Clear All')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { StoreProvider, mockStore } from '@deriv/stores';
import { fireEvent, render, screen } from '@testing-library/react';
import ClearAllFooter from '../notifications-clear-all-footer';

describe('ClearAllFooter', () => {
const mock_store_without_notifications = mockStore({ notifications: { notifications: [] } });
const mock_store_with_notifications = mockStore({
notifications: {
notifications: [
{
key: 'mock_security_notification',
header: 'Stronger security for your Deriv account',
message:
'With two-factor authentication, you’ll protect your account with both your password and your phone - so only you can access your account, even if someone knows your password.',
action: {
route: '/account/two-factor-authentication',
text: 'Secure my account',
},
type: 'warning',
},
],
},
});
const mock_props: React.ComponentProps<typeof ClearAllFooter> = { clearNotifications: jest.fn() };

it('should render the component', () => {
render(
<StoreProvider store={mock_store_without_notifications}>
<ClearAllFooter {...mock_props} />
</StoreProvider>
);
expect(screen.getByTestId('dt_clear_all_footer_button')).toBeInTheDocument();
});

it('should render the button', () => {
render(
<StoreProvider store={mock_store_without_notifications}>
<ClearAllFooter {...mock_props} />
</StoreProvider>
);
expect(screen.getByRole('button', { name: 'Clear All' })).toBeInTheDocument();
});

it('should render the button in disabled state if there are no notifications', () => {
render(
<StoreProvider store={mock_store_without_notifications}>
<ClearAllFooter {...mock_props} />
</StoreProvider>
);
expect(screen.getByRole('button', { name: 'Clear All' })).toBeDisabled();
});

it('should render the button in enabled state if there are notifications available', () => {
render(
<StoreProvider store={mock_store_with_notifications}>
<ClearAllFooter {...mock_props} />
</StoreProvider>
);
expect(screen.getByRole('button', { name: 'Clear All' })).toBeEnabled();
});

it('should fire the "clearNotifications" method on clicking the button', () => {
render(
<StoreProvider store={mock_store_with_notifications}>
<ClearAllFooter {...mock_props} />
</StoreProvider>
);
fireEvent.click(screen.getByRole('button', { name: 'Clear All' }));
expect(mock_props.clearNotifications).toBeCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { isDesktop, isMobile } from '@deriv/shared';
import { StoreProvider, mockStore } from '@deriv/stores';
import { render, screen } from '@testing-library/react';
import NotificationsDialog from '../notifications-dialog';

jest.mock('react-transition-group', () => ({ CSSTransition: () => 'MockedCSSTransition' }));
jest.mock('@deriv/components', () => ({
...jest.requireActual('@deriv/components'),
MobileDialog: () => 'MockedMobileDialog',
}));
jest.mock('@deriv/shared', () => ({
...jest.requireActual('@deriv/shared'),
isDesktop: jest.fn(() => true),
isMobile: jest.fn(() => false),
}));

describe('NotificationsDialog', () => {
const renderComponent = (mock_store_override = {}) => {
const mock_store = mockStore({
notifications: {
is_notifications_visible: true,
notifications: [
{
key: 'mock_notification_key',
header: 'Mock Notification Header',
message: 'Mock Notification Message',
action: {
route: '/mock/route',
text: 'Mock Notification Action',
},
type: 'mock_notification_type',
},
],
toggleNotificationsModal: jest.fn(),
},
...mock_store_override,
});
render(
<StoreProvider store={mock_store}>
<NotificationsDialog />
</StoreProvider>
);
};

it('should render the component CSSTranition in desktop mode', () => {
renderComponent();
expect(screen.getByText('MockedCSSTransition')).toBeInTheDocument();
expect(screen.queryByText('MockedMobileDialog')).not.toBeInTheDocument();
});

it('should render the component MobileDialog in mobile mode', () => {
(isDesktop as jest.Mock).mockReturnValue(false);
(isMobile as jest.Mock).mockReturnValue(true);
renderComponent();
expect(screen.getByText('MockedMobileDialog')).toBeInTheDocument();
expect(screen.queryByText('MockedCSSTransition')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import { StoreProvider, mockStore } from '@deriv/stores';
import { render, screen } from '@testing-library/react';
import NotificationsList from '../notifications-list';

jest.mock('App/Components/Routes', () => ({ BinaryLink: 'MockedBinaryLink' }));

describe('NotificationsList', () => {
const renderComponent = (mock_store_override = {}) => {
const mock_store = mockStore({
notifications: {
notifications: [
{
key: 'mock_notification_key',
header: 'Mock Notification Header',
message: 'Mock Notification Message',
action: {
route: '/mock/route',
text: 'Mock Notification Action',
},
type: 'mock_notification_type',
},
],
toggleNotificationsModal: jest.fn(),
},
...mock_store_override,
});
render(
<StoreProvider store={mock_store}>
<NotificationsList />
</StoreProvider>
);
};

it('should render the notification header', () => {
renderComponent();
expect(screen.getByText('Mock Notification Header')).toBeInTheDocument();
});

it('should render the notification message', () => {
renderComponent();
expect(screen.getByText('Mock Notification Message')).toBeInTheDocument();
});

it('should render the notification action button', () => {
renderComponent();
expect(screen.getByText('Mock Notification Action')).toBeInTheDocument();
});
});
3 changes: 0 additions & 3 deletions packages/core/src/App/Containers/NotificationsDialog/index.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './notifications-dialog';
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import classNames from 'classnames';
import React, { LegacyRef } from 'react';
import { Text, ThemedScrollbars } from '@deriv/components';
import { isMobile, routes } from '@deriv/shared';
import { observer, useStore } from '@deriv/stores';
import { Localize } from '@deriv/translations';
import EmptyNotification from 'App/Components/Elements/Notifications/empty-notification';
import ClearAllFooter from './notifications-clear-all-footer';
import NotificationsList from './notifications-list';

type TNotificationListWrapper = { clearNotifications: () => void };

const NotificationListWrapperForwardRef = React.forwardRef(
({ clearNotifications }: TNotificationListWrapper, ref: LegacyRef<HTMLDivElement> | undefined) => {
const { notifications } = useStore();
const { notifications: notifications_array } = notifications;
const is_empty = !notifications_array?.length;

const traders_hub = window.location.pathname === routes.traders_hub;

return (
<div
data-testid='dt_notifications_list_wrapper'
className={classNames('notifications-dialog', {
'notifications-dialog--pre-appstore':
traders_hub || window.location.pathname.startsWith(routes.account),
})}
ref={ref}
>
<div className='notifications-dialog__header'>
<Text
as='h2'
className='notifications-dialog__header-text'
size='s'
weight='bold'
color='prominent'
styles={{
lineHeight: '1.6rem',
}}
>
<Localize i18n_default_text='Notifications' />
</Text>
</div>
<div
className={classNames('notifications-dialog__content', {
'notifications-dialog__content--empty': is_empty,
})}
>
<ThemedScrollbars is_bypassed={isMobile() || is_empty}>
{is_empty ? <EmptyNotification /> : <NotificationsList />}
</ThemedScrollbars>
</div>
<ClearAllFooter clearNotifications={clearNotifications} />
</div>
);
}
);
NotificationListWrapperForwardRef.displayName = 'NotificationListWrapper';

const NotificationListWrapper = observer(NotificationListWrapperForwardRef);

export default NotificationListWrapper;
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import classNames from 'classnames';
import { Button, Text } from '@deriv/components';
import { isMobile } from '@deriv/shared';
import { localize } from '@deriv/translations';
import { observer, useStore } from '@deriv/stores';

type TClearAllFooter = {
clearNotifications: () => void;
};

const ClearAllFooter = observer(({ clearNotifications }: TClearAllFooter) => {
const { notifications } = useStore();
const { notifications: notifications_array } = notifications;
const is_empty = !notifications_array?.length;

return (
<React.Fragment>
<div className='notifications-dialog__separator' />
<div
data-testid='dt_clear_all_footer_button'
className={classNames('notifications-dialog__footer', {
'notifications-dialog__content--empty': is_empty,
'notifications-dialog__content--sticky': isMobile(),
})}
>
<Button
className={classNames('dc-btn--secondary', 'notifications-dialog__clear')}
disabled={is_empty}
onClick={clearNotifications}
>
<Text size='xxs' color='prominent' weight='bold'>
{localize('Clear All')}
</Text>
</Button>
</div>
</React.Fragment>
);
});

export default ClearAllFooter;
Loading

0 comments on commit 09b8b6a

Please sign in to comment.