Skip to content

Add Processing Loops section #38

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 4 commits into from
May 19, 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
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"lottie-web": "5.6.8",
"mobx": "5.15.4",
"mobx-react-lite": "2.0.6",
"mobx-utils": "5.5.7",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-i18next": "11.4.0",
Expand Down
7 changes: 2 additions & 5 deletions app/src/__stories__/LoopHistory.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import { useStore } from 'store';
import Tile from 'components/common/Tile';
import LoopHistory from 'components/loop/LoopHistory';

Expand All @@ -10,15 +9,13 @@ export default {
};

export const Default = () => {
const { swapStore } = useStore();
return <LoopHistory swaps={swapStore.sortedSwaps} />;
return <LoopHistory />;
};

export const InsideTile = () => {
const { swapStore } = useStore();
return (
<Tile title="Loop History">
<LoopHistory swaps={swapStore.sortedSwaps} />
<LoopHistory />
</Tile>
);
};
120 changes: 120 additions & 0 deletions app/src/__stories__/ProcessingSwaps.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, { useEffect } from 'react';
import { observable } from 'mobx';
import * as LOOP from 'types/generated/loop_pb';
import { loopListSwaps } from 'util/tests/sampleData';
import { useStore } from 'store';
import { Swap } from 'store/models';
import ProcessingSwaps from 'components/loop/processing/ProcessingSwaps';

const { LOOP_IN, LOOP_OUT } = LOOP.SwapType;
const {
INITIATED,
PREIMAGE_REVEALED,
HTLC_PUBLISHED,
SUCCESS,
INVOICE_SETTLED,
FAILED,
} = LOOP.SwapState;

export default {
title: 'Components/Processing Swaps',
component: ProcessingSwaps,
parameters: { contained: true },
decorators: [
(StoryFn: any) => (
<div style={{ padding: 100 }}>
<StoryFn />
</div>
),
],
};

// the multiple variations of swap types and states
const swapProps = [
[LOOP_IN, INITIATED],
[LOOP_IN, HTLC_PUBLISHED],
[LOOP_IN, INVOICE_SETTLED],
[LOOP_IN, SUCCESS],
[LOOP_IN, FAILED],
[LOOP_OUT, INITIATED],
[LOOP_OUT, PREIMAGE_REVEALED],
[LOOP_OUT, SUCCESS],
[LOOP_OUT, FAILED],
];
// const mockSwap = loopListSwaps.swapsList[0];
const mockSwap = (type: number, state: number, id?: string) => {
const swap = new Swap(loopListSwaps.swapsList[0]);
swap.id = `${id || ''}${swap.id}`;
swap.type = type;
swap.state = state;
swap.lastUpdateTime = Date.now() * 1000 * 1000;
return swap;
};
// create a list of swaps to use for stories
const createSwaps = () => {
return [...Array(9)]
.map((_, i) => mockSwap(swapProps[i][0], swapProps[i][1], `${i}`))
.reduce((map, swap) => {
map.set(swap.id, swap);
return map;
}, observable.map());
};

let timer: NodeJS.Timeout;
const delay = (timeout: number) =>
new Promise(resolve => (timer = setTimeout(resolve, timeout)));

export const AllSwapStates = () => {
const store = useStore();
store.swapStore.stopAutoPolling();
store.swapStore.swaps = createSwaps();
return <ProcessingSwaps />;
};

export const LoopInProgress = () => {
const store = useStore();
store.swapStore.stopAutoPolling();
const swap = mockSwap(LOOP_IN, INITIATED);
store.swapStore.swaps = observable.map({ [swap.id]: swap });

useEffect(() => {
const startTransitions = async () => {
await delay(2000);
swap.state = HTLC_PUBLISHED;
await delay(2000);
swap.state = INVOICE_SETTLED;
await delay(2000);
swap.state = SUCCESS;
await delay(2000);
swap.initiationTime = 0;
};

startTransitions();
return () => clearTimeout(timer);
}, []);

return <ProcessingSwaps />;
};

export const LoopOutProgress = () => {
const store = useStore();
store.swapStore.stopAutoPolling();
const swap = mockSwap(LOOP_OUT, INITIATED);
store.swapStore.swaps = observable.map({ [swap.id]: swap });

useEffect(() => {
const startTransitions = async () => {
await delay(2000);
swap.state = PREIMAGE_REVEALED;
await delay(2000);
swap.state = SUCCESS;
await delay(2000);
swap.initiationTime = 0;
};

startTransitions();
return () => clearTimeout(timer);
}, []);

return <ProcessingSwaps />;
};
2 changes: 1 addition & 1 deletion app/src/__stories__/Tile.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const WithChildren = () => (
);

