Skip to content

Add tooltips and toast alerts #44

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

Merged
merged 5 commits into from
May 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@
"mobx": "5.15.4",
"mobx-react-lite": "2.0.6",
"mobx-utils": "5.5.7",
"rc-tooltip": "4.2.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-i18next": "11.4.0",
"react-scripts": "3.4.1",
"react-toastify": "6.0.5",
"react-virtualized": "^9.21.2"
},
"devDependencies": {
Expand Down
6 changes: 6 additions & 0 deletions app/src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
@import '../node_modules/bootstrap/scss/grid';
@import '../node_modules/bootstrap/scss/custom-forms';

// rc-tooltip component styles
@import '../node_modules/rc-tooltip/assets/bootstrap_white.css';

// react-toastify styles
@import '../node_modules/react-toastify/dist/ReactToastify.css';

@font-face {
font-family: 'OpenSans Light';
src: url('./assets/fonts/OpenSans-Light.ttf') format('truetype');
Expand Down
2 changes: 2 additions & 0 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import './App.scss';
import { createStore, StoreProvider } from 'store';
import AlertContainer from 'components/common/AlertContainer';
import { Layout } from 'components/layout';
import Pages from 'components/Pages';
import { ThemeProvider } from 'components/theme';
Expand All @@ -13,6 +14,7 @@ const App = () => {
<ThemeProvider>
<Layout>
<Pages />
<AlertContainer />
</Layout>
</ThemeProvider>
</StoreProvider>
Expand Down
27 changes: 27 additions & 0 deletions app/src/__stories__/AlertContainer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { useStore } from 'store';
import AlertContainer from 'components/common/AlertContainer';
import { Button } from 'components/common/base';

export default {
title: 'Components/Alerts',
component: AlertContainer,
parameters: { centered: true },
};

export const Default = () => {
const store = useStore();
const handleClick = () => {
store.uiStore.notify(
'This is a sample message to be displayed inside of a toast alert',
'Sample Alert Title',
);
};

return (
<>
<Button onClick={handleClick}>Show Alert</Button>
<AlertContainer />
</>
);
};
31 changes: 31 additions & 0 deletions app/src/__stories__/Tip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import Tip from 'components/common/Tip';

export default {
title: 'Components/Tooltip',
component: Tip,
parameters: { contained: true },
};

const placements = [
'top',
'topRight',
'right',
'bottomRight',
'bottom',
'bottomLeft',
'left',
'topLeft',
];

export const Placements = () => {
return (
<div style={{ textAlign: 'center', marginTop: 300 }}>
{placements.map(p => (
<Tip key={p} placement={p} overlay="Tip of the day">
<span style={{ margin: 10 }}>{p}</span>
</Tip>
))}
</div>
);
};
20 changes: 20 additions & 0 deletions app/src/__tests__/components/common/AlertContainer.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import { renderWithProviders } from 'util/tests';
import { createStore, Store } from 'store';
import AlertContainer from 'components/common/AlertContainer';

describe('AlertContainer component', () => {
let store: Store;

const render = () => {
store = createStore();
return renderWithProviders(<AlertContainer />, store);
};

it('should display an alert when added to the store', async () => {
const { findByText } = render();
store.uiStore.notify('test error', 'test title');
expect(await findByText('test error')).toBeInTheDocument();
expect(await findByText('test title')).toBeInTheDocument();
});
});
49 changes: 49 additions & 0 deletions app/src/__tests__/components/common/Tip.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import { fireEvent, waitFor } from '@testing-library/react';
import { renderWithProviders } from 'util/tests';
import { createStore, Store } from 'store';
import Tip from 'components/common/Tip';

describe('Tip component', () => {
let store: Store;

const render = (placement?: string) => {
store = createStore();
const cmp = (
<Tip placement={placement} overlay="test tip">
<span>test content</span>
</Tip>
);
return renderWithProviders(cmp, store);
};

it('should display a tooltip on hover', async () => {
const { getByText } = render();
fireEvent.mouseEnter(getByText('test content'));
expect(getByText('test tip')).toBeInTheDocument();
});

it('should display a tooltip on bottom', async () => {
const { getByText, container } = render('bottom');
fireEvent.mouseEnter(getByText('test content'));
waitFor(() => {
expect(container.querySelector('.rc-tooltip-placement-bottom')).toBeInTheDocument();
});
});

it('should display a tooltip on left', async () => {
const { getByText, container } = render('left');
fireEvent.mouseEnter(getByText('test content'));
waitFor(() => {
expect(container.querySelector('.rc-tooltip-placement-left')).toBeInTheDocument();
});
});

it('should display a tooltip on right', async () => {
const { getByText, container } = render('right');
fireEvent.mouseEnter(getByText('test content'));
waitFor(() => {
expect(container.querySelector('.rc-tooltip-placement-right')).toBeInTheDocument();
});
});
});
10 changes: 5 additions & 5 deletions app/src/__tests__/components/layout/Layout.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ describe('Layout component', () => {
};

it('should display the hamburger menu', () => {
const { getByTitle } = render();
expect(getByTitle('menu')).toBeInTheDocument();
const { getByText } = render();
expect(getByText('menu.svg')).toBeInTheDocument();
});

it('should toggle collapsed state', () => {
const { getByTitle, store } = render();
const { getByText, store } = render();
expect(store.settingsStore.sidebarVisible).toBe(true);
fireEvent.click(getByTitle('menu'));
fireEvent.click(getByText('menu.svg'));
expect(store.settingsStore.sidebarVisible).toBe(false);
fireEvent.click(getByTitle('menu'));
fireEvent.click(getByText('menu.svg'));
expect(store.settingsStore.sidebarVisible).toBe(true);
});

Expand Down
6 changes: 0 additions & 6 deletions app/src/__tests__/components/loop/SwapWizard.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,5 @@ describe('SwapWizard component', () => {
const { getByText } = render();
expect(getByText('Configuring Loops')).toBeInTheDocument();
});

it('should display an error message', () => {
store.buildSwapStore.swapError = new Error('error-test');
const { getByText } = render();
expect(getByText('error-test')).toBeInTheDocument();
});
});
});
39 changes: 34 additions & 5 deletions app/src/__tests__/store/buildSwapStore.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { values } from 'mobx';
import { SwapDirection } from 'types/state';
import { grpc } from '@improbable-eng/grpc-web';
import { waitFor } from '@testing-library/react';
Expand Down Expand Up @@ -42,6 +43,19 @@ describe('BuildSwapStore', () => {
expect(store.terms.out).toEqual({ min: 250000, max: 1000000 });
});

