From fbbdaf04ed6b3a7cab4d09ec3d4de950f10490c5 Mon Sep 17 00:00:00 2001 From: Daniel <80175477+dan437@users.noreply.github.com> Date: Wed, 21 Apr 2021 12:34:35 -0700 Subject: [PATCH] Increase Jest unit test coverage for the Swaps feature to ~25% (#10900) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Swaps: Show a network name dynamically in a tooltip * Replace “Ethereum” with “$1”, change “Test” to “Testnet” * Replace 이더리움 with $1 * Translate network names, use ‘Ethereum’ by default if a translation is not available yet * Reorder messages to resolve ESLint issues * Add a snapshot test for the FeeCard component, increase Jest threshold * Enable snapshot testing into external .snap files in ESLint * Add the “networkNameEthereum” key in ko/messages.json, remove default “Ethereum” value * Throw an error if chain ID is not supported by the Swaps feature * Use string literals when calling the `t` fn, * Watch Jest tests silently (no React warnings in terminal, only errors) * Add @testing-library/jest-dom, import it before running Jest tests * Add snapshot testing of Swaps’ React components for happy paths, increase minimum threshold for Jest * Add the test/jest folder for Jest setup and shared functions, use it in Swaps Jest tests * Fix ESLint issues, update linting config * Enable ESLint for .snap files (Jest snapshots), throw an error if a snapshot is bigger than 50 lines * Don’t run lint:fix for .snap files * Move `createProps` outside of `describe` blocks, move store creation inside tests * Use translations instead of keys, update a rendering function to load translations * Make sure all Jest snapshots are shorter than 50 lines (default limit) * Add / update props for Swaps tests * Fix React warnings when running tests for Swaps --- .eslintrc.js | 17 +- jest.config.js | 9 +- package.json | 4 +- test/jest/index.js | 4 + test/jest/mock-store.js | 69 ++++++ test/jest/rendering.js | 59 +++++ test/jest/setup.js | 2 + ui/app/__mocks__/react-router-dom.js | 6 + .../actionable-message.test.js.snap | 16 ++ .../actionable-message/actionable-message.js | 2 +- .../actionable-message.test.js | 22 ++ .../__snapshots__/awaiting-swap.test.js.snap | 45 ++++ .../swaps/awaiting-swap/awaiting-swap.test.js | 37 ++++ .../swaps/countdown-timer/countdown-timer.js | 1 + .../countdown-timer/countdown-timer.test.js | 31 +++ .../dropdown-input-pair.test.js.snap | 20 ++ .../dropdown-input-pair.test.js | 23 ++ .../dropdown-search-list.test.js.snap | 40 ++++ .../dropdown-search-list.test.js | 25 +++ .../exchange-rate-display.test.js.snap | 45 ++++ .../exchange-rate-display.test.js | 28 +++ .../__snapshots__/fee-card.test.js.snap | 203 +++++------------- ui/app/pages/swaps/fee-card/fee-card.test.js | 76 +++++-- .../__snapshots__/intro-popup.test.js.snap | 9 + .../swaps/intro-popup/intro-popup.test.js | 24 +++ .../main-quote-summary.test.js.snap | 109 ++++++++++ .../main-quote-summary.test.js | 39 ++++ .../searchable-item-list.test.js.snap | 77 +++++++ .../searchable-item-list.test.js | 60 ++++++ .../select-quote-popover.test.js.snap | 9 + .../select-quote-popover.test.js | 24 +++ .../slippage-buttons.test.js.snap | 42 ++++ .../slippage-buttons/slippage-buttons.test.js | 31 +++ .../__snapshots__/swaps-footer.test.js.snap | 41 ++++ .../swaps/swaps-footer/swaps-footer.test.js | 28 +++ yarn.lock | 60 +++++- 36 files changed, 1149 insertions(+), 188 deletions(-) create mode 100644 test/jest/index.js create mode 100644 test/jest/mock-store.js create mode 100644 test/jest/rendering.js create mode 100644 test/jest/setup.js create mode 100644 ui/app/__mocks__/react-router-dom.js create mode 100644 ui/app/pages/swaps/actionable-message/__snapshots__/actionable-message.test.js.snap create mode 100644 ui/app/pages/swaps/actionable-message/actionable-message.test.js create mode 100644 ui/app/pages/swaps/awaiting-swap/__snapshots__/awaiting-swap.test.js.snap create mode 100644 ui/app/pages/swaps/awaiting-swap/awaiting-swap.test.js create mode 100644 ui/app/pages/swaps/countdown-timer/countdown-timer.test.js create mode 100644 ui/app/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap create mode 100644 ui/app/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js create mode 100644 ui/app/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap create mode 100644 ui/app/pages/swaps/dropdown-search-list/dropdown-search-list.test.js create mode 100644 ui/app/pages/swaps/exchange-rate-display/__snapshots__/exchange-rate-display.test.js.snap create mode 100644 ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.test.js create mode 100644 ui/app/pages/swaps/intro-popup/__snapshots__/intro-popup.test.js.snap create mode 100644 ui/app/pages/swaps/intro-popup/intro-popup.test.js create mode 100644 ui/app/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap create mode 100644 ui/app/pages/swaps/main-quote-summary/main-quote-summary.test.js create mode 100644 ui/app/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap create mode 100644 ui/app/pages/swaps/searchable-item-list/searchable-item-list.test.js create mode 100644 ui/app/pages/swaps/select-quote-popover/__snapshots__/select-quote-popover.test.js.snap create mode 100644 ui/app/pages/swaps/select-quote-popover/select-quote-popover.test.js create mode 100644 ui/app/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap create mode 100644 ui/app/pages/swaps/slippage-buttons/slippage-buttons.test.js create mode 100644 ui/app/pages/swaps/swaps-footer/__snapshots__/swaps-footer.test.js.snap create mode 100644 ui/app/pages/swaps/swaps-footer/swaps-footer.test.js diff --git a/.eslintrc.js b/.eslintrc.js index 6fa28872a..b764ad39c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -70,7 +70,7 @@ module.exports = { }, overrides: [ { - files: ['ui/**/*.js', 'test/lib/render-helpers.js'], + files: ['ui/**/*.js', 'test/lib/render-helpers.js', 'test/jest/*.js'], plugins: ['react'], extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'], rules: { @@ -108,17 +108,28 @@ module.exports = { }, { files: ['**/*.test.js'], - excludedFiles: ['ui/**/*.test.js'], + excludedFiles: ['ui/**/*.test.js', 'ui/app/__mocks__/*.js'], extends: ['@metamask/eslint-config-mocha'], rules: { 'mocha/no-setup-in-describe': 'off', }, }, { - files: ['ui/**/*.test.js'], + files: ['**/__snapshots__/*.snap'], + plugins: ['jest'], + rules: { + 'jest/no-large-snapshots': [ + 'error', + { maxSize: 50, inlineMaxSize: 50 }, + ], + }, + }, + { + files: ['ui/**/*.test.js', 'ui/app/__mocks__/*.js'], extends: ['@metamask/eslint-config-jest'], rules: { 'jest/no-restricted-matchers': 'off', + 'import/unambiguous': 'off', }, }, { diff --git a/jest.config.js b/jest.config.js index c788b29f6..9b3dc4459 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,12 +3,13 @@ module.exports = { coverageDirectory: 'jest-coverage/', coverageThreshold: { global: { - branches: 6.94, - functions: 8.85, - lines: 11.76, - statements: 11.78, + branches: 21.24, + functions: 23.01, + lines: 27.19, + statements: 27.07, }, }, setupFiles: ['./test/setup.js', './test/env.js'], + setupFilesAfterEnv: ['./test/jest/setup.js'], testMatch: ['**/ui/**/?(*.)+(test).js'], }; diff --git a/package.json b/package.json index d9504c93e..72eeba7ad 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test:unit:global": "mocha --exit --require test/env.js --require test/setup.js --recursive test/unit-global/*.test.js", "test:unit:jest": "jest", "test:unit:jest:watch": "jest --watch", + "test:unit:jest:watch:silent": "jest --watch --silent", "test:unit:jest:ci": "jest --maxWorkers=2", "test:unit:lax": "mocha --exit --require test/env.js --require test/setup.js --ignore './app/scripts/controllers/permissions/*.test.js' --recursive './{app,shared}/**/*.test.js'", "test:unit:strict": "mocha --exit --require test/env.js --require test/setup.js --recursive './app/scripts/controllers/permissions/*.test.js'", @@ -38,7 +39,7 @@ "test:coverage:path": "nyc --check-coverage yarn test:unit:path", "ganache:start": "./development/run-ganache.sh", "sentry:publish": "node ./development/sentry-publish.js", - "lint": "prettier --check '**/*.json' && eslint . --ext js --cache && yarn lint:styles", + "lint": "prettier --check '**/*.json' && eslint . --ext js,snap --cache && yarn lint:styles", "lint:fix": "prettier --write '**/*.json' && eslint . --ext js --cache --fix", "lint:changed": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' | tr '\\n' '\\0' | xargs -0 eslint", "lint:changed:fix": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' | tr '\\n' '\\0' | xargs -0 eslint --fix", @@ -218,6 +219,7 @@ "@storybook/core": "^6.1.17", "@storybook/react": "^6.1.17", "@storybook/storybook-deployer": "^2.8.7", + "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^10.4.8", "@testing-library/react-hooks": "^3.2.1", "@types/react": "^16.9.53", diff --git a/test/jest/index.js b/test/jest/index.js new file mode 100644 index 000000000..be8cd13bf --- /dev/null +++ b/test/jest/index.js @@ -0,0 +1,4 @@ +import { createSwapsMockStore } from './mock-store'; +import { renderWithProvider } from './rendering'; + +export { createSwapsMockStore, renderWithProvider }; diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js new file mode 100644 index 000000000..a90ada6d7 --- /dev/null +++ b/test/jest/mock-store.js @@ -0,0 +1,69 @@ +import { MAINNET_CHAIN_ID } from '../../shared/constants/network'; + +export const createSwapsMockStore = () => { + return { + swaps: { + customGas: { + fallBackPrice: 5, + }, + }, + metamask: { + provider: { + chainId: MAINNET_CHAIN_ID, + }, + cachedBalances: { + [MAINNET_CHAIN_ID]: 5, + }, + accounts: { + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + balance: '0x0', + }, + '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b': { + address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b', + balance: '0x0', + }, + }, + selectedAddress: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + frequentRpcListDetail: [], + swapsState: { + quotes: {}, + fetchParams: { + metaData: { + sourceTokenInfo: { + symbol: 'BAT', + }, + destinationTokenInfo: { + symbol: 'ETH', + }, + }, + }, + tokens: [ + { + erc20: true, + symbol: 'BAT', + decimals: 18, + address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', + }, + { + erc20: true, + symbol: 'USDT', + decimals: 6, + address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + }, + ], + tradeTxId: null, + approveTxId: null, + quotesLastFetched: null, + customMaxGas: '', + customGasPrice: null, + selectedAggId: null, + customApproveTxData: '', + errorKey: '', + topAggId: null, + routeState: '', + swapsFeatureIsLive: false, + }, + }, + }; +}; diff --git a/test/jest/rendering.js b/test/jest/rendering.js new file mode 100644 index 000000000..d215cbc0d --- /dev/null +++ b/test/jest/rendering.js @@ -0,0 +1,59 @@ +import React, { useMemo } from 'react'; +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import PropTypes from 'prop-types'; + +import { I18nContext, LegacyI18nProvider } from '../../ui/app/contexts/i18n'; +import { getMessage } from '../../ui/app/helpers/utils/i18n-helper'; +import * as en from '../../app/_locales/en/messages.json'; + +export const I18nProvider = (props) => { + const { currentLocale, current, en: eng } = props; + + const t = useMemo(() => { + return (key, ...args) => + getMessage(currentLocale, current, key, ...args) || + getMessage(currentLocale, eng, key, ...args); + }, [currentLocale, current, eng]); + + return ( + {props.children} + ); +}; + +I18nProvider.propTypes = { + currentLocale: PropTypes.string, + current: PropTypes.object, + en: PropTypes.object, + children: PropTypes.node, +}; + +I18nProvider.defaultProps = { + children: undefined, +}; + +export function renderWithProvider(component, store) { + const Wrapper = ({ children }) => { + const WithoutStore = () => ( + + + {children} + + + ); + return store ? ( + + + + ) : ( + + ); + }; + + Wrapper.propTypes = { + children: PropTypes.node, + }; + + return render(component, { wrapper: Wrapper }); +} diff --git a/test/jest/setup.js b/test/jest/setup.js new file mode 100644 index 000000000..bbdce03b8 --- /dev/null +++ b/test/jest/setup.js @@ -0,0 +1,2 @@ +// jest-setup.js is for Jest-specific setup only and runs before our Jest tests. +import '@testing-library/jest-dom'; diff --git a/ui/app/__mocks__/react-router-dom.js b/ui/app/__mocks__/react-router-dom.js new file mode 100644 index 000000000..981a488d7 --- /dev/null +++ b/ui/app/__mocks__/react-router-dom.js @@ -0,0 +1,6 @@ +const originalModule = jest.requireActual('react-router-dom'); + +module.exports = { + ...originalModule, + useHistory: jest.fn(), +}; diff --git a/ui/app/pages/swaps/actionable-message/__snapshots__/actionable-message.test.js.snap b/ui/app/pages/swaps/actionable-message/__snapshots__/actionable-message.test.js.snap new file mode 100644 index 000000000..a07e79541 --- /dev/null +++ b/ui/app/pages/swaps/actionable-message/__snapshots__/actionable-message.test.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ActionableMessage renders the component with initial props 1`] = ` +
+
+ +
+ I am an actionable message! +
+
+
+`; diff --git a/ui/app/pages/swaps/actionable-message/actionable-message.js b/ui/app/pages/swaps/actionable-message/actionable-message.js index df9b9da1d..2d4c9da5c 100644 --- a/ui/app/pages/swaps/actionable-message/actionable-message.js +++ b/ui/app/pages/swaps/actionable-message/actionable-message.js @@ -80,6 +80,6 @@ ActionableMessage.propTypes = { }), className: PropTypes.string, type: PropTypes.string, - withRightButton: PropTypes.boolean, + withRightButton: PropTypes.bool, infoTooltipText: PropTypes.string, }; diff --git a/ui/app/pages/swaps/actionable-message/actionable-message.test.js b/ui/app/pages/swaps/actionable-message/actionable-message.test.js new file mode 100644 index 000000000..6074f4576 --- /dev/null +++ b/ui/app/pages/swaps/actionable-message/actionable-message.test.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import { renderWithProvider } from '../../../../../test/jest'; +import ActionableMessage from '.'; + +const createProps = (customProps = {}) => { + return { + message: 'I am an actionable message!', + ...customProps, + }; +}; + +describe('ActionableMessage', () => { + it('renders the component with initial props', () => { + const props = createProps(); + const { container, getByText } = renderWithProvider( + , + ); + expect(getByText(props.message)).toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/app/pages/swaps/awaiting-swap/__snapshots__/awaiting-swap.test.js.snap b/ui/app/pages/swaps/awaiting-swap/__snapshots__/awaiting-swap.test.js.snap new file mode 100644 index 000000000..588a27dd9 --- /dev/null +++ b/ui/app/pages/swaps/awaiting-swap/__snapshots__/awaiting-swap.test.js.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AwaitingSwap renders the component with initial props 1`] = ` +
+ + + Your + + ETH + + will be added to your account once this transaction has processed. + + +
+`; + +exports[`AwaitingSwap renders the component with initial props 2`] = ` + +`; diff --git a/ui/app/pages/swaps/awaiting-swap/awaiting-swap.test.js b/ui/app/pages/swaps/awaiting-swap/awaiting-swap.test.js new file mode 100644 index 000000000..396bcfb51 --- /dev/null +++ b/ui/app/pages/swaps/awaiting-swap/awaiting-swap.test.js @@ -0,0 +1,37 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; + +import { + renderWithProvider, + createSwapsMockStore, +} from '../../../../../test/jest'; +import AwaitingSwap from '.'; + +const createProps = (customProps = {}) => { + return { + swapComplete: false, + txHash: 'txHash', + tokensReceived: 'tokensReceived', + submittingSwap: true, + inputValue: 5, + maxSlippage: 3, + ...customProps, + }; +}; + +describe('AwaitingSwap', () => { + it('renders the component with initial props', () => { + const store = configureMockStore()(createSwapsMockStore()); + const { getByText } = renderWithProvider( + , + store, + ); + expect(getByText('Processing')).toBeInTheDocument(); + expect(getByText('View on Etherscan')).toBeInTheDocument(); + expect(getByText('View in activity')).toBeInTheDocument(); + expect( + document.querySelector('.awaiting-swap__main-descrption'), + ).toMatchSnapshot(); + expect(document.querySelector('.swaps-footer')).toMatchSnapshot(); + }); +}); diff --git a/ui/app/pages/swaps/countdown-timer/countdown-timer.js b/ui/app/pages/swaps/countdown-timer/countdown-timer.js index 879eeff82..41767ef3c 100644 --- a/ui/app/pages/swaps/countdown-timer/countdown-timer.js +++ b/ui/app/pages/swaps/countdown-timer/countdown-timer.js @@ -94,6 +94,7 @@ export default function CountdownTimer({ return (
{ + return { + timeStarted: 1, + timeOnly: true, + timerBase: 5, + warningTime: '0:30', + labelKey: 'swapNewQuoteIn', + infoTooltipLabelKey: 'swapQuotesAreRefreshed', + ...customProps, + }; +}; + +describe('CountdownTimer', () => { + it('renders the component with initial props', () => { + const store = configureMockStore()(createSwapsMockStore()); + const { getByTestId } = renderWithProvider( + , + store, + ); + expect(getByTestId('countdown-timer__timer-container')).toBeInTheDocument(); + }); +}); diff --git a/ui/app/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap b/ui/app/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap new file mode 100644 index 000000000..d58907ed2 --- /dev/null +++ b/ui/app/pages/swaps/dropdown-input-pair/__snapshots__/dropdown-input-pair.test.js.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DropdownInputPair renders the component with initial props 1`] = ` + +`; diff --git a/ui/app/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js b/ui/app/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js new file mode 100644 index 000000000..69420a56c --- /dev/null +++ b/ui/app/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js @@ -0,0 +1,23 @@ +import React from 'react'; + +import { renderWithProvider } from '../../../../../test/jest'; +import DropdownInputPair from '.'; + +const createProps = (customProps = {}) => { + return { + ...customProps, + }; +}; + +describe('DropdownInputPair', () => { + it('renders the component with initial props', () => { + const props = createProps(); + const { getByPlaceholderText } = renderWithProvider( + , + ); + expect(getByPlaceholderText('0')).toBeInTheDocument(); + expect( + document.querySelector('.dropdown-input-pair__input'), + ).toMatchSnapshot(); + }); +}); diff --git a/ui/app/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap b/ui/app/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap new file mode 100644 index 000000000..6d8710731 --- /dev/null +++ b/ui/app/pages/swaps/dropdown-search-list/__snapshots__/dropdown-search-list.test.js.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DropdownSearchList renders the component with initial props 1`] = ` +
+ +
+`; diff --git a/ui/app/pages/swaps/dropdown-search-list/dropdown-search-list.test.js b/ui/app/pages/swaps/dropdown-search-list/dropdown-search-list.test.js new file mode 100644 index 000000000..70f0e6a68 --- /dev/null +++ b/ui/app/pages/swaps/dropdown-search-list/dropdown-search-list.test.js @@ -0,0 +1,25 @@ +import React from 'react'; + +import { renderWithProvider } from '../../../../../test/jest'; +import DropdownSearchList from '.'; + +const createProps = (customProps = {}) => { + return { + startingItem: { + iconUrl: 'iconUrl', + symbol: 'symbol', + }, + ...customProps, + }; +}; + +describe('DropdownSearchList', () => { + it('renders the component with initial props', () => { + const props = createProps(); + const { container, getByText } = renderWithProvider( + , + ); + expect(container).toMatchSnapshot(); + expect(getByText('symbol')).toBeInTheDocument(); + }); +}); diff --git a/ui/app/pages/swaps/exchange-rate-display/__snapshots__/exchange-rate-display.test.js.snap b/ui/app/pages/swaps/exchange-rate-display/__snapshots__/exchange-rate-display.test.js.snap new file mode 100644 index 000000000..84eb5177a --- /dev/null +++ b/ui/app/pages/swaps/exchange-rate-display/__snapshots__/exchange-rate-display.test.js.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExchangeRateDisplay renders the component with initial props 1`] = ` +
+
+ + 1 + + + ETH + + + = + + + 0.1 + + + BAT + +
+ + + +
+
+
+`; diff --git a/ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.test.js b/ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.test.js new file mode 100644 index 000000000..b0aa8db60 --- /dev/null +++ b/ui/app/pages/swaps/exchange-rate-display/exchange-rate-display.test.js @@ -0,0 +1,28 @@ +import React from 'react'; + +import { renderWithProvider } from '../../../../../test/jest'; +import ExchangeRateDisplay from '.'; + +const createProps = (customProps = {}) => { + return { + primaryTokenValue: '2000000000000000000', + primaryTokenDecimals: 18, + primaryTokenSymbol: 'ETH', + secondaryTokenValue: '200000000000000000', + secondaryTokenDecimals: 18, + secondaryTokenSymbol: 'BAT', + ...customProps, + }; +}; + +describe('ExchangeRateDisplay', () => { + it('renders the component with initial props', () => { + const props = createProps(); + const { container, getByText } = renderWithProvider( + , + ); + expect(getByText(props.primaryTokenSymbol)).toBeInTheDocument(); + expect(getByText(props.secondaryTokenSymbol)).toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/app/pages/swaps/fee-card/__snapshots__/fee-card.test.js.snap b/ui/app/pages/swaps/fee-card/__snapshots__/fee-card.test.js.snap index ebbd93237..784a1f4f9 100644 --- a/ui/app/pages/swaps/fee-card/__snapshots__/fee-card.test.js.snap +++ b/ui/app/pages/swaps/fee-card/__snapshots__/fee-card.test.js.snap @@ -1,172 +1,67 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FeeCard renders the component with initial props 1`] = ` -
+
-
-
+ +
+
+`; + +exports[`FeeCard renders the component with initial props 2`] = ` +
+
+ Quote includes a 0.875% MetaMask fee +
+
-
-
- [swapEstimatedNetworkFee] -
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
- [swapMaxNetworkFees] -
- -
-
-
-
-
-
-
- [swapThisWillAllowApprove] -
-
-
-
- -
-
-
-
- -
-
-
-
- [swapQuoteIncludesRate] -
-
-
-
- -
-
-
+
diff --git a/ui/app/pages/swaps/fee-card/fee-card.test.js b/ui/app/pages/swaps/fee-card/fee-card.test.js index 391cf5b7d..15c8b0c33 100644 --- a/ui/app/pages/swaps/fee-card/fee-card.test.js +++ b/ui/app/pages/swaps/fee-card/fee-card.test.js @@ -1,31 +1,61 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { renderWithProvider } from '../../../../../test/jest'; import { MAINNET_CHAIN_ID } from '../../../../../shared/constants/network'; -import FeeCard from './fee-card'; +import FeeCard from '.'; + +const createProps = (customProps = {}) => { + return { + primaryFee: { + fee: '0.0441 ETH', + maxFee: '0.04851 ETH', + }, + secondaryFee: { + fee: '$101.98', + maxFee: '$112.17', + }, + hideTokenApprovalRow: false, + onFeeCardMaxRowClick: jest.fn(), + tokenApprovalTextComponent: ( + + ABC + + ), + tokenApprovalSourceTokenSymbol: 'ABC', + onTokenApprovalClick: jest.fn(), + metaMaskFee: '0.875', + isBestQuote: true, + numberOfQuotes: 6, + onQuotesClick: jest.fn(), + tokenConversionRate: 0.015, + chainId: MAINNET_CHAIN_ID, + ...customProps, + }; +}; describe('FeeCard', () => { - const createProps = (customProps = {}) => { - return { - primaryFee: '1 ETH', - secondaryFee: '2500 USD', - hideTokenApprovalRow: false, - onFeeCardMaxRowClick: jest.fn(), - tokenApprovalTextComponent: <>, - tokenApprovalSourceTokenSymbol: 'ABC', - onTokenApprovalClick: jest.fn(), - metaMaskFee: '0.875', - isBestQuote: true, - numberOfQuotes: 6, - onQuotesClick: jest.fn(), - tokenConversionRate: 0.015, - chainId: MAINNET_CHAIN_ID, - ...customProps, - }; - }; - it('renders the component with initial props', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); + const props = createProps(); + const { getByText } = renderWithProvider(); + expect(getByText('Using the best quote')).toBeInTheDocument(); + expect(getByText('6 quotes')).toBeInTheDocument(); + expect(getByText('Max network fee')).toBeInTheDocument(); + expect(getByText('Estimated network fee')).toBeInTheDocument(); + expect(getByText(props.primaryFee.fee)).toBeInTheDocument(); + expect(getByText(props.primaryFee.maxFee)).toBeInTheDocument(); + expect(getByText(props.secondaryFee.fee)).toBeInTheDocument(); + expect(getByText(props.secondaryFee.maxFee)).toBeInTheDocument(); + expect( + getByText('Quote includes a 0.875% MetaMask fee'), + ).toBeInTheDocument(); + expect( + document.querySelector('.fee-card__savings-and-quotes-header'), + ).toMatchSnapshot(); + expect( + document.querySelector('.fee-card__top-bordered-row'), + ).toMatchSnapshot(); }); }); diff --git a/ui/app/pages/swaps/intro-popup/__snapshots__/intro-popup.test.js.snap b/ui/app/pages/swaps/intro-popup/__snapshots__/intro-popup.test.js.snap new file mode 100644 index 000000000..d9d0324df --- /dev/null +++ b/ui/app/pages/swaps/intro-popup/__snapshots__/intro-popup.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IntroPopup renders the component with initial props 1`] = ` +
+
+
+`; diff --git a/ui/app/pages/swaps/intro-popup/intro-popup.test.js b/ui/app/pages/swaps/intro-popup/intro-popup.test.js new file mode 100644 index 000000000..2ceec7e0e --- /dev/null +++ b/ui/app/pages/swaps/intro-popup/intro-popup.test.js @@ -0,0 +1,24 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; + +import { + renderWithProvider, + createSwapsMockStore, +} from '../../../../../test/jest'; +import IntroPopup from '.'; + +const createProps = (customProps = {}) => { + return { + onClose: jest.fn(), + ...customProps, + }; +}; + +describe('IntroPopup', () => { + it('renders the component with initial props', () => { + const store = configureMockStore()(createSwapsMockStore()); + const props = createProps(); + const { container } = renderWithProvider(, store); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/app/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap b/ui/app/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap new file mode 100644 index 000000000..36a520f6f --- /dev/null +++ b/ui/app/pages/swaps/main-quote-summary/__snapshots__/main-quote-summary.test.js.snap @@ -0,0 +1,109 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MainQuoteSummary renders the component with initial props 1`] = ` +
+ + 2 + + + E + + + ETH + +
+`; + +exports[`MainQuoteSummary renders the component with initial props 2`] = ` +
+ + B + + + BAT + +
+`; + +exports[`MainQuoteSummary renders the component with initial props 3`] = ` +
+
+
+ + 0.2 + +
+
+
+`; + +exports[`MainQuoteSummary renders the component with initial props 4`] = ` +
+
+ + 1 + + + ETH + + + = + + + 0.1 + + + BAT + +
+ + + +
+
+
+`; diff --git a/ui/app/pages/swaps/main-quote-summary/main-quote-summary.test.js b/ui/app/pages/swaps/main-quote-summary/main-quote-summary.test.js new file mode 100644 index 000000000..cc1775363 --- /dev/null +++ b/ui/app/pages/swaps/main-quote-summary/main-quote-summary.test.js @@ -0,0 +1,39 @@ +import React from 'react'; + +import { renderWithProvider } from '../../../../../test/jest'; +import MainQuoteSummary from '.'; + +const createProps = (customProps = {}) => { + return { + sourceValue: '2000000000000000000', + sourceDecimals: 18, + sourceSymbol: 'ETH', + destinationValue: '200000000000000000', + destinationDecimals: 18, + destinationSymbol: 'BAT', + ...customProps, + }; +}; + +describe('MainQuoteSummary', () => { + it('renders the component with initial props', () => { + const props = createProps(); + const { getAllByText } = renderWithProvider( + , + ); + expect(getAllByText(props.sourceSymbol)).toHaveLength(2); + expect(getAllByText(props.destinationSymbol)).toHaveLength(2); + expect( + document.querySelector('.main-quote-summary__source-row'), + ).toMatchSnapshot(); + expect( + document.querySelector('.main-quote-summary__destination-row'), + ).toMatchSnapshot(); + expect( + document.querySelector('.main-quote-summary__quote-large'), + ).toMatchSnapshot(); + expect( + document.querySelector('.main-quote-summary__exchange-rate-container'), + ).toMatchSnapshot(); + }); +}); diff --git a/ui/app/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap b/ui/app/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap new file mode 100644 index 000000000..29affeb5d --- /dev/null +++ b/ui/app/pages/swaps/searchable-item-list/__snapshots__/searchable-item-list.test.js.snap @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchableItemList renders the component with initial props 1`] = ` + +`; + +exports[`SearchableItemList renders the component with initial props 2`] = ` +
+ +
+
+ + primaryLabel + + + secondaryLabel + +
+
+ + rightPrimaryLabel + + + rightSecondaryLabel + +
+
+
+`; diff --git a/ui/app/pages/swaps/searchable-item-list/searchable-item-list.test.js b/ui/app/pages/swaps/searchable-item-list/searchable-item-list.test.js new file mode 100644 index 000000000..ef9a3a016 --- /dev/null +++ b/ui/app/pages/swaps/searchable-item-list/searchable-item-list.test.js @@ -0,0 +1,60 @@ +import React from 'react'; + +import { renderWithProvider } from '../../../../../test/jest'; +import SearchableItemList from '.'; + +const createProps = (customProps = {}) => { + return { + defaultToAll: true, + listTitle: 'listTitle', + itemsToSearch: [ + { + iconUrl: 'iconUrl', + selected: true, + primaryLabel: 'primaryLabel', + secondaryLabel: 'secondaryLabel', + rightPrimaryLabel: 'rightPrimaryLabel', + rightSecondaryLabel: 'rightSecondaryLabel', + }, + ], + fuseSearchKeys: [ + { + name: 'name', + weight: 0.499, + }, + { + name: 'symbol', + weight: 0.499, + }, + { + name: 'address', + weight: 0.002, + }, + ], + ...customProps, + }; +}; + +describe('SearchableItemList', () => { + it('renders the component with initial props', () => { + const props = createProps(); + const { getByText } = renderWithProvider(); + expect(getByText(props.listTitle)).toBeInTheDocument(); + expect(getByText(props.itemsToSearch[0].primaryLabel)).toBeInTheDocument(); + expect( + getByText(props.itemsToSearch[0].secondaryLabel), + ).toBeInTheDocument(); + expect( + getByText(props.itemsToSearch[0].rightPrimaryLabel), + ).toBeInTheDocument(); + expect( + getByText(props.itemsToSearch[0].rightSecondaryLabel), + ).toBeInTheDocument(); + expect( + document.querySelector('.searchable-item-list__search'), + ).toMatchSnapshot(); + expect( + document.querySelector('.searchable-item-list__item'), + ).toMatchSnapshot(); + }); +}); diff --git a/ui/app/pages/swaps/select-quote-popover/__snapshots__/select-quote-popover.test.js.snap b/ui/app/pages/swaps/select-quote-popover/__snapshots__/select-quote-popover.test.js.snap new file mode 100644 index 000000000..d6b1172e5 --- /dev/null +++ b/ui/app/pages/swaps/select-quote-popover/__snapshots__/select-quote-popover.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SelectQuotePopover renders the component with initial props 1`] = ` +
+
+
+`; diff --git a/ui/app/pages/swaps/select-quote-popover/select-quote-popover.test.js b/ui/app/pages/swaps/select-quote-popover/select-quote-popover.test.js new file mode 100644 index 000000000..b23a35f68 --- /dev/null +++ b/ui/app/pages/swaps/select-quote-popover/select-quote-popover.test.js @@ -0,0 +1,24 @@ +import React from 'react'; + +import { renderWithProvider } from '../../../../../test/jest'; +import SelectQuotePopover from '.'; + +const createProps = (customProps = {}) => { + return { + onClose: jest.fn(), + onSubmit: jest.fn(), + swapToSymbol: 'ETH', + initialAggId: 'initialAggId', + onQuoteDetailsIsOpened: jest.fn(), + ...customProps, + }; +}; + +describe('SelectQuotePopover', () => { + it('renders the component with initial props', () => { + const { container } = renderWithProvider( + , + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/app/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap b/ui/app/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap new file mode 100644 index 000000000..7dd4d9bab --- /dev/null +++ b/ui/app/pages/swaps/slippage-buttons/__snapshots__/slippage-buttons.test.js.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SlippageButtons renders the component with initial props 1`] = ` +
+
+ Advanced Options +
+
+`; + +exports[`SlippageButtons renders the component with initial props 2`] = ` +
+ + + +
+`; diff --git a/ui/app/pages/swaps/slippage-buttons/slippage-buttons.test.js b/ui/app/pages/swaps/slippage-buttons/slippage-buttons.test.js new file mode 100644 index 000000000..0108f07d2 --- /dev/null +++ b/ui/app/pages/swaps/slippage-buttons/slippage-buttons.test.js @@ -0,0 +1,31 @@ +import React from 'react'; + +import { renderWithProvider } from '../../../../../test/jest'; +import SlippageButtons from '.'; + +const createProps = (customProps = {}) => { + return { + onSelect: jest.fn(), + maxAllowedSlippage: 15, + currentSlippage: 3, + ...customProps, + }; +}; + +describe('SlippageButtons', () => { + it('renders the component with initial props', () => { + const { getByText } = renderWithProvider( + , + ); + expect(getByText('2%')).toBeInTheDocument(); + expect(getByText('3%')).toBeInTheDocument(); + expect(getByText('custom')).toBeInTheDocument(); + expect(getByText('Advanced Options')).toBeInTheDocument(); + expect( + document.querySelector('.slippage-buttons__header'), + ).toMatchSnapshot(); + expect( + document.querySelector('.slippage-buttons__button-group'), + ).toMatchSnapshot(); + }); +}); diff --git a/ui/app/pages/swaps/swaps-footer/__snapshots__/swaps-footer.test.js.snap b/ui/app/pages/swaps/swaps-footer/__snapshots__/swaps-footer.test.js.snap new file mode 100644 index 000000000..3edd2af66 --- /dev/null +++ b/ui/app/pages/swaps/swaps-footer/__snapshots__/swaps-footer.test.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SwapsFooter renders the component with initial props 1`] = ` +
+ +
+`; diff --git a/ui/app/pages/swaps/swaps-footer/swaps-footer.test.js b/ui/app/pages/swaps/swaps-footer/swaps-footer.test.js new file mode 100644 index 000000000..4cff41083 --- /dev/null +++ b/ui/app/pages/swaps/swaps-footer/swaps-footer.test.js @@ -0,0 +1,28 @@ +import React from 'react'; + +import { renderWithProvider } from '../../../../../test/jest'; +import SwapsFooter from '.'; + +const createProps = (customProps = {}) => { + return { + onCancel: jest.fn(), + onSubmit: jest.fn(), + submitText: 'submitText', + disabled: false, + showTermsOfService: true, + ...customProps, + }; +}; + +describe('SwapsFooter', () => { + it('renders the component with initial props', () => { + const props = createProps(); + const { container, getByText } = renderWithProvider( + , + ); + expect(getByText(props.submitText)).toBeInTheDocument(); + expect(getByText('Back')).toBeInTheDocument(); + expect(getByText('Terms of Service')).toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 4701fd8ff..6aac67d17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3522,6 +3522,20 @@ dom-accessibility-api "^0.5.0" pretty-format "^25.5.0" +"@testing-library/jest-dom@^5.11.10": + version "5.11.10" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.10.tgz#1cd90715023e1627f5ed26ab3b38e6f22d77046c" + integrity sha512-FuKiq5xuk44Fqm0000Z9w0hjOdwZRNzgx7xGGxQYepWFZy+OYUMOT/wPI4nLYXCaVltNVpU1W/qmD88wLWDsqQ== + dependencies: + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^4.2.2" + chalk "^3.0.0" + css "^3.0.0" + css.escape "^1.5.1" + lodash "^4.17.15" + redent "^3.0.0" + "@testing-library/react-hooks@^3.2.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.2.1.tgz#19b6caa048ef15faa69d439c469033873ea01294" @@ -3720,6 +3734,14 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/jest@*": + version "26.0.22" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.22.tgz#8308a1debdf1b807aa47be2838acdcd91e88fbe6" + integrity sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw== + dependencies: + jest-diff "^26.0.0" + pretty-format "^26.0.0" + "@types/json-schema@^7.0.3": version "7.0.7" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" @@ -3906,6 +3928,13 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74" integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== +"@types/testing-library__jest-dom@^5.9.1": + version "5.9.5" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz#5bf25c91ad2d7b38f264b12275e5c92a66d849b0" + integrity sha512-ggn3ws+yRbOHog9GxnXiEZ/35Mow6YtPZpd7Z5mKDeZS/o7zx3yAle0ov/wjhVB5QT4N2Dt+GNoGCdqkBGCajQ== + dependencies: + "@types/jest" "*" + "@types/testing-library__react-hooks@^3.0.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.2.0.tgz#52f3a109bef06080e3b1e3ae7ea1c014ce859897" @@ -5299,6 +5328,11 @@ atob@^2.0.0: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a" integrity sha1-ri1acpR38onWDdf5amMUoi3Wwio= +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + autoprefixer@^8.0.0: version "8.1.0" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-8.1.0.tgz#374cf35be1c0e8fce97408d876f95f66f5cb4641" @@ -8492,6 +8526,11 @@ css-what@2.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= + css@2.X, css@^2.0.0, css@^2.2.1: version "2.2.3" resolved "https://registry.yarnpkg.com/css/-/css-2.2.3.tgz#f861f4ba61e79bedc962aa548e5780fd95cbc6be" @@ -8502,6 +8541,15 @@ css@2.X, css@^2.0.0, css@^2.2.1: source-map-resolve "^0.5.1" urix "^0.1.0" +css@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" + integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== + dependencies: + inherits "^2.0.4" + source-map "^0.6.1" + source-map-resolve "^0.6.0" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -15490,7 +15538,7 @@ jest-config@^26.6.3: micromatch "^4.0.2" pretty-format "^26.6.2" -jest-diff@^26.6.2: +jest-diff@^26.0.0, jest-diff@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== @@ -20905,7 +20953,7 @@ pretty-format@^25.5.0: ansi-styles "^4.0.0" react-is "^16.12.0" -pretty-format@^26.6.2: +pretty-format@^26.0.0, pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== @@ -23984,6 +24032,14 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.1: source-map-url "^0.4.0" urix "^0.1.0" +source-map-resolve@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" + integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + source-map-support@0.5.12: version "0.5.12" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599"