Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 🎨 moves notification components to separate files #60

Merged
merged 4 commits into from
Aug 14, 2023
Merged
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
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