Skip to content

Commit

Permalink
chore: show snackbar if strategy changed while running bot (#10393)
Browse files Browse the repository at this point in the history
* chore: show snackbar if strategy changed while running bot

* chore: added test cases

* fix: notification icon

* fix: remove timer if mouse over

* fix: update notification timer on everytime the snackbar opens
  • Loading branch information
shafin-deriv committed Oct 11, 2023
1 parent 15cdb89 commit 1e73722
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/bot-web-ui/src/app/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
--zindex-drawer: 5;
--zindex-modal: 6;
--zindex-draggable-modal: 7;
--zindex-snackbar: 8;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react';
import { mockStore, StoreProvider } from '@deriv/stores';
// eslint-disable-next-line import/no-extraneous-dependencies
import { act, fireEvent, render, screen } from '@testing-library/react';
// eslint-disable-next-line import/no-extraneous-dependencies
import userEvent from '@testing-library/user-event';
import RootStore from '../../../stores/root-store';
import { DBotStoreProvider, mockDBotStore } from '../../../stores/useDBotStore';
import BotSnackbar from '../bot-snackbar';

jest.mock('@deriv/shared', () => ({
...jest.requireActual('@deriv/shared'),
isMobile: jest.fn(() => false),
}));

jest.mock('@deriv/bot-skeleton/src/scratch/blockly', () => jest.fn());
jest.mock('@deriv/bot-skeleton/src/scratch/dbot', () => ({
saveRecentWorkspace: jest.fn(),
unHighlightAllBlocks: jest.fn(),
}));
jest.mock('@deriv/bot-skeleton/src/scratch/hooks/block_svg', () => jest.fn());
jest.mock('@deriv/deriv-charts', () => ({
setSmartChartsPublicPath: jest.fn(),
}));

const mock_ws = {
authorized: {
subscribeProposalOpenContract: jest.fn(),
send: jest.fn(),
},
storage: {
send: jest.fn(),
},
contractUpdate: jest.fn(),
subscribeTicksHistory: jest.fn(),
forgetStream: jest.fn(),
activeSymbols: jest.fn(),
send: jest.fn(),
};

jest.useFakeTimers();

describe('BotSnackbar', () => {
let wrapper: ({ children }: { children: JSX.Element }) => JSX.Element, mock_DBot_store: RootStore | undefined;
const mockHandleClose = jest.fn();
beforeAll(() => {
const mock_store = mockStore({});
mock_DBot_store = mockDBotStore(mock_store, mock_ws);

wrapper = ({ children }: { children: JSX.Element }) => (
<StoreProvider store={mock_store}>
<DBotStoreProvider ws={mock_ws} mock={mock_DBot_store}>
{children}
</DBotStoreProvider>
</StoreProvider>
);
});
it('should render BotSnackbar with correct message', () => {
const test_message = 'message test';
render(<BotSnackbar message={test_message} handleClose={mockHandleClose} is_open={true} />, {
wrapper,
});
expect(screen.getByText(test_message)).toBeInTheDocument();
});

it('should not render BotSnackbar if snack bar is not opened', () => {
const test_message = 'message test';
render(<BotSnackbar message={test_message} handleClose={mockHandleClose} is_open={false} />, {
wrapper,
});
expect(screen.queryByText(test_message)).not.toBeInTheDocument();
});

it('should render close button if snackbar is open', () => {
render(<BotSnackbar message='test' handleClose={mockHandleClose} is_open={true} />, {
wrapper,
});
const cls_btn = screen.getByTestId('bot-snackbar-notification-close');
expect(cls_btn).toBeInTheDocument();
});

it('should hanlde close function on click close button', async () => {
render(<BotSnackbar message='test' handleClose={mockHandleClose} is_open={true} />, {
wrapper,
});
const cls_btn = screen.getByTestId('bot-snackbar-notification-close');
await userEvent.click(cls_btn);
expect(mockHandleClose).toBeCalled();
});

it('should close snackbar after timeout is passed and mouse is not over the snackbar', async () => {
render(<BotSnackbar message='test' handleClose={mockHandleClose} is_open={true} timeout={4000} />, {
wrapper,
});
const element = screen.getByTestId('bot-snackbar-notification-container');
act(() => {
fireEvent.mouseLeave(element);
jest.advanceTimersByTime(4100);
});
const cls_btn = screen.queryByTestId('bot-snackbar-notification-close');
expect(cls_btn).not.toBeInTheDocument();
});
});
36 changes: 36 additions & 0 deletions packages/bot-web-ui/src/components/bot-snackbar/bot-snackbar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.bot-snackbar {
position: fixed;
z-index: var(--zindex-snackbar);
right: 38rem;
top: 12rem;

.dc-toast {
width: 100%;
&__message {
background: var(--text-prominent);
color: var(--general-main-1);
padding: 1rem 1.6rem;
}
&__message-content {
display: flex;

@include mobile {
align-items: center;
}
}
}

@include mobile {
top: unset;
left: 0;
right: 0;
bottom: 10.5rem;
}

.notification-close {
cursor: pointer;
filter: invert(1);
margin-left: 1rem;
margin-top: 0.1rem;
}
}
57 changes: 57 additions & 0 deletions packages/bot-web-ui/src/components/bot-snackbar/bot-snackbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useState } from 'react';
import { Icon, Toast } from '@deriv/components';
import { Localize } from '@deriv/translations';

type TBotSnackbar = {
className?: string;
is_open: boolean;
onClick?: React.MouseEventHandler<HTMLDivElement>;
handleClose: () => void;
type?: 'error' | 'info' | 'notification';
timeout?: number;
msg_localize_components?: JSX.Element[];
message: string;
};

const BotSnackbar = ({
message,
msg_localize_components = [],
timeout = 6000,
is_open,
onClick,
handleClose,
type,
className,
}: TBotSnackbar) => {
const [notification_timer, setNotificationTimer] = useState(timeout);

React.useEffect(() => {
if (is_open) {
setNotificationTimer(timeout);
}
}, [is_open, timeout]);

return (
<div
onMouseOver={() => {
setNotificationTimer(0);
}}
onMouseLeave={() => {
setNotificationTimer(timeout);
}}
className={className ?? 'bot-snackbar'}
data-testid='bot-snackbar-notification-container'
>
<Toast is_open={is_open} type={type} timeout={notification_timer} onClick={onClick} onClose={handleClose}>
<div>{message && <Localize i18n_default_text={message} components={msg_localize_components} />}</div>
<Icon
icon='IcCross'
className={'notification-close'}
data_testid={'bot-snackbar-notification-close'}
onClick={handleClose}
/>
</Toast>
</div>
);
};
export default BotSnackbar;
4 changes: 4 additions & 0 deletions packages/bot-web-ui/src/components/bot-snackbar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import BotSnackbar from './bot-snackbar';
import './bot-snackbar.scss';

export default BotSnackbar;
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react';
import React, { useRef, useState } from 'react';
import classNames from 'classnames';

import { observer } from '@deriv/stores';

import { useDBotStore } from '../../../stores/useDBotStore';
import BotSnackbar from '../../bot-snackbar';
import LoadModal from '../../load-modal';
import SaveModal from '../dashboard-component/load-bot-preview/save-modal';
import BotBuilderTourHandler from '../dbot-tours/bot-builder-tour';
Expand All @@ -12,8 +13,11 @@ import QuickStrategy from '../quick-strategy';
import WorkspaceWrapper from './workspace-wrapper';

const BotBuilder = observer(() => {
const { dashboard, app } = useDBotStore();
const { dashboard, app, run_panel } = useDBotStore();
const { active_tab, active_tour, is_preview_on_popup } = dashboard;
const { is_running } = run_panel;
const is_blockly_listener_registered = useRef(false);
const [show_snackbar, setShowSnackbar] = useState(false);

const { onMount, onUnmount } = app;
const el_ref = React.useRef<HTMLInputElement | null>(null);
Expand All @@ -23,8 +27,43 @@ const BotBuilder = observer(() => {
return () => onUnmount();
}, []);

const handleBlockChangeOnBotRun = (e: Event) => {
if (e.type !== 'ui') {
setShowSnackbar(true);
removeBlockChangeListener();
}
};

const removeBlockChangeListener = () => {
is_blockly_listener_registered.current = false;
window.Blockly?.derivWorkspace?.removeChangeListener(handleBlockChangeOnBotRun);
};

React.useEffect(() => {
const workspace = window.Blockly?.derivWorkspace;
if (workspace && is_running && !is_blockly_listener_registered.current) {
is_blockly_listener_registered.current = true;
workspace.addChangeListener(handleBlockChangeOnBotRun);
} else {
setShowSnackbar(false);
removeBlockChangeListener();
}

return () => {
if (workspace && is_blockly_listener_registered.current) {
removeBlockChangeListener();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [is_running]);

return (
<>
<BotSnackbar
is_open={show_snackbar}
message='Changes you make will not affect your running bot.'
handleClose={() => setShowSnackbar(false)}
/>
<div
className={classNames('bot-builder', {
'bot-builder--active': active_tab === 1 && !is_preview_on_popup,
Expand Down

1 comment on commit 1e73722

@vercel
Copy link

@vercel vercel bot commented on 1e73722 Oct 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

deriv-app – ./

deriv-app.vercel.app
binary.sx
deriv-app.binary.sx
deriv-app-git-master.binary.sx

Please sign in to comment.