fix: disallow updating custom node before validation (#931)
* fix: disallow updating custom node before validation * fix: format * fix: lint * feat: add node switcher component tests * fix: environment filter
This commit is contained in:
parent
ab0b762cd2
commit
45d05b38f9
@ -1,6 +1,5 @@
|
||||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Navbar } from '../components/navbar';
|
||||
import { t, ThemeContext, useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
@ -8,11 +7,7 @@ import {
|
||||
VegaManageDialog,
|
||||
VegaWalletProvider,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import {
|
||||
useEnvironment,
|
||||
EnvironmentProvider,
|
||||
NetworkSwitcherDialog,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { EnvironmentProvider } from '@vegaprotocol/environment';
|
||||
import { Connectors } from '../lib/vega-connectors';
|
||||
import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
|
||||
import { AppLoader } from '../components/app-loader';
|
||||
@ -21,9 +16,7 @@ import './styles.css';
|
||||
import { useGlobalStore } from '../stores';
|
||||
|
||||
function AppBody({ Component, pageProps }: AppProps) {
|
||||
const { push } = useRouter();
|
||||
const store = useGlobalStore();
|
||||
const { VEGA_NETWORKS } = useEnvironment();
|
||||
const [theme, toggleTheme] = useThemeSwitcher();
|
||||
|
||||
return (
|
||||
@ -61,15 +54,6 @@ function AppBody({ Component, pageProps }: AppProps) {
|
||||
dialogOpen={store.vegaWalletManageDialog}
|
||||
setDialogOpen={(open) => store.setVegaWalletManageDialog(open)}
|
||||
/>
|
||||
<NetworkSwitcherDialog
|
||||
dialogOpen={store.vegaNetworkSwitcherDialog}
|
||||
setDialogOpen={(open) => store.setVegaNetworkSwitcherDialog(open)}
|
||||
onConnect={({ network }) => {
|
||||
if (VEGA_NETWORKS[network]) {
|
||||
push(VEGA_NETWORKS[network] ?? '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</AppLoader>
|
||||
</div>
|
||||
</ThemeContext.Provider>
|
||||
|
@ -6,4 +6,5 @@ module.exports = {
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/environment',
|
||||
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||
};
|
||||
|
@ -1,5 +1,3 @@
|
||||
export * from './network-loader';
|
||||
export * from './network-switcher';
|
||||
export * from './network-switcher-dialog';
|
||||
export * from './node-switcher';
|
||||
export * from './node-switcher-dialog';
|
||||
|
@ -0,0 +1,61 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import { useEnvironment } from '../../hooks';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { NetworkLoader } from './network-loader';
|
||||
|
||||
jest.mock('@apollo/client');
|
||||
jest.mock('../../hooks');
|
||||
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
ApolloProvider.mockImplementation(({ children }: { children: ReactNode }) => {
|
||||
return children;
|
||||
});
|
||||
|
||||
const SKELETON_TEXT = 'LOADING';
|
||||
const SUCCESS_TEXT = 'LOADED';
|
||||
|
||||
const createClient = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
createClient.mockReset();
|
||||
createClient.mockImplementation(() => {
|
||||
return jest.fn();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Network loader', () => {
|
||||
it('renders a skeleton when there is no vega url in the environment', () => {
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
VEGA_URL: undefined,
|
||||
}));
|
||||
|
||||
render(
|
||||
<NetworkLoader skeleton={SKELETON_TEXT} createClient={createClient}>
|
||||
{SUCCESS_TEXT}
|
||||
</NetworkLoader>
|
||||
);
|
||||
|
||||
expect(screen.getByText(SKELETON_TEXT)).toBeInTheDocument();
|
||||
expect(() => screen.getByText(SUCCESS_TEXT)).toThrow();
|
||||
expect(createClient).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders the child components wrapped in an apollo provider when the environment has a vega url', () => {
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
VEGA_URL: 'http://vega.node',
|
||||
}));
|
||||
|
||||
render(
|
||||
<NetworkLoader skeleton={SKELETON_TEXT} createClient={createClient}>
|
||||
{SUCCESS_TEXT}
|
||||
</NetworkLoader>
|
||||
);
|
||||
|
||||
expect(() => screen.getByText(SKELETON_TEXT)).toThrow();
|
||||
expect(screen.getByText(SUCCESS_TEXT)).toBeInTheDocument();
|
||||
expect(createClient).toHaveBeenCalledWith('http://vega.node');
|
||||
});
|
||||
});
|
@ -1 +0,0 @@
|
||||
export { NetworkSwitcherDialog } from './network-switcher-dialog';
|
@ -1,37 +0,0 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { NetworkSwitcher } from '../network-switcher';
|
||||
|
||||
type NetworkSwitcherDialogProps = Pick<
|
||||
ComponentProps<typeof NetworkSwitcher>,
|
||||
'onConnect' | 'onError'
|
||||
> & {
|
||||
dialogOpen: boolean;
|
||||
setDialogOpen: (dialogOpen: boolean) => void;
|
||||
intent?: Intent;
|
||||
};
|
||||
|
||||
export const NetworkSwitcherDialog = ({
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
intent,
|
||||
onConnect,
|
||||
onError,
|
||||
}: NetworkSwitcherDialogProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={dialogOpen}
|
||||
onChange={setDialogOpen}
|
||||
title={t('Choose a network')}
|
||||
intent={intent}
|
||||
>
|
||||
<NetworkSwitcher
|
||||
onConnect={onConnect}
|
||||
onError={onError}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export { NetworkSwitcher } from './network-switcher';
|
@ -1,49 +0,0 @@
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { Button, Select } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { useEnvironment } from '../../hooks';
|
||||
import type { Networks } from '../../types';
|
||||
|
||||
type NetworkState = {
|
||||
network: Networks;
|
||||
};
|
||||
|
||||
type NetworkSwitcherProps = {
|
||||
onConnect: (network: NetworkState) => void;
|
||||
onError?: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const NetworkSwitcher = ({ onConnect }: NetworkSwitcherProps) => {
|
||||
const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
|
||||
const { control, handleSubmit } = useForm<NetworkState>({
|
||||
defaultValues: {
|
||||
network: VEGA_ENV,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onConnect)}>
|
||||
<div className="py-16">
|
||||
<Controller
|
||||
name="network"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onChange={field.onChange}>
|
||||
{Object.keys(VEGA_NETWORKS).map((network, index) => (
|
||||
<option key={index} value={network}>
|
||||
{network}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="py-16">
|
||||
<Button data-testid="connect-network" type="submit">
|
||||
{t('Connect')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -0,0 +1,668 @@
|
||||
import { useState } from 'react';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { useEnvironment } from '../../hooks/use-environment';
|
||||
import { useNodes } from '../../hooks/use-nodes';
|
||||
import createMockClient from '../../hooks/mocks/apollo-client';
|
||||
import { STATS_QUERY } from '../../utils/request-node';
|
||||
import { NodeSwitcher } from './node-switcher';
|
||||
import { getErrorByType } from '../../utils/validate-node';
|
||||
import type { Configuration, NodeData } from '../../';
|
||||
import { Networks, ErrorType, CUSTOM_NODE_KEY } from '../../';
|
||||
|
||||
type NodeDataProp = 'responseTime' | 'block' | 'chain' | 'ssl';
|
||||
|
||||
jest.mock('../../hooks/use-environment');
|
||||
jest.mock('../../hooks/use-nodes');
|
||||
|
||||
const mockNodesImplementation =
|
||||
(
|
||||
updateNodeUrlMock: jest.Mock,
|
||||
getNodeState: typeof getValidNodeState = getValidNodeState
|
||||
) =>
|
||||
(config: Configuration) => {
|
||||
const [{ state, clients }, setImplementation] = useState<{
|
||||
state: Record<string, NodeData>;
|
||||
clients: Record<string, ReturnType<typeof createMockClients>>;
|
||||
}>({
|
||||
state: createMockState(Networks.TESTNET, config.hosts),
|
||||
clients: createMockClients(config.hosts),
|
||||
});
|
||||
|
||||
return {
|
||||
state,
|
||||
clients,
|
||||
updateNodeUrl: updateNodeUrlMock.mockImplementation(
|
||||
(node: string, url: string) => {
|
||||
setImplementation((prev) => ({
|
||||
state: {
|
||||
...prev.state,
|
||||
[node]: getNodeState(Networks.TESTNET, url),
|
||||
},
|
||||
clients: {
|
||||
...prev.clients,
|
||||
[node]: createMockClient({ network: Networks.TESTNET }),
|
||||
},
|
||||
}));
|
||||
}
|
||||
),
|
||||
updateNodeBlock: (node: string, value: number) => {
|
||||
setImplementation((prev) => ({
|
||||
state: {
|
||||
...prev.state,
|
||||
[node]: {
|
||||
...prev.state[node],
|
||||
block: {
|
||||
...prev.state[node].block,
|
||||
value,
|
||||
},
|
||||
},
|
||||
},
|
||||
clients: prev.clients,
|
||||
}));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const statsQueryMock = {
|
||||
request: {
|
||||
query: STATS_QUERY,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
statistics: {
|
||||
blockHeight: 1234,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const onConnect = jest.fn();
|
||||
|
||||
const HOSTS = ['https://host1.com', 'https://host2.com'];
|
||||
|
||||
const enum STATES {
|
||||
LOADING = 'is loading',
|
||||
HAS_ERROR = 'has an error',
|
||||
}
|
||||
|
||||
const getValidNodeState = (env: Networks, url: string) => ({
|
||||
url,
|
||||
initialized: true,
|
||||
responseTime: {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: 10,
|
||||
},
|
||||
block: {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: 123,
|
||||
},
|
||||
ssl: {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: true,
|
||||
},
|
||||
chain: {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
value: `${env.toLowerCase()}-1234`,
|
||||
},
|
||||
});
|
||||
|
||||
const createMockState = (env: Networks, nodes: string[]) =>
|
||||
nodes.reduce(
|
||||
(acc, node) => ({
|
||||
...acc,
|
||||
[node]: getValidNodeState(env, node),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
const createMockClients = (nodes: string[]) =>
|
||||
nodes.reduce(
|
||||
(acc, node) => ({
|
||||
...acc,
|
||||
[node]: createMockClient({ network: Networks.TESTNET }),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
onConnect.mockReset();
|
||||
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
VEGA_ENV: Networks.TESTNET,
|
||||
VEGA_URL: undefined,
|
||||
}));
|
||||
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useNodes.mockImplementation((config: Configuration) => ({
|
||||
state: createMockState(Networks.TESTNET, config.hosts),
|
||||
clients: createMockClients(config.hosts),
|
||||
updateNodeUrl: jest.fn(),
|
||||
updateNodeBlock: jest.fn(),
|
||||
}));
|
||||
});
|
||||
|
||||
describe('Node switcher', () => {
|
||||
it('renders with empty config', () => {
|
||||
render(<NodeSwitcher config={{ hosts: [] }} onConnect={onConnect} />);
|
||||
|
||||
expect(() => screen.getAllByTestId('node')).toThrow();
|
||||
expect(screen.getByRole('radio', { checked: false })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders with the provided config nodes', () => {
|
||||
render(<NodeSwitcher config={{ hosts: HOSTS }} onConnect={onConnect} />);
|
||||
|
||||
HOSTS.forEach((host) => {
|
||||
expect(
|
||||
screen.getByRole('radio', { checked: false, name: host })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.getByRole('radio', { checked: false, name: 'Other' })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
});
|
||||
|
||||
it('marks the node in the environment as selected', () => {
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
VEGA_ENV: Networks.TESTNET,
|
||||
VEGA_URL: HOSTS[0],
|
||||
}));
|
||||
render(<NodeSwitcher config={{ hosts: HOSTS }} onConnect={onConnect} />);
|
||||
|
||||
HOSTS.forEach((host) => {
|
||||
expect(
|
||||
screen.getByRole('radio', { checked: host === HOSTS[0], name: host })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.getByRole('radio', { checked: false, name: 'Other' })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).not.toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
});
|
||||
|
||||
it.each`
|
||||
dataProp | state
|
||||
${'responseTime'} | ${STATES.LOADING}
|
||||
${'responseTime'} | ${STATES.HAS_ERROR}
|
||||
${'block'} | ${STATES.LOADING}
|
||||
${'block'} | ${STATES.HAS_ERROR}
|
||||
${'chain'} | ${STATES.LOADING}
|
||||
${'chain'} | ${STATES.HAS_ERROR}
|
||||
${'ssl'} | ${STATES.LOADING}
|
||||
${'ssl'} | ${STATES.HAS_ERROR}
|
||||
`(
|
||||
'disables selecting a node when the $dataProp $state',
|
||||
({ dataProp, state }: { dataProp: NodeDataProp; state: STATES }) => {
|
||||
const mockUrl = 'https://host.url';
|
||||
const mockConfig = {
|
||||
hosts: [mockUrl],
|
||||
};
|
||||
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useNodes.mockImplementation((config: Configuration) => {
|
||||
const nodeState = getValidNodeState(Networks.TESTNET, mockUrl);
|
||||
return {
|
||||
state: {
|
||||
[mockUrl]: {
|
||||
...nodeState,
|
||||
[dataProp]: {
|
||||
...nodeState[dataProp],
|
||||
isLoading:
|
||||
state === STATES.LOADING
|
||||
? true
|
||||
: nodeState[dataProp].isLoading,
|
||||
hasError:
|
||||
state === STATES.HAS_ERROR
|
||||
? true
|
||||
: nodeState[dataProp].hasError,
|
||||
value: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
clients: createMockClients(config.hosts),
|
||||
updateNodeUrl: jest.fn(),
|
||||
updateNodeBlock: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
render(
|
||||
<MockedProvider mocks={[statsQueryMock]}>
|
||||
<NodeSwitcher config={mockConfig} onConnect={onConnect} />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('radio', { name: mockUrl })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('disables selecting a node when it has an invalid url', () => {
|
||||
const mockUrl = 'not-valid-url';
|
||||
const mockConfig = {
|
||||
hosts: [mockUrl],
|
||||
};
|
||||
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useNodes.mockImplementation((config: Configuration) => ({
|
||||
state: {
|
||||
[mockUrl]: getValidNodeState(Networks.TESTNET, mockUrl),
|
||||
},
|
||||
clients: createMockClients(config.hosts),
|
||||
updateNodeUrl: jest.fn(),
|
||||
updateNodeBlock: jest.fn(),
|
||||
}));
|
||||
|
||||
render(
|
||||
<MockedProvider mocks={[statsQueryMock]}>
|
||||
<NodeSwitcher config={mockConfig} onConnect={onConnect} />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('radio', { name: mockUrl })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
});
|
||||
|
||||
it('disables selecting a node when it is on an incorrect network', () => {
|
||||
const mockUrl = 'https://mock.url';
|
||||
const mockConfig = {
|
||||
hosts: [mockUrl],
|
||||
};
|
||||
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useNodes.mockImplementation((config: Configuration) => {
|
||||
const nodeState = getValidNodeState(Networks.TESTNET, mockUrl);
|
||||
return {
|
||||
state: {
|
||||
[mockUrl]: {
|
||||
...nodeState,
|
||||
chain: {
|
||||
...nodeState.chain,
|
||||
value: `some-network-id`,
|
||||
},
|
||||
},
|
||||
},
|
||||
clients: createMockClients(config.hosts),
|
||||
updateNodeUrl: jest.fn(),
|
||||
updateNodeBlock: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
render(
|
||||
<MockedProvider mocks={[statsQueryMock]}>
|
||||
<NodeSwitcher config={mockConfig} onConnect={onConnect} />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('radio', { name: mockUrl })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
});
|
||||
|
||||
it('allows connecting to a valid node', () => {
|
||||
render(<NodeSwitcher config={{ hosts: HOSTS }} onConnect={onConnect} />);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
fireEvent.click(screen.getByRole('radio', { name: HOSTS[0] }));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).not.toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Connect' }));
|
||||
|
||||
expect(onConnect).toHaveBeenCalledWith(HOSTS[0]);
|
||||
});
|
||||
|
||||
it('allows checking a custom node', () => {
|
||||
const updateNodeUrlMock = jest.fn();
|
||||
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useNodes.mockImplementation((config: Configuration) => ({
|
||||
state: createMockState(Networks.TESTNET, config.hosts),
|
||||
clients: createMockClients(config.hosts),
|
||||
updateNodeUrl: updateNodeUrlMock,
|
||||
updateNodeBlock: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUrl = 'https://custom.url';
|
||||
render(
|
||||
<MockedProvider mocks={[statsQueryMock]}>
|
||||
<NodeSwitcher config={{ hosts: [] }} onConnect={onConnect} />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'Other' }));
|
||||
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true'
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
target: {
|
||||
value: mockUrl,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('link', { name: 'Check' }));
|
||||
|
||||
expect(updateNodeUrlMock).toHaveBeenCalledWith(CUSTOM_NODE_KEY, mockUrl);
|
||||
});
|
||||
|
||||
it('allows connecting to a custom node', () => {
|
||||
const mockUrl = 'https://custom.url';
|
||||
const updateNodeUrlMock = jest.fn();
|
||||
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useNodes.mockImplementation(mockNodesImplementation(updateNodeUrlMock));
|
||||
|
||||
render(
|
||||
<MockedProvider mocks={[statsQueryMock]}>
|
||||
<NodeSwitcher config={{ hosts: [] }} onConnect={onConnect} />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'Other' }));
|
||||
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true'
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
target: {
|
||||
value: mockUrl,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('link', { name: 'Check' }));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).not.toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Connect' }));
|
||||
|
||||
expect(onConnect).toHaveBeenCalledWith(mockUrl);
|
||||
});
|
||||
|
||||
it.each`
|
||||
dataProp | state
|
||||
${'responseTime'} | ${STATES.LOADING}
|
||||
${'responseTime'} | ${STATES.HAS_ERROR}
|
||||
${'block'} | ${STATES.LOADING}
|
||||
${'block'} | ${STATES.HAS_ERROR}
|
||||
${'chain'} | ${STATES.LOADING}
|
||||
${'chain'} | ${STATES.HAS_ERROR}
|
||||
${'ssl'} | ${STATES.LOADING}
|
||||
${'ssl'} | ${STATES.HAS_ERROR}
|
||||
`(
|
||||
'disables selecting a custom node when the $dataProp $state',
|
||||
({ dataProp, state }: { dataProp: NodeDataProp; state: STATES }) => {
|
||||
const mockUrl = 'https://custom.url';
|
||||
const updateNodeUrlMock = jest.fn();
|
||||
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useNodes.mockImplementation(
|
||||
mockNodesImplementation(updateNodeUrlMock, (env) => {
|
||||
const nodeState = getValidNodeState(env, mockUrl);
|
||||
return {
|
||||
...nodeState,
|
||||
[dataProp]: {
|
||||
...nodeState[dataProp],
|
||||
isLoading:
|
||||
state === STATES.LOADING ? true : nodeState[dataProp].isLoading,
|
||||
hasError:
|
||||
state === STATES.HAS_ERROR
|
||||
? true
|
||||
: nodeState[dataProp].hasError,
|
||||
value: undefined,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
render(
|
||||
<MockedProvider mocks={[statsQueryMock]}>
|
||||
<NodeSwitcher config={{ hosts: [] }} onConnect={onConnect} />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'Other' }));
|
||||
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true'
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
target: {
|
||||
value: mockUrl,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('link', { name: 'Check' }));
|
||||
|
||||
if (state === STATES.LOADING) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(screen.getByRole('link', { name: 'Checking' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true'
|
||||
);
|
||||
}
|
||||
|
||||
if (state === STATES.HAS_ERROR) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(screen.getByRole('link', { name: 'Check' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
}
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
|
||||
if (state === STATES.HAS_ERROR) {
|
||||
const expectedErrorType =
|
||||
dataProp === 'ssl' ? ErrorType.SSL_ERROR : ErrorType.CONNECTION_ERROR;
|
||||
const error = getErrorByType(
|
||||
expectedErrorType,
|
||||
Networks.TESTNET,
|
||||
mockUrl
|
||||
);
|
||||
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(error?.headline).not.toBeNull();
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(screen.getByText(error?.headline ?? '')).toBeInTheDocument();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it('disables selecting a custom node when it has an invalid url', () => {
|
||||
const mockUrl = 'not-valid-url';
|
||||
const updateNodeUrlMock = jest.fn();
|
||||
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useNodes.mockImplementation(mockNodesImplementation(updateNodeUrlMock));
|
||||
|
||||
render(
|
||||
<MockedProvider mocks={[statsQueryMock]}>
|
||||
<NodeSwitcher config={{ hosts: [] }} onConnect={onConnect} />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'Other' }));
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
target: {
|
||||
value: mockUrl,
|
||||
},
|
||||
});
|
||||
fireEvent.click(screen.getByRole('link', { name: 'Check' }));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
|
||||
const error = getErrorByType(
|
||||
ErrorType.INVALID_URL,
|
||||
Networks.TESTNET,
|
||||
mockUrl
|
||||
);
|
||||
|
||||
expect(error?.headline).not.toBeNull();
|
||||
expect(screen.getByText(error?.headline ?? '')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables selecting a custom node when it is on an incorrect network', () => {
|
||||
const mockUrl = 'https://mock.url';
|
||||
const updateNodeUrlMock = jest.fn();
|
||||
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useNodes.mockImplementation(
|
||||
mockNodesImplementation(updateNodeUrlMock, (env, url) => {
|
||||
const nodeState = getValidNodeState(env, url);
|
||||
return {
|
||||
...nodeState,
|
||||
chain: {
|
||||
...nodeState.chain,
|
||||
value: 'network-chain-id',
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
render(
|
||||
<MockedProvider mocks={[statsQueryMock]}>
|
||||
<NodeSwitcher config={{ hosts: [] }} onConnect={onConnect} />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'Other' }));
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
target: {
|
||||
value: mockUrl,
|
||||
},
|
||||
});
|
||||
fireEvent.click(screen.getByRole('link', { name: 'Check' }));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
|
||||
const error = getErrorByType(
|
||||
ErrorType.INVALID_NETWORK,
|
||||
Networks.TESTNET,
|
||||
mockUrl
|
||||
);
|
||||
|
||||
expect(error?.headline).not.toBeNull();
|
||||
expect(screen.getByText(error?.headline ?? '')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each`
|
||||
description | errorType
|
||||
${'the node has an invalid url'} | ${ErrorType.INVALID_URL}
|
||||
${'the node is on an invalid network'} | ${ErrorType.INVALID_NETWORK}
|
||||
${'the node has an ssl issue'} | ${ErrorType.SSL_ERROR}
|
||||
${'the node cannot be reached'} | ${ErrorType.CONNECTION_ERROR}
|
||||
${'none of the config nodes can be connected to'} | ${ErrorType.CONNECTION_ERROR_ALL}
|
||||
${'the config cannot be loaded'} | ${ErrorType.CONFIG_LOAD_ERROR}
|
||||
${'the config is invalid'} | ${ErrorType.CONFIG_VALIDATION_ERROR}
|
||||
`(
|
||||
'displays initial error when $description',
|
||||
({ errorType }: { errorType: ErrorType }) => {
|
||||
const mockEnvUrl = 'https://mock.url';
|
||||
|
||||
// @ts-ignore Typescript doesn't recognise mocked instances
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
VEGA_ENV: Networks.TESTNET,
|
||||
VEGA_URL: mockEnvUrl,
|
||||
}));
|
||||
|
||||
render(
|
||||
<NodeSwitcher
|
||||
config={{ hosts: HOSTS }}
|
||||
initialErrorType={errorType}
|
||||
onConnect={onConnect}
|
||||
/>
|
||||
);
|
||||
|
||||
const error = getErrorByType(errorType, Networks.TESTNET, mockEnvUrl);
|
||||
|
||||
expect(error?.headline).not.toBeNull();
|
||||
expect(error?.message).not.toBeNull();
|
||||
expect(screen.getByText(error?.headline ?? '')).toBeInTheDocument();
|
||||
expect(screen.getByText(error?.message ?? '')).toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
RadioGroup,
|
||||
@ -55,14 +55,9 @@ export const NodeSwitcher = ({
|
||||
const [nodeRadio, setNodeRadio] = useState(
|
||||
getDefaultNode(config.hosts, VEGA_URL)
|
||||
);
|
||||
const { state, clients, updateNodeUrl, updateNodeBlock } = useNodes(
|
||||
VEGA_ENV,
|
||||
config
|
||||
);
|
||||
const highestBlock = useMemo(
|
||||
() => getHighestBlock(VEGA_ENV, state),
|
||||
[VEGA_ENV, state]
|
||||
);
|
||||
const { state, clients, updateNodeUrl, updateNodeBlock } = useNodes(config);
|
||||
const highestBlock = getHighestBlock(VEGA_ENV, state);
|
||||
|
||||
const customUrl = state[CUSTOM_NODE_KEY]?.url;
|
||||
|
||||
const onSubmit = (node: ReturnType<typeof getDefaultNode>) => {
|
||||
@ -71,12 +66,7 @@ export const NodeSwitcher = ({
|
||||
}
|
||||
};
|
||||
|
||||
const isSubmitDisabled = getIsFormDisabled(
|
||||
nodeRadio,
|
||||
customNodeText,
|
||||
VEGA_ENV,
|
||||
state
|
||||
);
|
||||
const isSubmitDisabled = getIsFormDisabled(nodeRadio, VEGA_ENV, state);
|
||||
|
||||
const customNodeData =
|
||||
nodeRadio &&
|
||||
@ -94,7 +84,12 @@ export const NodeSwitcher = ({
|
||||
return (
|
||||
<div className="text-black dark:text-white w-full lg:min-w-[800px]">
|
||||
<NodeError {...(customNodeError || networkError)} />
|
||||
<form onSubmit={() => onSubmit(nodeRadio)}>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
onSubmit(nodeRadio);
|
||||
}}
|
||||
>
|
||||
<p className="text-body-large font-bold mt-16 mb-32">
|
||||
{t('Select a GraphQL node to connect to:')}
|
||||
</p>
|
||||
@ -124,7 +119,7 @@ export const NodeSwitcher = ({
|
||||
highestBlock={highestBlock}
|
||||
setBlock={(block) => updateNodeBlock(node, block)}
|
||||
>
|
||||
<div className="mb-8 break-all">
|
||||
<div className="mb-8 break-all" data-testid="node">
|
||||
<Radio
|
||||
id={`node-url-${index}`}
|
||||
labelClassName="whitespace-nowrap text-ellipsis overflow-hidden"
|
||||
@ -158,6 +153,7 @@ export const NodeSwitcher = ({
|
||||
>
|
||||
<Input
|
||||
placeholder="https://"
|
||||
role="textbox"
|
||||
value={customNodeText}
|
||||
hasError={
|
||||
!!customNodeText &&
|
||||
|
@ -121,17 +121,6 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete process.env['NX_VEGA_URL'];
|
||||
delete process.env['NX_VEGA_ENV'];
|
||||
delete process.env['NX_VEGA_CONFIG_URL'];
|
||||
delete process.env['NX_VEGA_NETWORKS'];
|
||||
delete process.env['NX_ETHEREUM_PROVIDER_URL'];
|
||||
delete process.env['NX_ETHERSCAN_URL'];
|
||||
delete process.env['NX_GIT_BRANCH'];
|
||||
delete process.env['NX_GIT_ORIGIN_URL'];
|
||||
delete process.env['NX_GIT_COMMIT_HASH'];
|
||||
delete process.env['NX_GITHUB_FEEDBACK_URL'];
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
getIsNodeLoading,
|
||||
} from '../utils/validate-node';
|
||||
import { ErrorType } from '../types';
|
||||
import type { Environment, RawEnvironment, NodeData } from '../types';
|
||||
import type { Environment, Networks, RawEnvironment, NodeData } from '../types';
|
||||
|
||||
type EnvironmentProviderProps = {
|
||||
definitions?: Partial<RawEnvironment>;
|
||||
@ -26,8 +26,15 @@ export type EnvironmentState = Environment & {
|
||||
|
||||
const EnvironmentContext = createContext({} as EnvironmentState);
|
||||
|
||||
const hasFinishedLoading = (node: NodeData) =>
|
||||
node.initialized && !getIsNodeLoading(node) && !node.verified;
|
||||
const hasLoaded = (env: Networks, node: NodeData) =>
|
||||
node.initialized &&
|
||||
!getIsNodeLoading(node) &&
|
||||
getErrorType(env, node) === null;
|
||||
|
||||
const hasFailedLoading = (env: Networks, node: NodeData) =>
|
||||
node.initialized &&
|
||||
!getIsNodeLoading(node) &&
|
||||
getErrorType(env, node) !== null;
|
||||
|
||||
export const EnvironmentProvider = ({
|
||||
definitions,
|
||||
@ -47,14 +54,14 @@ export const EnvironmentProvider = ({
|
||||
error && console.warn(error.headline);
|
||||
}
|
||||
});
|
||||
const { state: nodes, clients } = useNodes(environment.VEGA_ENV, config);
|
||||
const { state: nodes, clients } = useNodes(config);
|
||||
const nodeKeys = Object.keys(nodes);
|
||||
|
||||
useEffect(() => {
|
||||
if (!environment.VEGA_URL) {
|
||||
const successfulNodeKey = nodeKeys.find(
|
||||
(key) => nodes[key].verified
|
||||
) as keyof typeof nodes;
|
||||
const successfulNodeKey = nodeKeys.find((key) =>
|
||||
hasLoaded(environment.VEGA_ENV, nodes[key])
|
||||
);
|
||||
if (successfulNodeKey && nodes[successfulNodeKey]) {
|
||||
Object.keys(clients).forEach((node) => clients[node]?.stop());
|
||||
updateEnvironment((prevEnvironment) => ({
|
||||
@ -81,8 +88,9 @@ export const EnvironmentProvider = ({
|
||||
// if the config doesn't contain nodes the app can connect to
|
||||
if (
|
||||
nodeKeys.length > 0 &&
|
||||
nodeKeys.filter((key) => hasFinishedLoading(nodes[key])).length ===
|
||||
nodeKeys.length
|
||||
nodeKeys.filter((key) =>
|
||||
hasFailedLoading(environment.VEGA_ENV, nodes[key])
|
||||
).length === nodeKeys.length
|
||||
) {
|
||||
Object.keys(clients).forEach((node) => clients[node]?.stop());
|
||||
setNetworkError(ErrorType.CONNECTION_ERROR_ALL);
|
||||
|
@ -2,19 +2,16 @@ import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { ApolloClient } from '@apollo/client';
|
||||
import createClient from '../utils/apollo-client';
|
||||
import { useNodes } from './use-nodes';
|
||||
import { Networks } from '../types';
|
||||
import createMockClient, {
|
||||
getMockStatisticsResult,
|
||||
} from './mocks/apollo-client';
|
||||
|
||||
jest.mock('../utils/apollo-client');
|
||||
|
||||
const MOCK_ENV = Networks.DEVNET;
|
||||
const MOCK_DURATION = 1073;
|
||||
|
||||
const initialState = {
|
||||
url: '',
|
||||
verified: false,
|
||||
initialized: false,
|
||||
responseTime: {
|
||||
isLoading: false,
|
||||
@ -61,7 +58,7 @@ afterAll(() => {
|
||||
|
||||
describe('useNodes hook', () => {
|
||||
it('returns the default state when empty config provided', () => {
|
||||
const { result } = renderHook(() => useNodes(MOCK_ENV, { hosts: [] }));
|
||||
const { result } = renderHook(() => useNodes({ hosts: [] }));
|
||||
|
||||
expect(result.current.state).toEqual({});
|
||||
});
|
||||
@ -69,13 +66,12 @@ describe('useNodes hook', () => {
|
||||
it('sets loading state while waiting for the results', async () => {
|
||||
const node = 'https://some.url';
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
useNodes({ hosts: [node] })
|
||||
);
|
||||
|
||||
expect(result.current.state[node]).toEqual({
|
||||
...initialState,
|
||||
url: node,
|
||||
verified: false,
|
||||
initialized: true,
|
||||
responseTime: {
|
||||
...initialState.responseTime,
|
||||
@ -101,9 +97,7 @@ describe('useNodes hook', () => {
|
||||
it('sets statistics results', async () => {
|
||||
const mockResult = getMockStatisticsResult();
|
||||
const node = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].block).toEqual({
|
||||
@ -128,9 +122,7 @@ describe('useNodes hook', () => {
|
||||
|
||||
it('sets subscription result', async () => {
|
||||
const node = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].ssl).toEqual({
|
||||
@ -143,9 +135,7 @@ describe('useNodes hook', () => {
|
||||
|
||||
it('sets error when host in not a valid url', async () => {
|
||||
const node = 'not-url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].block.hasError).toBe(true);
|
||||
@ -162,9 +152,7 @@ describe('useNodes hook', () => {
|
||||
);
|
||||
|
||||
const node = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].block).toEqual({
|
||||
@ -194,9 +182,7 @@ describe('useNodes hook', () => {
|
||||
);
|
||||
|
||||
const node = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].ssl).toEqual({
|
||||
@ -210,9 +196,7 @@ describe('useNodes hook', () => {
|
||||
it('allows updating block values', async () => {
|
||||
const mockResult = getMockStatisticsResult();
|
||||
const node = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].block.value).toEqual(
|
||||
@ -232,9 +216,7 @@ describe('useNodes hook', () => {
|
||||
it('does nothing when calling the block update on a non-existing node', async () => {
|
||||
const mockResult = getMockStatisticsResult();
|
||||
const node = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state[node].block.value).toEqual(
|
||||
@ -251,9 +233,7 @@ describe('useNodes hook', () => {
|
||||
|
||||
it('adds new node', async () => {
|
||||
const node = 'custom-node-key';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
|
||||
|
||||
expect(result.current.state[node]).toEqual(undefined);
|
||||
|
||||
@ -269,9 +249,7 @@ describe('useNodes hook', () => {
|
||||
it('sets new url for node', async () => {
|
||||
const node = 'https://some.url';
|
||||
const newUrl = 'https://some-other.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [node] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [node] }));
|
||||
|
||||
act(() => {
|
||||
result.current.updateNodeUrl(node, newUrl);
|
||||
@ -285,9 +263,7 @@ describe('useNodes hook', () => {
|
||||
it('sets error when custom node has an invalid url', async () => {
|
||||
const node = 'node-key';
|
||||
const url = 'not-url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
|
||||
|
||||
expect(result.current.state[node]).toBe(undefined);
|
||||
|
||||
@ -312,9 +288,7 @@ describe('useNodes hook', () => {
|
||||
|
||||
const node = 'node-key';
|
||||
const url = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
|
||||
|
||||
expect(result.current.state[node]).toBe(undefined);
|
||||
|
||||
@ -351,9 +325,7 @@ describe('useNodes hook', () => {
|
||||
|
||||
const node = 'node-key';
|
||||
const url = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
|
||||
|
||||
expect(result.current.state[node]).toBe(undefined);
|
||||
|
||||
@ -374,7 +346,7 @@ describe('useNodes hook', () => {
|
||||
const url1 = 'https://some.url';
|
||||
const url2 = 'https://some-other.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [url1, url2] })
|
||||
useNodes({ hosts: [url1, url2] })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
@ -386,9 +358,7 @@ describe('useNodes hook', () => {
|
||||
it('exposes a client for the custom node', async () => {
|
||||
const node = 'node-key';
|
||||
const url = 'https://some.url';
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useNodes(MOCK_ENV, { hosts: [] })
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useNodes({ hosts: [] }));
|
||||
|
||||
act(() => {
|
||||
result.current.updateNodeUrl(node, url);
|
||||
|
@ -3,8 +3,7 @@ import { useState, useEffect, useReducer } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import type createClient from '../utils/apollo-client';
|
||||
import { initializeNode } from '../utils/initialize-node';
|
||||
import { getErrorType, getIsNodeLoading } from '../utils/validate-node';
|
||||
import type { NodeData, Configuration, Networks } from '../types';
|
||||
import type { NodeData, Configuration } from '../types';
|
||||
|
||||
type StatisticsPayload = {
|
||||
block: NodeData['block']['value'];
|
||||
@ -59,7 +58,6 @@ function withError<T>(value?: T) {
|
||||
|
||||
const getNodeData = (url?: string): NodeData => ({
|
||||
url: url ?? '',
|
||||
verified: false,
|
||||
initialized: false,
|
||||
responseTime: withData(),
|
||||
block: withData(),
|
||||
@ -108,88 +106,81 @@ const initializeNodes = (
|
||||
);
|
||||
};
|
||||
|
||||
const reducer =
|
||||
(env: Networks) => (state: Record<string, NodeData>, action: Action) => {
|
||||
switch (action.type) {
|
||||
case ACTIONS.GET_STATISTICS:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) {
|
||||
state[action.node] = getNodeData(action.payload?.url);
|
||||
}
|
||||
state[action.node].url = action.payload?.url ?? '';
|
||||
state[action.node].initialized = true;
|
||||
state[action.node].block.isLoading = true;
|
||||
state[action.node].chain.isLoading = true;
|
||||
state[action.node].responseTime.isLoading = true;
|
||||
});
|
||||
case ACTIONS.GET_STATISTICS_SUCCESS:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].block = withData(action.payload?.block);
|
||||
state[action.node].chain = withData(action.payload?.chain);
|
||||
state[action.node].responseTime = withData(
|
||||
action.payload?.responseTime
|
||||
);
|
||||
state[action.node].verified =
|
||||
!getIsNodeLoading(state[action.node]) &&
|
||||
getErrorType(env, state[action.node]) === null;
|
||||
});
|
||||
case ACTIONS.GET_STATISTICS_FAILURE:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].block = withError();
|
||||
state[action.node].chain = withError();
|
||||
state[action.node].responseTime = withError();
|
||||
});
|
||||
case ACTIONS.CHECK_SUBSCRIPTION:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) {
|
||||
state[action.node] = getNodeData(action.payload?.url);
|
||||
}
|
||||
state[action.node].url = action.payload?.url ?? '';
|
||||
state[action.node].ssl.isLoading = true;
|
||||
state[action.node].initialized = true;
|
||||
});
|
||||
case ACTIONS.CHECK_SUBSCRIPTION_SUCCESS:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].ssl = withData(true);
|
||||
state[action.node].verified =
|
||||
!getIsNodeLoading(state[action.node]) &&
|
||||
getErrorType(env, state[action.node]) === null;
|
||||
});
|
||||
case ACTIONS.CHECK_SUBSCRIPTION_FAILURE:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].ssl = withError();
|
||||
});
|
||||
case ACTIONS.ADD_NODE:
|
||||
return produce(state, (state) => {
|
||||
state[action.node] = getNodeData();
|
||||
});
|
||||
case ACTIONS.UPDATE_NODE_URL:
|
||||
return produce(state, (state) => {
|
||||
const existingNode = Object.keys(state).find(
|
||||
(node) =>
|
||||
action.node !== node && state[node].url === action.payload?.url
|
||||
);
|
||||
state[action.node] = existingNode
|
||||
? state[existingNode]
|
||||
: getNodeData(action.payload?.url);
|
||||
});
|
||||
case ACTIONS.UPDATE_NODE_BLOCK:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].block.value = action.payload;
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
const reducer = (state: Record<string, NodeData>, action: Action) => {
|
||||
switch (action.type) {
|
||||
case ACTIONS.GET_STATISTICS:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) {
|
||||
state[action.node] = getNodeData(action.payload?.url);
|
||||
}
|
||||
state[action.node].url = action.payload?.url ?? '';
|
||||
state[action.node].initialized = true;
|
||||
state[action.node].block.isLoading = true;
|
||||
state[action.node].chain.isLoading = true;
|
||||
state[action.node].responseTime.isLoading = true;
|
||||
});
|
||||
case ACTIONS.GET_STATISTICS_SUCCESS:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].block = withData(action.payload?.block);
|
||||
state[action.node].chain = withData(action.payload?.chain);
|
||||
state[action.node].responseTime = withData(
|
||||
action.payload?.responseTime
|
||||
);
|
||||
});
|
||||
case ACTIONS.GET_STATISTICS_FAILURE:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].block = withError();
|
||||
state[action.node].chain = withError();
|
||||
state[action.node].responseTime = withError();
|
||||
});
|
||||
case ACTIONS.CHECK_SUBSCRIPTION:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) {
|
||||
state[action.node] = getNodeData(action.payload?.url);
|
||||
}
|
||||
state[action.node].url = action.payload?.url ?? '';
|
||||
state[action.node].ssl.isLoading = true;
|
||||
state[action.node].initialized = true;
|
||||
});
|
||||
case ACTIONS.CHECK_SUBSCRIPTION_SUCCESS:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].ssl = withData(true);
|
||||
});
|
||||
case ACTIONS.CHECK_SUBSCRIPTION_FAILURE:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].ssl = withError();
|
||||
});
|
||||
case ACTIONS.ADD_NODE:
|
||||
return produce(state, (state) => {
|
||||
state[action.node] = getNodeData();
|
||||
});
|
||||
case ACTIONS.UPDATE_NODE_URL:
|
||||
return produce(state, (state) => {
|
||||
const existingNode = Object.keys(state).find(
|
||||
(node) =>
|
||||
action.node !== node && state[node].url === action.payload?.url
|
||||
);
|
||||
state[action.node] = existingNode
|
||||
? state[existingNode]
|
||||
: getNodeData(action.payload?.url);
|
||||
});
|
||||
case ACTIONS.UPDATE_NODE_BLOCK:
|
||||
return produce(state, (state) => {
|
||||
if (!state[action.node]) return;
|
||||
state[action.node].block.value = action.payload;
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const useNodes = (env: Networks, config?: Configuration) => {
|
||||
export const useNodes = (config?: Configuration) => {
|
||||
const [clients, setClients] = useState<ClientCollection>({});
|
||||
const [state, dispatch] = useReducer(reducer(env), getInitialState(config));
|
||||
const [state, dispatch] = useReducer(reducer, getInitialState(config));
|
||||
const configCacheKey = config?.hosts.join(';');
|
||||
const allUrls = Object.keys(state).map((node) => state[node].url);
|
||||
|
||||
|
8
libs/environment/src/setup-tests.ts
Normal file
8
libs/environment/src/setup-tests.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
global.ResizeObserver = ResizeObserver;
|
@ -37,7 +37,6 @@ type NodeCheck<T> = {
|
||||
|
||||
export type NodeData = {
|
||||
url: string;
|
||||
verified: boolean;
|
||||
initialized: boolean;
|
||||
ssl: NodeCheck<boolean>;
|
||||
block: NodeCheck<number>;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { CUSTOM_NODE_KEY, ErrorType } from '../types';
|
||||
import { ErrorType } from '../types';
|
||||
import type { Networks, NodeData } from '../types';
|
||||
|
||||
export const getIsNodeLoading = (node?: NodeData): boolean => {
|
||||
@ -40,7 +40,6 @@ export const getIsNodeDisabled = (env: Networks, data?: NodeData) => {
|
||||
|
||||
export const getIsFormDisabled = (
|
||||
currentNode: string | undefined,
|
||||
inputText: string,
|
||||
env: Networks,
|
||||
state: Record<string, NodeData>
|
||||
) => {
|
||||
@ -48,16 +47,8 @@ export const getIsFormDisabled = (
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
currentNode === CUSTOM_NODE_KEY &&
|
||||
state[CUSTOM_NODE_KEY] &&
|
||||
inputText !== state[CUSTOM_NODE_KEY].url
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const data = state[currentNode];
|
||||
return getIsNodeDisabled(env, data);
|
||||
return data ? getIsNodeDisabled(env, data) : true;
|
||||
};
|
||||
|
||||
export const getErrorByType = (
|
||||
|
@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
"types": ["jest", "node", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.test.ts",
|
||||
|
Loading…
Reference in New Issue
Block a user