diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index f1975fbdc..a6cd0a70d 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -189,19 +189,10 @@ async function setupMocking(server, testSpecificMock) { }; }); + // It disables loading of token icons, e.g. this URL: https://static.metaswap.codefi.network/api/v1/tokenIcons/1337/0x0000000000000000000000000000000000000000.png await server .forGet( - 'https://static.metaswap.codefi.network/api/v1/tokenIcons/1337/0x0d8775f648430679a709e98d2b0cb6250d2887ef.png', - ) - .thenCallback(() => { - return { - statusCode: 200, - }; - }); - - await server - .forGet( - 'https://static.metaswap.codefi.network/api/v1/tokenIcons/1337/0x2efa2cb29c2341d8e5ba7d3262c9e9d6f1bf3711.png', + /^https:\/\/static\.metaswap\.codefi\.network\/api\/v1\/tokenIcons\/1337\/.*\.png/u, ) .thenCallback(() => { return { diff --git a/test/e2e/run-all.js b/test/e2e/run-all.js index ad538f3f1..2623f9ba9 100644 --- a/test/e2e/run-all.js +++ b/test/e2e/run-all.js @@ -51,8 +51,7 @@ async function main() { if (!snaps) { testPaths = [ ...testPaths, - // TODO: Enable the next line once the Swaps E2E tests are stable. - // ...(await getTestPathsForTestDir(path.join(__dirname, 'swaps'))), + ...(await getTestPathsForTestDir(path.join(__dirname, 'swaps'))), path.join(__dirname, 'metamask-ui.spec.js'), ]; } diff --git a/test/e2e/swaps/shared.js b/test/e2e/swaps/shared.js index 8eaea0766..1a2dc40a3 100644 --- a/test/e2e/swaps/shared.js +++ b/test/e2e/swaps/shared.js @@ -1,3 +1,5 @@ +const FixtureBuilder = require('../fixture-builder'); + const ganacheOptions = { accounts: [ { @@ -8,6 +10,11 @@ const ganacheOptions = { ], }; +const withFixturesOptions = { + fixtures: new FixtureBuilder().build(), + ganacheOptions, +}; + const loadSwaps = async (driver) => { await driver.navigate(); await driver.fill('#password', 'correct horse battery staple'); @@ -28,19 +35,37 @@ const buildQuote = async (driver, options) => { await driver.wait(async () => { const tokens = await driver.findElements('.searchable-item-list__item'); return tokens.length > 1; - }, 10000); + }); await driver.clickElement('.searchable-item-list__labels'); await driver.clickElement('.dropdown-input-pair__to'); - await driver.clickElement('[placeholder="Search name or paste address"]'); + await driver.clickElement('input[data-testid="search-list-items"]'); await driver.fill( - '[placeholder="Search name or paste address"]', - options.swapTo, + 'input[data-testid="search-list-items"]', + options.swapTo || options.swapToContractAddress, ); + if (options.swapTo) { + await driver.wait(async () => { + const tokenNames = await driver.findElements( + '.searchable-item-list__primary-label', + ); + if (tokenNames.length === 0) { + return false; + } + const tokenName = await tokenNames[0].getText(); + return tokenName === options.swapTo; + }); + } + if (options.swapToContractAddress) { + await driver.waitForSelector({ + css: '.searchable-item-list__item button.btn-primary', + text: 'Import', + }); + } await driver.clickElement('.searchable-item-list__primary-label'); }; module.exports = { - ganacheOptions, + withFixturesOptions, loadSwaps, buildQuote, }; diff --git a/test/e2e/swaps/swap-eth.spec.js b/test/e2e/swaps/swap-eth.spec.js index fb4d98b1d..d67c258d8 100644 --- a/test/e2e/swaps/swap-eth.spec.js +++ b/test/e2e/swaps/swap-eth.spec.js @@ -1,16 +1,13 @@ const { strict: assert } = require('assert'); const { withFixtures } = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); -const { ganacheOptions, loadSwaps, buildQuote } = require('./shared'); +const { withFixturesOptions, loadSwaps, buildQuote } = require('./shared'); describe('Swap Eth for another Token', function () { it('Completes a Swap between Eth and Matic', async function () { await withFixtures( { - fixtures: new FixtureBuilder().build(), - ganacheOptions, - failOnConsoleError: false, + ...withFixturesOptions, title: this.test.title, }, async ({ driver }) => { diff --git a/test/e2e/swaps/swaps-notifications.spec.js b/test/e2e/swaps/swaps-notifications.spec.js index d93e08a88..07c845f98 100644 --- a/test/e2e/swaps/swaps-notifications.spec.js +++ b/test/e2e/swaps/swaps-notifications.spec.js @@ -1,16 +1,14 @@ const { strict: assert } = require('assert'); const { withFixtures } = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); -const { ganacheOptions, loadSwaps, buildQuote } = require('./shared'); + +const { withFixturesOptions, loadSwaps, buildQuote } = require('./shared'); describe('Swaps - notifications', function () { it('tests notifications for verified token on 1 source and price difference', async function () { await withFixtures( { - fixtures: new FixtureBuilder().build(), - ganacheOptions, - failOnConsoleError: false, + ...withFixturesOptions, title: this.test.title, }, async ({ driver }) => { @@ -46,19 +44,52 @@ describe('Swaps - notifications', function () { ); }); + it('tests a notification for not enough balance', async function () { + await withFixtures( + { + ...withFixturesOptions, + title: this.test.title, + }, + async ({ driver }) => { + await loadSwaps(driver); + await buildQuote(driver, { + amount: 50, + swapTo: 'USDC', + }); + const reviewSwapButton = await driver.findElement( + '[data-testid="page-container-footer-next"]', + ); + assert.equal(await reviewSwapButton.getText(), 'Review swap'); + assert.equal(await reviewSwapButton.isEnabled(), true); + await reviewSwapButton.click(); + await driver.waitForSelector({ + css: '[class*="box--align-items-center"]', + text: 'Estimated gas fee', + }); + await driver.waitForSelector({ + css: '[class*="actionable-message__message"]', + text: 'You need 43.4467 more TESTETH to complete this swap', + }); + const swapButton = await driver.findElement( + '[data-testid="page-container-footer-next"]', + ); + assert.equal(await swapButton.getText(), 'Swap'); + assert.equal(await swapButton.isEnabled(), false); + }, + ); + }); + it('tests notifications for verified token on 0 sources and high slippage', async function () { await withFixtures( { - fixtures: new FixtureBuilder().build(), - ganacheOptions, - failOnConsoleError: false, + ...withFixturesOptions, title: this.test.title, }, async ({ driver }) => { await loadSwaps(driver); await buildQuote(driver, { amount: 2, - swapTo: '0x72c9Fb7ED19D3ce51cea5C56B3e023cd918baaDf', + swapToContractAddress: '0x72c9Fb7ED19D3ce51cea5C56B3e023cd918baaDf', }); await driver.waitForSelector({ css: '.popover-header__title', @@ -96,41 +127,4 @@ describe('Swaps - notifications', function () { }, ); }); - - it('tests a notification for not enough balance', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions, - failOnConsoleError: false, - title: this.test.title, - }, - async ({ driver }) => { - await loadSwaps(driver); - await buildQuote(driver, { - amount: 50, - swapTo: 'USDC', - }); - const reviewSwapButton = await driver.findElement( - '[data-testid="page-container-footer-next"]', - ); - assert.equal(await reviewSwapButton.getText(), 'Review swap'); - assert.equal(await reviewSwapButton.isEnabled(), true); - await reviewSwapButton.click(); - await driver.waitForSelector({ - css: '[class*="box--align-items-center"]', - text: 'Estimated gas fee', - }); - await driver.waitForSelector({ - css: '[class*="actionable-message__message"]', - text: 'You need 43.4467 more TESTETH to complete this swap', - }); - const swapButton = await driver.findElement( - '[data-testid="page-container-footer-next"]', - ); - assert.equal(await swapButton.getText(), 'Swap'); - assert.equal(await swapButton.isEnabled(), false); - }, - ); - }); }); diff --git a/test/jest/index.js b/test/jest/index.js index 9e6510ca8..1b4174afa 100644 --- a/test/jest/index.js +++ b/test/jest/index.js @@ -1,4 +1,4 @@ -export { screen, fireEvent } from '@testing-library/react'; +export { screen, fireEvent, waitFor } from '@testing-library/react'; export { createSwapsMockStore } from './mock-store'; export { renderWithProvider } from './rendering'; export { setBackgroundConnection } from './background'; diff --git a/ui/pages/swaps/fee-card/fee-card.js b/ui/pages/swaps/fee-card/fee-card.js index 3f0da0bb4..6883c57fa 100644 --- a/ui/pages/swaps/fee-card/fee-card.js +++ b/ui/pages/swaps/fee-card/fee-card.js @@ -192,5 +192,5 @@ FeeCard.propTypes = { onQuotesClick: PropTypes.func.isRequired, numberOfQuotes: PropTypes.number.isRequired, chainId: PropTypes.string.isRequired, - isBestQuote: PropTypes.bool.isRequired, + isBestQuote: PropTypes.bool, }; diff --git a/ui/pages/swaps/import-token/import-token.js b/ui/pages/swaps/import-token/import-token.js index 1376d81b6..e8e4f0f52 100644 --- a/ui/pages/swaps/import-token/import-token.js +++ b/ui/pages/swaps/import-token/import-token.js @@ -67,7 +67,7 @@ export default function ImportToken({ fontWeight={FONT_WEIGHT.BOLD} boxProps={{ marginTop: 2, marginBottom: 3 }} > - {tokenForImport.name} + {tokenForImport.name || ''} {t('contract')}: - {tokenForImport.address} + {tokenForImport.address || ''} diff --git a/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js b/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js index 4d4299486..079089ef3 100644 --- a/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js +++ b/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js @@ -17,6 +17,8 @@ const renderAdornment = () => ( ); +let timeoutIdForSearch; + export default function ListItemSearch({ onSearch, error, @@ -37,7 +39,6 @@ export default function ListItemSearch({ * @param {string} contractAddress */ const handleSearchTokenForImport = async (contractAddress) => { - setSearchQuery(contractAddress); try { const token = await fetchToken(contractAddress, chainId); if (token) { @@ -60,22 +61,32 @@ export default function ListItemSearch({ }; const handleSearch = async (newSearchQuery) => { - const trimmedNewSearchQuery = newSearchQuery.trim(); - const validHexAddress = isValidHexAddress(trimmedNewSearchQuery); - const fuseSearchResult = fuseRef.current.search(newSearchQuery); - const results = - defaultToAll && newSearchQuery === '' ? listToSearch : fuseSearchResult; - if (shouldSearchForImports && results.length === 0 && validHexAddress) { - await handleSearchTokenForImport(trimmedNewSearchQuery); - return; - } setSearchQuery(newSearchQuery); - onSearch({ - searchQuery: newSearchQuery, - results, - }); + if (timeoutIdForSearch) { + clearTimeout(timeoutIdForSearch); + } + timeoutIdForSearch = setTimeout(async () => { + timeoutIdForSearch = null; + const trimmedNewSearchQuery = newSearchQuery.trim(); + const validHexAddress = isValidHexAddress(trimmedNewSearchQuery); + const fuseSearchResult = fuseRef.current.search(newSearchQuery); + const results = + defaultToAll && newSearchQuery === '' ? listToSearch : fuseSearchResult; + if (shouldSearchForImports && results.length === 0 && validHexAddress) { + await handleSearchTokenForImport(trimmedNewSearchQuery); + return; + } + onSearch({ + searchQuery: newSearchQuery, + results, + }); + }, 350); }; + useEffect(() => { + return () => clearTimeout(timeoutIdForSearch); + }, []); + useEffect(() => { if (!fuseRef.current) { fuseRef.current = new Fuse(listToSearch, { @@ -128,6 +139,6 @@ ListItemSearch.propTypes = { searchPlaceholderText: PropTypes.string, defaultToAll: PropTypes.bool, shouldSearchForImports: PropTypes.bool, - searchQuery: PropTypes.func, + searchQuery: PropTypes.string, setSearchQuery: PropTypes.func, }; diff --git a/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.test.js b/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.test.js index a6a1e476c..0f82c3a09 100644 --- a/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.test.js +++ b/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.test.js @@ -74,6 +74,7 @@ describe('ListItemSearch', () => { }); it('changes the search query', () => { + jest.useFakeTimers(); const store = configureMockStore(middleware)(createSwapsMockStore()); const props = createProps(); const { getByTestId } = renderWithProvider( @@ -82,6 +83,7 @@ describe('ListItemSearch', () => { ); const input = getByTestId('search-list-items'); fireEvent.change(input, { target: { value: 'USD' } }); + jest.runAllTimers(); expect(props.setSearchQuery).toHaveBeenCalledWith('USD'); expect(props.onSearch).toHaveBeenCalledWith({ searchQuery: 'USD', @@ -90,6 +92,7 @@ describe('ListItemSearch', () => { }); it('imports a token', async () => { + jest.useFakeTimers(); const store = configureMockStore(middleware)(createSwapsMockStore()); const props = createProps({ shouldSearchForImports: true }); const { getByTestId } = renderWithProvider( @@ -98,6 +101,7 @@ describe('ListItemSearch', () => { ); const input = getByTestId('search-list-items'); await fireEvent.change(input, { target: { value: token.address } }); + await jest.runAllTimers(); expect(props.setSearchQuery).toHaveBeenCalledWith(token.address); expect(props.onSearch).toHaveBeenCalledWith({ searchQuery: token.address, diff --git a/ui/pages/swaps/searchable-item-list/searchable-item-list.js b/ui/pages/swaps/searchable-item-list/searchable-item-list.js index e7fb04447..c02d5f70c 100644 --- a/ui/pages/swaps/searchable-item-list/searchable-item-list.js +++ b/ui/pages/swaps/searchable-item-list/searchable-item-list.js @@ -93,6 +93,6 @@ SearchableItemList.propTypes = { hideItemIf: PropTypes.func, listContainerClassName: PropTypes.string, shouldSearchForImports: PropTypes.bool, - searchQuery: PropTypes.func, + searchQuery: PropTypes.string, setSearchQuery: PropTypes.func, }; diff --git a/ui/pages/swaps/view-quote/view-quote.js b/ui/pages/swaps/view-quote/view-quote.js index 09326c206..7897b61a7 100644 --- a/ui/pages/swaps/view-quote/view-quote.js +++ b/ui/pages/swaps/view-quote/view-quote.js @@ -741,7 +741,7 @@ export default function ViewQuote() { const priceSlippageUnknownFiatValue = !priceSlippageFromSource || !priceSlippageFromDestination || - usedQuote?.priceSlippage?.calculationError; + Boolean(usedQuote?.priceSlippage?.calculationError); let priceDifferencePercentage = 0; if (usedQuote?.priceSlippage?.ratio) { @@ -1038,7 +1038,7 @@ export default function ViewQuote() { } hideCancel disabled={isSwapButtonDisabled} - className={isShowingWarning && 'view-quote__thin-swaps-footer'} + className={isShowingWarning ? 'view-quote__thin-swaps-footer' : ''} showTopBorder />