export const WithArrowIcon = () => (
<Tile title="Tile With Arrow" onArrowClick={() => action('ArrowIcon')}>
<Tile title="Tile With Arrow" onMaximizeClick={() => action('ArrowIcon')}>
Sample Text
</Tile>
);
Expand Down
4 changes: 2 additions & 2 deletions app/src/__tests__/components/common/Tile.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('Tile component', () => {

const render = (text?: string, children?: ReactNode) => {
const cmp = (
<Tile title="Test Tile" text={text} onArrowClick={handleArrowClick}>
<Tile title="Test Tile" text={text} onMaximizeClick={handleArrowClick}>
{children}
</Tile>
);
Expand All @@ -32,7 +32,7 @@ describe('Tile component', () => {

it('should handle the arrow click event', () => {
const { getByText } = render();
fireEvent.click(getByText('arrow-right.svg'));
fireEvent.click(getByText('maximize.svg'));
expect(handleArrowClick).toBeCalled();
});
});
9 changes: 7 additions & 2 deletions app/src/__tests__/components/loop/LoopHistory.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ describe('LoopHistory component', () => {
beforeEach(async () => {
store = createStore();
await store.init();

// remove all but one swap to prevent `getByText` from
// complaining about multiple elements in tests
const swap = store.swapStore.sortedSwaps[0];
store.swapStore.swaps.clear();
store.swapStore.swaps.set(swap.id, swap);
});

const render = () => {
const swaps = store.swapStore.sortedSwaps.slice(0, 1);
return renderWithProviders(<LoopHistory swaps={swaps} />);
return renderWithProviders(<LoopHistory />, store);
};

it('should display a successful swap', async () => {
Expand Down
133 changes: 133 additions & 0 deletions app/src/__tests__/components/loop/ProcessingSwaps.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React from 'react';
import * as LOOP from 'types/generated/loop_pb';
import { fireEvent } from '@testing-library/react';
import { ellipseInside } from 'util/strings';
import { renderWithProviders } from 'util/tests';
import { loopListSwaps } from 'util/tests/sampleData';
import { createStore, Store } from 'store';
import { Swap } from 'store/models';
import ProcessingSwaps from 'components/loop/processing/ProcessingSwaps';

const { LOOP_IN, LOOP_OUT } = LOOP.SwapType;
const {
INITIATED,
PREIMAGE_REVEALED,
HTLC_PUBLISHED,
SUCCESS,
INVOICE_SETTLED,
FAILED,
} = LOOP.SwapState;
const width = (el: any) => window.getComputedStyle(el).width;

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

const addSwap = (type: number, state: number, id?: string) => {
const swap = new Swap(loopListSwaps.swapsList[0]);
swap.id = `${id || ''}${swap.id}`;
swap.type = type;
swap.state = state;
swap.lastUpdateTime = Date.now() * 1000 * 1000;
store.swapStore.swaps.set(swap.id, swap);
return swap;
};

beforeEach(async () => {
store = createStore();
});

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

it('should display the title', async () => {
const { getByText } = render();
expect(getByText('Processing Loops')).toBeInTheDocument();
});

it('should display an INITIATED Loop In', () => {
const { getByText, getByTitle } = render();
const swap = addSwap(LOOP_IN, INITIATED);
expect(getByText('dot.svg')).toHaveClass('warn');
expect(getByText(ellipseInside(swap.id))).toBeInTheDocument();
expect(getByTitle(swap.stateLabel)).toBeInTheDocument();
expect(width(getByTitle(swap.stateLabel))).toBe('25%');
});

it('should display an HTLC_PUBLISHED Loop In', () => {
const { getByText, getByTitle } = render();
const swap = addSwap(LOOP_IN, HTLC_PUBLISHED);
expect(getByText('dot.svg')).toHaveClass('warn');
expect(getByText(ellipseInside(swap.id))).toBeInTheDocument();
expect(getByTitle(swap.stateLabel)).toBeInTheDocument();
expect(width(getByTitle(swap.stateLabel))).toBe('50%');
});

it('should display an INVOICE_SETTLED Loop In', () => {
const { getByText, getByTitle } = render();
const swap = addSwap(LOOP_IN, INVOICE_SETTLED);
expect(getByText('dot.svg')).toHaveClass('warn');
expect(getByText(ellipseInside(swap.id))).toBeInTheDocument();
expect(getByTitle(swap.stateLabel)).toBeInTheDocument();
expect(width(getByTitle(swap.stateLabel))).toBe('75%');
});

it('should display an SUCCESS Loop In', () => {
const { getByText, getByTitle } = render();
const swap = addSwap(LOOP_IN, SUCCESS);
expect(getByText(ellipseInside(swap.id))).toBeInTheDocument();
expect(getByTitle(swap.stateLabel)).toBeInTheDocument();
expect(width(getByTitle(swap.stateLabel))).toBe('100%');
});

it('should display an FAILED Loop In', () => {
const { getByText } = render();
const swap = addSwap(LOOP_IN, FAILED);
expect(getByText(ellipseInside(swap.id))).toBeInTheDocument();
expect(getByText(swap.stateLabel)).toBeInTheDocument();
expect(getByText('close.svg')).toBeInTheDocument();
});

it('should display an INITIATED Loop Out', () => {
const { getByText, getByTitle } = render();
const swap = addSwap(LOOP_OUT, INITIATED);
expect(getByText('dot.svg')).toHaveClass('warn');
expect(getByText(ellipseInside(swap.id))).toBeInTheDocument();
expect(getByTitle(swap.stateLabel)).toBeInTheDocument();
expect(width(getByTitle(swap.stateLabel))).toBe('33%');
});

it('should display an PREIMAGE_REVEALED Loop Out', () => {
const { getByText, getByTitle } = render();
const swap = addSwap(LOOP_OUT, PREIMAGE_REVEALED);
expect(getByText('dot.svg')).toHaveClass('warn');
expect(getByText(ellipseInside(swap.id))).toBeInTheDocument();
expect(getByTitle(swap.stateLabel)).toBeInTheDocument();
expect(width(getByTitle(swap.stateLabel))).toBe('66%');
});

it('should display an SUCCESS Loop Out', () => {
const { getByText, getByTitle } = render();
const swap = addSwap(LOOP_OUT, SUCCESS);
expect(getByText('dot.svg')).toHaveClass('success');
expect(getByText(ellipseInside(swap.id))).toBeInTheDocument();
expect(getByTitle(swap.stateLabel)).toBeInTheDocument();
expect(width(getByTitle(swap.stateLabel))).toBe('100%');
});

it('should display an FAILED Loop Out', () => {
const { getByText } = render();
const swap = addSwap(LOOP_OUT, FAILED);
expect(getByText(ellipseInside(swap.id))).toBeInTheDocument();
expect(getByText(swap.stateLabel)).toBeInTheDocument();
expect(getByText('close.svg')).toBeInTheDocument();
});

it('should dismiss a failed Loop', () => {
const { getByText } = render();
addSwap(LOOP_OUT, FAILED);
expect(store.swapStore.dismissedSwapIds).toHaveLength(0);
fireEvent.click(getByText('close.svg'));
expect(store.swapStore.dismissedSwapIds).toHaveLength(1);
});
});
34 changes: 34 additions & 0 deletions app/src/__tests__/store/swapStore.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as LOOP from 'types/generated/loop_pb';
import { waitFor } from '@testing-library/react';
import { loopListSwaps } from 'util/tests/sampleData';
import { createStore, SwapStore } from 'store';

Expand Down Expand Up @@ -55,4 +56,37 @@ describe('SwapStore', () => {
swap.type = type;
expect(swap.typeName).toEqual(label);
});

it('should poll for swap updates', async () => {
await store.fetchSwaps();
const swap = store.sortedSwaps[0];
// create a pending swap to trigger auto-polling
swap.state = LOOP.SwapState.INITIATED;
expect(store.pendingSwaps).toHaveLength(1);
// wait for polling to start
await waitFor(() => {
expect(store.pollingInterval).toBeDefined();
});
// change the swap to complete
swap.state = LOOP.SwapState.SUCCESS;
expect(store.pendingSwaps).toHaveLength(0);
// confirm polling has stopped
await waitFor(() => {
expect(store.pollingInterval).toBeUndefined();
});
});

it('should handle startPolling when polling is already running', () => {
expect(store.pollingInterval).toBeUndefined();
store.startPolling();
expect(store.pollingInterval).toBeDefined();
store.startPolling();
expect(store.pollingInterval).toBeDefined();
});

it('should handle stopPolling when polling is already stopped', () => {
expect(store.pollingInterval).toBeUndefined();
store.stopPolling();
expect(store.pollingInterval).toBeUndefined();
});
});
1 change: 1 addition & 0 deletions app/src/assets/icons/maximize.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/src/assets/icons/minimize.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading