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

Akmal | Henry / feat: dtrader guides improvements #17806

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
70645f9
Merge with master (#17763)
akmal-deriv Dec 9, 2024
d547813
Revert "Merge with master (#17763)" (#17764)
akmal-deriv Dec 9, 2024
ad044aa
Merge branch 'master' into feature/dtrader-guides-improvements
akmal-deriv Dec 9, 2024
4cbcd2a
Henry/grwt 2580/onboarding guide improvement (#17741)
henry-deriv Dec 12, 2024
ba22157
feat: update welcome modal (#17770)
akmal-deriv Dec 12, 2024
d46c7c9
Henry/grwt 2580/onboarding guide improvement (#17808)
henry-deriv Dec 12, 2024
f3a242d
feat: update trade type selection guide (#17809)
akmal-deriv Dec 13, 2024
0728fce
feat: trade page guide updates (#17824)
akmal-deriv Dec 17, 2024
2005c14
Henry/grwt 2580/onboarding guide improvement (#17813)
henry-deriv Dec 18, 2024
1f1627d
Akmal / fix: do not reset local storage at every guide step (#17848)
akmal-deriv Dec 18, 2024
a7bfc84
Henry/grwt 2580/onboarding guide improvement (#17849)
henry-deriv Dec 19, 2024
2c117db
Akmal / fix: issues with trade page guide (#17862)
akmal-deriv Dec 19, 2024
55458b3
Henry/grwt 2580/onboarding guide improvement (#17865)
henry-deriv Dec 23, 2024
835c76e
feat: merge with master
akmal-deriv Dec 23, 2024
449f6af
Revert "feat: merge with master"
akmal-deriv Dec 23, 2024
a21adfe
feat: merge with master
akmal-deriv Dec 23, 2024
91ed23a
Henry/grwt 2580/onboarding guide improvement (#17881)
henry-deriv Dec 23, 2024
617e047
fix: guide review suggestions #1 (#17887)
akmal-deriv Dec 24, 2024
e0419b5
Henry/grwt 2580/onboarding guide improvement (#17890)
henry-deriv Dec 24, 2024
797d465
Merge branch 'master' into feature/dtrader-guides-improvements
akmal-deriv Dec 27, 2024
bcf57e7
fix: add missing import
akmal-deriv Dec 27, 2024
4550b5f
Henry/grwt 2580/onboarding guide improvement (#17944)
henry-deriv Jan 8, 2025
9f2dbe1
chore: merge with master
akmal-deriv Jan 8, 2025
73d86dc
Update types.ts
akmal-deriv Jan 8, 2025
00d40d6
Update i18next.ts
akmal-deriv Jan 8, 2025
08d9d5b
Henry/grwt 2580/onboarding guide improvement (#18002)
henry-deriv Jan 20, 2025
0742661
Henry/grwt 2580/onboarding guide improvement (#18007)
henry-deriv Jan 21, 2025
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
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import { screen, render } from '@testing-library/react';
import { screen, render, act } from '@testing-library/react';
import ActiveSymbolsList from '../active-symbols-list';
import TraderProviders from '../../../../trader-providers';
import { TCoreStores } from '@deriv/stores/types';
import { mockStore } from '@deriv/stores';
import { TRADE_TYPES } from '@deriv/shared';
import userEvent from '@testing-library/user-event';
import useGuideStates from 'AppV2/Hooks/useGuideStates';

const input_placeholder_text = 'Search markets on Rise/Fall';
const symbol_search_results = 'MockedSymbolSearchResults';
@@ -14,6 +15,27 @@ const market_categories = 'MockedMarketCategories';
jest.mock('AppV2/Components/SymbolSearchResults', () => jest.fn(() => <div>{symbol_search_results}</div>));
jest.mock('AppV2/Components/MarketCategories', () => jest.fn(() => <div>{market_categories}</div>));

jest.mock('AppV2/Components/OnboardingGuide/GuideForPages/guide-container', () => ({
__esModule: true,
default: jest.fn(({ should_run, callback }) => (
<div data-testid='mock-guide-container' data-should-run={should_run}>
<button onClick={callback} data-testid='guide-callback-button'>
Close Guide
</button>
</div>
)),
}));

jest.mock('AppV2/Hooks/useGuideStates', () => ({
__esModule: true,
default: jest.fn(() => ({
guideStates: {
should_run_market_selector_guide: false,
},
setGuideState: jest.fn(),
})),
}));

jest.mock('AppV2/Hooks/useActiveSymbols', () => ({
...jest.requireActual('AppV2/Hooks/useActiveSymbols'),
__esModule: true,
@@ -86,4 +108,71 @@ describe('<ActiveSymbolsList />', () => {
expect(default_mock_store.modules.trade.setTickData).toBeCalledWith(null);
expect(default_mock_store.modules.trade.setDigitStats).toBeCalledWith([]);
});
it('should show guide when should_run_market_selector_guide is true and component is open', () => {
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: true,
},
setGuideState: jest.fn(),
});

render(MockActiveSymbolsList(default_mock_store));

act(() => {
jest.advanceTimersByTime(300);
});

const guideContainer = screen.getByTestId('mock-guide-container');
expect(guideContainer).toBeInTheDocument();
});
it('should not show guide when should_run_market_selector_guide is false', () => {
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: false,
},
setGuideState: jest.fn(),
});

render(MockActiveSymbolsList(default_mock_store));

act(() => {
jest.advanceTimersByTime(300);
});

const guideContainer = screen.getByTestId('mock-guide-container');
expect(guideContainer).toHaveAttribute('data-should-run', 'false');
});
it('should call setGuideState when guide is closed', async () => {
const mockSetGuideState = jest.fn();
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: true,
},
setGuideState: mockSetGuideState,
});

render(MockActiveSymbolsList(default_mock_store));

act(() => {
jest.advanceTimersByTime(300);
});

await userEvent.click(screen.getByTestId('guide-callback-button'));
expect(mockSetGuideState).toHaveBeenCalledWith('should_run_market_selector_guide', false);
});
it('should clean up timer and hide guide on unmount', () => {
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: true,
},
setGuideState: jest.fn(),
});

const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
const { unmount } = render(MockActiveSymbolsList(default_mock_store));

unmount();

expect(clearTimeoutSpy).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -6,6 +6,10 @@ import MarketCategories from '../MarketCategories';
import SymbolSearchResults from '../SymbolSearchResults';
import { useTraderStore } from 'Stores/useTraderStores';
import { sendMarketTypeToAnalytics } from '../../../Analytics';
import GuideContainer from '../OnboardingGuide/GuideForPages/guide-container';
import { Localize } from '@deriv/translations';
import useGuideStates from 'AppV2/Hooks/useGuideStates';
import { Step } from 'react-joyride';

type TActiveSymbolsList = {
isOpen: boolean;
@@ -17,6 +21,38 @@ const ActiveSymbolsList = observer(({ isOpen, setIsOpen }: TActiveSymbolsList) =
const [isSearching, setIsSearching] = useState(false);
const [selectedSymbol, setSelectedSymbol] = useState(symbol);
const [searchValue, setSearchValue] = useState('');
const { guideStates, setGuideState } = useGuideStates();
const { should_run_market_selector_guide } = guideStates;

const STEPS = [
{
content: <Localize i18n_default_text='Explore available markets here.' />,
placement: 'top' as Step['placement'],
target: '.joyride-element',
title: <Localize i18n_default_text='Select a market' />,
disableBeacon: true,
offset: 0,
spotlightPadding: 4,
},
];

const [show_guide, setShowGuide] = useState(false);
const timerRef = useRef<NodeJS.Timeout>();

useEffect(() => {
if (should_run_market_selector_guide && isOpen) {
timerRef.current = setTimeout(() => {
setShowGuide(true);
}, 300);
}

return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
setShowGuide(false);
};
}, [should_run_market_selector_guide, isOpen]);

const marketCategoriesRef = useRef<HTMLDivElement>(null);

@@ -30,6 +66,11 @@ const ActiveSymbolsList = observer(({ isOpen, setIsOpen }: TActiveSymbolsList) =
<React.Fragment>
<ActionSheet.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>
<ActionSheet.Portal shouldCloseOnDrag fullHeightOnOpen>
<GuideContainer
should_run={show_guide}
steps={STEPS}
callback={() => setGuideState('should_run_market_selector_guide', false)}
/>
<SymbolsSearchField
searchValue={searchValue}
setSearchValue={setSearchValue}
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ $BOTTOM_NAV_HEIGHT: var(--core-spacing-2800);
}
&-selection {
flex: 1;
overflow: auto;
overflow: auto !important;
-ms-overflow-style: none;
scrollbar-width: none;
}
Original file line number Diff line number Diff line change
@@ -2,14 +2,40 @@ import React from 'react';
import { screen, render } from '@testing-library/react';
import MarketCategory from '../market-category';
import { ActiveSymbols } from '@deriv/api-types';
import { mockStore } from '@deriv/stores';
import { TCoreStores } from '@deriv/stores/types';
import TraderProviders from '../../../../trader-providers';
import useGuideStates from 'AppV2/Hooks/useGuideStates';

jest.mock('AppV2/Components/MarketCategoryItem', () =>
jest.fn(props => <div ref={props.ref}>MockedMarketCategoryItem</div>)
);

jest.mock('AppV2/Components/FavoriteSymbols', () => jest.fn(() => <div>MockedFavoriteSymbols</div>));

jest.mock('AppV2/Hooks/useGuideStates', () => ({
__esModule: true,
default: jest.fn(() => ({
guideStates: {
should_run_market_selector_guide: false,
},
})),
}));

const mockedMarketCategory = (mocked_store: TCoreStores, mock_props: React.ComponentProps<typeof MarketCategory>) => {
return (
<TraderProviders store={mocked_store}>
<MarketCategory {...mock_props} />
</TraderProviders>
);
};
describe('<MarketCategory />', () => {
const default_mock_store = {
client: {
is_logged_in: false,
},
};

const mocked_props = {
category: {
market: 'forex',
@@ -54,7 +80,7 @@ describe('<MarketCategory />', () => {
};

it('should render correct labels', () => {
render(<MarketCategory {...mocked_props} />);
render(mockedMarketCategory(mockStore(default_mock_store), mocked_props));
expect(screen.getByText('Major Pairs')).toBeInTheDocument();
expect(screen.getAllByText('MockedMarketCategoryItem')).toHaveLength(2);
});
@@ -67,7 +93,37 @@ describe('<MarketCategory />', () => {
subgroups: {},
},
};
render(<MarketCategory {...favoriteProps} />);
render(mockedMarketCategory(mockStore(default_mock_store), favoriteProps));
expect(screen.getByText('MockedFavoriteSymbols')).toBeInTheDocument();
});
it('should not render joyride-element when user is not logged in', () => {
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: true,
},
});

render(mockedMarketCategory(mockStore({ client: { is_logged_in: false } }), mocked_props));
expect(screen.queryByTestId('joyride-element')).not.toBeInTheDocument();
});
it('should not render joyride-element when should_run_market_selector_guide is false', () => {
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: false,
},
});

render(mockedMarketCategory(mockStore({ client: { is_logged_in: true } }), mocked_props));
expect(screen.queryByTestId('joyride-element')).not.toBeInTheDocument();
});
it('should render joyride-element when user is logged in and should_run_market_selector_guide is true', () => {
(useGuideStates as jest.Mock).mockReturnValue({
guideStates: {
should_run_market_selector_guide: true,
},
});

render(mockedMarketCategory(mockStore({ client: { is_logged_in: true } }), mocked_props));
expect(screen.getByTestId('joyride-element')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
$HANDLEBAR_HEIGHT: 2rem;
$SEARCHFIELD_HEIGHT: 4.8rem;
$TABS_HEIGHT: 4.3rem;

.market-category {
&-content {
&__container {
@@ -25,3 +29,11 @@
flex-direction: column;
}
}

.joyride-element {
position: fixed;
top: calc(100dvh - 90dvh + $HANDLEBAR_HEIGHT + $SEARCHFIELD_HEIGHT + $TABS_HEIGHT);
width: 100%;
background-color: transparent;
height: 34rem;
}
Original file line number Diff line number Diff line change
@@ -5,6 +5,9 @@ import MarketCategoryItem from '../MarketCategoryItem';
import { ActiveSymbols } from '@deriv/api-types';
import FavoriteSymbols from '../FavoriteSymbols';
import { usePrevious } from '@deriv/components';
import { useLocalStorageData } from '@deriv/hooks';
import useGuideStates from 'AppV2/Hooks/useGuideStates';
import { useStore } from '@deriv/stores';

type TMarketCategory = {
category: MarketGroup;
@@ -17,15 +20,39 @@ type TMarketCategory = {
const MarketCategory = ({ category, selectedSymbol, setSelectedSymbol, setIsOpen, isOpen }: TMarketCategory) => {
const itemRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const prevSymbol = usePrevious(selectedSymbol);
const [guide_dtrader_v2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2');
const { guideStates } = useGuideStates();
const { should_run_market_selector_guide } = guideStates;
const {
client: { is_logged_in },
} = useStore();

useEffect(() => {
if (isOpen && category.market === 'all' && selectedSymbol && itemRefs.current[selectedSymbol] && !prevSymbol) {
if (
isOpen &&
category.market === 'all' &&
selectedSymbol &&
itemRefs.current[selectedSymbol] &&
!prevSymbol &&
!should_run_market_selector_guide &&
guide_dtrader_v2?.market_selector
) {
itemRefs.current[selectedSymbol]?.scrollIntoView({ block: 'center' });
}
}, [isOpen, category.market, selectedSymbol, prevSymbol]);
}, [
isOpen,
category.market,
selectedSymbol,
prevSymbol,
should_run_market_selector_guide,
guide_dtrader_v2?.market_selector,
]);

return (
<Tab.Panel key={category.market_display_name}>
{should_run_market_selector_guide && is_logged_in && (
<div className='joyride-element' data-testid='joyride-element' />
)}
{category.market !== 'favorites' ? (
Object.entries(category.subgroups).map(([subgroupKey, subgroup]) => (
<div key={subgroupKey} className='market-category-content__container'>
Original file line number Diff line number Diff line change
@@ -7,11 +7,16 @@ import { Localize } from '@deriv/translations';
import { LabelPairedChevronDownMdRegularIcon } from '@deriv/quill-icons';
import { observer } from '@deriv/stores';
import { useTraderStore } from 'Stores/useTraderStores';
import { useLocalStorageData } from '@deriv/hooks';
import { getLocalStorage } from '@deriv/utils';
import useGuideStates from 'AppV2/Hooks/useGuideStates';

const MarketSelector = observer(() => {
const [isOpen, setIsOpen] = useState(false);
const { activeSymbols } = useActiveSymbols();
const { symbol: storeSymbol, tick_data, is_market_closed } = useTraderStore();
const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2');
const { setGuideState } = useGuideStates();

const currentSymbol = activeSymbols.find(({ symbol }) => symbol === storeSymbol);
const { pip_size, quote } = tick_data ?? {};
@@ -26,9 +31,20 @@ const MarketSelector = observer(() => {
if (typeof currentSymbol?.exchange_is_open === 'undefined')
return <Skeleton.Square height={42} width={240} rounded />;

const onClick = () => {
if (guide_dtrader_v2?.market_selector) {
setGuideState('should_run_market_selector_guide', false);
} else {
const latest_guide_dtrader_v2 = getLocalStorage('guide_dtrader_v2');
setGuideDtraderV2({ ...latest_guide_dtrader_v2, market_selector: true });
setGuideState('should_run_market_selector_guide', true);
}
setIsOpen(true);
};

return (
<React.Fragment>
<div className='market-selector__container' onClick={() => setIsOpen(true)}>
<div className='market-selector__container' onClick={onClick}>
<div className='market-selector'>
<SymbolIconsMapper symbol={storeSymbol} />
<div className='market-selector-info'>
@@ -58,4 +74,4 @@ const MarketSelector = observer(() => {
);
});

export default MarketSelector;
export default React.memo(MarketSelector);
Original file line number Diff line number Diff line change
@@ -6,31 +6,62 @@ import GuideContainer from '../guide-container';

jest.mock('react-joyride', () => ({
__esModule: true,
default: jest.fn(({ callback }: { callback: (data: CallBackProps) => void }) => (
<div>
default: jest.fn(({ callback, run }: { callback: (data: CallBackProps) => void; run: boolean }) => (
<div data-testid='mock-joyride' data-run={run}>
<p>Joyride</p>
<button onClick={() => callback({ status: 'finished' } as CallBackProps)} />
<button onClick={() => callback({ status: 'finished' } as CallBackProps)} data-testid='finish-button'>
Finish
</button>
<button onClick={() => callback({ status: 'skipped' } as CallBackProps)} data-testid='skip-button'>
Skip
</button>
<button onClick={() => callback({ action: 'close' } as CallBackProps)} data-testid='close-button'>
Close
</button>
</div>
)),
STATUS: { SKIPPED: 'skipped', FINISHED: 'finished' },
ACTIONS: { CLOSE: 'close' },
}));

const mock_props = {
should_run: true,
onFinishGuide: jest.fn(),
steps: [
{
content: 'Test content',
target: '.test-target',
title: 'Test title',
},
],
callback: jest.fn(),
};

describe('GuideContainer', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should render component', () => {
render(<GuideContainer {...mock_props} />);

expect(screen.getByText('Joyride')).toBeInTheDocument();
});

it('should call onFinishGuide inside of callbackHandle if passed status is equal to "skipped" or "finished"', () => {
it('should call callback when status is finished', async () => {
render(<GuideContainer {...mock_props} />);
userEvent.click(screen.getByRole('button'));

expect(mock_props.onFinishGuide).toBeCalled();
await userEvent.click(screen.getByTestId('finish-button'));
expect(mock_props.callback).toHaveBeenCalled();
});
it('should call callback when status is skipped', async () => {
render(<GuideContainer {...mock_props} />);
await userEvent.click(screen.getByTestId('skip-button'));
expect(mock_props.callback).toHaveBeenCalled();
});
it('should call callback when action is close', async () => {
render(<GuideContainer {...mock_props} />);
await userEvent.click(screen.getByTestId('close-button'));
expect(mock_props.callback).toHaveBeenCalled();
});
it('should pass correct run prop to Joyride', () => {
render(<GuideContainer {...mock_props} should_run={false} />);
expect(screen.getByTestId('mock-joyride')).toHaveAttribute('data-run', 'false');
});
});
Original file line number Diff line number Diff line change
@@ -1,47 +1,105 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import GuideTooltip, { GuideTooltipProps } from '../guide-tooltip';

jest.mock('react-joyride', () => jest.fn(() => <div>Joyride</div>));

const mock_props = {
isLastStep: false,
primaryProps: {
title: 'Title',
},
skipProps: {
title: 'Title',
},
step: {
title: 'Title',
content: 'Step content',
},
setStepIndex: jest.fn(),
tooltipProps: {},
} as unknown as GuideTooltipProps;
import userEvent from '@testing-library/user-event';
import GuideTooltip from '../guide-tooltip';
import { TooltipRenderProps } from 'react-joyride';

jest.mock('@deriv-com/quill-ui', () => ({
CaptionText: ({ children, className, bold }: React.PropsWithChildren<{ className?: string; bold?: boolean }>) => (
<span className={className} data-bold={bold}>
{children}
</span>
),
IconButton: ({ onClick, className, icon }: { onClick: () => void; className?: string; icon: React.ReactNode }) => (
<button onClick={onClick} className={className}>
{icon}
</button>
),
}));

jest.mock('@deriv/quill-icons', () => ({
LabelPairedXmarkSmBoldIcon: ({ fill }: { fill: string }) => (
<div data-testid='xmark-icon' data-fill={fill}>
XMarkIcon
</div>
),
}));

describe('GuideTooltip', () => {
it('should render correct content for tooltip if isLastStep === false', () => {
render(<GuideTooltip {...mock_props} />);
const mockOnClick = jest.fn();

const defaultProps: TooltipRenderProps = {
closeProps: {
onClick: mockOnClick,
},
step: {
title: 'Test Title',
content: 'Test Content',
placement: 'center',
target: '.test',
},
tooltipProps: {
'data-testid': 'tooltip-wrapper',
},
} as unknown as TooltipRenderProps;

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

it('should render tooltip with title and content', () => {
render(<GuideTooltip {...defaultProps} />);

expect(screen.getByText('Test Title')).toBeInTheDocument();
expect(screen.getByText('Test Content')).toBeInTheDocument();
expect(screen.getByTestId('xmark-icon')).toBeInTheDocument();
});
it('should render tooltip without title section when title is not provided', () => {
const propsWithoutTitle = {
...defaultProps,
step: {
...defaultProps.step,
title: undefined,
content: 'Only Content',
},
};

render(<GuideTooltip {...propsWithoutTitle} />);

expect(screen.queryByTestId('guide-tooltip__header')).not.toBeInTheDocument();
expect(screen.getByText('Only Content')).toBeInTheDocument();
});
it('should render tooltip without content section when content is not provided', () => {
const propsWithoutContent = {
...defaultProps,
step: {
...defaultProps.step,
content: undefined,
title: 'Only Title',
},
};

render(<GuideTooltip {...propsWithoutContent} />);

expect(screen.getByText('Title')).toBeInTheDocument();
expect(screen.getByText('Step content')).toBeInTheDocument();
expect(screen.getByText('Next')).toBeInTheDocument();
expect(screen.queryByText('Done')).not.toBeInTheDocument();
expect(screen.getByText('Only Title')).toBeInTheDocument();
expect(screen.queryByTestId('guide-tooltip__content')).not.toBeInTheDocument();
});
it('should call closeProps.onClick when close button is clicked', async () => {
render(<GuideTooltip {...defaultProps} />);

it('should render correct content for tooltip if isLastStep === true', () => {
render(<GuideTooltip {...mock_props} isLastStep={true} />);
const closeButton = screen.getByRole('button');
await userEvent.click(closeButton);

expect(screen.getByText('Title')).toBeInTheDocument();
expect(screen.getByText('Step content')).toBeInTheDocument();
expect(screen.getByText('Done')).toBeInTheDocument();
expect(screen.queryByText('Next')).not.toBeInTheDocument();
expect(mockOnClick).toHaveBeenCalled();
});
it('should render close button with correct props', () => {
render(<GuideTooltip {...defaultProps} />);

it('should render scroll icon if title of the step is scroll-icon', () => {
mock_props.step.title = 'scroll-icon';
render(<GuideTooltip {...mock_props} isLastStep={true} />);
expect(screen.getByText('Swipe up to see the chart')).toBeInTheDocument();
const closeButton = screen.getByRole('button');
expect(closeButton).toHaveClass('guide-tooltip__close');
expect(screen.getByTestId('xmark-icon')).toHaveAttribute(
'data-fill',
'var(--component-textIcon-inverse-prominent)'
);
});
});
Original file line number Diff line number Diff line change
@@ -1,124 +1,123 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import OnboardingGuide from '../onboarding-guide';
import { useLocalStorageData } from '@deriv/hooks';
import { getLocalStorage } from '@deriv/utils';

const trading_modal_text = 'Welcome to the new Deriv Trader';
const trading_modal_text = 'Welcome to the Deriv Trader';
const positions_modal_text = 'View your positions';
const guide_container = 'GuideContainer';
const localStorage_key = 'guide_dtrader_v2';

jest.mock('../guide-container', () =>
jest.fn(({ should_run }: { should_run?: boolean }) => <div>{should_run && guide_container}</div>)
);
jest.mock('@deriv/hooks', () => ({
useLocalStorageData: jest.fn(),
}));

jest.mock('@deriv/utils', () => ({
getLocalStorage: jest.fn(),
}));

jest.mock('../onboarding-video', () => jest.fn(() => <div>OnboardingVideo</div>));

describe('OnboardingGuide', () => {
beforeEach(() => {
localStorage.clear();
jest.clearAllMocks();
const mockSetGuideDtraderV2 = jest.fn();
(useLocalStorageData as jest.Mock).mockReturnValue([
{ trade_page: false, positions_page: false },
mockSetGuideDtraderV2,
]);
(getLocalStorage as jest.Mock).mockReturnValue({
trade_page: false,
positions_page: false,
});
});

it('should render Modal with correct content for trading page after 800ms after mounting', async () => {
jest.useFakeTimers({ legacyFakeTimers: true });
it('should render Modal with correct content for trading page after 800ms', async () => {
jest.useFakeTimers();
render(<OnboardingGuide />);

await waitFor(() => jest.advanceTimersByTime(800));
act(() => {
jest.advanceTimersByTime(800);
});

expect(screen.getByText('OnboardingVideo')).toBeInTheDocument();
expect(screen.getByText(trading_modal_text)).toBeInTheDocument();
expect(screen.getByText("Let's begin")).toBeInTheDocument();
expect(screen.getByText("Let's go")).toBeInTheDocument();

jest.useRealTimers();
});

it('should render Modal with correct content for positions page after 800ms after mounting', async () => {
jest.useFakeTimers({ legacyFakeTimers: true });
it('should render Modal with correct content for positions page after 800ms', async () => {
jest.useFakeTimers();
render(<OnboardingGuide type='positions_page' />);

await waitFor(() => jest.advanceTimersByTime(800));
act(() => {
jest.advanceTimersByTime(800);
});

expect(screen.getByText('OnboardingVideo')).toBeInTheDocument();
expect(screen.getByText(positions_modal_text)).toBeInTheDocument();
expect(screen.getByText('Got it')).toBeInTheDocument();

jest.useRealTimers();
});
it('should update localStorage and close modal when "Got it" is clicked', async () => {
const mockSetGuideDtraderV2 = jest.fn();
(useLocalStorageData as jest.Mock).mockReturnValue([{ positions_page: false }, mockSetGuideDtraderV2]);
(getLocalStorage as jest.Mock).mockReturnValue({ positions_page: false });

it('should close the Modal for trading page and start the guide after user clicks on "Let\'s begin" button', async () => {
jest.useFakeTimers({ legacyFakeTimers: true });
render(<OnboardingGuide />);
jest.useFakeTimers();

await waitFor(() => jest.advanceTimersByTime(800));
render(<OnboardingGuide type='positions_page' />);

expect(screen.getByText(trading_modal_text)).toBeInTheDocument();
expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
act(() => {
jest.advanceTimersByTime(800);
});

await userEvent.click(screen.getByRole('button'));
await waitFor(() => jest.advanceTimersByTime(300));
await userEvent.click(screen.getByText('Got it'));

expect(screen.queryByText(trading_modal_text)).not.toBeInTheDocument();
expect(screen.getByText(guide_container)).toBeInTheDocument();
expect(mockSetGuideDtraderV2).toHaveBeenCalledWith({
positions_page: true,
});

jest.useRealTimers();
});
it('should execute callback when modal is closed', async () => {
const callback = jest.fn();
jest.useFakeTimers();
render(<OnboardingGuide callback={callback} />);

it('should close the Modal for positions page, set flag to localStorage equal to true and do NOT start the guide after user clicks on "Got it" button', async () => {
const field = 'positions_page';
jest.useFakeTimers({ legacyFakeTimers: true });
render(<OnboardingGuide type='positions_page' />);

await waitFor(() => jest.advanceTimersByTime(800));

expect(screen.getByText(positions_modal_text)).toBeInTheDocument();
expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
expect(JSON.parse(localStorage.getItem(localStorage_key) as string)[field]).toBe(false);

await userEvent.click(screen.getByRole('button'));
await waitFor(() => jest.advanceTimersByTime(300));
act(() => {
jest.advanceTimersByTime(800);
});

expect(screen.queryByText(positions_modal_text)).not.toBeInTheDocument();
expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
expect(JSON.parse(localStorage.getItem(localStorage_key) as string)[field]).toBe(true);
await userEvent.click(screen.getByText("Let's go"));

expect(callback).toHaveBeenCalled();
jest.useRealTimers();
});

it('should close the Modal for trading page and set flag to localStorage equal to true if user clicks on overlay and do NOT start the guide', async () => {
const field = 'trade_page';
jest.useFakeTimers({ legacyFakeTimers: true });
render(<OnboardingGuide />);

await waitFor(() => jest.advanceTimersByTime(800));

expect(screen.getByText(trading_modal_text)).toBeInTheDocument();
expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
expect(JSON.parse(localStorage.getItem(localStorage_key) as string)[field]).toBe(false);
it('should handle dark mode correctly', async () => {
jest.useFakeTimers();
render(<OnboardingGuide is_dark_mode_on />);

await userEvent.click(screen.getByTestId('dt-actionsheet-overlay'));
await waitFor(() => jest.advanceTimersByTime(300));
act(() => {
jest.advanceTimersByTime(800);
});

expect(screen.queryByText(trading_modal_text)).not.toBeInTheDocument();
expect(screen.queryByText(guide_container)).not.toBeInTheDocument();
expect(JSON.parse(localStorage.getItem(localStorage_key) as string)[field]).toBe(true);
expect(screen.getByText('OnboardingVideo')).toBeInTheDocument();

jest.useRealTimers();
});
it('should not show modal if guide is already completed', () => {
(useLocalStorageData as jest.Mock).mockReturnValue([{ trade_page: true }, jest.fn()]);

it('should execute callback function after Modal is closed', async () => {
const callback = jest.fn();
jest.useFakeTimers({ legacyFakeTimers: true });
render(<OnboardingGuide callback={callback} type='positions_page' />);

await waitFor(() => jest.advanceTimersByTime(800));

expect(screen.getByText(positions_modal_text)).toBeInTheDocument();
expect(screen.queryByText(guide_container)).not.toBeInTheDocument();

await userEvent.click(screen.getByRole('button'));
await waitFor(() => jest.advanceTimersByTime(300));
jest.useFakeTimers();
render(<OnboardingGuide />);

expect(callback).toBeCalled();
act(() => {
jest.advanceTimersByTime(800);
});

expect(screen.queryByText(trading_modal_text)).not.toBeInTheDocument();
jest.useRealTimers();
});
});
Original file line number Diff line number Diff line change
@@ -1,60 +1,54 @@
import React from 'react';
import Joyride, { CallBackProps, STATUS } from 'react-joyride';
import Joyride, { ACTIONS, CallBackProps, STATUS } from 'react-joyride';
import GuideTooltip from './guide-tooltip';
import STEPS from './steps-config';

type TGuideContainerProps = {
export type TGuideContainerProps = {
should_run: boolean;
onFinishGuide: () => void;
steps: CallBackProps['step'][];
callback?: () => void;
};

type TFinishedStatuses = CallBackProps['status'][];

const GuideContainer = ({ should_run, onFinishGuide }: TGuideContainerProps) => {
const GuideContainer = ({ should_run, steps, callback }: TGuideContainerProps) => {
const [step_index, setStepIndex] = React.useState(0);

const callbackHandle = (data: CallBackProps) => {
const { status, step, index } = data;
if (index === 0) {
step.disableBeacon = true;
}
const { status, action } = data;
const finished_statuses: TFinishedStatuses = [STATUS.FINISHED, STATUS.SKIPPED];

if (finished_statuses.includes(status)) onFinishGuide();
if (finished_statuses.includes(status) || action === ACTIONS.CLOSE) callback?.();
};

return (
<Joyride
continuous
callback={callbackHandle}
disableCloseOnEsc
disableOverlayClose
disableScrolling
disableScrollParentFix
floaterProps={{
styles: {
arrow: {
length: 4,
spread: 8,
display: step_index === 3 ? 'none' : 'inline-flex',
},
},
}}
run={should_run}
showSkipButton
steps={STEPS}
steps={steps}
spotlightPadding={0}
scrollToFirstStep
styles={{
options: {
arrowColor: 'var(--component-textIcon-normal-prominent)',
overlayColor: 'var(--core-color-opacity-black-600)',
zIndex: 1000,
},
spotlight: {
borderRadius: 'unset',
},
}}
stepIndex={step_index}
tooltipComponent={props => <GuideTooltip {...props} setStepIndex={setStepIndex} />}
tooltipComponent={props => <GuideTooltip {...props} />}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,46 +1,19 @@
import React from 'react';
import { Button, CaptionText, IconButton, Text } from '@deriv-com/quill-ui';
import { LabelPairedChevronsUpXlBoldIcon, LabelPairedXmarkSmBoldIcon } from '@deriv/quill-icons';
import { Localize } from '@deriv/translations';
import { CaptionText, IconButton } from '@deriv-com/quill-ui';
import { LabelPairedXmarkSmBoldIcon } from '@deriv/quill-icons';
import { TooltipRenderProps } from 'react-joyride';
import { useSwipeable } from 'react-swipeable';

export interface GuideTooltipProps extends TooltipRenderProps {
setStepIndex: React.Dispatch<React.SetStateAction<number>>;
}

const GuideTooltip = ({ isLastStep, primaryProps, skipProps, step, tooltipProps, setStepIndex }: GuideTooltipProps) => {
const swipe_handlers = useSwipeable({
onSwipedUp: () => {
document.querySelector('.trade__chart')?.scrollIntoView();
setStepIndex((prev: number) => prev + 1);
},
preventDefaultTouchmoveEvent: true,
trackTouch: true,
trackMouse: true,
});

if (step.title === 'scroll-icon') {
return (
<div {...swipe_handlers} className='guide-tooltip__wrapper-scroll'>
<LabelPairedChevronsUpXlBoldIcon className='guide-tooltip--bounce' />
<Text size='sm' bold className='guide-tooltip__wrapper-scroll-text'>
<Localize i18n_default_text='Swipe up to see the chart' />
</Text>
</div>
);
}

const GuideTooltip = ({ closeProps, step, tooltipProps }: TooltipRenderProps) => {
return (
<div {...tooltipProps} className='guide-tooltip__wrapper'>
<div>
{step.title && (
<div className='guide-tooltip__header'>
<div className='guide-tooltip__header' data-testid='guide-tooltip__header'>
<CaptionText bold className='guide-tooltip__header__title'>
{step.title}
</CaptionText>
<IconButton
onClick={skipProps.onClick}
onClick={closeProps.onClick}
icon={
<LabelPairedXmarkSmBoldIcon
fill='var(--component-textIcon-inverse-prominent)'
@@ -54,19 +27,12 @@ const GuideTooltip = ({ isLastStep, primaryProps, skipProps, step, tooltipProps,
/>
</div>
)}
{step.content && <CaptionText className='guide-tooltip__content'>{step.content}</CaptionText>}
{step.content && (
<CaptionText className='guide-tooltip__content' data-testid='guide-tooltip__content'>
{step.content}
</CaptionText>
)}
</div>
<Button
onClick={e => {
setStepIndex((prev: number) => prev + 1);
primaryProps.onClick(e);
}}
color='white-black'
className='guide-tooltip__button'
variant='secondary'
size='sm'
label={isLastStep ? <Localize i18n_default_text='Done' /> : <Localize i18n_default_text='Next' />}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { Modal } from '@deriv-com/quill-ui';
import { useLocalStorageData } from '@deriv/hooks';
import useGuideStates from 'AppV2/Hooks/useGuideStates';
import { Localize } from '@deriv/translations';
import GuideContainer from './guide-container';
import { getLocalStorage } from '@deriv/utils';
import OnboardingVideo from './onboarding-video';

type TOnboardingGuideProps = {
@@ -13,35 +14,24 @@ type TOnboardingGuideProps = {

const OnboardingGuide = ({ type = 'trade_page', is_dark_mode_on, callback }: TOnboardingGuideProps) => {
const [is_modal_open, setIsModalOpen] = React.useState(false);
const [should_run_guide, setShouldRunGuide] = React.useState(false);
const { setGuideState } = useGuideStates();
const guide_timeout_ref = React.useRef<ReturnType<typeof setTimeout>>();
const is_button_clicked_ref = React.useRef(false);

const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2', {
trade_types_selection: false,
trade_page: false,
positions_page: false,
});
const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2');

const is_trade_page_guide = type === 'trade_page';

const onFinishGuide = React.useCallback(() => {
setShouldRunGuide(false);
setGuideDtraderV2({ ...guide_dtrader_v2, [type]: true });
const latest_guide_dtrader_v2 = getLocalStorage('guide_dtrader_v2');
setGuideDtraderV2({ ...latest_guide_dtrader_v2, [type]: true });
callback?.();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setGuideDtraderV2]);

const onGuideSkip = () => {
if (is_button_clicked_ref.current) return;
const onGuideDismiss = () => {
onFinishGuide();
setIsModalOpen(false);
};

const onGuideStart = () => {
is_button_clicked_ref.current = true;
setShouldRunGuide(true);
setIsModalOpen(false);
is_trade_page_guide && setGuideState('should_run_trade_page_guide', true);
};

const modal_content = {
@@ -51,15 +41,13 @@ const OnboardingGuide = ({ type = 'trade_page', is_dark_mode_on, callback }: TOn
<Localize i18n_default_text='You can view your open and closed positions here. Tap an item for more details.' />
),
button_label: <Localize i18n_default_text='Got it' />,
primaryButtonCallback: onGuideSkip,
primaryButtonCallback: onGuideDismiss,
...(is_trade_page_guide
? {
title: <Localize i18n_default_text='Welcome to the new Deriv Trader' />,
content: (
<Localize i18n_default_text='Enjoy a smoother, more intuitive trading experience. Here’s a quick tour to get you started.' />
),
button_label: <Localize i18n_default_text="Let's begin" />,
primaryButtonCallback: onGuideStart,
title: <Localize i18n_default_text='Welcome to the Deriv Trader' />,
content: <Localize i18n_default_text='Discover a smoother, more intuitive trading experience.' />,
button_label: <Localize i18n_default_text="Let's go" />,
primaryButtonCallback: onGuideDismiss,
}
: {}),
};
@@ -79,14 +67,13 @@ const OnboardingGuide = ({ type = 'trade_page', is_dark_mode_on, callback }: TOn
isMobile
showHandleBar
shouldCloseModalOnSwipeDown
toggleModal={onGuideSkip}
toggleModal={onGuideDismiss}
primaryButtonLabel={modal_content.button_label}
primaryButtonCallback={modal_content.primaryButtonCallback}
>
<Modal.Header image={modal_content.image} title={modal_content.title} />
<Modal.Body>{modal_content.content}</Modal.Body>
</Modal>
{is_trade_page_guide && <GuideContainer should_run={should_run_guide} onFinishGuide={onFinishGuide} />}
</React.Fragment>
);
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import React from 'react';
import { render, act, waitFor, screen } from '@testing-library/react';
import { useLocalStorageData } from '@deriv/hooks';
import userEvent from '@testing-library/user-event';
import { getLocalStorage } from '@deriv/utils';
import QuickAdjGuide from '../quick-adj-guide';
import { TGuideContainerProps } from '../../GuideForPages/guide-container';

let lastPassedProps;

jest.mock('@deriv/hooks', () => ({
useLocalStorageData: jest.fn(),
}));

jest.mock('@deriv/utils', () => ({
getLocalStorage: jest.fn(),
}));

jest.mock('@deriv/translations', () => ({
Localize: ({ i18n_default_text }: { i18n_default_text: string }) => i18n_default_text,
}));

jest.mock('../../GuideForPages/guide-container', () => ({
__esModule: true,
default: (props: TGuideContainerProps) => {
lastPassedProps = props;
return (
<div data-testid='mock-guide-container' data-should-run={props.should_run}>
<button onClick={props.callback} data-testid='callback-button'>
Close Guide
</button>
</div>
);
},
}));

describe('QuickAdjGuide', () => {
let mockSetGuideDtraderV2: jest.Mock;

beforeEach(() => {
jest.useFakeTimers();
lastPassedProps = null;
mockSetGuideDtraderV2 = jest.fn();
(useLocalStorageData as jest.Mock).mockReturnValue([
{
trade_param_quick_adjustment: false,
},
mockSetGuideDtraderV2,
]);
(getLocalStorage as jest.Mock).mockReturnValue({
trade_param_quick_adjustment: false,
});
});

afterEach(() => {
jest.clearAllMocks();
jest.clearAllTimers();
});

it('should not render GuideContainer initially', () => {
render(<QuickAdjGuide is_minimized_visible={false} />);
expect(screen.queryByTestId('mock-guide-container')).not.toBeInTheDocument();
});
it('should show guide when minimized and visible', async () => {
render(<QuickAdjGuide is_minimized={true} is_minimized_visible={true} />);

act(() => {
jest.advanceTimersByTime(300);
});

await waitFor(() => {
expect(screen.getByTestId('mock-guide-container')).toBeInTheDocument();
});

expect(mockSetGuideDtraderV2).toHaveBeenCalledWith({
trade_param_quick_adjustment: true,
});
});
it('should not show guide when already shown (trade_param_quick_adjustment is true)', () => {
(useLocalStorageData as jest.Mock).mockReturnValue([
{
trade_param_quick_adjustment: true,
},
mockSetGuideDtraderV2,
]);

render(<QuickAdjGuide is_minimized={true} is_minimized_visible={true} />);

act(() => {
jest.advanceTimersByTime(300);
});

expect(screen.queryByTestId('mock-guide-container')).not.toBeInTheDocument();
});
it('should not show guide when not minimized', () => {
render(<QuickAdjGuide is_minimized={false} is_minimized_visible={true} />);

act(() => {
jest.advanceTimersByTime(300);
});

expect(screen.queryByTestId('mock-guide-container')).not.toBeInTheDocument();
});
it('should not show guide when not visible', () => {
render(<QuickAdjGuide is_minimized={true} is_minimized_visible={false} />);

act(() => {
jest.advanceTimersByTime(300);
});

expect(screen.queryByTestId('mock-guide-container')).not.toBeInTheDocument();
});
it('should clean up timeout and hide guide on unmount', () => {
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');

const { unmount } = render(<QuickAdjGuide is_minimized={true} is_minimized_visible={true} />);

act(() => {
jest.advanceTimersByTime(300);
});

unmount();

expect(clearTimeoutSpy).toHaveBeenCalled();
});
it('should use latest localStorage value when showing guide', async () => {
const mockLatestStorage = {
some_other_setting: true,
trade_param_quick_adjustment: false,
};

(getLocalStorage as jest.Mock).mockReturnValue(mockLatestStorage);

render(<QuickAdjGuide is_minimized={true} is_minimized_visible={true} />);

act(() => {
jest.advanceTimersByTime(300);
});

await waitFor(() => {
expect(mockSetGuideDtraderV2).toHaveBeenCalledWith({
...mockLatestStorage,
trade_param_quick_adjustment: true,
});
});
});
it('should handle callback to hide guide', async () => {
render(<QuickAdjGuide is_minimized={true} is_minimized_visible={true} />);

act(() => {
jest.advanceTimersByTime(300);
});

await waitFor(() => {
expect(screen.getByTestId('mock-guide-container')).toBeInTheDocument();
});

const callback_button = screen.getByTestId('callback-button');
await userEvent.click(callback_button);

await waitFor(() => {
expect(screen.queryByTestId('mock-guide-container')).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useLocalStorageData } from '@deriv/hooks';
import { Localize } from '@deriv/translations';
import { getLocalStorage } from '@deriv/utils';
import React from 'react';
import { Step } from 'react-joyride';
import GuideContainer from '../GuideForPages/guide-container';

type TQuickAdjGuide = {
is_minimized?: boolean;
is_minimized_visible: boolean;
};

const QuickAdjGuide = React.memo(({ is_minimized, is_minimized_visible }: TQuickAdjGuide) => {
const is_minimized_and_visible = is_minimized && is_minimized_visible;
const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2');
const [show_guide, setShowGuide] = React.useState(false);
const timerRef = React.useRef<NodeJS.Timeout>();

React.useEffect(() => {
if (is_minimized_and_visible && !guide_dtrader_v2?.trade_param_quick_adjustment) {
const latest_guide_dtrader_v2 = getLocalStorage('guide_dtrader_v2');
timerRef.current = setTimeout(() => {
setShowGuide(true);
setGuideDtraderV2({ ...latest_guide_dtrader_v2, trade_param_quick_adjustment: true });
}, 800);
}

return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
setShowGuide(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [is_minimized_and_visible]);

const STEPS = React.useMemo(
() => [
{
content: <Localize i18n_default_text='Scroll left or right to adjust your trade parameters.' />,
placement: 'top' as Step['placement'],
target: '.trade-params__options__wrapper--minimized',
offset: 0,
title: <Localize i18n_default_text='Make quick adjustments.' />,
disableBeacon: true,
styles: {
spotlight: {
height: 73,
},
},
spotlightPadding: 2,
},
],
[]
);

const handleCallback = React.useCallback(() => {
setShowGuide(false);
}, []);

return show_guide ? <GuideContainer should_run={show_guide} steps={STEPS} callback={handleCallback} /> : null;
});

QuickAdjGuide.displayName = 'QuickAdjGuide';

export default QuickAdjGuide;
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import React from 'react';
import { render, act, waitFor, screen } from '@testing-library/react';
import { useLocalStorageData } from '@deriv/hooks';
import { getLocalStorage } from '@deriv/utils';
import TradeParamsGuide from '../trade-params-guide';
import { TGuideContainerProps } from '../../GuideForPages/guide-container';

let lastPassedProps: TGuideContainerProps;

jest.mock('@deriv/hooks', () => ({
useLocalStorageData: jest.fn(),
}));

jest.mock('@deriv/utils', () => ({
getLocalStorage: jest.fn(),
}));

const MockLocalize = ({ i18n_default_text }: { i18n_default_text: string }) => i18n_default_text;

jest.mock('@deriv/translations', () => ({
Localize: (props: { i18n_default_text: string }) => MockLocalize(props),
}));

jest.mock('../../GuideForPages/guide-container', () => ({
__esModule: true,
default: (props: TGuideContainerProps) => {
lastPassedProps = props;
return (
<div data-testid='mock-guide-container' data-should-run={props.should_run}>
<button onClick={props.callback} data-testid='callback-button'>
Close Guide
</button>
</div>
);
},
}));

describe('TradeParamsGuide', () => {
let mockSetGuideDtraderV2: jest.Mock;

beforeEach(() => {
jest.useFakeTimers();
lastPassedProps = {
should_run: false,
steps: [
{
content: 'test',
target: '',
},
],
callback: jest.fn(),
};
mockSetGuideDtraderV2 = jest.fn();
(useLocalStorageData as jest.Mock).mockReturnValue([
{
trade_types_selection: false,
trade_page: false,
positions_page: false,
market_selector: false,
trade_param_quick_adjustment: false,
trade_params: false,
},
mockSetGuideDtraderV2,
]);
(getLocalStorage as jest.Mock).mockReturnValue({
trade_types_selection: false,
trade_page: false,
positions_page: false,
market_selector: false,
trade_param_quick_adjustment: false,
trade_params: false,
});
});

afterEach(() => {
jest.clearAllMocks();
jest.clearAllTimers();
});

it('should render GuideContainer with correct initial props', () => {
render(<TradeParamsGuide />);
expect(lastPassedProps.should_run).toBe(false);
});
it('should show guide when trade parameter tooltip appears', async () => {
const tradeElement = document.createElement('div');
tradeElement.className = 'trade';
document.body.appendChild(tradeElement);

jest.spyOn(document, 'querySelector').mockReturnValue(tradeElement);

render(<TradeParamsGuide />);

const tooltipElement = document.createElement('div');
tooltipElement.className = 'trade__parameter-tooltip-info';
tradeElement.appendChild(tooltipElement);

act(() => {
jest.advanceTimersByTime(300);
});

await waitFor(() => {
expect(lastPassedProps.should_run).toBe(true);
});

expect(mockSetGuideDtraderV2).toHaveBeenCalledWith({
trade_types_selection: false,
trade_page: false,
positions_page: false,
market_selector: false,
trade_param_quick_adjustment: false,
trade_params: true,
});

document.body.removeChild(tradeElement);
});

it('should not show guide when trade_params is already true', () => {
(useLocalStorageData as jest.Mock).mockReturnValue([
{
trade_types_selection: false,
trade_page: false,
positions_page: false,
market_selector: false,
trade_param_quick_adjustment: false,
trade_params: true,
},
mockSetGuideDtraderV2,
]);

render(<TradeParamsGuide />);
expect(lastPassedProps.should_run).toBe(false);
});
it('should handle callback to hide guide', async () => {
render(<TradeParamsGuide />);

act(() => {
lastPassedProps.callback?.();
});

expect(lastPassedProps.should_run).toBe(false);
});
it('should use latest localStorage value when showing guide', async () => {
const mockLatestStorage = {
trade_types_selection: false,
trade_page: true,
positions_page: false,
market_selector: false,
trade_param_quick_adjustment: false,
trade_params: false,
};

(getLocalStorage as jest.Mock).mockReturnValue(mockLatestStorage);

const tradeElement = document.createElement('div');
tradeElement.className = 'trade';
document.body.appendChild(tradeElement);

jest.spyOn(document, 'querySelector').mockReturnValue(tradeElement);

render(<TradeParamsGuide />);

const tooltipElement = document.createElement('div');
tooltipElement.className = 'trade__parameter-tooltip-info';
tradeElement.appendChild(tooltipElement);

act(() => {
jest.advanceTimersByTime(300);
});

await waitFor(() => {
expect(mockSetGuideDtraderV2).toHaveBeenCalledWith({
...mockLatestStorage,
trade_params: true,
});
});

document.body.removeChild(tradeElement);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useLocalStorageData } from '@deriv/hooks';
import { Localize } from '@deriv/translations';
import { getLocalStorage } from '@deriv/utils';
import React from 'react';
import { Step } from 'react-joyride';
import GuideContainer from '../GuideForPages/guide-container';

const TradeParamsGuide = React.memo(() => {
const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2');
const [show_guide, setShowGuide] = React.useState(false);
const timerRef = React.useRef<NodeJS.Timeout>();

React.useEffect(() => {
if (!guide_dtrader_v2?.trade_params) {
const element = document.querySelector('.trade');
const observer = new MutationObserver(() => {
if (document.querySelector('.trade__parameter-tooltip-info')) {
const latest_guide_dtrader_v2 = getLocalStorage('guide_dtrader_v2');
timerRef.current = setTimeout(() => {
setShowGuide(true);
setGuideDtraderV2({ ...latest_guide_dtrader_v2, trade_params: true });
}, 300);
observer.disconnect();
}
});
if (element) observer.observe(element, { childList: true, subtree: true });
}
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [guide_dtrader_v2, setGuideDtraderV2]);

const STEPS = React.useMemo(
() => [
{
content: <Localize i18n_default_text='Define your trade parameters.' />,
placement: 'top' as Step['placement'],
target: '.trade__parameter-tooltip-info',
spotlightPadding: 4,
title: <Localize i18n_default_text='Set your trade' />,
disableBeacon: true,
offset: 0,
},
],
[]
);

const handleCallback = React.useCallback(() => {
setShowGuide(false);
}, []);

return <GuideContainer should_run={show_guide} steps={STEPS} callback={handleCallback} />;
});

TradeParamsGuide.displayName = 'TradeParamsGuide';

export default TradeParamsGuide;
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TradeTypesSelectionGuide from '../trade-types-selection-guide';

const modal_text = 'Pin, rearrange, or remove your favorite trade types for easy access.';
const modal_text = 'Manage your preferred trade types for easy access on the trade page.';
const localStorage_key = 'guide_dtrader_v2';
const video = 'Video';

@@ -43,21 +43,33 @@ describe('TradeTypesSelectionGuide', () => {

it('should close the Modal and set flag to localStorage equal to true after user clicks on "Got it" button', async () => {
const field = 'trade_types_selection';

localStorage.setItem(
localStorage_key,
JSON.stringify({
[field]: false,
})
);

jest.useFakeTimers();
render(<TradeTypesSelectionGuide />);

await waitFor(() => jest.advanceTimersByTime(800));

expect(screen.getByText(video)).toBeInTheDocument();
expect(screen.getByText(modal_text)).toBeInTheDocument();
expect(JSON.parse(localStorage.getItem(localStorage_key) as string)[field]).toBe(false);

userEvent.click(screen.getByRole('button'));
const initialState = JSON.parse(localStorage.getItem(localStorage_key) || '{}');
expect(initialState[field]).toBe(false);

await userEvent.click(screen.getByRole('button'));
await waitFor(() => jest.advanceTimersByTime(300));

expect(screen.queryByText(video)).not.toBeInTheDocument();
expect(screen.queryByText(modal_text)).not.toBeInTheDocument();
expect(JSON.parse(localStorage.getItem(localStorage_key) as string)[field]).toBe(true);

const finalState = JSON.parse(localStorage.getItem(localStorage_key) || '{}');
expect(finalState[field]).toBe(true);

jest.useRealTimers();
});
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import React from 'react';
import { Modal } from '@deriv-com/quill-ui';
import { useLocalStorageData } from '@deriv/hooks';
import { Localize } from '@deriv/translations';
import { getLocalStorage } from '@deriv/utils';
import { DESCRIPTION_VIDEO_ID } from 'Modules/Trading/Helpers/video-config';
import StreamIframe from '../../StreamIframe';

@@ -13,20 +14,17 @@ const TradeTypesSelectionGuide: React.FC<TTradeTypeSelectionGuideProps> = ({ is_
const [is_modal_open, setIsModalOpen] = React.useState(false);
const guide_timeout_ref = React.useRef<ReturnType<typeof setTimeout>>();

const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2', {
trade_types_selection: false,
trade_page: false,
positions_page: false,
});
const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2');
const { trade_types_selection } = guide_dtrader_v2 || {};

const video_src = is_dark_mode_on
? DESCRIPTION_VIDEO_ID.trade_type_selection.dark
: DESCRIPTION_VIDEO_ID.trade_type_selection.light;

const onFinishGuide = () => {
const latest_guide_dtrader_v2 = getLocalStorage('guide_dtrader_v2');
setIsModalOpen(false);
setGuideDtraderV2({ ...guide_dtrader_v2, trade_types_selection: true });
setGuideDtraderV2({ ...latest_guide_dtrader_v2, trade_types_selection: true });
};

React.useEffect(() => {
@@ -50,10 +48,10 @@ const TradeTypesSelectionGuide: React.FC<TTradeTypeSelectionGuideProps> = ({ is_
>
<Modal.Header
image={<StreamIframe src={video_src} title='trade_types_selection' />}
title={<Localize i18n_default_text='Manage your trade types' />}
title={<Localize i18n_default_text='Add, reorder or remove from pinned' />}
/>
<Modal.Body>
<Localize i18n_default_text='Pin, rearrange, or remove your favorite trade types for easy access.' />
<Localize i18n_default_text='Manage your preferred trade types for easy access on the trade page.' />
</Modal.Body>
</Modal>
);
Original file line number Diff line number Diff line change
@@ -101,6 +101,7 @@ const Barrier = observer(({ is_minimized }: TTradeParametersProps) => {
shouldBlurOnClose={is_open}
>
<ActionSheet.Portal shouldCloseOnDrag>
<div className='trade__parameter-tooltip-info trade__parameter-tooltip-info-small' />
<Carousel
header={CarouselHeader}
title={<Localize i18n_default_text='Barrier' />}
Original file line number Diff line number Diff line change
@@ -196,6 +196,12 @@ const Duration = observer(({ is_minimized }: TTradeParametersProps) => {
shouldBlurOnClose={is_open}
>
<ActionSheet.Portal shouldCloseOnDrag>
<div
className={clsx('trade__parameter-tooltip-info', {
'trade__parameter-tooltip-info-small': duration_units_list.length <= 1,
'trade__parameter-tooltip-info-large': duration_units_list.length > 1,
})}
/>
<DurationActionSheetContainer
selected_hour={selected_hour}
setSelectedHour={setSelectedHour}
Original file line number Diff line number Diff line change
@@ -107,6 +107,7 @@ const GrowthRate = observer(({ is_minimized }: TTradeParametersProps) => {
shouldBlurOnClose={is_open}
>
<ActionSheet.Portal shouldCloseOnDrag>
<div className='trade__parameter-tooltip-info trade__parameter-tooltip-info-large' />
<Carousel
classname={clsx('growth-rate__carousel', is_small_screen && 'growth-rate__carousel--small')}
header={CarouselHeader}
Original file line number Diff line number Diff line change
@@ -77,6 +77,7 @@ const Multiplier = observer(({ is_minimized }: TTradeParametersProps) => {
shouldBlurOnClose={isOpen}
>
<ActionSheet.Portal shouldCloseOnDrag>
<div className='trade__parameter-tooltip-info trade__parameter-tooltip-info-large' />
<Carousel
classname={clsx(
'multiplier__carousel',
Original file line number Diff line number Diff line change
@@ -88,6 +88,7 @@ const PayoutPerPoint = observer(({ is_minimized }: TTradeParametersProps) => {
shouldBlurOnClose={is_open}
>
<ActionSheet.Portal shouldCloseOnDrag>
<div className='trade__parameter-tooltip-info trade__parameter-tooltip-info-large' />
<Carousel
classname={clsx(
'payout-per-point__carousel',
Original file line number Diff line number Diff line change
@@ -97,6 +97,7 @@ const RiskManagement = observer(({ is_minimized }: TTradeParametersProps) => {
shouldBlurOnClose={is_open}
>
<ActionSheet.Portal shouldCloseOnDrag>
<div className='trade__parameter-tooltip-info trade__parameter-tooltip-info-large' />
<Carousel
classname={clsx(
'risk-management__carousel',
Original file line number Diff line number Diff line change
@@ -59,6 +59,7 @@ const Stake = observer(({ is_minimized }: TTradeParametersProps) => {
shouldBlurOnClose={is_open}
>
<ActionSheet.Portal shouldCloseOnDrag>
<div className='trade__parameter-tooltip-info trade__parameter-tooltip-info-small' />
<ActionSheet.Header title={<Localize i18n_default_text='Stake' />} />
<StakeInput onClose={onClose} is_open={is_open} />
</ActionSheet.Portal>
Original file line number Diff line number Diff line change
@@ -110,6 +110,7 @@ const Strike = observer(({ is_minimized }: TTradeParametersProps) => {
shouldBlurOnClose={is_open}
>
<ActionSheet.Portal shouldCloseOnDrag>
<div className='trade__parameter-tooltip-info trade__parameter-tooltip-info-large' />
<Carousel
classname={clsx('strike__carousel', is_small_screen && 'strike__carousel--small')}
header={CarouselHeader}
Original file line number Diff line number Diff line change
@@ -58,6 +58,7 @@ const TakeProfit = observer(({ is_minimized }: TTradeParametersProps) => {
shouldBlurOnClose={is_open}
>
<ActionSheet.Portal shouldCloseOnDrag>
<div className='trade__parameter-tooltip-info trade__parameter-tooltip-info-small' />
<Carousel
header={CarouselHeader}
pages={action_sheet_content}
Original file line number Diff line number Diff line change
@@ -15,11 +15,7 @@ import OnboardingGuide from 'AppV2/Components/OnboardingGuide/GuideForPages';
const Positions = observer(() => {
const [hasButtonsDemo, setHasButtonsDemo] = React.useState(false);
const [activeTab, setActiveTab] = React.useState(getPositionsV2TabIndexFromURL());
const [guide_dtrader_v2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2', {
trade_types_selection: false,
trade_page: false,
positions_page: false,
});
const [guide_dtrader_v2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2');
const history = useHistory();

const {
33 changes: 30 additions & 3 deletions packages/trader/src/AppV2/Containers/Trade/trade-types.tsx
Original file line number Diff line number Diff line change
@@ -3,16 +3,17 @@ import clsx from 'clsx';

import { LabelPairedPresentationScreenSmRegularIcon } from '@deriv/quill-icons';
import { Localize, localize } from '@deriv/translations';
import { safeParse } from '@deriv/utils';
import { safeParse, getLocalStorage } from '@deriv/utils';
import { ActionSheet, Button, Chip, Text } from '@deriv-com/quill-ui';

import { useLocalStorageData } from '@deriv/hooks';
import Carousel from 'AppV2/Components/Carousel';
import CarouselHeader from 'AppV2/Components/Carousel/carousel-header';
import TradeTypesSelectionGuide from 'AppV2/Components/OnboardingGuide/TradeTypesSelectionGuide';
import { checkContractTypePrefix } from 'AppV2/Utils/contract-type';
import { getTradeTypesList, sortCategoriesInTradeTypeOrder } from 'AppV2/Utils/trade-types-utils';
import { useTraderStore } from 'Stores/useTraderStores';

import GuideContainer from '../../Components/OnboardingGuide/GuideForPages/guide-container';
import useGuideStates from 'AppV2/Hooks/useGuideStates';
import { sendOpenGuideToAnalytics } from '../../../Analytics';
import Guide from '../../Components/Guide';

@@ -46,8 +47,17 @@ export type TResultItem = {
const TradeTypes = ({ contract_type, onTradeTypeSelect, trade_types, is_dark_mode_on }: TTradeTypesProps) => {
const [is_open, setIsOpen] = React.useState<boolean>(false);
const [is_editing, setIsEditing] = React.useState<boolean>(false);
const [guide_dtrader_v2, setGuideDtraderV2] = useLocalStorageData<Record<string, boolean>>('guide_dtrader_v2');
const { guideStates, setGuideState } = useGuideStates();
const { should_run_trade_page_guide } = guideStates;
const trade_types_ref = React.useRef<HTMLDivElement>(null);

const onCloseGuide = () => {
const latest_guide_dtrader_v2 = getLocalStorage('guide_dtrader_v2');
setGuideState('should_run_trade_page_guide', false);
setGuideDtraderV2({ ...latest_guide_dtrader_v2, trade_page: true });
};

const createArrayFromCategories = (data: TTradeTypesProps['trade_types']): TItem[] => {
const result: TItem[] = [];

@@ -256,8 +266,25 @@ const TradeTypes = ({ contract_type, onTradeTypeSelect, trade_types, is_dark_mod
},
];

const STEPS = [
{
content: <Localize i18n_default_text='Swipe left or right to explore trade types.' />,
offset: 0,
spotlightPadding: 2,
target: '.trade__trade-types',
disableBeacon: true,
spotlightClicks: true,
title: <Localize i18n_default_text='Explore trade types' />,
},
];

return (
<div className='trade__trade-types' ref={trade_types_ref}>
<GuideContainer
should_run={should_run_trade_page_guide && !guide_dtrader_v2?.trade_types_selection}
steps={STEPS}
callback={onCloseGuide}
/>
{trade_type_chips.map(({ title, id }: TItem) => (
<Chip.Selectable
key={id}
12 changes: 12 additions & 0 deletions packages/trader/src/AppV2/Containers/Trade/trade.scss
Original file line number Diff line number Diff line change
@@ -69,6 +69,18 @@
&--with-button {
min-height: 13.6rem;
}

&-tooltip-info {
position: fixed;
bottom: 8rem;
width: 100%;
&-large {
height: 38rem;
}
&-small {
height: 30rem;
}
}
}
}

13 changes: 10 additions & 3 deletions packages/trader/src/AppV2/Containers/Trade/trade.tsx
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@ import OnboardingGuide from 'AppV2/Components/OnboardingGuide/GuideForPages';
import PurchaseButton from 'AppV2/Components/PurchaseButton';
import ServiceErrorSheet from 'AppV2/Components/ServiceErrorSheet';
import TradeErrorSnackbar from 'AppV2/Components/TradeErrorSnackbar';
import TradeParamsGuide from 'AppV2/Components/OnboardingGuide/TradeParamsGuide/trade-params-guide';
import QuickAdjGuide from 'AppV2/Components/OnboardingGuide/QuickAdjGuide/quick-adj-guide';
import { TradeParameters, TradeParametersContainer } from 'AppV2/Components/TradeParameters';
import useContractsForCompany from 'AppV2/Hooks/useContractsForCompany';
import { getChartHeight, HEIGHT } from 'AppV2/Utils/layout-utils';
@@ -55,6 +57,9 @@ const Trade = observer(() => {
trade_types_selection: false,
trade_page: false,
positions_page: false,
market_selector: false,
trade_param_quick_adjustment: false,
trade_params: false,
});

// For handling edge cases of snackbar:
@@ -93,7 +98,7 @@ const Trade = observer(() => {
const onScroll = React.useCallback(() => {
const current_chart_ref = chart_ref?.current;
if (current_chart_ref) {
const chart_bottom_Y = current_chart_ref.getBoundingClientRect().bottom;
const chart_bottom_Y = current_chart_ref.getBoundingClientRect().bottom + (is_accumulator ? 150 : 65);
const container_bottom_Y = window.innerHeight - HEIGHT.BOTTOM_NAV;
setIsMinimizedParamsVisible(chart_bottom_Y <= container_bottom_Y);
}
@@ -147,8 +152,10 @@ const Trade = observer(() => {
</TradeParametersContainer>
{!is_market_closed && <PurchaseButton />}
</div>
{!guide_dtrader_v2?.trade_page && is_logged_in && (
<OnboardingGuide type='trade_page' is_dark_mode_on={is_dark_mode_on} />
{!guide_dtrader_v2?.trade_page && is_logged_in && <OnboardingGuide type='trade_page' />}
{!guide_dtrader_v2?.trade_params && is_logged_in && <TradeParamsGuide />}
{!guide_dtrader_v2?.trade_param_quick_adjustment && is_logged_in && (
<QuickAdjGuide is_minimized_visible={is_minimized_params_visible} is_minimized />
)}
</React.Fragment>
) : (
115 changes: 115 additions & 0 deletions packages/trader/src/AppV2/Hooks/__tests__/useGuideStates.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { act } from '@testing-library/react';
import useGuideStates from '../useGuideStates';
import { renderHook } from '@testing-library/react-hooks';

describe('useGuideStates', () => {
beforeEach(() => {
act(() => {
const { result } = renderHook(() => useGuideStates());
result.current.setGuideState('should_run_trade_page_guide', false);
result.current.setGuideState('should_run_market_selector_guide', false);
});
});
it('should initialize with default values', () => {
const { result } = renderHook(() => useGuideStates());

expect(result.current.guideStates).toEqual({
should_run_trade_page_guide: false,
should_run_market_selector_guide: false,
});
});
it('should update guide state when setGuideState is called', () => {
const { result } = renderHook(() => useGuideStates());

act(() => {
result.current.setGuideState('should_run_trade_page_guide', true);
});

expect(result.current.guideStates.should_run_trade_page_guide).toBe(true);
expect(result.current.guideStates.should_run_market_selector_guide).toBe(false);
});
it('should maintain state across multiple hook instances', () => {
const { result: result1 } = renderHook(() => useGuideStates());
const { result: result2 } = renderHook(() => useGuideStates());

act(() => {
result1.current.setGuideState('should_run_market_selector_guide', true);
});

expect(result1.current.guideStates.should_run_market_selector_guide).toBe(true);
expect(result2.current.guideStates.should_run_market_selector_guide).toBe(true);
});
it('should update all instances when state changes', () => {
const { result: result1 } = renderHook(() => useGuideStates());
const { result: result2 } = renderHook(() => useGuideStates());

act(() => {
result1.current.setGuideState('should_run_trade_page_guide', true);
});

expect(result1.current.guideStates.should_run_trade_page_guide).toBe(true);
expect(result2.current.guideStates.should_run_trade_page_guide).toBe(true);
});
it('should handle multiple state updates', () => {
const { result } = renderHook(() => useGuideStates());

act(() => {
result.current.setGuideState('should_run_trade_page_guide', true);
result.current.setGuideState('should_run_market_selector_guide', true);
});

expect(result.current.guideStates).toEqual({
should_run_trade_page_guide: true,
should_run_market_selector_guide: true,
});
});
it('should cleanup listeners on unmount', () => {
const { unmount } = renderHook(() => useGuideStates());
const listenersSpy = jest.spyOn(Set.prototype, 'delete');

unmount();

expect(listenersSpy).toHaveBeenCalled();
});
it('should maintain independent updates for different flags', () => {
const { result } = renderHook(() => useGuideStates());

act(() => {
result.current.setGuideState('should_run_trade_page_guide', true);
});

expect(result.current.guideStates.should_run_trade_page_guide).toBe(true);
expect(result.current.guideStates.should_run_market_selector_guide).toBe(false);

act(() => {
result.current.setGuideState('should_run_market_selector_guide', true);
});

expect(result.current.guideStates.should_run_trade_page_guide).toBe(true);
expect(result.current.guideStates.should_run_market_selector_guide).toBe(true);
});
it('should handle rapid consecutive updates', () => {
const { result } = renderHook(() => useGuideStates());

act(() => {
result.current.setGuideState('should_run_trade_page_guide', true);
result.current.setGuideState('should_run_trade_page_guide', false);
result.current.setGuideState('should_run_trade_page_guide', true);
});

expect(result.current.guideStates.should_run_trade_page_guide).toBe(true);
});
it('should notify all listeners when state changes', () => {
const { result: result1 } = renderHook(() => useGuideStates());
const { result: result2 } = renderHook(() => useGuideStates());
const { result: result3 } = renderHook(() => useGuideStates());

act(() => {
result1.current.setGuideState('should_run_trade_page_guide', true);
});

[result1, result2, result3].forEach(result => {
expect(result.current.guideStates.should_run_trade_page_guide).toBe(true);
});
});
});
39 changes: 39 additions & 0 deletions packages/trader/src/AppV2/Hooks/useGuideStates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';

type GuideFlags = 'should_run_trade_page_guide' | 'should_run_market_selector_guide';

type GuideStates = Record<GuideFlags, boolean>;

let sharedGuideStates: GuideStates = {
should_run_trade_page_guide: false,
should_run_market_selector_guide: false,
};

const listeners = new Set<() => void>();

const useGuideStates = () => {
const [, setUpdate] = React.useState({});

React.useEffect(() => {
const update = () => setUpdate({});
listeners.add(update);
return () => {
listeners.delete(update);
};
}, []);

const setGuideState = (flag: GuideFlags, value: boolean) => {
sharedGuideStates = {
...sharedGuideStates,
[flag]: value,
};
listeners.forEach(listener => listener());
};

return {
guideStates: sharedGuideStates,
setGuideState,
};
};

export default useGuideStates;

Unchanged files with check annotations Beta

renderComponent();
await waitFor(() => {
expect(screen.queryByRole('button')).toBeInTheDocument();

Check warning on line 41 in packages/wallets/src/components/Base/WalletClipboard/__tests__/WalletClipboard.spec.tsx

GitHub Actions / Build And Test

Use `getBy*` queries rather than `queryBy*` for checking element is present
expect(screen.queryByTestId('dt_legacy_copy_icon')).toBeInTheDocument();

Check warning on line 42 in packages/wallets/src/components/Base/WalletClipboard/__tests__/WalletClipboard.spec.tsx

GitHub Actions / Build And Test

Avoid using multiple assertions within `waitFor` callback

Check warning on line 42 in packages/wallets/src/components/Base/WalletClipboard/__tests__/WalletClipboard.spec.tsx

GitHub Actions / Build And Test

Use `getBy*` queries rather than `queryBy*` for checking element is present
});
});
it('clears timeout on unmount', async () => {
await userEvent.hover(screen.getByRole('button'));
await waitFor(() => {
expect(screen.queryByText('Copy')).toBeInTheDocument();

Check warning on line 61 in packages/wallets/src/components/Base/WalletClipboard/__tests__/WalletClipboard.spec.tsx

GitHub Actions / Build And Test

Use `getBy*` queries rather than `queryBy*` for checking element is present
});
});
});
await renderScenario();
await waitFor(() => {
expect(screen.queryByTestId('dt_legacy_won_icon')).toBeInTheDocument();

Check warning on line 80 in packages/wallets/src/components/Base/WalletClipboard/__tests__/WalletClipboard.spec.tsx

GitHub Actions / Build And Test

Use `getBy*` queries rather than `queryBy*` for checking element is present
});
});
it('calls copy function with textCopy', async () => {
await renderScenario();
await waitFor(() => {
expect(screen.queryByText('Copied!')).toBeInTheDocument();

Check warning on line 92 in packages/wallets/src/components/Base/WalletClipboard/__tests__/WalletClipboard.spec.tsx

GitHub Actions / Build And Test

Use `getBy*` queries rather than `queryBy*` for checking element is present
});
});
it('resets the icon and message after 2 seconds', async () => {
await renderScenario();
await waitFor(() => {
expect(screen.queryByText('Copied!')).toBeInTheDocument();

Check warning on line 100 in packages/wallets/src/components/Base/WalletClipboard/__tests__/WalletClipboard.spec.tsx

GitHub Actions / Build And Test

Use `getBy*` queries rather than `queryBy*` for checking element is present
expect(screen.queryByTestId('dt_legacy_won_icon')).toBeInTheDocument();

Check warning on line 101 in packages/wallets/src/components/Base/WalletClipboard/__tests__/WalletClipboard.spec.tsx

GitHub Actions / Build And Test

Avoid using multiple assertions within `waitFor` callback

Check warning on line 101 in packages/wallets/src/components/Base/WalletClipboard/__tests__/WalletClipboard.spec.tsx

GitHub Actions / Build And Test

Use `getBy*` queries rather than `queryBy*` for checking element is present
});
act(() => {
await waitFor(() => {
expect(screen.queryByText('Copied!')).not.toBeInTheDocument();
expect(screen.queryByTestId('dt_legacy_copy_icon')).toBeInTheDocument();

Check warning on line 110 in packages/wallets/src/components/Base/WalletClipboard/__tests__/WalletClipboard.spec.tsx

GitHub Actions / Build And Test

Avoid using multiple assertions within `waitFor` callback
});
});
});