1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 11:22:43 +02:00

Add currency symbol validation in the add network form (#12431)

* validate ticker symbol in add/edit network form
This commit is contained in:
Alex Donesky 2022-02-04 12:14:52 -06:00 committed by GitHub
parent 87daae708c
commit 48cc9d5ad3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 523 additions and 247 deletions

View File

@ -444,6 +444,10 @@
"chainIdExistsErrorMsg": {
"message": "This Chain ID is currently used by the $1 network."
},
"chainListReturnedDifferentTickerSymbol": {
"message": "The network with chain ID $1 may use a different currency symbol ($2) than the one you have entered. Please verify before continuing.",
"description": "$1 is the chain id currently entered in the network form and $2 is the return value of nativeCurrency.symbol from chainlist.network"
},
"chromeRequiredForHardwareWallets": {
"message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet."
},
@ -1017,7 +1021,7 @@
"message": "Learn more."
},
"endpointReturnedDifferentChainId": {
"message": "The endpoint returned a different chain ID: $1",
"message": "The RPC URL you have entered returned a different chain ID ($1). Please update the Chain ID to match the RPC URL of the network you are trying to add.",
"description": "$1 is the return value of eth_chainId from an RPC endpoint"
},
"ensIllegalCharacter": {
@ -1126,6 +1130,9 @@
"failedToFetchChainId": {
"message": "Could not fetch chain ID. Is your RPC URL correct?"
},
"failedToFetchTickerSymbolData": {
"message": "Ticker symbol verification data is currently unavailable, make sure that the symbol you have entered is correct. It will impact the conversion rates that you see for this network"
},
"failureMessage": {
"message": "Something went wrong, and we were unable to complete the action"
},

View File

@ -14,6 +14,7 @@ describe('Stores custom RPC history', function () {
it(`creates first custom RPC entry`, async function () {
const port = 8546;
const chainId = 1338;
const symbol = 'TEST';
await withFixtures(
{
fixtures: 'imported-account',
@ -38,6 +39,7 @@ describe('Stores custom RPC history', function () {
const networkNameInput = customRpcInputs[0];
const rpcUrlInput = customRpcInputs[1];
const chainIdInput = customRpcInputs[2];
const symbolInput = customRpcInputs[3];
await networkNameInput.clear();
await networkNameInput.sendKeys(networkName);
@ -48,6 +50,9 @@ describe('Stores custom RPC history', function () {
await chainIdInput.clear();
await chainIdInput.sendKeys(chainId.toString());
await symbolInput.clear();
await symbolInput.sendKeys(symbol);
await driver.clickElement(
'.networks-tab__add-network-form-footer .btn-primary',
);
@ -70,7 +75,8 @@ describe('Stores custom RPC history', function () {
await driver.press('#password', driver.Key.ENTER);
// duplicate network
const duplicateRpcUrl = 'http://localhost:8545';
const duplicateRpcUrl =
'https://mainnet.infura.io/v3/00000000000000000000000000000000';
await driver.clickElement('.network-display');
@ -84,7 +90,7 @@ describe('Stores custom RPC history', function () {
await rpcUrlInput.clear();
await rpcUrlInput.sendKeys(duplicateRpcUrl);
await driver.findElement({
text: 'This URL is currently used by the Localhost 8545 network.',
text: 'This URL is currently used by the mainnet network.',
tag: 'h6',
});
},
@ -97,6 +103,7 @@ describe('Stores custom RPC history', function () {
fixtures: 'imported-account',
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
},
async ({ driver }) => {
await driver.navigate();
@ -117,9 +124,6 @@ describe('Stores custom RPC history', function () {
const rpcUrlInput = customRpcInputs[1];
const chainIdInput = customRpcInputs[2];
await rpcUrlInput.clear();
await rpcUrlInput.sendKeys(newRpcUrl);
await chainIdInput.clear();
await chainIdInput.sendKeys(duplicateChainId);
await driver.findElement({
@ -127,6 +131,14 @@ describe('Stores custom RPC history', function () {
'This Chain ID is currently used by the Localhost 8545 network.',
tag: 'h6',
});
await rpcUrlInput.clear();
await rpcUrlInput.sendKeys(newRpcUrl);
await driver.findElement({
text: 'Could not fetch chain ID. Is your RPC URL correct?',
tag: 'h6',
});
},
);
});
@ -184,6 +196,7 @@ describe('Stores custom RPC history', function () {
fixtures: 'custom-rpc',
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
},
async ({ driver }) => {
await driver.navigate();

View File

@ -31,6 +31,7 @@ export default function FormField({
allowDecimals,
disabled,
placeholder,
warning,
}) {
return (
<div
@ -92,6 +93,7 @@ export default function FormField({
<input
className={classNames('form-field__input', {
'form-field__input--error': error,
'form-field__input--warning': warning,
})}
onChange={(e) => onChange(e.target.value)}
value={value}
@ -111,6 +113,15 @@ export default function FormField({
{error}
</Typography>
)}
{warning && (
<Typography
color={COLORS.UI4}
variant={TYPOGRAPHY.H7}
className="form-field__warning"
>
{warning}
</Typography>
)}
</label>
</div>
);
@ -141,6 +152,10 @@ FormField.propTypes = {
* Show error message
*/
error: PropTypes.string,
/**
* Show warning message
*/
warning: PropTypes.string,
/**
* Handler when fields change
*/

View File

@ -45,5 +45,9 @@
&--error {
border-color: var(--error-1);
}
&--warning {
border-color: var(--alert-3);
}
}
}

View File

@ -48,7 +48,7 @@
&__network-form {
display: flex;
flex: 1 0 auto;
flex: 1 0;
flex-direction: column;
justify-content: space-between;
max-height: 465px;

View File

@ -26,6 +26,8 @@ import {
DEFAULT_ROUTE,
NETWORKS_ROUTE,
} from '../../../../helpers/constants/routes';
import fetchWithCache from '../../../../helpers/utils/fetch-with-cache';
import { usePrevious } from '../../../../hooks/usePrevious';
/**
* Attempts to convert the given chainId to a decimal string, for display
@ -83,6 +85,7 @@ const NetworksForm = ({
selectedNetwork?.blockExplorerUrl || '',
);
const [errors, setErrors] = useState({});
const [warnings, setWarnings] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = useCallback(() => {
@ -92,6 +95,7 @@ const NetworksForm = ({
setTicker(selectedNetwork?.ticker);
setBlockExplorerUrl(selectedNetwork?.blockExplorerUrl);
setErrors({});
setWarnings({});
setIsSubmitting(false);
}, [selectedNetwork, selectedNetworkName]);
@ -170,216 +174,297 @@ const NetworksForm = ({
dispatch,
]);
const setErrorTo = (errorKey, errorVal) => {
setErrors({ ...errors, [errorKey]: errorVal });
};
const setErrorEmpty = (errorKey) => {
setErrors({
...errors,
[errorKey]: {
msg: '',
key: '',
},
});
};
const hasError = (errorKey, errorKeyVal) => {
return errors[errorKey]?.key === errorKeyVal;
};
const hasErrors = () => {
return Object.keys(errors).some((key) => {
const error = errors[key];
// Do not factor in duplicate chain id error for submission disabling
if (key === 'chainId' && error.key === 'chainIdExistsErrorMsg') {
if (key === 'chainId' && error?.key === 'chainIdExistsErrorMsg') {
return false;
}
return error.key && error.msg;
return error?.key && error?.msg;
});
};
const validateChainIdOnChange = (chainArg = '') => {
const formChainId = chainArg.trim();
let errorKey = '';
let errorMessage = '';
let radix = 10;
let hexChainId = formChainId;
const validateBlockExplorerURL = useCallback(
(url) => {
if (!validUrl.isWebUri(url) && url !== '') {
let errorKey;
let errorMessage;
if (!hexChainId.startsWith('0x')) {
try {
hexChainId = `0x${decimalToHex(hexChainId)}`;
} catch (err) {
setErrorTo('chainId', {
key: 'invalidHexNumber',
msg: t('invalidHexNumber'),
});
return;
if (isValidWhenAppended(url)) {
errorKey = 'urlErrorMsg';
errorMessage = t('urlErrorMsg');
} else {
errorKey = 'invalidBlockExplorerURL';
errorMessage = t('invalidBlockExplorerURL');
}
return {
key: errorKey,
msg: errorMessage,
};
}
}
return null;
},
[t],
);
const [matchingChainId] = networksToRender.filter(
(e) => e.chainId === hexChainId && e.rpcUrl !== rpcUrl,
);
const validateChainId = useCallback(
async (chainArg = '') => {
const formChainId = chainArg.trim();
let errorKey = '';
let errorMessage = '';
let radix = 10;
let hexChainId = formChainId;
if (formChainId === '') {
setErrorEmpty('chainId');
return;
} else if (matchingChainId) {
errorKey = 'chainIdExistsErrorMsg';
errorMessage = t('chainIdExistsErrorMsg', [
matchingChainId.label ?? matchingChainId.labelKey,
]);
} else if (formChainId.startsWith('0x')) {
radix = 16;
if (!/^0x[0-9a-f]+$/iu.test(formChainId)) {
errorKey = 'invalidHexNumber';
errorMessage = t('invalidHexNumber');
} else if (!isPrefixedFormattedHexString(formChainId)) {
errorMessage = t('invalidHexNumberLeadingZeros');
}
} else if (!/^[0-9]+$/u.test(formChainId)) {
errorKey = 'invalidNumber';
errorMessage = t('invalidNumber');
} else if (formChainId.startsWith('0')) {
errorKey = 'invalidNumberLeadingZeros';
errorMessage = t('invalidNumberLeadingZeros');
} else if (!isSafeChainId(parseInt(formChainId, radix))) {
errorKey = 'invalidChainIdTooBig';
errorMessage = t('invalidChainIdTooBig');
}
setErrorTo('chainId', {
key: errorKey,
msg: errorMessage,
});
};
/**
* Validates the chain ID by checking it against the `eth_chainId` return
* value from the given RPC URL.
* Assumes that all strings are non-empty and correctly formatted.
*
* @param {string} formChainId - Non-empty, hex or decimal number string from
* the form.
* @param {string} parsedChainId - The parsed, hex string chain ID.
* @param {string} formRpcUrl - The RPC URL from the form.
*/
const validateChainIdOnSubmit = async (
formChainId,
parsedChainId,
formRpcUrl,
) => {
let errorKey;
let errorMessage;
let endpointChainId;
let providerError;
try {
endpointChainId = await jsonRpcRequest(formRpcUrl, 'eth_chainId');
} catch (err) {
log.warn('Failed to fetch the chainId from the endpoint.', err);
providerError = err;
}
if (providerError || typeof endpointChainId !== 'string') {
errorKey = 'failedToFetchChainId';
errorMessage = t('failedToFetchChainId');
} else if (parsedChainId !== endpointChainId) {
// Here, we are in an error state. The endpoint should always return a
// hexadecimal string. If the user entered a decimal string, we attempt
// to convert the endpoint's return value to decimal before rendering it
// in an error message in the form.
if (!formChainId.startsWith('0x')) {
if (!hexChainId.startsWith('0x')) {
try {
endpointChainId = parseInt(endpointChainId, 16).toString(10);
hexChainId = `0x${decimalToHex(hexChainId)}`;
} catch (err) {
log.warn(
'Failed to convert endpoint chain ID to decimal',
endpointChainId,
);
return {
key: 'invalidHexNumber',
msg: t('invalidHexNumber'),
};
}
}
errorKey = 'endpointReturnedDifferentChainId';
errorMessage = t('endpointReturnedDifferentChainId', [
endpointChainId.length <= 12
? endpointChainId
: `${endpointChainId.slice(0, 9)}...`,
]);
}
const [matchingChainId] = networksToRender.filter(
(e) => e.chainId === hexChainId && e.rpcUrl !== rpcUrl,
);
if (errorKey) {
setErrorTo('chainId', {
key: errorKey,
msg: errorMessage,
});
return false;
}
setErrorEmpty('chainId');
return true;
};
const validateBlockExplorerURL = (url) => {
if (!validUrl.isWebUri(url) && url !== '') {
let errorKey;
let errorMessage;
if (isValidWhenAppended(url)) {
errorKey = 'urlErrorMsg';
errorMessage = t('urlErrorMsg');
} else {
errorKey = 'invalidBlockExplorerURL';
errorMessage = t('invalidBlockExplorerURL');
if (formChainId === '') {
return null;
} else if (matchingChainId) {
errorKey = 'chainIdExistsErrorMsg';
errorMessage = t('chainIdExistsErrorMsg', [
matchingChainId.label ?? matchingChainId.labelKey,
]);
} else if (formChainId.startsWith('0x')) {
radix = 16;
if (!/^0x[0-9a-f]+$/iu.test(formChainId)) {
errorKey = 'invalidHexNumber';
errorMessage = t('invalidHexNumber');
} else if (!isPrefixedFormattedHexString(formChainId)) {
errorMessage = t('invalidHexNumberLeadingZeros');
}
} else if (!/^[0-9]+$/u.test(formChainId)) {
errorKey = 'invalidNumber';
errorMessage = t('invalidNumber');
} else if (formChainId.startsWith('0')) {
errorKey = 'invalidNumberLeadingZeros';
errorMessage = t('invalidNumberLeadingZeros');
} else if (!isSafeChainId(parseInt(formChainId, radix))) {
errorKey = 'invalidChainIdTooBig';
errorMessage = t('invalidChainIdTooBig');
}
setErrorTo('blockExplorerUrl', {
key: errorKey,
msg: errorMessage,
});
} else {
setErrorEmpty('blockExplorerUrl');
}
};
let endpointChainId;
let providerError;
const validateUrlRpcUrl = (url) => {
const isValidUrl = validUrl.isWebUri(url);
const chainIdFetchFailed = hasError('chainId', 'failedToFetchChainId');
const [matchingRPCUrl] = networksToRender.filter((e) => e.rpcUrl === url);
if (!isValidUrl && url !== '') {
let errorKey;
let errorMessage;
if (isValidWhenAppended(url)) {
errorKey = 'urlErrorMsg';
errorMessage = t('urlErrorMsg');
} else {
errorKey = 'invalidRPC';
errorMessage = t('invalidRPC');
try {
endpointChainId = await jsonRpcRequest(rpcUrl, 'eth_chainId');
} catch (err) {
log.warn('Failed to fetch the chainId from the endpoint.', err);
providerError = err;
}
setErrorTo('rpcUrl', {
key: errorKey,
msg: errorMessage,
});
} else if (matchingRPCUrl) {
setErrorTo('rpcUrl', {
key: 'urlExistsErrorMsg',
msg: t('urlExistsErrorMsg', [
matchingRPCUrl.label ?? matchingRPCUrl.labelKey,
]),
});
} else {
setErrorEmpty('rpcUrl');
if (rpcUrl && formChainId) {
if (providerError || typeof endpointChainId !== 'string') {
errorKey = 'failedToFetchChainId';
errorMessage = t('failedToFetchChainId');
} else if (hexChainId !== endpointChainId) {
// Here, we are in an error state. The endpoint should always return a
// hexadecimal string. If the user entered a decimal string, we attempt
// to convert the endpoint's return value to decimal before rendering it
// in an error message in the form.
if (!formChainId.startsWith('0x')) {
try {
endpointChainId = parseInt(endpointChainId, 16).toString(10);
} catch (err) {
log.warn(
'Failed to convert endpoint chain ID to decimal',
endpointChainId,
);
}
}
errorKey = 'endpointReturnedDifferentChainId';
errorMessage = t('endpointReturnedDifferentChainId', [
endpointChainId.length <= 12
? endpointChainId
: `${endpointChainId.slice(0, 9)}...`,
]);
}
}
if (errorKey) {
return {
key: errorKey,
msg: errorMessage,
};
}
return null;
},
[rpcUrl, networksToRender, t],
);
/**
* Validates the ticker symbol by checking it against the nativeCurrency.symbol return
* value from chainid.network trusted chain data
* Assumes that all strings are non-empty and correctly formatted.
*
* @param {string} formChainId - The Chain ID currently entered in the form.
* @param {string} formTickerSymbol - The ticker/currency symbol currently entered in the form.
*/
const validateTickerSymbol = useCallback(
async (formChainId, formTickerSymbol) => {
let warningKey;
let warningMessage;
let safeChainsList;
let providerError;
if (!formChainId || !formTickerSymbol) {
return null;
}
try {
safeChainsList = await fetchWithCache(
'https://chainid.network/chains.json',
);
} catch (err) {
log.warn('Failed to fetch the chainList from chainid.network', err);
providerError = err;
}
if (providerError || !Array.isArray(safeChainsList)) {
warningKey = 'failedToFetchTickerSymbolData';
warningMessage = t('failedToFetchTickerSymbolData');
} else {
const matchedChain = safeChainsList?.find(
(chain) => chain.chainId.toString() === formChainId,
);
if (matchedChain === undefined) {
warningKey = 'failedToFetchTickerSymbolData';
warningMessage = t('failedToFetchTickerSymbolData');
} else {
const returnedTickerSymbol = matchedChain.nativeCurrency?.symbol;
if (returnedTickerSymbol !== formTickerSymbol) {
warningKey = 'chainListReturnedDifferentTickerSymbol';
warningMessage = t('chainListReturnedDifferentTickerSymbol', [
formChainId,
returnedTickerSymbol,
]);
}
}
}
if (warningKey) {
return {
key: warningKey,
msg: warningMessage,
};
}
return null;
},
[t],
);
const validateRPCUrl = useCallback(
(url) => {
const isValidUrl = validUrl.isWebUri(url);
const [
{
rpcUrl: matchingRPCUrl = null,
label: matchingRPCLabel,
labelKey: matchingRPCLabelKey,
} = {},
] = networksToRender.filter((e) => e.rpcUrl === url);
const { rpcUrl: selectedNetworkRpcUrl } = selectedNetwork;
if (!isValidUrl && url !== '') {
let errorKey;
let errorMessage;
if (isValidWhenAppended(url)) {
errorKey = 'urlErrorMsg';
errorMessage = t('urlErrorMsg');
} else {
errorKey = 'invalidRPC';
errorMessage = t('invalidRPC');
}
return {
key: errorKey,
msg: errorMessage,
};
} else if (matchingRPCUrl && matchingRPCUrl !== selectedNetworkRpcUrl) {
return {
key: 'urlExistsErrorMsg',
msg: t('urlExistsErrorMsg', [
matchingRPCLabel ?? matchingRPCLabelKey,
]),
};
}
return null;
},
[selectedNetwork, networksToRender, t],
);
// validation effect
const previousRpcUrl = usePrevious(rpcUrl);
const previousChainId = usePrevious(chainId);
const previousTicker = usePrevious(ticker);
const previousBlockExplorerUrl = usePrevious(blockExplorerUrl);
useEffect(() => {
if (viewOnly) {
return;
}
// Re-validate the chain id if it could not be found with previous rpc url
if (chainId && isValidUrl && chainIdFetchFailed) {
const formChainId = chainId.trim().toLowerCase();
const prefixedChainId = prefixChainId(formChainId);
validateChainIdOnSubmit(formChainId, prefixedChainId, url);
if (
previousRpcUrl === rpcUrl &&
previousChainId === chainId &&
previousTicker === ticker &&
previousBlockExplorerUrl === blockExplorerUrl
) {
return;
}
};
async function validate() {
const chainIdError = await validateChainId(chainId);
const tickerWarning = await validateTickerSymbol(chainId, ticker);
const blockExplorerError = validateBlockExplorerURL(blockExplorerUrl);
const rpcUrlError = validateRPCUrl(rpcUrl);
setErrors({
...errors,
chainId: chainIdError,
blockExplorerUrl: blockExplorerError,
rpcUrl: rpcUrlError,
});
setWarnings({
...warnings,
ticker: tickerWarning,
});
}
validate();
}, [
errors,
warnings,
rpcUrl,
chainId,
ticker,
blockExplorerUrl,
viewOnly,
label,
previousRpcUrl,
previousChainId,
previousTicker,
previousBlockExplorerUrl,
validateBlockExplorerURL,
validateChainId,
validateTickerSymbol,
validateRPCUrl,
]);
const onSubmit = async () => {
setIsSubmitting(true);
@ -387,13 +472,6 @@ const NetworksForm = ({
const formChainId = chainId.trim().toLowerCase();
const prefixedChainId = prefixChainId(formChainId);
if (
!(await validateChainIdOnSubmit(formChainId, prefixedChainId, rpcUrl))
) {
setIsSubmitting(false);
return;
}
// After this point, isSubmitting will be reset in componentDidUpdate
if (selectedNetwork.rpcUrl && rpcUrl !== selectedNetwork.rpcUrl) {
await dispatch(
@ -453,7 +531,12 @@ const NetworksForm = ({
const deletable = !isCurrentRpcTarget && !viewOnly && !addNewNetwork;
const stateUnchanged = stateIsUnchanged();
const isSubmitDisabled =
hasErrors() || isSubmitting || stateUnchanged || !rpcUrl || !chainId;
hasErrors() ||
isSubmitting ||
stateUnchanged ||
!rpcUrl ||
!chainId ||
!ticker;
return (
<div
@ -488,39 +571,29 @@ const NetworksForm = ({
/>
<FormField
error={errors.rpcUrl?.msg || ''}
onChange={(value) => {
setRpcUrl(value);
validateUrlRpcUrl(value);
}}
onChange={setRpcUrl}
titleText={t('rpcUrl')}
value={rpcUrl}
disabled={viewOnly}
/>
<FormField
error={errors.chainId?.msg || ''}
onChange={(value) => {
setChainId(value);
validateChainIdOnChange(value);
}}
onChange={setChainId}
titleText={t('chainId')}
value={chainId}
disabled={viewOnly}
tooltipText={viewOnly ? null : t('networkSettingsChainIdDescription')}
/>
<FormField
error={errors.ticker?.msg || ''}
warning={warnings.ticker?.msg || ''}
onChange={setTicker}
titleText={t('currencySymbol')}
titleUnit={t('optionalWithParanthesis')}
value={ticker}
disabled={viewOnly}
/>
<FormField
error={errors.blockExplorerUrl?.msg || ''}
onChange={(value) => {
setBlockExplorerUrl(value);
validateBlockExplorerURL(value);
}}
onChange={setBlockExplorerUrl}
titleText={t('blockExplorerUrl')}
titleUnit={t('optionalWithParanthesis')}
value={blockExplorerUrl}

View File

@ -1,8 +1,10 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { fireEvent } from '@testing-library/react';
import { fireEvent, screen } from '@testing-library/react';
import nock from 'nock';
import { renderWithProvider } from '../../../../../test/jest/rendering';
import { defaultNetworksData } from '../networks-tab.constants';
import { MAINNET_RPC_URL } from '../../../../../shared/constants/network';
import NetworksForm from '.';
const renderComponent = (props) => {
@ -36,7 +38,51 @@ const propNetworkDisplay = {
};
describe('NetworkForm Component', () => {
it('should render Add new network form correctly', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.enableNetConnect();
});
beforeEach(() => {
nock('https://chainid.network:443', { encodedQueryParams: true })
.get('/chains.json')
.reply(200, [
{
name: 'Polygon Mainnet',
chain: 'Polygon',
rpc: [
'https://polygon-rpc.com/',
'https://rpc-mainnet.matic.network',
'https://matic-mainnet.chainstacklabs.com',
'https://rpc-mainnet.maticvigil.com',
'https://rpc-mainnet.matic.quiknode.pro',
'https://matic-mainnet-full-rpc.bwarelabs.com',
],
nativeCurrency: {
name: 'MATIC',
symbol: 'MATIC',
decimals: 18,
},
shortName: 'MATIC',
chainId: 137,
},
]);
nock('https://bsc-dataseed.binance.org:443', {
encodedQueryParams: true,
})
.post('/')
.reply(200, { jsonrpc: '2.0', id: '1643927040523', result: '0x38' });
});
afterEach(() => {
nock.cleanAll();
});
it('should render add new network form correctly', async () => {
const { queryByText, queryAllByText } = renderComponent(propNewNetwork);
expect(
queryByText(
@ -48,9 +94,30 @@ describe('NetworkForm Component', () => {
expect(queryByText('Chain ID')).toBeInTheDocument();
expect(queryByText('Currency Symbol')).toBeInTheDocument();
expect(queryByText('Block Explorer URL')).toBeInTheDocument();
expect(queryAllByText('(Optional)')).toHaveLength(2);
expect(queryAllByText('(Optional)')).toHaveLength(1);
expect(queryByText('Cancel')).toBeInTheDocument();
expect(queryByText('Save')).toBeInTheDocument();
await fireEvent.change(screen.getByRole('textbox', { name: 'Chain ID' }), {
target: { value: '1' },
});
expect(
await screen.findByText(
'This Chain ID is currently used by the mainnet network.',
),
).toBeInTheDocument();
await fireEvent.change(
screen.getByRole('textbox', { name: 'New RPC URL' }),
{
target: { value: 'test' },
},
);
expect(
await screen.findByText(
'URLs require the appropriate HTTP/HTTPS prefix.',
),
).toBeInTheDocument();
});
it('should render network form correctly', () => {
@ -81,31 +148,121 @@ describe('NetworkForm Component', () => {
expect(
getByDisplayValue(propNetworkDisplay.selectedNetwork.blockExplorerUrl),
).toBeInTheDocument();
fireEvent.change(
getByDisplayValue(propNetworkDisplay.selectedNetwork.label),
{
target: { value: 'LocalHost 8545' },
},
);
expect(getByDisplayValue('LocalHost 8545')).toBeInTheDocument();
fireEvent.change(
getByDisplayValue(propNetworkDisplay.selectedNetwork.chainId),
{
target: { value: '1' },
},
);
});
it('should validate RPC URL field correctly', async () => {
renderComponent(propNewNetwork);
const rpcUrlField = screen.getByRole('textbox', { name: 'New RPC URL' });
await fireEvent.change(rpcUrlField, {
target: { value: 'test' },
});
expect(
queryByText('This Chain ID is currently used by the mainnet network.'),
await screen.findByText(
'URLs require the appropriate HTTP/HTTPS prefix.',
),
).toBeInTheDocument();
fireEvent.change(
getByDisplayValue(propNetworkDisplay.selectedNetwork.rpcUrl),
{
target: { value: 'test' },
},
);
await fireEvent.change(rpcUrlField, {
target: { value: ' ' },
});
expect(await screen.findByText('Invalid RPC URL')).toBeInTheDocument();
await fireEvent.change(rpcUrlField, {
target: { value: MAINNET_RPC_URL },
});
expect(
queryByText('URLs require the appropriate HTTP/HTTPS prefix.'),
await screen.findByText(
'This URL is currently used by the mainnet network.',
),
).toBeInTheDocument();
});
it('should validate chain id field correctly', async () => {
renderComponent(propNewNetwork);
const chainIdField = screen.getByRole('textbox', { name: 'Chain ID' });
const rpcUrlField = screen.getByRole('textbox', { name: 'New RPC URL' });
fireEvent.change(chainIdField, {
target: { value: '1' },
});
expect(
await screen.findByText(
'This Chain ID is currently used by the mainnet network.',
),
).toBeInTheDocument();
fireEvent.change(rpcUrlField, {
target: { value: 'https://bsc-dataseed.binance.org/' },
});
const expectedWarning =
'The RPC URL you have entered returned a different chain ID (56). Please update the Chain ID to match the RPC URL of the network you are trying to add.';
expect(await screen.findByText(expectedWarning)).toBeInTheDocument();
fireEvent.change(chainIdField, {
target: { value: 'a' },
});
expect(
await screen.findByText('Invalid hexadecimal number.'),
).toBeInTheDocument();
// reset RCP URL field
fireEvent.change(rpcUrlField, {
target: { value: '' },
});
fireEvent.change(chainIdField, {
target: { value: '00000012314' },
});
expect(
await screen.findByText('Invalid number. Remove any leading zeros.'),
).toBeInTheDocument();
});
it('should validate currency symbol field correctly', async () => {
renderComponent(propNewNetwork);
const chainIdField = screen.getByRole('textbox', { name: 'Chain ID' });
const currencySymbolField = screen.getByRole('textbox', {
name: 'Currency Symbol',
});
fireEvent.change(chainIdField, {
target: { value: '1234' },
});
fireEvent.change(currencySymbolField, {
target: { value: 'abcd' },
});
const expectedWarning =
'Ticker symbol verification data is currently unavailable, make sure that the symbol you have entered is correct. It will impact the conversion rates that you see for this network';
expect(await screen.findByText(expectedWarning)).toBeInTheDocument();
fireEvent.change(chainIdField, {
target: { value: '137' },
});
const secondExpectedWarning =
'The network with chain ID 137 may use a different currency symbol (MATIC) than the one you have entered. Please verify before continuing.';
expect(await screen.findByText(secondExpectedWarning)).toBeInTheDocument();
});
it('should validate block explorer url field correctly', async () => {
renderComponent(propNewNetwork);
const blockExplorerUrlField = screen.getByRole('textbox', {
name: 'Block Explorer URL (Optional)',
});
fireEvent.change(blockExplorerUrlField, {
target: { value: '1234' },
});
expect(
await screen.findByText(
'URLs require the appropriate HTTP/HTTPS prefix.',
),
).toBeInTheDocument();
});
});

View File

@ -1,6 +1,6 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { fireEvent } from '@testing-library/react';
import { fireEvent, screen } from '@testing-library/react';
import { renderWithProvider } from '../../../../../test/jest/rendering';
import { defaultNetworksData } from '../networks-tab.constants';
import NetworksTabContent from '.';
@ -45,7 +45,7 @@ const props = {
};
describe('NetworksTabContent Component', () => {
it('should render networks tab content correctly', () => {
it('should render networks tab content correctly', async () => {
const { queryByText, getByDisplayValue } = renderComponent(props);
expect(queryByText('Ethereum Mainnet')).toBeInTheDocument();
@ -71,22 +71,29 @@ describe('NetworksTabContent Component', () => {
expect(
getByDisplayValue(props.selectedNetwork.blockExplorerUrl),
).toBeInTheDocument();
fireEvent.change(getByDisplayValue(props.selectedNetwork.label), {
target: { value: 'LocalHost 8545' },
});
expect(getByDisplayValue('LocalHost 8545')).toBeInTheDocument();
fireEvent.change(getByDisplayValue(props.selectedNetwork.chainId), {
target: { value: '1' },
});
expect(
queryByText('This Chain ID is currently used by the mainnet network.'),
).toBeInTheDocument();
expect(await getByDisplayValue('LocalHost 8545')).toBeInTheDocument();
fireEvent.change(getByDisplayValue(props.selectedNetwork.rpcUrl), {
target: { value: 'test' },
});
expect(
queryByText('URLs require the appropriate HTTP/HTTPS prefix.'),
await screen.findByText(
'URLs require the appropriate HTTP/HTTPS prefix.',
),
).toBeInTheDocument();
fireEvent.change(getByDisplayValue(props.selectedNetwork.chainId), {
target: { value: '1' },
});
expect(
await screen.findByText(
'Could not fetch chain ID. Is your RPC URL correct?',
),
).toBeInTheDocument();
});
});