it('should handle errors fetching loop terms', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'GetLoopInTerms') throw new Error('test-err');
return undefined as any;
});
expect(rootStore.uiStore.alerts.size).toBe(0);
await store.getTerms();
await waitFor(() => {
expect(rootStore.uiStore.alerts.size).toBe(1);
expect(values(rootStore.uiStore.alerts)[0].message).toBe('test-err');
});
});

it('should adjust the amount after fetching the loop terms', async () => {
store.setAmount(100);
await store.getTerms();
Expand Down Expand Up @@ -78,6 +92,21 @@ describe('BuildSwapStore', () => {
expect(store.quote.prepayAmount).toEqual(1337);
});

it('should handle errors fetching loop quote', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'LoopOutQuote') throw new Error('test-err');
return undefined as any;
});
store.setDirection(SwapDirection.OUT);
store.setAmount(600);
expect(rootStore.uiStore.alerts.size).toBe(0);
await store.getQuote();
await waitFor(() => {
expect(rootStore.uiStore.alerts.size).toBe(1);
expect(values(rootStore.uiStore.alerts)[0].message).toBe('test-err');
});
});

it('should perform a loop in', async () => {
store.setDirection(SwapDirection.IN);
store.setAmount(600);
Expand Down Expand Up @@ -129,18 +158,18 @@ describe('BuildSwapStore', () => {
await waitFor(() => expect(deadline).toEqual(0));
});

it('should handle loop errors', async () => {
it('should handle errors when performing a loop', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'LoopIn') throw new Error('asdf');
if (desc.methodName === 'LoopIn') throw new Error('test-err');
return undefined as any;
});
store.setDirection(SwapDirection.IN);
store.setAmount(600);

expect(store.swapError).toBeUndefined();
expect(rootStore.uiStore.alerts.size).toBe(0);
store.requestSwap();
await waitFor(() => {
expect(store.swapError).toBeDefined();
expect(rootStore.uiStore.alerts.size).toBe(1);
expect(values(rootStore.uiStore.alerts)[0].message).toBe('test-err');
});
});

