From be65dbe66f5dac45f7b542f7700ddac26c61ef9b Mon Sep 17 00:00:00 2001 From: jamaljsr <1356600+jamaljsr@users.noreply.github.com> Date: Wed, 22 Jul 2020 23:32:46 -0400 Subject: [PATCH 1/2] tour: add in-app walk through --- app/package.json | 5 +- app/src/App.tsx | 2 + app/src/__stories__/LoopPage.stories.tsx | 35 +-- app/src/__stories__/StoryWrapper.tsx | 1 + app/src/api/grpc.ts | 17 ++ app/src/api/lnd.ts | 2 +- app/src/api/loop.ts | 2 +- app/src/assets/icons/help-circle.svg | 5 + app/src/components/NodeStatus.tsx | 2 +- app/src/components/base/icons.tsx | 2 + app/src/components/common/PageHeader.tsx | 22 +- app/src/components/common/Tile.tsx | 9 +- app/src/components/loop/ChannelList.tsx | 2 +- app/src/components/loop/ChannelRow.tsx | 12 +- app/src/components/loop/LoopActions.tsx | 5 +- app/src/components/loop/LoopPage.tsx | 15 +- app/src/components/loop/LoopTiles.tsx | 18 +- .../loop/processing/ProcessingSwaps.tsx | 4 +- .../loop/processing/SwapProgress.tsx | 2 +- .../components/loop/swap/SwapConfigStep.tsx | 2 +- .../components/loop/swap/SwapProcessing.tsx | 17 +- .../components/loop/swap/SwapReviewStep.tsx | 2 +- app/src/components/tour/SuccessStep.tsx | 52 +++++ app/src/components/tour/TextStep.tsx | 79 +++++++ app/src/components/tour/TourHost.tsx | 150 ++++++++++++ app/src/components/tour/WelcomeStep.tsx | 73 ++++++ app/src/i18n/locales/en-US.json | 37 +++ app/src/store/stores/buildSwapStore.ts | 3 + app/src/store/stores/settingsStore.ts | 6 + app/src/store/stores/uiStore.ts | 83 +++++++ app/src/util/tests/sampleData.ts | 6 +- app/yarn.lock | 213 +++++++++++++++++- 32 files changed, 817 insertions(+), 68 deletions(-) create mode 100644 app/src/assets/icons/help-circle.svg create mode 100644 app/src/components/tour/SuccessStep.tsx create mode 100644 app/src/components/tour/TextStep.tsx create mode 100644 app/src/components/tour/TourHost.tsx create mode 100644 app/src/components/tour/WelcomeStep.tsx diff --git a/app/package.json b/app/package.json index 9b4b1e382..050a6ab1c 100644 --- a/app/package.json +++ b/app/package.json @@ -43,7 +43,9 @@ "react-router": "5.2.0", "react-scripts": "3.4.1", "react-toastify": "6.0.6", - "react-virtualized": "9.21.2" + "react-virtualized": "9.21.2", + "reactour": "1.18.0", + "styled-components": "5.1.1" }, "devDependencies": { "@storybook/addon-actions": "5.3.19", @@ -67,6 +69,7 @@ "@types/react-dom": "16.9.8", "@types/react-router": "5.1.8", "@types/react-virtualized": "9.21.10", + "@types/reactour": "1.17.1", "@typescript-eslint/eslint-plugin": "3.5.0", "@typescript-eslint/parser": "3.5.0", "cross-env": "7.0.2", diff --git a/app/src/App.tsx b/app/src/App.tsx index 5567d2d3c..e12a0a27e 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -3,6 +3,7 @@ import './App.scss'; import { createStore, StoreProvider } from 'store'; import AlertContainer from 'components/common/AlertContainer'; import { ThemeProvider } from 'components/theme'; +import TourHost from 'components/tour/TourHost'; import Routes from './Routes'; const App = () => { @@ -13,6 +14,7 @@ const App = () => { + ); diff --git a/app/src/__stories__/LoopPage.stories.tsx b/app/src/__stories__/LoopPage.stories.tsx index d0713c6a5..0396e4f8c 100644 --- a/app/src/__stories__/LoopPage.stories.tsx +++ b/app/src/__stories__/LoopPage.stories.tsx @@ -1,5 +1,6 @@ import React, { useEffect } from 'react'; -import { observable, ObservableMap, values } from 'mobx'; +import { observable } from 'mobx'; +import { lndListChannelsMany } from 'util/tests/sampleData'; import { useStore } from 'store'; import { Channel } from 'store/models'; import { Layout } from 'components/layout'; @@ -11,35 +12,23 @@ export default { parameters: { contained: true }, }; -const channelSubset = (channels: ObservableMap) => { - const few = values(channels) - .slice(0, 20) - .reduce((result, c) => { - result[c.chanId] = c; - return result; - }, {} as Record); - return observable.map(few); +export const Default = () => { + return ; }; -export const Default = () => { - const { channelStore } = useStore(); +export const ManyChannels = () => { + const store = useStore(); useEffect(() => { - // only use a small set of channels - channelStore.channels = channelSubset(channelStore.channels); - }, [channelStore]); - + store.channelStore.channels = observable.map(); + lndListChannelsMany.channelsList.forEach(c => { + const chan = new Channel(store, c); + store.channelStore.channels.set(chan.chanId, chan); + }); + }); return ; }; -export const ManyChannels = () => ; - export const InsideLayout = () => { - const { channelStore } = useStore(); - useEffect(() => { - // only use a small set of channels - channelStore.channels = channelSubset(channelStore.channels); - }, [channelStore]); - return ( diff --git a/app/src/__stories__/StoryWrapper.tsx b/app/src/__stories__/StoryWrapper.tsx index d397739ba..25e2f32bd 100644 --- a/app/src/__stories__/StoryWrapper.tsx +++ b/app/src/__stories__/StoryWrapper.tsx @@ -11,6 +11,7 @@ import { ThemeProvider } from 'components/theme'; // mock the GRPC client to return sample data instead of making an actual request const grpc = { + useSampleData: true, request: (methodDescriptor: any, opts: any, metadata: any) => { // fail any authenticated requests to simulate incorrect login attempts if (metadata && metadata.authorization) throw new AuthenticationError(); diff --git a/app/src/api/grpc.ts b/app/src/api/grpc.ts index 06d110839..3dfc87445 100644 --- a/app/src/api/grpc.ts +++ b/app/src/api/grpc.ts @@ -8,8 +8,14 @@ import { import { DEV_HOST } from 'config'; import { AuthenticationError } from 'util/errors'; import { grpcLog as log } from 'util/log'; +import { sampleApiResponses } from 'util/tests/sampleData'; class GrpcClient { + /** + * Indicates if the API should return sample data instead of making real GRPC requests + */ + useSampleData = false; + /** * Executes a single GRPC request and returns a promise which will resolve with the response * @param methodDescriptor the GRPC method to call on the service @@ -22,6 +28,15 @@ class GrpcClient { metadata?: Metadata.ConstructorArg, ): Promise { return new Promise((resolve, reject) => { + if (this.useSampleData) { + const endpoint = `${methodDescriptor.service.serviceName}.${methodDescriptor.methodName}`; + const data = sampleApiResponses[endpoint] || {}; + // the calling function expects the return value to have a `toObject` function + const response: any = { toObject: () => data }; + resolve(response); + return; + } + const method = `${methodDescriptor.methodName}`; log.debug(`${method} request`, request.toObject()); grpc.unary(methodDescriptor, { @@ -59,6 +74,8 @@ class GrpcClient { onMessage: (res: TRes) => void, metadata?: Metadata.ConstructorArg, ) { + if (this.useSampleData) return; + const method = `${methodDescriptor.methodName}`; const client = grpc.client(methodDescriptor, { host: DEV_HOST, diff --git a/app/src/api/lnd.ts b/app/src/api/lnd.ts index 52e687079..8e9bd213d 100644 --- a/app/src/api/lnd.ts +++ b/app/src/api/lnd.ts @@ -13,7 +13,7 @@ interface LndEvents { * An API wrapper to communicate with the LND node via GRPC */ class LndApi extends BaseApi { - private _grpc: GrpcClient; + _grpc: GrpcClient; constructor(grpc: GrpcClient) { super(); diff --git a/app/src/api/loop.ts b/app/src/api/loop.ts index f650fe462..ccea47394 100644 --- a/app/src/api/loop.ts +++ b/app/src/api/loop.ts @@ -14,7 +14,7 @@ interface LoopEvents { * An API wrapper to communicate with the Loop daemon via GRPC */ class LoopApi extends BaseApi { - private _grpc: GrpcClient; + _grpc: GrpcClient; constructor(grpc: GrpcClient) { super(); diff --git a/app/src/assets/icons/help-circle.svg b/app/src/assets/icons/help-circle.svg new file mode 100644 index 000000000..03e159820 --- /dev/null +++ b/app/src/assets/icons/help-circle.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/components/NodeStatus.tsx b/app/src/components/NodeStatus.tsx index 253f60f15..e612d54da 100644 --- a/app/src/components/NodeStatus.tsx +++ b/app/src/components/NodeStatus.tsx @@ -30,7 +30,7 @@ const NodeStatus: React.FC = () => { const { Wrapper, Balance, Divider } = Styled; return ( - + {l('title')} diff --git a/app/src/components/base/icons.tsx b/app/src/components/base/icons.tsx index 488198603..6208c738a 100644 --- a/app/src/components/base/icons.tsx +++ b/app/src/components/base/icons.tsx @@ -11,6 +11,7 @@ import { ReactComponent as CloseIcon } from 'assets/icons/close.svg'; import { ReactComponent as CopyIcon } from 'assets/icons/copy.svg'; import { ReactComponent as DotIcon } from 'assets/icons/dot.svg'; import { ReactComponent as DownloadIcon } from 'assets/icons/download.svg'; +import { ReactComponent as HelpCircleIcon } from 'assets/icons/help-circle.svg'; import { ReactComponent as MaximizeIcon } from 'assets/icons/maximize.svg'; import { ReactComponent as MenuIcon } from 'assets/icons/menu.svg'; import { ReactComponent as MinimizeIcon } from 'assets/icons/minimize.svg'; @@ -73,6 +74,7 @@ export const ChevronsRight = Icon.withComponent(ChevronsRightIcon); export const Close = Icon.withComponent(CloseIcon); export const Copy = Icon.withComponent(CopyIcon); export const Dot = Icon.withComponent(DotIcon); +export const HelpCircle = Icon.withComponent(HelpCircleIcon); export const Menu = Icon.withComponent(MenuIcon); export const Minimize = Icon.withComponent(MinimizeIcon); export const Maximize = Icon.withComponent(MaximizeIcon); diff --git a/app/src/components/common/PageHeader.tsx b/app/src/components/common/PageHeader.tsx index b63167f95..8729ba941 100644 --- a/app/src/components/common/PageHeader.tsx +++ b/app/src/components/common/PageHeader.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite'; import { usePrefixedTranslation } from 'hooks'; import { useStore } from 'store'; import { styled } from 'components/theme'; -import { ArrowLeft, Download, HeaderThree } from '../base'; +import { ArrowLeft, Download, HeaderThree, HelpCircle } from '../base'; import Tip from './Tip'; const Styled = { @@ -25,7 +25,7 @@ const Styled = { text-align: right; svg { - margin-left: 50px; + margin-left: 20px; } `, BackLink: styled.a` @@ -44,10 +44,17 @@ interface Props { title: ReactNode; onBackClick?: () => void; backText?: string; + onHelpClick?: () => void; onExportClick?: () => void; } -const PageHeader: React.FC = ({ title, onBackClick, backText, onExportClick }) => { +const PageHeader: React.FC = ({ + title, + onBackClick, + backText, + onHelpClick, + onExportClick, +}) => { const { l } = usePrefixedTranslation('cmps.common.PageHeader'); const { settingsStore } = useStore(); @@ -63,12 +70,17 @@ const PageHeader: React.FC = ({ title, onBackClick, backText, onExportCli )}
- {title} + {title}
+ {onHelpClick && ( + + + + )} {onExportClick && ( - + )} diff --git a/app/src/components/common/Tile.tsx b/app/src/components/common/Tile.tsx index 4790a6b0d..c925b5f4b 100644 --- a/app/src/components/common/Tile.tsx +++ b/app/src/components/common/Tile.tsx @@ -43,6 +43,11 @@ interface Props { * provided, then the `children` will be displayed instead */ text?: ReactNode; + /** + * optional value to set as the `data-tour` attribute on + * the tile's dom element + */ + tour?: string; /** * optional click handler for the icon which will not be * visible if this prop is not defined @@ -50,12 +55,12 @@ interface Props { onMaximizeClick?: () => void; } -const Tile: React.FC = ({ title, text, onMaximizeClick, children }) => { +const Tile: React.FC = ({ title, text, tour, onMaximizeClick, children }) => { const { l } = usePrefixedTranslation('cmps.common.Tile'); const { TileWrap, Header, MaximizeIcon, Text } = Styled; return ( - +
{title} {onMaximizeClick && ( diff --git a/app/src/components/loop/ChannelList.tsx b/app/src/components/loop/ChannelList.tsx index 8c92850a0..0b5a6eb7d 100644 --- a/app/src/components/loop/ChannelList.tsx +++ b/app/src/components/loop/ChannelList.tsx @@ -32,7 +32,7 @@ const ChannelList: React.FC = () => { const { Wrapper, ListContainer } = Styled; return ( - + diff --git a/app/src/components/loop/ChannelRow.tsx b/app/src/components/loop/ChannelRow.tsx index a95603ee5..96e6d086e 100644 --- a/app/src/components/loop/ChannelRow.tsx +++ b/app/src/components/loop/ChannelRow.tsx @@ -88,25 +88,25 @@ export const ChannelRowHeader: React.FC = () => { - {l('canReceive')} + {l('canReceive')} - {l('canSend')} + {l('canSend')} - {l('feeRate')} + {l('feeRate')} - {l('upTime')} + {l('upTime')} - {l('peer')} + {l('peer')} - {l('capacity')} + {l('capacity')} ); diff --git a/app/src/components/loop/LoopActions.tsx b/app/src/components/loop/LoopActions.tsx index bc096fa9d..69858da1c 100644 --- a/app/src/components/loop/LoopActions.tsx +++ b/app/src/components/loop/LoopActions.tsx @@ -71,7 +71,7 @@ const LoopActions: React.FC = () => { const { Wrapper, Actions, ActionBar, CloseIcon, Selected, Note } = Styled; return ( - + {buildSwapStore.showActions ? ( @@ -82,6 +82,7 @@ const LoopActions: React.FC = () => { borderless onClick={handleLoopOut} disabled={!isLoopOutMinimumMet} + data-tour="loop-out" > {l('common.loopOut')} @@ -101,7 +102,7 @@ const LoopActions: React.FC = () => { {note && {note}} ) : ( - diff --git a/app/src/components/loop/LoopPage.tsx b/app/src/components/loop/LoopPage.tsx index 1156815c7..93c418b88 100644 --- a/app/src/components/loop/LoopPage.tsx +++ b/app/src/components/loop/LoopPage.tsx @@ -8,9 +8,8 @@ import { styled } from 'components/theme'; import ChannelList from './ChannelList'; import LoopActions from './LoopActions'; import LoopTiles from './LoopTiles'; - -const LazySwapWizard = React.lazy(() => import('./swap/SwapWizard')); -const LazyProcessingSwaps = React.lazy(() => import('./processing/ProcessingSwaps')); +import ProcessingSwaps from './processing/ProcessingSwaps'; +import SwapWizard from './swap/SwapWizard'; const Styled = { PageWrap: styled.div` @@ -37,12 +36,16 @@ const LoopPage: React.FC = () => { return ( {uiStore.processingSwapsVisible ? ( - + ) : buildSwapStore.showWizard ? ( - + ) : ( <> - + diff --git a/app/src/components/loop/LoopTiles.tsx b/app/src/components/loop/LoopTiles.tsx index 57e7b26c0..bc548c204 100644 --- a/app/src/components/loop/LoopTiles.tsx +++ b/app/src/components/loop/LoopTiles.tsx @@ -23,15 +23,27 @@ const LoopTiles: React.FC = () => { - + - } /> + } + /> - } /> + } + /> diff --git a/app/src/components/loop/processing/ProcessingSwaps.tsx b/app/src/components/loop/processing/ProcessingSwaps.tsx index c7f3ce8ee..fcbc36775 100644 --- a/app/src/components/loop/processing/ProcessingSwaps.tsx +++ b/app/src/components/loop/processing/ProcessingSwaps.tsx @@ -50,10 +50,10 @@ const ProcessingSwaps: React.FC = () => {
{l('title')} - +
- + {swapStore.processingSwaps.map(swap => ( ))} diff --git a/app/src/components/loop/processing/SwapProgress.tsx b/app/src/components/loop/processing/SwapProgress.tsx index 371df88de..1e26ef6f7 100644 --- a/app/src/components/loop/processing/SwapProgress.tsx +++ b/app/src/components/loop/processing/SwapProgress.tsx @@ -63,7 +63,7 @@ const SwapProgress: React.FC = ({ swap }) => { const { Wrapper, Status, Track, Fill } = Styled; return ( - + {swap.typeName} {swap.stateLabel} diff --git a/app/src/components/loop/swap/SwapConfigStep.tsx b/app/src/components/loop/swap/SwapConfigStep.tsx index 0000e6df7..4c5010757 100644 --- a/app/src/components/loop/swap/SwapConfigStep.tsx +++ b/app/src/components/loop/swap/SwapConfigStep.tsx @@ -34,7 +34,7 @@ const SwapConfigStep: React.FC = () => { const { Wrapper, Summary, Config } = Styled; return ( - + { const { l } = usePrefixedTranslation('cmps.loop.swap.SwapProcessingStep'); - return ; + const { Wrapper } = Styled; + return ( + + + + ); }; export default observer(SwapProcessingStep); diff --git a/app/src/components/loop/swap/SwapReviewStep.tsx b/app/src/components/loop/swap/SwapReviewStep.tsx index b8b20d921..67feb846c 100644 --- a/app/src/components/loop/swap/SwapReviewStep.tsx +++ b/app/src/components/loop/swap/SwapReviewStep.tsx @@ -45,7 +45,7 @@ const SwapReviewStep: React.FC = () => { const { Wrapper, Summary, Invoice, InvoiceRow, Divider } = Styled; return ( - + diff --git a/app/src/components/tour/SuccessStep.tsx b/app/src/components/tour/SuccessStep.tsx new file mode 100644 index 000000000..86bad8f69 --- /dev/null +++ b/app/src/components/tour/SuccessStep.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { ReactourStepContentArgs } from 'reactour'; +import { observer } from 'mobx-react-lite'; +import confirmJson from 'assets/animations/confirm.json'; +import { usePrefixedTranslation } from 'hooks'; +import { useStore } from 'store'; +import { Button, HeaderThree } from 'components/base'; +import Animation from 'components/common/Animation'; +import { styled } from 'components/theme'; +import TextStep from './TextStep'; + +const Styled = { + Centered: styled.div` + text-align: center; + `, + ConfirmAnimation: styled(Animation)` + width: 200px; + height: 200px; + margin: auto; + `, + SmallButton: styled(Button)` + font-size: ${props => props.theme.sizes.xs}; + min-width: auto; + height: 34px; + + &:hover { + color: ${props => props.theme.colors.offWhite}; + background-color: ${props => props.theme.colors.darkBlue}; + } + `, +}; + +const SuccessStep: React.FC = props => { + const { l } = usePrefixedTranslation('cmps.tour.SuccessStep'); + const { uiStore } = useStore(); + + const { Centered, ConfirmAnimation, SmallButton } = Styled; + return ( + + + + {l('header')} +

{l('complete')}

+

+ {l('close')} +

+
+
+ ); +}; + +export default observer(SuccessStep); diff --git a/app/src/components/tour/TextStep.tsx b/app/src/components/tour/TextStep.tsx new file mode 100644 index 000000000..46c3cab36 --- /dev/null +++ b/app/src/components/tour/TextStep.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { ReactourStepContentArgs } from 'reactour'; +import { usePrefixedTranslation } from 'hooks'; +import { Button, HeaderThree } from 'components/base'; +import { styled } from 'components/theme'; + +const Styled = { + Wrapper: styled.div` + color: ${props => props.theme.colors.darkBlue}; + padding-top: 15px; + `, + Footer: styled.div` + text-align: right; + `, + SmallButton: styled(Button)` + font-size: ${props => props.theme.sizes.xs}; + min-width: auto; + height: 34px; + + &:hover { + color: ${props => props.theme.colors.offWhite}; + background-color: ${props => props.theme.colors.darkBlue}; + } + `, +}; + +interface Props extends ReactourStepContentArgs { + i18nKey?: string; + header?: string; + showNext?: boolean; +} + +const TextStep: React.FC = ({ + step, + goTo, + header, + i18nKey, + showNext = true, + children, +}) => { + const { l } = usePrefixedTranslation('cmps.tour.TextStep'); + + let content = children; + if (i18nKey) { + // split multiple lines of text into paragraphs + const text = l(i18nKey) as string; + content = text + .split('\n') + .map((line, i) =>

); + } + + const { Wrapper, Footer, SmallButton } = Styled; + return ( + + {header && {header}} + {content} + {showNext && ( +

+ goTo(step)}>{l('next')} +
+ )} +
+ ); +}; + +/** + * Returns a function which creates a Reactour step using the additional parameters + * @param i18nKey the key to lookup the i18n string + * @param showNext indicates if the Next button should be displayed + */ +export const createTextStep = (i18nKey: string, showNext = true) => { + const createFunc = (p: ReactourStepContentArgs) => ( + + ); + + return createFunc; +}; + +export default TextStep; diff --git a/app/src/components/tour/TourHost.tsx b/app/src/components/tour/TourHost.tsx new file mode 100644 index 000000000..fe6fcd223 --- /dev/null +++ b/app/src/components/tour/TourHost.tsx @@ -0,0 +1,150 @@ +import React from 'react'; +import Tour, { ReactourStep } from 'reactour'; +import { observer } from 'mobx-react-lite'; +import { useTheme } from 'emotion-theming'; +import { useStore } from 'store'; +import { styled, Theme } from 'components/theme'; +import SuccessStep from './SuccessStep'; +import { createTextStep } from './TextStep'; +import WelcomeStep from './WelcomeStep'; + +const tourSteps: ReactourStep[] = [ + { + // eslint-disable-next-line react/display-name + content: p => , + style: { maxWidth: 900 }, + }, + { + selector: '[data-tour="node-status"]', + content: createTextStep('nodeStatus'), + }, + { + selector: '[data-tour="history"]', + content: createTextStep('history'), + stepInteraction: false, + }, + { + selector: '[data-tour="inbound"]', + content: createTextStep('inbound'), + }, + { + selector: '[data-tour="outbound"]', + content: createTextStep('outbound'), + }, + { + selector: '[data-tour="channel-list"]', + content: createTextStep('channelList'), + }, + { + selector: '[data-tour="channel-list-receive"]', + content: createTextStep('channelListReceive'), + }, + { + selector: '[data-tour="channel-list-send"]', + content: createTextStep('channelListSend'), + }, + { + selector: '[data-tour="channel-list-fee"]', + content: createTextStep('channelListFee'), + }, + { + selector: '[data-tour="channel-list-uptime"]', + content: createTextStep('channelListUptime'), + }, + { + selector: '[data-tour="channel-list-peer"]', + content: createTextStep('channelListPeer'), + }, + { + selector: '[data-tour="channel-list-capacity"]', + content: createTextStep('channelListCapacity'), + }, + { + selector: '[data-tour="export"]', + content: createTextStep('export'), + }, + { + selector: '[data-tour="loop"]', + content: createTextStep('loop', false), + }, + { + selector: '[data-tour="loop-actions"]', + content: createTextStep('loopActions'), + stepInteraction: false, + }, + { + selector: '[data-tour="channel-list"]', + content: createTextStep('channelListSelect'), + style: { maxWidth: 800 }, + }, + { + selector: '[data-tour="loop-out"]', + content: createTextStep('loopOut', false), + }, + { + selector: '[data-tour="loop-amount"]', + content: createTextStep('loopAmount', false), + }, + { + selector: '[data-tour="loop-review"]', + content: createTextStep('loopReview', false), + style: { maxWidth: 500 }, + }, + { + selector: '[data-tour="loop-progress"]', + content: createTextStep('loopProgress', false), + style: { maxWidth: 500 }, + }, + { + selector: '[data-tour="processing-swaps"]', + content: createTextStep('processingSwaps'), + style: { maxWidth: 500 }, + }, + { + selector: '[data-tour="swap-progress"]', + content: createTextStep('swapProgress'), + }, + { + selector: '[data-tour="swap-minimize"]', + content: createTextStep('swapMinimize', false), + }, + { + // eslint-disable-next-line react/display-name + content: p => , + style: { maxWidth: 900 }, + }, +]; + +const Styled = { + Tour: styled(Tour)` + [data-tour-elem='badge'] { + font-family: ${props => props.theme.fonts.open.regular}; + font-size: ${props => props.theme.sizes.xxs}; + } + `, +}; + +const TourHost: React.FC = () => { + const { uiStore } = useStore(); + const theme = useTheme(); + + const { Tour } = Styled; + return ( + `${curr} of ${tot}`} + startAt={0} + /> + ); +}; + +export default observer(TourHost); diff --git a/app/src/components/tour/WelcomeStep.tsx b/app/src/components/tour/WelcomeStep.tsx new file mode 100644 index 000000000..969d66ab0 --- /dev/null +++ b/app/src/components/tour/WelcomeStep.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { ReactourStepContentArgs } from 'reactour'; +import { usePrefixedTranslation } from 'hooks'; +import { useStore } from 'store'; +import { Button } from 'components/base'; +import { styled } from 'components/theme'; +import TextStep from './TextStep'; + +const Styled = { + Note: styled.p` + font-size: ${props => props.theme.sizes.xs}; + font-style: italic; + opacity: 0.8; + margin-bottom: 50px; + `, + Footer: styled.div` + display: flex; + justify-content: space-between; + `, + LinkButton: styled(Button)` + color: ${props => props.theme.colors.darkBlue}; + padding: 0; + min-width: auto; + height: auto; + + &:hover { + color: ${props => props.theme.colors.blue}; + } + `, + SmallButton: styled(Button)` + font-size: ${props => props.theme.sizes.xs}; + min-width: auto; + height: 34px; + + &:hover { + color: ${props => props.theme.colors.offWhite}; + background-color: ${props => props.theme.colors.darkBlue}; + } + `, +}; + +const WelcomeStep: React.FC = props => { + const { l } = usePrefixedTranslation('cmps.tour.WelcomeStep'); + const { uiStore } = useStore(); + + const { Note, Footer, LinkButton, SmallButton } = Styled; + return ( + +

{l('desc')}

+

+ {l('walkthrough1')}{' '} + + {l('walkthrough2')} + {' '} + {l('walkthrough3')} +

+ {l('note')} +

{l('question')}

+
+ {l('yes')} + + {l('noThanks')} + +
+
+ ); +}; + +export default WelcomeStep; diff --git a/app/src/i18n/locales/en-US.json b/app/src/i18n/locales/en-US.json index f550621eb..ffb636225 100644 --- a/app/src/i18n/locales/en-US.json +++ b/app/src/i18n/locales/en-US.json @@ -12,6 +12,7 @@ "cmps.auth.AuthPage.submitBtn": "Submit", "cmps.common.Tile.maximizeTip": "Maximize", "cmps.common.PageHeader.exportTip": "Download CSV", + "cmps.common.PageHeader.helpTip": "Take a Tour", "cmps.history.HistoryPage.backText": "Lightning Loop", "cmps.history.HistoryPage.pageTitle": "Loop History", "cmps.history.HistoryRowHeader.status": "Status", @@ -78,6 +79,42 @@ "cmps.settings.GeneralSettings.pageTitle": "Settings", "cmps.settings.UnitSettings.pageTitle": "Bitcoin Unit", "cmps.settings.UnitSettings.backText": "Settings", + "cmps.tour.TextStep.next": "Next", + "cmps.tour.TextStep.nodeStatus": "Here are the confirmed balances on the Lightning Network and on-chain", + "cmps.tour.TextStep.export": "Click here if you want to export your channels as a CSV file", + "cmps.tour.TextStep.history": "This tile displays the two most recent swaps that have been initiated", + "cmps.tour.TextStep.inbound": "This tile displays the amount of Bitcoin that this node can receive over your Lightning channels", + "cmps.tour.TextStep.outbound": "This tile displays the amount of Bitcoin that this node can send over your Lightning channels", + "cmps.tour.TextStep.channelList": "This is the list of open channels that this node has with other peers on the Lightning Network", + "cmps.tour.TextStep.channelListReceive": "The amount that can be received over the channel", + "cmps.tour.TextStep.channelListSend": "The amount that can be sent over this channel", + "cmps.tour.TextStep.channelListFee": "The routing fee rate charged by the peer to receive payments over the channel. The percent is rounded to two decimal places. Hover over the value to see the exact rate expressed as parts-per-million", + "cmps.tour.TextStep.channelListUptime": "The uptime percentage of the channel peer", + "cmps.tour.TextStep.channelListPeer": "The alias of the channel peer. Hover over this field to see the peer's pubkey", + "cmps.tour.TextStep.channelListCapacity": "The total capacity of the channel", + "cmps.tour.TextStep.loop": "Let's perform a Loop!\n Click on the Loop button to start.", + "cmps.tour.TextStep.channelListSelect": "Optionally, you can select one or more channels if you want the swap to only transfer funds over those chosen channels. This is helpful if you want to only adjust the balance of specific channels.\n If you choose more than one, there is no guarantee how much funds will be transferred over each channel. LND will use its knowledge of the network to determine how to best split up the payment, if necessary. It is possible that only one of the channels will be used if the payment can be successfully routed through it.\n If you do not care which channels to use, then do not select any. The Lightning payment will be routed through any channel(s) with enough balance to allow it to succeed.", + "cmps.tour.TextStep.loopActions": "The action bar displays information about the selected channels and the buttons to Loop Out or Loop In.", + "cmps.tour.TextStep.loopOut": "We will use Loop Out to transfer funds from your channel balances to your on-chain wallet.\n Click on the Loop Out button to continue.", + "cmps.tour.TextStep.loopAmount": "You now need to specify the amount you would like to Loop Out. Drag the slider to adjust the amount.\n Click the Next button to continue.", + "cmps.tour.TextStep.loopReview": "Review the Loop amount and the fee. Keep in mind that you will only be charged the fee. The Loop amount will remain on your node once the swap completes. The balance will just be shifted from your channel balance to your on-chain wallet balance.\n Click the Confirm button to continue.", + "cmps.tour.TextStep.loopProgress": "Wait for the swap to be submitted to the server", + "cmps.tour.TextStep.processingSwaps": "Your swap will now be displayed in the Processing Loops section. It shows you the swap id, amount, and current status as a progress bar.\n Swaps require on-chain transactions, so you will need to wait for confirmations before they will complete successfully.", + "cmps.tour.TextStep.swapMinimize": "You can minimize the Processing Loops section any time by clicking on this minimize icon.\n It is perfectly safe to perform multiple swaps at one time.\n Click in the icon to continue.", + "cmps.tour.TextStep.swapProgress": "Once the progress bar turns green, your swap has been completed successfully.", + "cmps.tour.TextStep.congrats": "Congratulations!\n You have completed the tour.\n Happy Looping!", + "cmps.tour.WelcomeStep.header": "Welcome to Lightning Terminal!", + "cmps.tour.WelcomeStep.desc": "This tour will walk you through the different sections of the dashboard and show you how to perform a Loop.", + "cmps.tour.WelcomeStep.walkthrough1": "You may also reference the ", + "cmps.tour.WelcomeStep.walkthrough2": "Walkthrough", + "cmps.tour.WelcomeStep.walkthrough3": " document for additional guidance on how to use the product.", + "cmps.tour.WelcomeStep.note": "Note: sample data will be displayed during this tour, so none of your actual funds will be touched.", + "cmps.tour.WelcomeStep.question": "Would you like to take a tour?", + "cmps.tour.WelcomeStep.noThanks": "No Thanks", + "cmps.tour.WelcomeStep.yes": "Yes! Let's Go", + "cmps.tour.SuccessStep.header": "Congratulations!", + "cmps.tour.SuccessStep.complete": "You have completed the tour. Happy Looping!", + "cmps.tour.SuccessStep.close": "Close", "stores.authStore.emptyPassErr": "oops, password is required", "stores.authStore.invalidPassErr": "oops, that password is incorrect", "stores.buildSwapStore.noChannelsMsg": "You cannot perform a swap without any active channels", diff --git a/app/src/store/stores/buildSwapStore.ts b/app/src/store/stores/buildSwapStore.ts index 9eae0d369..dde4ed2b0 100644 --- a/app/src/store/stores/buildSwapStore.ts +++ b/app/src/store/stores/buildSwapStore.ts @@ -233,6 +233,7 @@ class BuildSwapStore { this._store.uiStore.notify(l('noChannelsMsg')); return; } + this._store.uiStore.tourGoToNext(); this.currentStep = BuildSwapSteps.SelectDirection; await this.getTerms(); this._store.log.info(`updated buildSwapStore.currentStep`, this.currentStep); @@ -315,6 +316,7 @@ class BuildSwapStore { this.currentStep++; this._store.log.info(`updated buildSwapStore.currentStep`, this.currentStep); + this._store.uiStore.tourGoToNext(); } /** @@ -436,6 +438,7 @@ class BuildSwapStore { // hide the swap UI after it is complete this.cancel(); this._store.uiStore.toggleProcessingSwaps(); + this._store.uiStore.tourGoToNext(); }); } catch (error) { this._store.uiStore.handleError(error, `Unable to Perform ${direction}`); diff --git a/app/src/store/stores/settingsStore.ts b/app/src/store/stores/settingsStore.ts index 7b76e658c..24ac8fa7a 100644 --- a/app/src/store/stores/settingsStore.ts +++ b/app/src/store/stores/settingsStore.ts @@ -6,6 +6,7 @@ export interface PersistentSettings { sidebarVisible: boolean; unit: Unit; balanceMode: BalanceMode; + tourAutoShown: boolean; } export default class SettingsStore { @@ -17,6 +18,9 @@ export default class SettingsStore { /** determines if the sidebar should collapse automatically for smaller screen widths */ @observable autoCollapse = false; + /** determines if the tour was automatically displayed on the first visit */ + @observable tourAutoShown = false; + /** specifies which denomination to show units in */ @observable unit: Unit = Unit.sats; @@ -74,6 +78,7 @@ export default class SettingsStore { sidebarVisible: this.sidebarVisible, unit: this.unit, balanceMode: this.balanceMode, + tourAutoShown: this.tourAutoShown, }; this._store.storage.set('settings', settings); this._store.log.info('saved settings to localStorage', settings); @@ -93,6 +98,7 @@ export default class SettingsStore { this.sidebarVisible = settings.sidebarVisible; this.unit = settings.unit; this.balanceMode = settings.balanceMode; + this.tourAutoShown = settings.tourAutoShown; this._store.log.info('loaded settings', settings); } diff --git a/app/src/store/stores/uiStore.ts b/app/src/store/stores/uiStore.ts index f5eb3d0f8..584ae1c55 100644 --- a/app/src/store/stores/uiStore.ts +++ b/app/src/store/stores/uiStore.ts @@ -1,4 +1,5 @@ import { action, observable, toJS } from 'mobx'; +import { SwapState } from 'types/generated/loop_pb'; import { Alert } from 'types/state'; import { AuthenticationError } from 'util/errors'; import { prefixTranslation } from 'util/translate'; @@ -13,6 +14,9 @@ export default class UiStore { /** indicates if the Processing Loops section is displayed on the Loop page */ @observable processingSwapsVisible = false; + /** indicates if the tour is visible */ + @observable tourVisible = false; + @observable tourActiveStep = 0; /** a collection of alerts to display as toasts */ @observable alerts = observable.map(); @@ -39,6 +43,10 @@ export default class UiStore { goToLoop() { this.goTo('/loop'); this._store.settingsStore.autoCollapseSidebar(); + if (!this._store.settingsStore.tourAutoShown) { + this.showTour(); + this._store.settingsStore.tourAutoShown = true; + } this._store.log.info('Go to the Loop page'); } @@ -62,6 +70,81 @@ export default class UiStore { @action.bound toggleProcessingSwaps() { this.processingSwapsVisible = !this.processingSwapsVisible; + if (!this.processingSwapsVisible) { + this.tourGoToNext(); + } + } + + /** Display the tour */ + @action.bound + showTour() { + this.tourVisible = true; + this.tourActiveStep = 0; + this._store.buildSwapStore.cancel(); + } + + /** Close the tour and switch back to using real data */ + @action.bound + closeTour() { + this.tourVisible = false; + if (this._store.api.lnd._grpc.useSampleData) { + // when the tour is closed, clear the sample data and load the real data + this._store.api.lnd._grpc.useSampleData = false; + // clear the sample data + this._store.channelStore.channels.clear(); + this._store.swapStore.swaps.clear(); + // fetch all the real data from the backend + this._store.fetchAllData(); + // connect and subscribe to the server-side streams + this._store.connectToStreams(); + this._store.subscribeToStreams(); + } + } + + /** set the current step in the tour */ + @action.bound + setTourActiveStep(step: number) { + this.tourActiveStep = step; + + if (step === 1) { + // #1 is the node-status step + // show the sidebar if autoCollapse is enabled + if (!this._store.settingsStore.sidebarVisible) { + this._store.settingsStore.sidebarVisible = true; + } + // clear the real data from the UI and load sample data + this._store.api.lnd._grpc.useSampleData = true; + // clear the real data + this._store.channelStore.channels.clear(); + this._store.swapStore.swaps.clear(); + // unsubscribe from streams since we are no longer authenticated + this._store.unsubscribeFromStreams(); + // fetch all the sample data from the backend + this._store.fetchAllData(); + } else if (step === 2) { + // #2 is the export icon + // hide the sidebar if autoCollapse is enabled + if (this._store.settingsStore.autoCollapse) { + this._store.settingsStore.sidebarVisible = false; + } + } else if (step === 3) { + // #3 is the history step + // change the most recent swap to be pending + this._store.swapStore.sortedSwaps[0].state = SwapState.INITIATED; + this._store.swapStore.sortedSwaps[0].lastUpdateTime = Date.now() * 1000 * 1000; + } else if (step === 21 /* swap-progress */) { + // #21 is the swap-progress step + // force the swap to be 100% complete + this._store.swapStore.sortedSwaps[0].state = SwapState.SUCCESS; + } + } + + /** Go to the next step in the tour */ + @action.bound + tourGoToNext() { + if (this.tourVisible) { + this.tourActiveStep = this.tourActiveStep + 1; + } } /** sets the selected setting to display */ diff --git a/app/src/util/tests/sampleData.ts b/app/src/util/tests/sampleData.ts index 9ccefd09e..fe3566042 100644 --- a/app/src/util/tests/sampleData.ts +++ b/app/src/util/tests/sampleData.ts @@ -110,7 +110,7 @@ export const lndChannel: LND.Channel.AsObject = { closeAddress: '', }; -export const lndListChannels: LND.ListChannelsResponse.AsObject = { +export const lndListChannelsMany: LND.ListChannelsResponse.AsObject = { channelsList: [...Array(500)].map((_, i) => { const c = lndChannel; // pick a random capacity between 0.5 and 1 BTC @@ -130,6 +130,10 @@ export const lndListChannels: LND.ListChannelsResponse.AsObject = { }), }; +export const lndListChannels: LND.ListChannelsResponse.AsObject = { + channelsList: lndListChannelsMany.channelsList.slice(0, 10), +}; + const txIdBytes = Buffer.from(txId, 'hex').reverse().toString('base64'); export const lndChannelEvent: Required = { type: LND.ChannelEventUpdate.UpdateType.OPEN_CHANNEL, diff --git a/app/yarn.lock b/app/yarn.lock index 067048069..9414797e1 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -21,6 +21,13 @@ dependencies: "@babel/highlight" "^7.8.3" +"@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c" @@ -73,6 +80,15 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/generator@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69" + integrity sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig== + dependencies: + "@babel/types" "^7.10.5" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/generator@^7.4.0", "@babel/generator@^7.9.0": version "7.9.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.4.tgz#12441e90c3b3c4159cdecf312075bf1a8ce2dbce" @@ -93,6 +109,13 @@ lodash "^4.17.13" source-map "^0.5.0" +"@babel/helper-annotate-as-pure@^7.0.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" + integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-annotate-as-pure@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" @@ -174,6 +197,15 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/helper-function-name@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca" @@ -192,6 +224,13 @@ "@babel/template" "^7.8.3" "@babel/types" "^7.9.5" +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-get-function-arity@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" @@ -281,6 +320,13 @@ "@babel/template" "^7.8.3" "@babel/types" "^7.8.3" +"@babel/helper-split-export-declaration@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz#2c70576eaa3b5609b24cb99db2888cc3fc4251d1" + integrity sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg== + dependencies: + "@babel/types" "^7.10.4" + "@babel/helper-split-export-declaration@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" @@ -288,6 +334,11 @@ dependencies: "@babel/types" "^7.8.3" +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + "@babel/helper-validator-identifier@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed" @@ -326,11 +377,25 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.1.6", "@babel/parser@^7.4.2", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0", "@babel/parser@^7.7.0", "@babel/parser@^7.8.4", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0": version "7.9.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== +"@babel/parser@^7.10.4", "@babel/parser@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" + integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== + "@babel/plugin-proposal-async-generator-functions@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f" @@ -1109,6 +1174,15 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/template@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" @@ -1133,6 +1207,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.4.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564" + integrity sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.10.5" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/parser" "^7.10.5" + "@babel/types" "^7.10.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + "@babel/traverse@^7.8.4": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.5.tgz#6e7c56b44e2ac7011a948c21e283ddd9d9db97a2" @@ -1157,6 +1246,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.10.4", "@babel/types@^7.10.5": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15" + integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@babel/types@^7.6.0", "@babel/types@^7.9.5": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" @@ -1230,7 +1328,7 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== -"@emotion/is-prop-valid@0.8.8": +"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.8": version "0.8.8" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== @@ -1276,12 +1374,12 @@ "@emotion/styled-base" "^10.0.27" babel-plugin-emotion "^10.0.27" -"@emotion/stylis@0.8.5": +"@emotion/stylis@0.8.5", "@emotion/stylis@^0.8.4": version "0.8.5" resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== -"@emotion/unitless@0.7.5": +"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.4": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== @@ -1570,6 +1668,11 @@ prop-types "^15.6.1" react-lifecycles-compat "^3.0.4" +"@rooks/use-mutation-observer@3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@rooks/use-mutation-observer/-/use-mutation-observer-3.4.0.tgz#5e4c122401ae1f790a24c64763a2910d0199a909" + integrity sha512-q10+v3WbvSt5fj55VMikTPaUZ9Yl+IYDsymodWr2+cKx0PD97VBeWYjk3xHJPqJgejBHwnrwiNkJKGFY5iW+WQ== + "@sinonjs/commons@^1.7.0": version "1.7.2" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2" @@ -2434,6 +2537,13 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/reactour@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@types/reactour/-/reactour-1.17.1.tgz#3bea0713276152ca321e1128586e2c5f8df70208" + integrity sha512-IZigAkKwfP883ZgPZJhVkTE7suDB+HzuWiamPzwZfRQ9kwAv1qUN3pjweXb0vVyLv/cgcGbY8jsip0CcaWOSqw== + dependencies: + "@types/react" "*" + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -3741,6 +3851,16 @@ babel-plugin-react-docgen@^4.0.0, babel-plugin-react-docgen@^4.1.0: react-docgen "^5.0.0" recast "^0.14.7" +"babel-plugin-styled-components@>= 1": + version "1.10.7" + resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.7.tgz#3494e77914e9989b33cc2d7b3b29527a949d635c" + integrity sha512-MBMHGcIA22996n9hZRf/UJLVVgkEOITuR2SvjHLb5dSTUyR4ZRGn+ngITapes36FI3WLxZHfRhkA1ffHxihOrg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-module-imports" "^7.0.0" + babel-plugin-syntax-jsx "^6.18.0" + lodash "^4.17.11" + babel-plugin-syntax-jsx@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" @@ -4357,6 +4477,11 @@ camelcase@^2.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= +camelize@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" + integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= + can-use-dom@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz#22cc4a34a0abc43950f42c6411024a3f6366b45a" @@ -4536,7 +4661,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@2.x, classnames@^2.2.5, classnames@^2.2.6: +classnames@2.2.6, classnames@2.x, classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -5086,6 +5211,11 @@ css-blank-pseudo@^0.1.4: dependencies: postcss "^7.0.5" +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU= + css-color-names@0.0.4, css-color-names@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" @@ -5176,6 +5306,15 @@ css-select@^2.0.0: domutils "^1.7.0" nth-check "^1.0.2" +css-to-react-native@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.0.0.tgz#62dbe678072a824a689bcfee011fc96e02a7d756" + integrity sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + css-tree@1.0.0-alpha.37: version "1.0.0-alpha.37" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" @@ -6731,6 +6870,11 @@ focus-lock@^0.6.6: resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.6.6.tgz#98119a755a38cfdbeda0280eaa77e307eee850c7" integrity sha512-Dx69IXGCq1qsUExWuG+5wkiMqVM/zGx/reXSJSLogECwp3x6KeNQZ+NAetgxEFpnC41rD8U3+jRCW68+LNzdtw== +focus-outline-manager@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/focus-outline-manager/-/focus-outline-manager-1.0.2.tgz#7bf3658865341fb6b08d042a037b9d2868b119b5" + integrity sha1-e/NliGU0H7awjQQqA3udKGixGbU= + follow-redirects@^1.0.0: version "1.11.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.11.0.tgz#afa14f08ba12a52963140fe43212658897bc0ecb" @@ -7392,7 +7536,7 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -9358,7 +9502,7 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= -lodash.debounce@^4.0.8: +lodash.debounce@4.0.8, lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= @@ -9368,6 +9512,11 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.pick@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -9403,6 +9552,11 @@ lodash@4.17.15, "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17. resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@^4.17.19: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + log-symbols@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" @@ -11724,7 +11878,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.4" -prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@15.7.2, prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -12218,7 +12372,7 @@ react-fast-compare@^3.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.0.1.tgz#884d339ce1341aad22392e7a88664c71da48600e" integrity sha512-C5vP0J644ofZGd54P8++O7AvrqMEbrGf8Ue0eAUJLJyw168dAX2aiYyX/zcY/eSNwO0IDjsKUaLE6n83D+TnEg== -react-focus-lock@^2.1.0: +react-focus-lock@2.2.1, react-focus-lock@^2.1.0: version "2.2.1" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.2.1.tgz#1d12887416925dc53481914b7cedd39494a3b24a" integrity sha512-47g0xYcCTZccdzKRGufepY8oZ3W1Qg+2hn6u9SHZ0zUB6uz/4K4xJe7yYFNZ1qT6m+2JDm82F6QgKeBTbjW4PQ== @@ -12441,6 +12595,21 @@ react@16.13.1, react@^16.8.3: object-assign "^4.1.1" prop-types "^15.6.2" +reactour@1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/reactour/-/reactour-1.18.0.tgz#a5e7a037b147ec22306f98d42c40d8022520f903" + integrity sha512-de0Pa5NkDU6I8IyGl+7+rWdDcx3AskmJYK/yIKU11D9EPIN79qzn852gjJgvH/jXZqeEfa+rmMWg72vA0UkmgA== + dependencies: + "@rooks/use-mutation-observer" "3.4.0" + classnames "2.2.6" + focus-outline-manager "^1.0.2" + lodash.debounce "4.0.8" + lodash.pick "4.4.0" + prop-types "15.7.2" + react-focus-lock "2.2.1" + scroll-smooth "1.1.0" + scrollparent "2.0.1" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -13153,6 +13322,16 @@ schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.0, schema-utils@^2.6 ajv "^6.12.0" ajv-keywords "^3.4.1" +scroll-smooth@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/scroll-smooth/-/scroll-smooth-1.1.0.tgz#467994d5bb57ffe7407e9a85bd0303b3d4524ded" + integrity sha512-68OUOXKN/ykM/Dbp4Lhza3O9QQUuW/c01WTsZzDOUyVgb1I5QjT/awOHCCbuYTSV1QnExUQ9w+KcxmVxlXIiAg== + +scrollparent@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/scrollparent/-/scrollparent-2.0.1.tgz#715d5b9cc57760fb22bdccc3befb5bfe06b1a317" + integrity sha1-cV1bnMV3YPsivczDvvtb/gaxoxc= + scss-tokenizer@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" @@ -13957,6 +14136,22 @@ style-to-object@^0.2.1: dependencies: inline-style-parser "0.1.1" +styled-components@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.1.1.tgz#96dfb02a8025794960863b9e8e365e3b6be5518d" + integrity sha512-1ps8ZAYu2Husx+Vz8D+MvXwEwvMwFv+hqqUwhNlDN5ybg6A+3xyW1ECrAgywhvXapNfXiz79jJyU0x22z0FFTg== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/traverse" "^7.4.5" + "@emotion/is-prop-valid" "^0.8.8" + "@emotion/stylis" "^0.8.4" + "@emotion/unitless" "^0.7.4" + babel-plugin-styled-components ">= 1" + css-to-react-native "^3.0.0" + hoist-non-react-statics "^3.0.0" + shallowequal "^1.1.0" + supports-color "^5.5.0" + stylehacks@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" @@ -13971,7 +14166,7 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== From 32b5de1217fd474102aa9bb25ca0196b5092eab3 Mon Sep 17 00:00:00 2001 From: jamaljsr <1356600+jamaljsr@users.noreply.github.com> Date: Thu, 23 Jul 2020 01:00:30 -0400 Subject: [PATCH 2/2] test: add unit tests for the tour --- .../components/loop/LoopPage.spec.tsx | 5 + .../components/tour/TourHost.spec.tsx | 138 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 app/src/__tests__/components/tour/TourHost.spec.tsx diff --git a/app/src/__tests__/components/loop/LoopPage.spec.tsx b/app/src/__tests__/components/loop/LoopPage.spec.tsx index 752f9d704..b73bb8f66 100644 --- a/app/src/__tests__/components/loop/LoopPage.spec.tsx +++ b/app/src/__tests__/components/loop/LoopPage.spec.tsx @@ -79,6 +79,11 @@ describe('LoopPage component', () => { expect(getByText('download.svg')).toBeInTheDocument(); }); + it('should display the help icon', () => { + const { getByText } = render(); + expect(getByText('help-circle.svg')).toBeInTheDocument(); + }); + it('should export channels', () => { const { getByText } = render(); fireEvent.click(getByText('download.svg')); diff --git a/app/src/__tests__/components/tour/TourHost.spec.tsx b/app/src/__tests__/components/tour/TourHost.spec.tsx new file mode 100644 index 000000000..8dd05626c --- /dev/null +++ b/app/src/__tests__/components/tour/TourHost.spec.tsx @@ -0,0 +1,138 @@ +import React, { Suspense } from 'react'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { renderWithProviders } from 'util/tests'; +import { prefixTranslation } from 'util/translate'; +import { createStore, Store } from 'store'; +import { Layout } from 'components/layout/Layout'; +import LoopPage from 'components/loop/LoopPage'; +import TourHost from 'components/tour/TourHost'; + +describe('TourHost component', () => { + let store: Store; + + beforeEach(async () => { + store = createStore(); + await store.fetchAllData(); + }); + + const firstLine = (text: string) => text.split('\n')[0]; + + const render = () => { + const cmp = ( + + + + + + + ); + return renderWithProviders(cmp, store); + }; + + it('should open and dismiss the tour', () => { + const { getByText, queryByText } = render(); + fireEvent.click(getByText('help-circle.svg')); + expect(getByText('Welcome to Lightning Terminal!')).toBeInTheDocument(); + fireEvent.click(getByText('No Thanks')); + expect(queryByText('Welcome to Lightning Terminal!')).not.toBeInTheDocument(); + }); + + it('should open the sidebar if it is collapsed', () => { + const { getByText } = render(); + store.settingsStore.sidebarVisible = false; + store.settingsStore.autoCollapse = true; + + fireEvent.click(getByText('help-circle.svg')); + expect(getByText('Welcome to Lightning Terminal!')).toBeInTheDocument(); + + fireEvent.click(getByText("Yes! Let's Go")); + expect(store.settingsStore.sidebarVisible).toBe(true); + + fireEvent.click(getByText('Next')); + expect(store.settingsStore.sidebarVisible).toBe(false); + }); + + it('should walk through the full tour', async () => { + const { getByText } = render(); + const { l } = prefixTranslation('cmps.tour.TextStep'); + + fireEvent.click(getByText('help-circle.svg')); + expect(getByText('Welcome to Lightning Terminal!')).toBeInTheDocument(); + + fireEvent.click(getByText("Yes! Let's Go")); + expect(getByText(l('nodeStatus'))).toBeInTheDocument(); + + // sample data is fetch after step #1 and we need to wait for it + await waitFor(() => expect(store.swapStore.sortedSwaps).toHaveLength(7)); + + fireEvent.click(getByText('Next')); + expect(getByText(l('history'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(l('inbound'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(l('outbound'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(l('channelList'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(l('channelListReceive'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(l('channelListSend'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(l('channelListFee'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(l('channelListUptime'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(l('channelListPeer'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(l('channelListCapacity'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(l('export'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(firstLine(l('loop')))).toBeInTheDocument(); + + fireEvent.click(getByText('Loop', { selector: 'button' })); + expect(getByText(l('loopActions'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(firstLine(l('channelListSelect')))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(firstLine(l('loopOut')))).toBeInTheDocument(); + + fireEvent.click(getByText('Loop Out', { selector: 'button' })); + expect(getByText(firstLine(l('loopAmount')))).toBeInTheDocument(); + + fireEvent.click(getByText('Next', { selector: 'button' })); + expect(getByText(firstLine(l('loopReview')))).toBeInTheDocument(); + + fireEvent.click(getByText('Confirm', { selector: 'button' })); + expect(getByText(firstLine(l('loopProgress')))).toBeInTheDocument(); + + await waitFor(() => { + expect(getByText(firstLine(l('processingSwaps')))).toBeInTheDocument(); + }); + + fireEvent.click(getByText('Next')); + expect(getByText(l('swapProgress'))).toBeInTheDocument(); + + fireEvent.click(getByText('Next')); + expect(getByText(firstLine(l('swapMinimize')))).toBeInTheDocument(); + + fireEvent.click(getByText('minimize.svg')); + expect(getByText('Congratulations!')).toBeInTheDocument(); + + fireEvent.click(getByText('Close')); + expect(() => getByText('Congratulations!')).toThrow(); + }); +});