Browse Source

test(contextmenu): add and update unit tests for context menus

master
jamaljsr 5 years ago
parent
commit
b7403616d9
  1. 177
      src/components/common/RemoveNode.spec.tsx
  2. 78
      src/components/designer/LinkContextMenu.spec.tsx
  3. 4
      src/components/designer/LinkContextMenu.tsx
  4. 165
      src/components/designer/NodeContextMenu.spec.tsx
  5. 10
      src/components/designer/NodeContextMenu.tsx
  6. 2
      src/components/designer/custom/Link.tsx
  7. 2
      src/components/designer/custom/NodeInner.tsx

177
src/components/common/RemoveNode.spec.tsx

@ -1,25 +1,33 @@
import React from 'react';
import { fireEvent, waitForElement } from '@testing-library/dom';
import { Status } from 'shared/types';
import { DockerLibrary } from 'types';
import { BitcoindLibrary, DockerLibrary } from 'types';
import { initChartFromNetwork } from 'utils/chart';
import { defaultRepoState } from 'utils/constants';
import { createBitcoindNetworkNode } from 'utils/network';
import {
getNetwork,
injections,
lightningServiceMock,
renderWithProviders,
suppressConsoleErrors,
testNodeDocker,
} from 'utils/tests';
import RemoveNode from './RemoveNode';
const dockerServiceMock = injections.dockerService as jest.Mocked<DockerLibrary>;
const bitcoindServiceMock = injections.bitcoindService as jest.Mocked<BitcoindLibrary>;
describe('RemoveNode', () => {
const renderComponent = (status?: Status) => {
const renderComponent = (status?: Status, useBitcoinNode = false) => {
const network = getNetwork(1, 'test network', status);
if (status === Status.Error) {
network.nodes.lightning.forEach(n => (n.errorMsg = 'test-error'));
}
const btcLatest = defaultRepoState.images.bitcoind.latest;
network.nodes.bitcoin.push(
createBitcoindNetworkNode(network, btcLatest, testNodeDocker),
);
const initialState = {
network: {
networks: [network],
@ -31,70 +39,131 @@ describe('RemoveNode', () => {
activeId: 1,
},
};
const { lightning } = network.nodes;
const node = lightning[status === Status.Started ? 0 : 1];
const { lightning, bitcoin } = network.nodes;
const node = useBitcoinNode
? bitcoin[0]
: lightning[status === Status.Started ? 0 : 1];
const cmp = <RemoveNode node={node} />;
const result = renderWithProviders(cmp, { initialState, wrapForm: true });
return {
...result,
node,
};
return renderWithProviders(cmp, { initialState, wrapForm: true });
};
beforeEach(() => {
lightningServiceMock.getChannels.mockResolvedValue([]);
});
describe('lightning node', () => {
beforeEach(() => {
lightningServiceMock.getChannels.mockResolvedValue([]);
});
it('should show the remove node modal', async () => {
const { getByText } = renderComponent(Status.Started);
expect(getByText('Remove')).toBeInTheDocument();
fireEvent.click(getByText('Remove'));
expect(
getByText('Are you sure you want to remove alice from the network?'),
).toBeInTheDocument();
expect(getByText('Yes')).toBeInTheDocument();
expect(getByText('Cancel')).toBeInTheDocument();
});
it('should show the remove node modal', async () => {
const { getByText } = renderComponent(Status.Started);
expect(getByText('Remove')).toBeInTheDocument();
fireEvent.click(getByText('Remove'));
expect(
getByText('Are you sure you want to remove alice from the network?'),
).toBeInTheDocument();
expect(getByText('Yes')).toBeInTheDocument();
expect(getByText('Cancel')).toBeInTheDocument();
});
it('should remove the node with the network stopped', async () => {
const { getByText, getByLabelText } = renderComponent(Status.Started);
expect(getByText('Remove')).toBeInTheDocument();
fireEvent.click(getByText('Remove'));
fireEvent.click(getByText('Yes'));
// wait for the error notification to be displayed
await waitForElement(() => getByLabelText('check-circle'));
expect(
getByText('The node alice has been removed from the network'),
).toBeInTheDocument();
expect(dockerServiceMock.removeNode).toBeCalledTimes(1);
});
it('should remove the node with the network stopped', async () => {
const { getByText, getByLabelText } = renderComponent(Status.Started);
expect(getByText('Remove')).toBeInTheDocument();
fireEvent.click(getByText('Remove'));
fireEvent.click(getByText('Yes'));
// wait for the error notification to be displayed
await waitForElement(() => getByLabelText('check-circle'));
expect(
getByText('The node alice has been removed from the network'),
).toBeInTheDocument();
expect(dockerServiceMock.removeNode).toBeCalledTimes(1);
});
it('should remove the node with the network started', async () => {
const { getByText, getByLabelText } = renderComponent(Status.Started);
expect(getByText('Remove')).toBeInTheDocument();
fireEvent.click(getByText('Remove'));
fireEvent.click(getByText('Yes'));
// wait for the error notification to be displayed
await waitForElement(() => getByLabelText('check-circle'));
expect(
getByText('The node alice has been removed from the network'),
).toBeInTheDocument();
expect(dockerServiceMock.removeNode).toBeCalledTimes(1);
it('should remove the node with the network started', async () => {
const { getByText, getByLabelText } = renderComponent(Status.Started);
expect(getByText('Remove')).toBeInTheDocument();
fireEvent.click(getByText('Remove'));
fireEvent.click(getByText('Yes'));
// wait for the error notification to be displayed
await waitForElement(() => getByLabelText('check-circle'));
expect(
getByText('The node alice has been removed from the network'),
).toBeInTheDocument();
expect(dockerServiceMock.removeNode).toBeCalledTimes(1);
});
it('should display an error if removing the node fails', async () => {
// antd Modal.confirm logs a console error when onOk fails
// this suppresses those errors from being displayed in test runs
await suppressConsoleErrors(async () => {
dockerServiceMock.removeNode.mockRejectedValue(new Error('test error'));
const { getByText, getByLabelText } = renderComponent(Status.Started);
expect(getByText('Remove')).toBeInTheDocument();
fireEvent.click(getByText('Remove'));
fireEvent.click(getByText('Yes'));
// wait for the error notification to be displayed
await waitForElement(() => getByLabelText('close-circle'));
expect(getByText('Unable to remove the node')).toBeInTheDocument();
expect(getByText('test error')).toBeInTheDocument();
});
});
});
it('should display an error if removing the node fails', async () => {
// antd Modal.confirm logs a console error when onOk fails
// this suppresses those errors from being displayed in test runs
await suppressConsoleErrors(async () => {
dockerServiceMock.removeNode.mockRejectedValue(new Error('test error'));
const { getByText, getByLabelText } = renderComponent(Status.Started);
describe('bitcoin node', () => {
beforeEach(() => {
lightningServiceMock.getChannels.mockResolvedValue([]);
lightningServiceMock.waitUntilOnline.mockResolvedValue(Promise.resolve());
bitcoindServiceMock.waitUntilOnline.mockResolvedValue(Promise.resolve());
});
it('should show the remove node modal', async () => {
const { getByText } = renderComponent(Status.Started, true);
expect(getByText('Remove')).toBeInTheDocument();
fireEvent.click(getByText('Remove'));
expect(
getByText('Are you sure you want to remove backend1 from the network?'),
).toBeInTheDocument();
expect(getByText('Yes')).toBeInTheDocument();
expect(getByText('Cancel')).toBeInTheDocument();
});
it('should remove the node with the network stopped', async () => {
const { getByText, getByLabelText } = renderComponent(Status.Stopped, true);
expect(getByText('Remove')).toBeInTheDocument();
fireEvent.click(getByText('Remove'));
fireEvent.click(getByText('Yes'));
// wait for the error notification to be displayed
await waitForElement(() => getByLabelText('close-circle'));
expect(getByText('Unable to remove the node')).toBeInTheDocument();
expect(getByText('test error')).toBeInTheDocument();
await waitForElement(() => getByLabelText('check-circle'));
expect(
getByText('The node backend1 has been removed from the network'),
).toBeInTheDocument();
expect(dockerServiceMock.saveComposeFile).toBeCalledTimes(1);
});
it('should remove the node with the network started', async () => {
const { getByText, getByLabelText } = renderComponent(Status.Started, true);
expect(getByText('Remove')).toBeInTheDocument();
fireEvent.click(getByText('Remove'));
fireEvent.click(getByText('Yes'));
// wait for the error notification to be displayed
await waitForElement(() => getByLabelText('check-circle'));
expect(
getByText('The node backend1 has been removed from the network'),
).toBeInTheDocument();
expect(dockerServiceMock.saveComposeFile).toBeCalledTimes(2);
});
it('should display an error if removing the node fails', async () => {
// antd Modal.confirm logs a console error when onOk fails
// this suppresses those errors from being displayed in test runs
await suppressConsoleErrors(async () => {
dockerServiceMock.saveComposeFile.mockRejectedValue(new Error('test error'));
const { getByText, getByLabelText } = renderComponent(Status.Stopped, true);
expect(getByText('Remove')).toBeInTheDocument();
fireEvent.click(getByText('Remove'));
fireEvent.click(getByText('Yes'));
// wait for the error notification to be displayed
await waitForElement(() => getByLabelText('close-circle'));
expect(getByText('Unable to remove the node')).toBeInTheDocument();
expect(getByText('test error')).toBeInTheDocument();
});
});
});
});

78
src/components/designer/LinkContextMenu.spec.tsx

@ -0,0 +1,78 @@
import React from 'react';
import { ILink } from '@mrblenny/react-flow-chart';
import { fireEvent } from '@testing-library/dom';
import { initChartFromNetwork } from 'utils/chart';
import { getNetwork, renderWithProviders } from 'utils/tests';
import LinkContextMenu from './LinkContextMenu';
describe('LinkContextMenu', () => {
const createChannelLink = (): ILink => ({
id: 'alice-carol',
from: { nodeId: 'alice', portId: 'alice-carol' },
to: { nodeId: 'carol', portId: 'alice-carol' },
properties: {
type: 'open-channel',
capacity: '1000',
fromBalance: '600',
toBalance: '400',
direction: 'ltr',
status: 'Open',
},
});
const createBackendLink = (): ILink => ({
id: 'alice-backend1',
from: { nodeId: 'alice', portId: 'alice-backend1' },
to: { nodeId: 'backend1', portId: 'alice-backend1' },
properties: {
type: 'backend',
},
});
const renderComponent = (link: ILink) => {
const network = getNetwork(1, 'test network');
const chart = initChartFromNetwork(network);
chart.links[link.id] = link;
const initialState = {
network: {
networks: [network],
},
designer: {
activeId: network.id,
allCharts: {
[network.id]: chart,
},
},
};
const cmp = (
<LinkContextMenu link={link}>
<span>test-child</span>
</LinkContextMenu>
);
const result = renderWithProviders(cmp, { initialState });
// always open the context menu for all tests
fireEvent.contextMenu(result.getByText('test-child'));
return result;
};
it('should display the correct options for an open channel', async () => {
const { getByText } = renderComponent(createChannelLink());
expect(getByText('Close Channel')).toBeInTheDocument();
});
it('should display the correct options for a backend connection', async () => {
const { getByText } = renderComponent(createBackendLink());
expect(getByText('Change Backend')).toBeInTheDocument();
});
it('should not display a menu for an invalid link', async () => {
const { queryByText } = renderComponent({} as ILink);
expect(queryByText('Close Channel')).not.toBeInTheDocument();
expect(queryByText('Change Backend')).not.toBeInTheDocument();
});
it('should not display Close Channel an invalid node', async () => {
const link = createChannelLink();
link.from.nodeId = 'invalid';
const { queryByText } = renderComponent(link);
expect(queryByText('Close Channel')).not.toBeInTheDocument();
});
});

4
src/components/designer/custom/LinkContextMenu.tsx → src/components/designer/LinkContextMenu.tsx

@ -3,8 +3,8 @@ import { ILink } from '@mrblenny/react-flow-chart';
import { Dropdown, Menu } from 'antd';
import { useStoreState } from 'store';
import { LinkProperties } from 'utils/chart';
import ChangeBackendButton from '../link/ChangeBackendButton';
import CloseChannelButton from '../link/CloseChannelButton';
import ChangeBackendButton from './link/ChangeBackendButton';
import CloseChannelButton from './link/CloseChannelButton';
interface Props {
link: ILink;

165
src/components/designer/NodeContextMenu.spec.tsx

@ -0,0 +1,165 @@
import React from 'react';
import { fireEvent } from '@testing-library/dom';
import { act } from '@testing-library/react';
import { ipcChannels } from 'shared';
import { Status } from 'shared/types';
import { initChartFromNetwork } from 'utils/chart';
import { getNetwork, injections, renderWithProviders } from 'utils/tests';
import NodeContextMenu from './NodeContextMenu';
describe('NodeContextMenu', () => {
const renderComponent = (nodeName: string, status?: Status) => {
const network = getNetwork(1, 'test network', status);
const chart = initChartFromNetwork(network);
if (nodeName === 'invalid') {
chart.nodes.alice.id = 'invalid';
nodeName = 'alice';
}
const initialState = {
network: {
networks: [network],
},
designer: {
activeId: network.id,
allCharts: {
[network.id]: chart,
},
},
};
const cmp = (
<NodeContextMenu node={chart.nodes[nodeName]}>
<span>test-child</span>
</NodeContextMenu>
);
const result = renderWithProviders(cmp, { initialState });
// always open the context menu for all tests
fireEvent.contextMenu(result.getByText('test-child'));
return result;
};
it('should display the correct options for a started lightning node', async () => {
const { getByText } = renderComponent('alice', Status.Started);
expect(getByText('Create Invoice')).toBeInTheDocument();
expect(getByText('Pay Invoice')).toBeInTheDocument();
expect(getByText('Open Outgoing Channel')).toBeInTheDocument();
expect(getByText('Open Incoming Channel')).toBeInTheDocument();
expect(getByText('Launch Terminal')).toBeInTheDocument();
expect(getByText('Stop')).toBeInTheDocument();
expect(getByText('Advanced Options')).toBeInTheDocument();
expect(getByText('Remove')).toBeInTheDocument();
});
it('should display the correct options for a stopped lightning node', async () => {
const { getByText, queryByText } = renderComponent('alice', Status.Stopped);
expect(queryByText('Create Invoice')).not.toBeInTheDocument();
expect(queryByText('Pay Invoice')).not.toBeInTheDocument();
expect(queryByText('Open Outgoing Channel')).not.toBeInTheDocument();
expect(queryByText('Open Incoming Channel')).not.toBeInTheDocument();
expect(queryByText('Launch Terminal')).not.toBeInTheDocument();
expect(getByText('Start')).toBeInTheDocument();
expect(getByText('Advanced Options')).toBeInTheDocument();
expect(getByText('Remove')).toBeInTheDocument();
});
it('should display the correct options for a started bitcoin node', async () => {
const { getByText } = renderComponent('backend1', Status.Started);
expect(getByText('Launch Terminal')).toBeInTheDocument();
expect(getByText('Stop')).toBeInTheDocument();
expect(getByText('Advanced Options')).toBeInTheDocument();
expect(getByText('Remove')).toBeInTheDocument();
});
it('should display the correct options for a stopped bitcoin node', async () => {
const { getByText, queryByText } = renderComponent('backend1', Status.Stopped);
expect(queryByText('Launch Terminal')).not.toBeInTheDocument();
expect(getByText('Start')).toBeInTheDocument();
expect(getByText('Advanced Options')).toBeInTheDocument();
expect(getByText('Remove')).toBeInTheDocument();
});
it('should display a menu for an invalid node', async () => {
const { queryByText } = renderComponent('invalid', Status.Started);
expect(queryByText('Create Invoice')).not.toBeInTheDocument();
expect(queryByText('Pay Invoice')).not.toBeInTheDocument();
expect(queryByText('Open Outgoing Channel')).not.toBeInTheDocument();
expect(queryByText('Open Incoming Channel')).not.toBeInTheDocument();
expect(queryByText('Launch Terminal')).not.toBeInTheDocument();
expect(queryByText('Stop')).not.toBeInTheDocument();
expect(queryByText('Advanced Options')).not.toBeInTheDocument();
expect(queryByText('Remove')).not.toBeInTheDocument();
});
it('should show the create invoice modal', async () => {
const { getByText, store } = renderComponent('alice', Status.Started);
expect(store.getState().modals.createInvoice.visible).toBe(false);
fireEvent.click(getByText('Create Invoice'));
expect(store.getState().modals.createInvoice.visible).toBe(true);
});
it('should show the pay invoice modal', async () => {
const { getByText, store } = renderComponent('alice', Status.Started);
expect(store.getState().modals.payInvoice.visible).toBe(false);
fireEvent.click(getByText('Pay Invoice'));
expect(store.getState().modals.payInvoice.visible).toBe(true);
});
it('should show the open outgoing channel modal', async () => {
const { getByText, store } = renderComponent('alice', Status.Started);
expect(store.getState().modals.openChannel.visible).toBe(false);
fireEvent.click(getByText('Open Outgoing Channel'));
expect(store.getState().modals.openChannel.visible).toBe(true);
expect(store.getState().modals.openChannel.from).toBe('alice');
});
it('should show the open incoming channel modal', async () => {
const { getByText, store } = renderComponent('alice', Status.Started);
expect(store.getState().modals.openChannel.visible).toBe(false);
fireEvent.click(getByText('Open Incoming Channel'));
expect(store.getState().modals.openChannel.visible).toBe(true);
expect(store.getState().modals.openChannel.to).toBe('alice');
});
it('should open the terminal', async () => {
const ipcMock = injections.ipc as jest.Mock;
ipcMock.mockResolvedValue(true);
const { getByText, store } = renderComponent('alice', Status.Started);
expect(store.getState().modals.openChannel.visible).toBe(false);
await act(async () => {
fireEvent.click(getByText('Launch Terminal'));
});
const url = '/terminal/LND/polar-n1-alice';
expect(ipcMock).toBeCalledWith(ipcChannels.openWindow, { url });
});
it('should show the start node confirmation modal', async () => {
const { getByText, findByText } = renderComponent('alice', Status.Stopped);
fireEvent.click(getByText('Start'));
expect(
await findByText('Would you like to start the alice node?'),
).toBeInTheDocument();
});
it('should show the stop node confirmation modal', async () => {
const { getByText, findByText } = renderComponent('alice', Status.Started);
fireEvent.click(getByText('Stop'));
expect(
await findByText('Are you sure you want to stop the alice node?'),
).toBeInTheDocument();
});
it('should show the advanced options modal', async () => {
const { getByText, store } = renderComponent('alice', Status.Started);
expect(store.getState().modals.advancedOptions.visible).toBe(false);
fireEvent.click(getByText('Advanced Options'));
expect(store.getState().modals.advancedOptions.visible).toBe(true);
expect(store.getState().modals.advancedOptions.nodeName).toBe('alice');
});
it('should show the remove node confirmation modal', async () => {
const { getByText, findByText } = renderComponent('alice', Status.Started);
fireEvent.click(getByText('Remove'));
expect(
await findByText('Are you sure you want to remove alice from the network?'),
).toBeInTheDocument();
});
});

10
src/components/designer/custom/NodeContextMenu.tsx → src/components/designer/NodeContextMenu.tsx

@ -6,7 +6,7 @@ import { LightningNode, Status } from 'shared/types';
import { useStoreState } from 'store';
import { AdvancedOptionsButton, RemoveNode, RestartNode } from 'components/common';
import { OpenTerminalButton } from 'components/terminal';
import { OpenChannelButtons, PaymentButtons } from '../lightning/actions';
import { OpenChannelButtons, PaymentButtons } from './lightning/actions';
const Styled = {
MenuItem: styled(Menu.Item)`
@ -44,13 +44,13 @@ const NodeContextMenu: React.FC<Props> = ({ node: { id }, children }) => {
<Menu style={{ width: 200 }}>
{isStarted && [
createItem(
'pay',
<PaymentButtons menuType="pay" node={node as LightningNode} />,
'inv',
<PaymentButtons menuType="create" node={node as LightningNode} />,
isLN,
),
createItem(
'inv',
<PaymentButtons menuType="create" node={node as LightningNode} />,
'pay',
<PaymentButtons menuType="pay" node={node as LightningNode} />,
isLN,
),
createItem(

2
src/components/designer/custom/Link.tsx

@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import { ILinkDefaultProps, IPosition } from '@mrblenny/react-flow-chart';
import { useTheme } from 'hooks/useTheme';
import { LinkProperties } from 'utils/chart';
import LinkContextMenu from './LinkContextMenu';
import LinkContextMenu from '../LinkContextMenu';
export const generateCurvePath = (startPos: IPosition, endPos: IPosition): string => {
const width = Math.abs(startPos.x - endPos.x);

2
src/components/designer/custom/NodeInner.tsx

@ -5,7 +5,7 @@ import { useTheme } from 'hooks/useTheme';
import { ThemeColors } from 'theme/colors';
import { LOADING_NODE_ID } from 'utils/constants';
import { Loader, StatusBadge } from 'components/common';
import NodeContextMenu from './NodeContextMenu';
import NodeContextMenu from '../NodeContextMenu';
const Styled = {
Node: styled.div<{ size?: ISize; colors: ThemeColors['node'] }>`

Loading…
Cancel
Save