Expand Down
30 changes: 23 additions & 7 deletions app/src/__tests__/store/channelStore.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { observable, ObservableMap, values } from 'mobx';
import { grpc } from '@improbable-eng/grpc-web';
import { waitFor } from '@testing-library/react';
import { BalanceMode } from 'util/constants';
import { lndListChannels } from 'util/tests/sampleData';
import { createStore, SettingsStore } from 'store';
import { createStore, Store } from 'store';
import Channel from 'store/models/channel';
import ChannelStore from 'store/stores/channelStore';

const grpcMock = grpc as jest.Mocked<typeof grpc>;

describe('ChannelStore', () => {
let settingsStore: SettingsStore;
let rootStore: Store;
let store: ChannelStore;

const channelSubset = (channels: ObservableMap<string, Channel>) => {
Expand All @@ -20,9 +24,8 @@ describe('ChannelStore', () => {
};

beforeEach(() => {
const rootStore = createStore();
rootStore = createStore();
store = rootStore.channelStore;
settingsStore = rootStore.settingsStore;
});

it('should fetch list of channels', async () => {
Expand All @@ -31,6 +34,19 @@ describe('ChannelStore', () => {
expect(store.channels.size).toEqual(lndListChannels.channelsList.length);
});

it('should handle errors fetching channels', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'ListChannels') throw new Error('test-err');
return undefined as any;
});
expect(rootStore.uiStore.alerts.size).toBe(0);
await store.fetchChannels();
await waitFor(() => {
expect(rootStore.uiStore.alerts.size).toBe(1);
expect(values(rootStore.uiStore.alerts)[0].message).toBe('test-err');
});
});

it('should update existing channels with the same id', async () => {
expect(store.channels.size).toEqual(0);
await store.fetchChannels();
Expand All @@ -47,7 +63,7 @@ describe('ChannelStore', () => {

it('should sort channels correctly when using receive mode', async () => {
await store.fetchChannels();
settingsStore.setBalanceMode(BalanceMode.receive);
rootStore.settingsStore.setBalanceMode(BalanceMode.receive);
store.channels = channelSubset(store.channels);
store.sortedChannels.forEach((c, i) => {
if (i === 0) return;
Expand All @@ -59,7 +75,7 @@ describe('ChannelStore', () => {

it('should sort channels correctly when using send mode', async () => {
await store.fetchChannels();
settingsStore.setBalanceMode(BalanceMode.send);
rootStore.settingsStore.setBalanceMode(BalanceMode.send);
store.channels = channelSubset(store.channels);
store.sortedChannels.forEach((c, i) => {
if (i === 0) return;
Expand All @@ -71,7 +87,7 @@ describe('ChannelStore', () => {

it('should sort channels correctly when using routing mode', async () => {
await store.fetchChannels();
settingsStore.setBalanceMode(BalanceMode.routing);
rootStore.settingsStore.setBalanceMode(BalanceMode.routing);
store.channels = channelSubset(store.channels);
store.sortedChannels.forEach((c, i) => {
if (i === 0) return;
Expand Down
24 changes: 22 additions & 2 deletions app/src/__tests__/store/nodeStore.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { values } from 'mobx';
import { grpc } from '@improbable-eng/grpc-web';
import { waitFor } from '@testing-library/react';
import { lndChannelBalance, lndWalletBalance } from 'util/tests/sampleData';
import { createStore, NodeStore } from 'store';
import { createStore, NodeStore, Store } from 'store';

const grpcMock = grpc as jest.Mocked<typeof grpc>;

describe('NodeStore', () => {
let rootStore: Store;
let store: NodeStore;

beforeEach(() => {
store = createStore().nodeStore;
rootStore = createStore();
store = rootStore.nodeStore;
});

it('should fetch node balances', async () => {
Expand All @@ -15,4 +22,17 @@ describe('NodeStore', () => {
expect(store.wallet.channelBalance).toEqual(lndChannelBalance.balance);
expect(store.wallet.walletBalance).toEqual(lndWalletBalance.totalBalance);
});

it('should handle errors fetching channels', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'ChannelBalance') throw new Error('test-err');
return undefined as any;
});
expect(rootStore.uiStore.alerts.size).toBe(0);
await store.fetchBalances();
await waitFor(() => {
expect(rootStore.uiStore.alerts.size).toBe(1);
expect(values(rootStore.uiStore.alerts)[0].message).toBe('test-err');
});
});
});
Loading