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

Refactor ConfirmTransaction to Functional Component (#17473)

* ConfirmTransaction: rm unused metricEvent context

* ConfirmTransaction: rm unused mapToProps props:
- unapprovedTxs
- id

* ConfirmTransaction: convert to FC

* ConfirmTransaction: use const ORIGIN_METAMASK

* ConfirmTransaction: rm mapStateToProps

* ConfirmTransaction: mv confirm-transaction ducks

* ConfirmTransaction: mv getContractMethodData duck

* ConfirmTransaction: rm container file

* ConfirmTransaction: use ORIGIN_METAMASK const

* ConfirmationTransaction: add tests

* ConfirmTransaction: update jest mock after merge

* tests: update snapshot after mock-state updates
This commit is contained in:
Ariella Vu 2023-02-08 18:00:25 +07:00 committed by GitHub
parent 92367dff79
commit b5c1de900d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 736 additions and 222 deletions

View File

@ -140,12 +140,14 @@
"incomingTransactions": {}, "incomingTransactions": {},
"unapprovedTxs": { "unapprovedTxs": {
"8393540981007587": { "8393540981007587": {
"chainId": "0x5",
"id": 8393540981007587, "id": 8393540981007587,
"time": 1536268017676, "time": 1536268017676,
"status": "unapproved", "status": "unapproved",
"metamaskNetworkId": "4", "metamaskNetworkId": "4",
"loadingDefaults": false, "loadingDefaults": false,
"txParams": { "txParams": {
"data": "0xa9059cbb000000000000000000000000b19ac54efa18cc3a14a5b821bfec73d284bf0c5e0000000000000000000000000000000000000000000000003782dace9d900000",
"from": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", "from": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"to": "0xc42edfcc21ed14dda456aa0756c153f7985d8813", "to": "0xc42edfcc21ed14dda456aa0756c153f7985d8813",
"value": "0x0", "value": "0x0",
@ -185,7 +187,7 @@
} }
] ]
], ],
"origin": "MetaMask" "origin": "metamask"
} }
}, },
"selectedAddress": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc", "selectedAddress": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",

View File

@ -44,7 +44,7 @@ exports[`SignatureRequestOriginal should match snapshot 1`] = `
of of
0 1
</div> </div>
<div <div
class="confirm-page-container-navigation__longtext" class="confirm-page-container-navigation__longtext"
@ -54,7 +54,7 @@ exports[`SignatureRequestOriginal should match snapshot 1`] = `
</div> </div>
<div <div
class="confirm-page-container-navigation__container" class="confirm-page-container-navigation__container"
style="visibility: hidden;" style="visibility: initial;"
> >
<button <button
class="confirm-page-container-navigation__arrow" class="confirm-page-container-navigation__arrow"

View File

@ -41,7 +41,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
of of
0 1
</div> </div>
<div <div
class="confirm-page-container-navigation__longtext" class="confirm-page-container-navigation__longtext"
@ -51,7 +51,7 @@ exports[`Signature Request Component render should match snapshot 1`] = `
</div> </div>
<div <div
class="confirm-page-container-navigation__container" class="confirm-page-container-navigation__container"
style="visibility: hidden;" style="visibility: initial;"
> >
<button <button
class="confirm-page-container-navigation__arrow" class="confirm-page-container-navigation__arrow"

View File

@ -0,0 +1,279 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Confirmation Transaction Page should display the Loading component when the transaction is invalid 1`] = `
<div
class="loading-overlay"
>
<div
class="loading-overlay__container"
>
<div
class="spinner loading-overlay__spinner"
>
<svg
class="lds-spinner"
height="100%"
preserveAspectRatio="xMidYMid"
style="background: none;"
viewBox="0 0 100 100"
width="100%"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g
transform="rotate(0 50 50)"
>
<rect
fill="var(--color-warning-default)"
height="30"
rx="0"
ry="0"
width="10"
x="45"
y="0"
>
<animate
attributeName="opacity"
begin="-0.9166666666666666s"
dur="1s"
repeatCount="indefinite"
values="1;0"
/>
</rect>
</g>
<g
transform="rotate(30 50 50)"
>
<rect
fill="var(--color-warning-default)"
height="30"
rx="0"
ry="0"
width="10"
x="45"
y="0"
>
<animate
attributeName="opacity"
begin="-0.8333333333333334s"
dur="1s"
repeatCount="indefinite"
values="1;0"
/>
</rect>
</g>
<g
transform="rotate(60 50 50)"
>
<rect
fill="var(--color-warning-default)"
height="30"
rx="0"
ry="0"
width="10"
x="45"
y="0"
>
<animate
attributeName="opacity"
begin="-0.75s"
dur="1s"
repeatCount="indefinite"
values="1;0"
/>
</rect>
</g>
<g
transform="rotate(90 50 50)"
>
<rect
fill="var(--color-warning-default)"
height="30"
rx="0"
ry="0"
width="10"
x="45"
y="0"
>
<animate
attributeName="opacity"
begin="-0.6666666666666666s"
dur="1s"
repeatCount="indefinite"
values="1;0"
/>
</rect>
</g>
<g
transform="rotate(120 50 50)"
>
<rect
fill="var(--color-warning-default)"
height="30"
rx="0"
ry="0"
width="10"
x="45"
y="0"
>
<animate
attributeName="opacity"
begin="-0.5833333333333334s"
dur="1s"
repeatCount="indefinite"
values="1;0"
/>
</rect>
</g>
<g
transform="rotate(150 50 50)"
>
<rect
fill="var(--color-warning-default)"
height="30"
rx="0"
ry="0"
width="10"
x="45"
y="0"
>
<animate
attributeName="opacity"
begin="-0.5s"
dur="1s"
repeatCount="indefinite"
values="1;0"
/>
</rect>
</g>
<g
transform="rotate(180 50 50)"
>
<rect
fill="var(--color-warning-default)"
height="30"
rx="0"
ry="0"
width="10"
x="45"
y="0"
>
<animate
attributeName="opacity"
begin="-0.4166666666666667s"
dur="1s"
repeatCount="indefinite"
values="1;0"
/>
</rect>
</g>
<g
transform="rotate(210 50 50)"
>
<rect
fill="var(--color-warning-default)"
height="30"
rx="0"
ry="0"
width="10"
x="45"
y="0"
>
<animate
attributeName="opacity"
begin="-0.3333333333333333s"
dur="1s"
repeatCount="indefinite"
values="1;0"
/>
</rect>
</g>
<g
transform="rotate(240 50 50)"
>
<rect
fill="var(--color-warning-default)"
height="30"
rx="0"
ry="0"
width="10"
x="45"
y="0"
>
<animate
attributeName="opacity"
begin="-0.25s"
dur="1s"
repeatCount="indefinite"
values="1;0"
/>
</rect>
</g>
<g
transform="rotate(270 50 50)"
>
<rect
fill="var(--color-warning-default)"
height="30"
rx="0"
ry="0"
width="10"
x="45"
y="0"
>
<animate
attributeName="opacity"
begin="-0.16666666666666666s"
dur="1s"
repeatCount="indefinite"
values="1;0"
/>
</rect>
</g>
<g
transform="rotate(300 50 50)"
>
<rect
fill="var(--color-warning-default)"
height="30"
rx="0"
ry="0"
width="10"
x="45"
y="0"
>
<animate
attributeName="opacity"
begin="-0.08333333333333333s"
dur="1s"
repeatCount="indefinite"
values="1;0"
/>
</rect>
</g>
<g
transform="rotate(330 50 50)"
>
<rect
fill="var(--color-warning-default)"
height="30"
rx="0"
ry="0"
width="10"
x="45"
y="0"
>
<animate
attributeName="opacity"
begin="0s"
dur="1s"
repeatCount="indefinite"
values="1;0"
/>
</rect>
</g>
</svg>
</div>
</div>
</div>
`;

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux';
import { Switch, Route } from 'react-router-dom'; import { Switch, Route, useHistory, useParams } from 'react-router-dom';
import Loading from '../../components/ui/loading-screen'; import Loading from '../../components/ui/loading-screen';
import ConfirmTransactionSwitch from '../confirm-transaction-switch'; import ConfirmTransactionSwitch from '../confirm-transaction-switch';
import ConfirmContractInteraction from '../confirm-contract-interaction'; import ConfirmContractInteraction from '../confirm-contract-interaction';
@ -9,6 +9,14 @@ import ConfirmDeployContract from '../confirm-deploy-contract';
import ConfirmDecryptMessage from '../confirm-decrypt-message'; import ConfirmDecryptMessage from '../confirm-decrypt-message';
import ConfirmEncryptionPublicKey from '../confirm-encryption-public-key'; import ConfirmEncryptionPublicKey from '../confirm-encryption-public-key';
import { ORIGIN_METAMASK } from '../../../shared/constants/app';
import {
clearConfirmTransaction,
setTransactionToConfirm,
} from '../../ducks/confirm-transaction/confirm-transaction.duck';
import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { getSendTo } from '../../ducks/send';
import { import {
CONFIRM_TRANSACTION_ROUTE, CONFIRM_TRANSACTION_ROUTE,
CONFIRM_DEPLOY_CONTRACT_PATH, CONFIRM_DEPLOY_CONTRACT_PATH,
@ -19,188 +27,191 @@ import {
ENCRYPTION_PUBLIC_KEY_REQUEST_PATH, ENCRYPTION_PUBLIC_KEY_REQUEST_PATH,
DEFAULT_ROUTE, DEFAULT_ROUTE,
} from '../../helpers/constants/routes'; } from '../../helpers/constants/routes';
import { isTokenMethodAction } from '../../helpers/utils/transactions.util';
import { usePrevious } from '../../hooks/usePrevious';
import {
getUnapprovedTransactions,
unconfirmedTransactionsListSelector,
unconfirmedTransactionsHashSelector,
} from '../../selectors';
import { import {
disconnectGasFeeEstimatePoller, disconnectGasFeeEstimatePoller,
getContractMethodData,
getGasFeeEstimatesAndStartPolling, getGasFeeEstimatesAndStartPolling,
addPollingTokenToAppState, addPollingTokenToAppState,
removePollingTokenFromAppState, removePollingTokenFromAppState,
setDefaultHomeActiveTabName,
} from '../../store/actions'; } from '../../store/actions';
import ConfirmSignatureRequest from '../confirm-signature-request'; import ConfirmSignatureRequest from '../confirm-signature-request';
import ConfirmTokenTransactionSwitch from './confirm-token-transaction-switch'; import ConfirmTokenTransactionSwitch from './confirm-token-transaction-switch';
export default class ConfirmTransaction extends Component { const ConfirmTransaction = () => {
static contextTypes = { const dispatch = useDispatch();
metricsEvent: PropTypes.func, const history = useHistory();
}; const { id: paramsTransactionId } = useParams();
static propTypes = { const [isMounted, setIsMounted] = useState(false);
history: PropTypes.object.isRequired, const [pollingToken, setPollingToken] = useState();
totalUnapprovedCount: PropTypes.number.isRequired,
sendTo: PropTypes.string,
setTransactionToConfirm: PropTypes.func,
clearConfirmTransaction: PropTypes.func,
mostRecentOverviewPage: PropTypes.string.isRequired,
transaction: PropTypes.object,
getContractMethodData: PropTypes.func,
transactionId: PropTypes.string,
paramsTransactionId: PropTypes.string,
isTokenMethodAction: PropTypes.bool,
setDefaultHomeActiveTabName: PropTypes.func,
};
constructor(props) { const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage);
super(props); const sendTo = useSelector(getSendTo);
this.state = {}; const unapprovedTxs = useSelector(getUnapprovedTransactions);
} const unconfirmedTransactions = useSelector(
unconfirmedTransactionsListSelector,
);
const unconfirmedMessages = useSelector(unconfirmedTransactionsHashSelector);
_beforeUnload = () => { const totalUnapprovedCount = unconfirmedTransactions.length || 0;
this._isMounted = false; const transaction = useMemo(() => {
if (this.state.pollingToken) { return totalUnapprovedCount
disconnectGasFeeEstimatePoller(this.state.pollingToken); ? unapprovedTxs[paramsTransactionId] ||
removePollingTokenFromAppState(this.state.pollingToken); unconfirmedMessages[paramsTransactionId] ||
unconfirmedTransactions[0]
: {};
}, [
paramsTransactionId,
totalUnapprovedCount,
unapprovedTxs,
unconfirmedMessages,
unconfirmedTransactions,
]);
const { id, type } = transaction;
const transactionId = id && String(id);
const isValidERC20TokenMethod = isTokenMethodAction(type);
const prevParamsTransactionId = usePrevious(paramsTransactionId);
const prevTransactionId = usePrevious(transactionId);
const _beforeUnload = useCallback(() => {
setIsMounted(false);
if (pollingToken) {
disconnectGasFeeEstimatePoller(pollingToken);
removePollingTokenFromAppState(pollingToken);
} }
}; }, [pollingToken]);
componentDidMount() { useEffect(() => {
this._isMounted = true; setIsMounted(true);
const {
totalUnapprovedCount = 0,
sendTo,
history,
mostRecentOverviewPage,
transaction: { txParams: { data } = {}, origin } = {},
getContractMethodData,
transactionId,
paramsTransactionId,
} = this.props;
getGasFeeEstimatesAndStartPolling().then((pollingToken) => { const { txParams: { data } = {}, origin } = transaction;
if (this._isMounted) {
this.setState({ pollingToken }); getGasFeeEstimatesAndStartPolling().then((_pollingToken) => {
addPollingTokenToAppState(pollingToken); if (isMounted) {
setPollingToken(_pollingToken);
addPollingTokenToAppState(_pollingToken);
} else { } else {
disconnectGasFeeEstimatePoller(pollingToken); disconnectGasFeeEstimatePoller(_pollingToken);
removePollingTokenFromAppState(pollingToken); removePollingTokenFromAppState(_pollingToken);
} }
}); });
window.addEventListener('beforeunload', this._beforeUnload); window.addEventListener('beforeunload', _beforeUnload);
if (!totalUnapprovedCount && !sendTo) { if (!totalUnapprovedCount && !sendTo) {
history.replace(mostRecentOverviewPage); history.replace(mostRecentOverviewPage);
return; } else {
if (origin !== ORIGIN_METAMASK) {
dispatch(getContractMethodData(data));
}
const txId = transactionId || paramsTransactionId;
if (txId) {
dispatch(setTransactionToConfirm(txId));
}
} }
if (origin !== 'metamask') { return () => {
getContractMethodData(data); _beforeUnload();
} window.removeEventListener('beforeunload', _beforeUnload);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const txId = transactionId || paramsTransactionId; useEffect(() => {
if (txId) { const { txData: { txParams: { data } = {}, origin } = {} } = transaction;
this.props.setTransactionToConfirm(txId);
}
}
componentWillUnmount() {
this._beforeUnload();
window.removeEventListener('beforeunload', this._beforeUnload);
}
componentDidUpdate(prevProps) {
const {
setTransactionToConfirm,
transaction: { txData: { txParams: { data } = {}, origin } = {} },
clearConfirmTransaction,
getContractMethodData,
paramsTransactionId,
transactionId,
history,
mostRecentOverviewPage,
totalUnapprovedCount,
setDefaultHomeActiveTabName,
} = this.props;
if ( if (
paramsTransactionId && paramsTransactionId &&
transactionId && transactionId &&
prevProps.paramsTransactionId !== paramsTransactionId prevParamsTransactionId !== paramsTransactionId
) { ) {
clearConfirmTransaction(); dispatch(clearConfirmTransaction());
setTransactionToConfirm(paramsTransactionId); dispatch(setTransactionToConfirm(paramsTransactionId));
if (origin !== 'metamask') { if (origin !== ORIGIN_METAMASK) {
getContractMethodData(data); dispatch(getContractMethodData(data));
} }
} else if ( } else if (prevTransactionId && !transactionId && !totalUnapprovedCount) {
prevProps.transactionId && dispatch(setDefaultHomeActiveTabName('activity')).then(() => {
!transactionId &&
!totalUnapprovedCount
) {
setDefaultHomeActiveTabName('activity').then(() => {
history.replace(DEFAULT_ROUTE); history.replace(DEFAULT_ROUTE);
}); });
} else if ( } else if (
prevProps.transactionId && prevTransactionId &&
transactionId && transactionId &&
prevProps.transactionId !== transactionId prevTransactionId !== transactionId
) { ) {
history.replace(mostRecentOverviewPage); history.replace(mostRecentOverviewPage);
} }
}, [
dispatch,
transaction,
paramsTransactionId,
transactionId,
history,
mostRecentOverviewPage,
prevParamsTransactionId,
prevTransactionId,
totalUnapprovedCount,
]);
const validTransactionId =
transactionId &&
(!paramsTransactionId || paramsTransactionId === transactionId);
if (isValidERC20TokenMethod && validTransactionId) {
return <ConfirmTokenTransactionSwitch transaction={transaction} />;
} }
// Show routes when state.confirmTransaction has been set and when either the ID in the params
// isn't specified or is specified and matches the ID in state.confirmTransaction in order to
// support URLs of /confirm-transaction or /confirm-transaction/<transactionId>
return validTransactionId ? (
<Switch>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_DEPLOY_CONTRACT_PATH}`}
component={ConfirmDeployContract}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_ETHER_PATH}`}
component={ConfirmSendEther}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`}
component={ConfirmContractInteraction}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`}
component={ConfirmSignatureRequest}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${DECRYPT_MESSAGE_REQUEST_PATH}`}
component={ConfirmDecryptMessage}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}`}
component={ConfirmEncryptionPublicKey}
/>
<Route path="*" component={ConfirmTransactionSwitch} />
</Switch>
) : (
<Loading />
);
};
render() { export default ConfirmTransaction;
const {
transactionId,
paramsTransactionId,
isTokenMethodAction,
transaction,
} = this.props;
const validTransactionId =
transactionId &&
(!paramsTransactionId || paramsTransactionId === transactionId);
if (isTokenMethodAction && validTransactionId) {
return <ConfirmTokenTransactionSwitch transaction={transaction} />;
}
// Show routes when state.confirmTransaction has been set and when either the ID in the params
// isn't specified or is specified and matches the ID in state.confirmTransaction in order to
// support URLs of /confirm-transaction or /confirm-transaction/<transactionId>
return validTransactionId ? (
<Switch>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_DEPLOY_CONTRACT_PATH}`}
component={ConfirmDeployContract}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_ETHER_PATH}`}
component={ConfirmSendEther}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`}
component={ConfirmContractInteraction}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`}
component={ConfirmSignatureRequest}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${DECRYPT_MESSAGE_REQUEST_PATH}`}
component={ConfirmDecryptMessage}
/>
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}`}
component={ConfirmEncryptionPublicKey}
/>
<Route path="*" component={ConfirmTransactionSwitch} />
</Switch>
) : (
<Loading />
);
}
}

View File

@ -1,68 +0,0 @@
import { connect } from 'react-redux';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import {
setTransactionToConfirm,
clearConfirmTransaction,
} from '../../ducks/confirm-transaction/confirm-transaction.duck';
import { isTokenMethodAction } from '../../helpers/utils/transactions.util';
import {
getContractMethodData,
setDefaultHomeActiveTabName,
} from '../../store/actions';
import {
unconfirmedTransactionsListSelector,
unconfirmedTransactionsHashSelector,
} from '../../selectors';
import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { getSendTo } from '../../ducks/send';
import ConfirmTransaction from './confirm-transaction.component';
const mapStateToProps = (state, ownProps) => {
const {
metamask: { unapprovedTxs },
} = state;
const {
match: { params = {} },
} = ownProps;
const { id } = params;
const sendTo = getSendTo(state);
const unconfirmedTransactions = unconfirmedTransactionsListSelector(state);
const unconfirmedMessages = unconfirmedTransactionsHashSelector(state);
const totalUnconfirmed = unconfirmedTransactions.length;
const transaction = totalUnconfirmed
? unapprovedTxs[id] || unconfirmedMessages[id] || unconfirmedTransactions[0]
: {};
const { id: transactionId, type } = transaction;
return {
totalUnapprovedCount: totalUnconfirmed,
sendTo,
unapprovedTxs,
id,
mostRecentOverviewPage: getMostRecentOverviewPage(state),
paramsTransactionId: id && String(id),
transactionId: transactionId && String(transactionId),
transaction,
isTokenMethodAction: isTokenMethodAction(type),
};
};
const mapDispatchToProps = (dispatch) => {
return {
setTransactionToConfirm: (transactionId) => {
dispatch(setTransactionToConfirm(transactionId));
},
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
getContractMethodData: (data) => dispatch(getContractMethodData(data)),
setDefaultHomeActiveTabName: (tabName) =>
dispatch(setDefaultHomeActiveTabName(tabName)),
};
};
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps),
)(ConfirmTransaction);

View File

@ -0,0 +1,290 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import ReactRouterDOM from 'react-router-dom';
import * as ConfirmTransactionDucks from '../../ducks/confirm-transaction/confirm-transaction.duck';
import * as Actions from '../../store/actions';
import _mockState from '../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../test/lib/render-helpers';
import { setBackgroundConnection } from '../../../test/jest';
import {
CONFIRM_TRANSACTION_ROUTE,
CONFIRM_DEPLOY_CONTRACT_PATH,
CONFIRM_SEND_ETHER_PATH,
CONFIRM_TOKEN_METHOD_PATH,
SIGNATURE_REQUEST_PATH,
DECRYPT_MESSAGE_REQUEST_PATH,
ENCRYPTION_PUBLIC_KEY_REQUEST_PATH,
} from '../../helpers/constants/routes';
import ConfirmTransaction from '.';
const mockUnapprovedTx = Object.values(_mockState.metamask.unapprovedTxs)[0];
const middleware = [thunk];
const mockState = {
metamask: {
..._mockState.metamask,
},
appState: {
gasLoadingAnimationIsShowing: false,
},
history: {
mostRecentOverviewPage: '/',
},
send: {
draftTransactions: {},
},
};
setBackgroundConnection({
addPollingTokenToAppState: jest.fn(),
disconnectGasFeeEstimatePoller: jest.fn(),
getContractMethodData: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest.fn(),
removePollingTokenFromAppState: jest.fn(),
setDefaultHomeActiveTabName: jest.fn(),
});
jest.mock('../../ducks/confirm-transaction/confirm-transaction.duck', () => ({
setTransactionToConfirm: jest.fn().mockImplementation((txId) => {
return { type: 'mock-set-transaction-to-confirm', value: txId };
}),
}));
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
return {
...original,
useHistory: () => ({
replace: jest.fn(),
}),
};
});
jest.mock('../confirm-contract-interaction', () => {
return {
__esModule: true,
default: () => {
return <div className="mock-confirm-contract-interaction" />;
},
};
});
jest.mock('../confirm-decrypt-message', () => {
return {
__esModule: true,
default: () => {
return <div className="mock-confirm-decrypt-message" />;
},
};
});
jest.mock('../confirm-deploy-contract', () => {
return {
__esModule: true,
default: () => {
return <div className="mock-confirm-deploy-contract" />;
},
};
});
jest.mock('../confirm-encryption-public-key', () => {
return {
__esModule: true,
default: () => {
return <div className="mock-confirm-encryption-public-key" />;
},
};
});
jest.mock('../confirm-send-ether', () => {
return {
__esModule: true,
default: () => {
return <div className="mock-confirm-send-ether" />;
},
};
});
jest.mock('../confirm-signature-request', () => {
return {
__esModule: true,
default: () => {
return <div className="mock-confirm-signature-request" />;
},
};
});
jest.mock('./confirm-token-transaction-switch', () => {
return {
__esModule: true,
default: () => {
return <div className="mock-confirm-token-transaction-switch" />;
},
};
});
jest.mock('../confirm-transaction-switch', () => {
return {
__esModule: true,
default: () => {
return <div className="mock-confirm-transaction-switch" />;
},
};
});
describe('Confirmation Transaction Page', () => {
it('should display the Loading component when the transaction is invalid', () => {
const mockStore = configureMockStore(middleware)({
...mockState,
metamask: {
...mockState.metamask,
unapprovedTxs: {},
},
});
const { container } = renderWithProvider(<ConfirmTransaction />, mockStore);
expect(container.querySelector('.loading-overlay')).toBeInTheDocument();
expect(container.querySelector('.loading-overlay')).toMatchSnapshot();
});
it('should not display the Loading component when the transaction is valid', () => {
const mockStore = configureMockStore(middleware)({ ...mockState });
const { container } = renderWithProvider(<ConfirmTransaction />, mockStore);
expect(container.querySelector('.loading-overlay')).toBeNull();
});
[
[CONFIRM_DEPLOY_CONTRACT_PATH, '.mock-confirm-deploy-contract'],
[CONFIRM_SEND_ETHER_PATH, '.mock-confirm-send-ether'],
[CONFIRM_TOKEN_METHOD_PATH, '.mock-confirm-contract-interaction'],
[DECRYPT_MESSAGE_REQUEST_PATH, '.mock-confirm-decrypt-message'],
[ENCRYPTION_PUBLIC_KEY_REQUEST_PATH, '.mock-confirm-encryption-public-key'],
[SIGNATURE_REQUEST_PATH, '.mock-confirm-signature-request'],
].forEach(([componentPath, mockClassNameMatch]) => {
it(`should render "${componentPath}" route`, () => {
const mockStore = configureMockStore(middleware)(mockState);
const { container } = renderWithProvider(
<ConfirmTransaction />,
mockStore,
`${CONFIRM_TRANSACTION_ROUTE}/${mockUnapprovedTx.id}${componentPath}`,
);
expect(container.querySelector(mockClassNameMatch)).toBeInTheDocument();
});
});
it(`should render ConfirmTokenTransactionSwitch component if it's a valid ERC20 token method`, () => {
const mockStore = configureMockStore(middleware)({
...mockState,
metamask: {
...mockState.metamask,
unapprovedTxs: {
[mockUnapprovedTx.id]: {
...mockUnapprovedTx,
type: 'transfer',
},
},
},
});
const { container } = renderWithProvider(
<ConfirmTransaction />,
mockStore,
// use valid matched route path to check against ConfirmTokenTransactionSwitch
`${CONFIRM_TRANSACTION_ROUTE}/${mockUnapprovedTx.id}${CONFIRM_DEPLOY_CONTRACT_PATH}`,
);
expect(
container.querySelector('.mock-confirm-token-transaction-switch'),
).toBeInTheDocument();
});
it(`should render ConfirmTransactionSwitch component if the route path is unmatched and the transaction is valid`, () => {
const mockStore = configureMockStore(middleware)(mockState);
const { container } = renderWithProvider(
<ConfirmTransaction />,
mockStore,
`/unknown-path`,
);
expect(
container.querySelector('.mock-confirm-transaction-switch'),
).toBeInTheDocument();
});
describe('initialization', () => {
it('should poll for gas estimates', () => {
const mockStore = configureMockStore(middleware)(mockState);
const gasEstimationPollingSpy = jest.spyOn(
Actions,
'getGasFeeEstimatesAndStartPolling',
);
renderWithProvider(<ConfirmTransaction />, mockStore);
expect(gasEstimationPollingSpy).toHaveBeenCalled();
});
it('should call setTransactionToConfirm if transaction id is provided', () => {
const mockStore = configureMockStore(middleware)({ ...mockState });
ConfirmTransactionDucks.setTransactionToConfirm.mockClear();
renderWithProvider(<ConfirmTransaction />, mockStore);
expect(
ConfirmTransactionDucks.setTransactionToConfirm,
).toHaveBeenCalled();
});
it('should not call setTransactionToConfirm when transaction id is not provided', () => {
const mockStore = configureMockStore(middleware)({
...mockState,
metamask: { ...mockState.metamask, unapprovedTxs: {} },
});
jest.spyOn(ReactRouterDOM, 'useParams').mockImplementation(() => {
return { id: null };
});
ConfirmTransactionDucks.setTransactionToConfirm.mockClear();
renderWithProvider(<ConfirmTransaction />, mockStore);
expect(
ConfirmTransactionDucks.setTransactionToConfirm,
).not.toHaveBeenCalled();
});
describe('when unapproved transactions exist or a sendTo recipient exists', () => {
it('should not call history.replace(mostRecentOverviewPage)', () => {
const mockStore = configureMockStore(middleware)(mockState);
const replaceSpy = jest.fn();
jest.spyOn(ReactRouterDOM, 'useHistory').mockImplementation(() => {
return {
replace: replaceSpy,
};
});
renderWithProvider(<ConfirmTransaction />, mockStore);
expect(replaceSpy).not.toHaveBeenCalled();
});
});
describe('when no unapproved transactions and no sendTo recipient exist', () => {
it('should call history.replace(mostRecentOverviewPage)', () => {
const mockStore = configureMockStore(middleware)({
...mockState,
metamask: {
...mockState.metamask,
unapprovedTxs: {},
},
});
const replaceSpy = jest.fn();
jest.spyOn(ReactRouterDOM, 'useHistory').mockImplementation(() => {
return {
replace: replaceSpy,
};
});
renderWithProvider(<ConfirmTransaction />, mockStore, '/asdfb');
expect(replaceSpy).toHaveBeenCalled();
});
});
});
});

View File

@ -1,3 +1,3 @@
import ConfirmTransaction from './confirm-transaction.container'; import ConfirmTransaction from './confirm-transaction.component';
export default ConfirmTransaction; export default ConfirmTransaction;