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

Collectible Unit Tests (#17632)

* Add ipfs gateway and collectible state to mock-state.json

* Add collectible-default-image test with snapshot and testids

* Add Collectible Details test, snapshot, and test-ids

* Add Collectible Options tests and test-ids

* Add Collectible Items test and test-ids.

* Add more tests to Add Collectible component

* Update Security Tab snapshot with ipfs gateway state value

* Add data-testid to Menu component for menu background

* Lint

* Bump coverage targets

* Remove commented import

---------

Co-authored-by: David Walsh <davidwalsh83@gmail.com>
This commit is contained in:
Thomas Huang 2023-02-07 10:06:37 -08:00 committed by GitHub
parent fd53de6af5
commit 14efbf1eea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1050 additions and 20 deletions

View File

@ -6,10 +6,10 @@
// subset of files to check against these targets.
module.exports = {
global: {
branches: 50,
functions: 55,
lines: 62.25,
branches: 50.5,
statements: 61.5,
functions: 55,
},
transforms: {
branches: 100,

View File

@ -22,7 +22,7 @@
"mostRecentOverviewPage": "/mostRecentOverviewPage"
},
"metamask": {
"ipfsGateway": "",
"ipfsGateway": "dweb.link",
"dismissSeedBackUpReminder": false,
"usePhishDetect": true,
"useMultiAccountBalanceChecker": false,
@ -259,6 +259,161 @@
"maxBaseFee": "75",
"priorityFee": "2"
},
"collectiblesDropdownState": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"0x5": {
"0x495f947276749Ce646f68AC8c248420045cb7b5e": false
}
}
},
"allNftContracts": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"1": [
{
"address": "0xDc7382Eb0Bc9C352A4CbA23c909bDA01e0206414",
"tokenId": "1",
"name": "MUNK #1",
"description": null,
"image": "ipfs://QmTSZUNt8AKyDabkyXXXP4oHWDnaVXgNdXoJGEyaYzLbeL",
"standard": "ERC721"
}
],
"137": [
{
"address": "0xDc7382Eb0Bc9C352A4CbA23c909bDA01e0206414",
"tokenId": "1",
"name": "MUNK #1",
"description": null,
"image": "ipfs://QmTSZUNt8AKyDabkyXXXP4oHWDnaVXgNdXoJGEyaYzLbeL",
"standard": "ERC721"
}
],
"11155111": [
{
"address": "0xDc7382Eb0Bc9C352A4CbA23c909bDA01e0206414",
"tokenId": "1",
"name": "MUNK #1",
"description": null,
"image": "ipfs://QmTSZUNt8AKyDabkyXXXP4oHWDnaVXgNdXoJGEyaYzLbeL",
"standard": "ERC721"
}
],
"5": [
{
"address": "0x495f947276749Ce646f68AC8c248420045cb7b5e",
"tokenId": "58076532811975507823669075598676816378162417803895263482849101575514658701313",
"name": "Punk #4",
"creator": {
"user": {
"username": null
},
"profile_img_url": null,
"address": "0x806627172af48bd5b0765d3449a7def80d6576ff",
"config": ""
},
"description": "Red Mohawk bam!",
"image": "https://lh3.googleusercontent.com/BdxvLseXcfl57BiuQcQYdJ64v-aI8din7WPk0Pgo3qQFhAUH-B6i-dCqqc_mCkRIzULmwzwecnohLhrcH8A9mpWIZqA7ygc52Sr81hE",
"standard": "ERC1155"
},
{
"address": "0x495f947276749Ce646f68AC8c248420045cb7b5e",
"tokenId": "58076532811975507823669075598676816378162417803895263482849101574415147073537",
"name": "Punk #3",
"creator": {
"user": {
"username": null
},
"profile_img_url": null,
"address": "0x806627172af48bd5b0765d3449a7def80d6576ff",
"config": ""
},
"description": "Clown PUNK!!!",
"image": "https://lh3.googleusercontent.com/H7VrxaalZv4PF1B8U7ADuc8AfuqTVyzmMEDQ5OXKlx0Tqu5XiwsKYj4j_pAF6wUJjLMQbSN_0n3fuj84lNyRhFW9hyrxqDfY1IiQEQ",
"standard": "ERC1155"
},
{
"address": "0x495f947276749Ce646f68AC8c248420045cb7b5e",
"tokenId": "58076532811975507823669075598676816378162417803895263482849101573315635445761",
"name": "Punk #2",
"creator": {
"user": {
"username": null
},
"profile_img_url": null,
"address": "0x806627172af48bd5b0765d3449a7def80d6576ff",
"config": ""
},
"description": "Got glasses and black hair!",
"image": "https://lh3.googleusercontent.com/CHNTSlKB_Gob-iwTq8jcag6XwBkTqBMLt_vEKeBv18Q4AoPFAEPceqK6mRzkad2s5djx6CT5zbGQwDy81WwtNzViK5dQbG60uAWv",
"standard": "ERC1155"
},
{
"address": "0x495f947276749Ce646f68AC8c248420045cb7b5e",
"tokenId": "58076532811975507823669075598676816378162417803895263482849101572216123817985",
"name": "Punk #1",
"creator": {
"user": {
"username": null
},
"profile_img_url": null,
"address": "0x806627172af48bd5b0765d3449a7def80d6576ff",
"config": ""
},
"image": "https://lh3.googleusercontent.com/4jfPi-nQNWCUXD5qVNVWX7LX2UufU_elEJcvICFlsTdcBXv70asnDEOlI8oKECZxlXq1wseeIXMwmP5tLyOUxMKk",
"standard": "ERC1155"
},
{
"address": "0x495f947276749Ce646f68AC8c248420045cb7b5e",
"tokenId": "58076532811975507823669075598676816378162417803895263482849101571116612190209",
"name": "Punk #4651",
"creator": {
"user": {
"username": null
},
"profile_img_url": null,
"address": "0x806627172af48bd5b0765d3449a7def80d6576ff",
"config": ""
},
"image": "https://lh3.googleusercontent.com/BdxvLseXcfl57BiuQcQYdJ64v-aI8din7WPk0Pgo3qQFhAUH-B6i-dCqqc_mCkRIzULmwzwecnohLhrcH8A9mpWIZqA7ygc52Sr81hE",
"standard": "ERC1155"
},
{
"address": "0xDc7382Eb0Bc9C352A4CbA23c909bDA01e0206414",
"tokenId": "1",
"name": "MUNK #1",
"description": null,
"image": "ipfs://QmTSZUNt8AKyDabkyXXXP4oHWDnaVXgNdXoJGEyaYzLbeL",
"standard": "ERC721"
},
{
"address": "0xDc7382Eb0Bc9C352A4CbA23c909bDA01e0206414",
"tokenId": "2",
"name": "MUNK #2",
"description": null,
"image": "ipfs://QmTSZUNt8AKyDabkyXXXP4oHWDnaVXgNdXoJGEyaYzLbeL",
"standard": "ERC721"
},
{
"address": "0xDc7382Eb0Bc9C352A4CbA23c909bDA01e0206414",
"tokenId": "3",
"name": "MUNK #3",
"description": null,
"image": "ipfs://QmTSZUNt8AKyDabkyXXXP4oHWDnaVXgNdXoJGEyaYzLbeL",
"standard": "ERC721"
}
],
"153": [
{
"address": "0xDc7382Eb0Bc9C352A4CbA23c909bDA01e0206414",
"tokenId": "1",
"name": "MUNK #1",
"description": null,
"image": "ipfs://QmTSZUNt8AKyDabkyXXXP4oHWDnaVXgNdXoJGEyaYzLbeL",
"standard": "ERC721"
}
]
}
},
"tokenList": {
"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": {
"address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",

View File

@ -0,0 +1,60 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Collectible Default Image should match snapshot with all provided props 1`] = `
<div>
<button
class="collectible-default collectible-default--clickable"
data-testid="collectible-default-image"
tabindex="0"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography collectible-default__text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
>
Collectible Name
<br />
#
123
</h6>
</button>
</div>
`;
exports[`Collectible Default Image should match snapshot with missing image click handler 1`] = `
<div>
<div
class="collectible-default"
data-testid="collectible-default-image"
tabindex="0"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography collectible-default__text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
>
Collectible Name
<br />
#
123
</h6>
</div>
</div>
`;
exports[`Collectible Default Image should render with no props 1`] = `
<div>
<div
class="collectible-default"
data-testid="collectible-default-image"
tabindex="0"
>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography collectible-default__text typography--h6 typography--weight-normal typography--style-normal typography--color-text-default"
>
[unknownCollection]
<br />
#
</h6>
</div>
</div>
`;

View File

@ -15,6 +15,7 @@ export default function CollectibleDefaultImage({
return (
<Tag
tabIndex={0}
data-testid="collectible-default-image"
className={classnames('collectible-default', {
'collectible-default--clickable': handleImageClick,
})}

View File

@ -0,0 +1,83 @@
import React from 'react';
import { fireEvent } from '@testing-library/react';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import CollectibleDefaultImage from '.';
describe('Collectible Default Image', () => {
it('should render with no props', () => {
const { container } = renderWithProvider(<CollectibleDefaultImage />);
expect(container).toMatchSnapshot();
});
it('should match snapshot with all provided props', () => {
const props = {
name: 'Collectible Name',
tokenId: '123',
handleImageClick: jest.fn(),
};
const { container } = renderWithProvider(
<CollectibleDefaultImage {...props} />,
);
expect(container).toMatchSnapshot();
});
it('should match snapshot with missing image click handler', () => {
const props = {
name: 'Collectible Name',
tokenId: '123',
};
const { container } = renderWithProvider(
<CollectibleDefaultImage {...props} />,
);
expect(container).toMatchSnapshot();
});
it('should render collectible name', () => {
const props = {
name: 'Collectible Name',
};
const { queryByText } = renderWithProvider(
<CollectibleDefaultImage {...props} />,
);
const collectibleElement = queryByText(`${props.name} #`);
expect(collectibleElement).toBeInTheDocument();
});
it('should render collectible name and tokenId', () => {
const props = {
name: 'Collectible Name',
tokenId: '123',
};
const { queryByText } = renderWithProvider(
<CollectibleDefaultImage {...props} />,
);
const collectibleElement = queryByText(`${props.name} #${props.tokenId}`);
expect(collectibleElement).toBeInTheDocument();
});
it('should handle image click', () => {
const props = {
handleImageClick: jest.fn(),
};
const { queryByTestId } = renderWithProvider(
<CollectibleDefaultImage {...props} />,
);
const collectibleImageElement = queryByTestId('collectible-default-image');
fireEvent.click(collectibleImageElement);
expect(props.handleImageClick).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,167 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Collectible Details should match minimal props and state snapshot 1`] = `
<div>
<div
class="asset-navigation"
>
<button
class="asset-breadcrumb"
>
<i
class="fas fa-chevron-left asset-breadcrumb__chevron"
data-testid="asset__back"
/>
<span>
Test Account
</span>
 / 
<span
class="asset-breadcrumb__asset"
>
MUNK #1
</span>
</button>
<button
class="fas fa-ellipsis-v collectible-options__button"
data-testid="collectible-options__button"
/>
</div>
<div
class="box collectible-details box--flex-direction-row"
>
<div
class="collectible-details__top-section"
>
<div
class="box collectible-details__card box--flex-direction-row box--justify-content-center box--background-color-background-default box--rounded-md box--border-style-solid box--border-color-border-muted box--border-width-1 box--display-flex"
>
<img
alt="MUNK #1 1"
class="collectible-details__image"
src="https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link"
/>
</div>
<div
class="box collectible-details__info box--flex-direction-column box--justify-content-space-between box--display-flex"
>
<div>
<h4
class="box box--margin-top-1 box--margin-bottom-2 box--flex-direction-row typography typography--h4 typography--weight-bold typography--style-normal typography--color-text-default"
>
MUNK #1
</h4>
<h5
class="box box--margin-top-1 box--margin-bottom-4 box--flex-direction-row typography typography--h5 typography--weight-normal typography--style-normal typography--color-text-muted typography--overflowwrap-break-word"
>
#
1
</h5>
</div>
<div
class="box box--display-flex box--flex-direction-row box--width-1/2"
>
<button
class="button btn--rounded btn-primary collectible-details__send-button"
data-testid="collectible-send-button"
role="button"
tabindex="0"
>
Send
</button>
</div>
</div>
</div>
<div
class="box box--margin-bottom-2 box--flex-direction-row"
>
<div
class="box box--display-flex box--flex-direction-row"
>
<h6
class="box box--margin-top-1 box--margin-right-2 box--margin-bottom-4 box--flex-direction-row typography collectible-details__link-title typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
Source
</h6>
<h6
class="box box--margin-top-1 box--margin-bottom-4 box--flex-direction-row typography collectible-details__image-source typography--h6 typography--weight-normal typography--style-normal typography--color-primary-default"
>
<a
href="https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link"
rel="noopener noreferrer"
target="_blank"
title="https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link"
>
https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link
</a>
</h6>
</div>
<div
class="box box--display-flex box--flex-direction-row"
>
<h6
class="box box--margin-top-1 box--margin-right-2 box--margin-bottom-4 box--flex-direction-row typography collectible-details__link-title typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
Link
</h6>
<h6
class="box box--margin-top-1 box--margin-bottom-4 box--flex-direction-row typography collectible-details__image-source typography--h6 typography--weight-normal typography--style-normal typography--color-primary-default"
>
<a
href="https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link"
rel="noopener noreferrer"
target="_blank"
title="https://bafybeiclzx7zfjvuiuwobn5ip3ogc236bjqfjzoblumf4pau4ep6dqramu.ipfs.dweb.link"
/>
</h6>
</div>
<div
class="box box--display-flex box--flex-direction-row"
>
<h6
class="box box--margin-top-1 box--margin-right-2 box--margin-bottom-4 box--flex-direction-row typography collectible-details__link-title typography--h6 typography--weight-bold typography--style-normal typography--color-text-default"
>
Contract address
</h6>
<div
class="box collectible-details__contract-wrapper box--display-flex box--flex-direction-row"
>
<h6
class="box box--margin-top-1 box--margin-bottom-4 box--flex-direction-row typography typography--h6 typography--weight-normal typography--style-normal typography--color-text-alternative typography--overflowwrap-break-word"
>
0xDc7...6414
</h6>
<div
class="collectible-details__tooltip-wrapper"
>
<div
aria-describedby="tippy-tooltip-1"
class=""
data-original-title="Copy to clipboard"
data-tooltipped=""
style="display: inline;"
tabindex="0"
>
<button
aria-label="copy"
class="box mm-button-icon mm-button-icon--size-lg collectible-details__contract-copy-button box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-icon-alternative box--background-color-transparent box--rounded-lg"
data-testid="collectible-address-copy"
>
<div
class="box mm-icon mm-icon--size-lg box--flex-direction-row box--color-inherit"
style="mask-image: url('./images/icons/copy.svg');"
/>
</button>
</div>
</div>
</div>
</div>
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h7 typography--weight-normal typography--style-normal typography--color-text-alternative"
>
Disclaimer: MetaMask pulls the media file from the source url. This url sometimes is changed by the marketplace the NFT was minted on.
</h6>
</div>
</div>
</div>
`;

View File

@ -150,6 +150,7 @@ export default function CollectibleDetails({ collectible }) {
onClick={onSend}
disabled={sendDisabled}
className="collectible-details__send-button"
data-testid="collectible-send-button"
>
{t('send')}
</Button>
@ -416,6 +417,7 @@ export default function CollectibleDetails({ collectible }) {
ariaLabel="copy"
color={IconColor.iconAlternative}
className="collectible-details__contract-copy-button"
data-testid="collectible-address-copy"
onClick={() => {
handleAddressCopy(address);
}}

View File

@ -0,0 +1,286 @@
import { fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import copyToClipboard from 'copy-to-clipboard';
import { startNewDraftTransaction } from '../../../ducks/send';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import mockState from '../../../../test/data/mock-state.json';
import { DEFAULT_ROUTE, SEND_ROUTE } from '../../../helpers/constants/routes';
import { AssetType } from '../../../../shared/constants/transaction';
import {
removeAndIgnoreNft,
setRemoveCollectibleMessage,
} from '../../../store/actions';
import CollectibleDetails from './collectible-details';
jest.mock('copy-to-clipboard');
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(() => ({ search: '' })),
useHistory: () => ({
push: mockHistoryPush,
}),
}));
jest.mock('../../../ducks/send/index.js', () => ({
...jest.requireActual('../../../ducks/send/index.js'),
startNewDraftTransaction: jest
.fn()
.mockReturnValue(jest.fn().mockResolvedValue()),
}));
jest.mock('../../../store/actions.ts', () => ({
...jest.requireActual('../../../store/actions.ts'),
checkAndUpdateSingleNftOwnershipStatus: jest.fn().mockReturnValue(jest.fn()),
removeAndIgnoreNft: jest.fn().mockReturnValue(jest.fn()),
setRemoveCollectibleMessage: jest.fn().mockReturnValue(jest.fn()),
}));
describe('Collectible Details', () => {
const mockStore = configureMockStore([thunk])(mockState);
const collectibles =
mockState.metamask.allNftContracts[mockState.metamask.selectedAddress][5];
const props = {
collectible: collectibles[5],
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should match minimal props and state snapshot', () => {
const { container } = renderWithProvider(
<CollectibleDetails {...props} />,
mockStore,
);
expect(container).toMatchSnapshot();
});
it(`should route to '/' route when the back button is clicked`, () => {
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
mockStore,
);
const backButton = queryByTestId('asset__back');
fireEvent.click(backButton);
expect(mockHistoryPush).toHaveBeenCalledWith(DEFAULT_ROUTE);
});
it(`should call removeAndIgnoreNft with proper collectible details and route to '/' when removing collectible`, () => {
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
mockStore,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
fireEvent.click(openOptionMenuButton);
const removeCollectibleButton = queryByTestId('collectible-item-remove');
fireEvent.click(removeCollectibleButton);
expect(removeAndIgnoreNft).toHaveBeenCalledWith(
collectibles[5].address,
collectibles[5].tokenId,
);
expect(setRemoveCollectibleMessage).toHaveBeenCalledWith('success');
expect(mockHistoryPush).toHaveBeenCalledWith(DEFAULT_ROUTE);
});
it('should copy collectible address', async () => {
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
mockStore,
);
const copyAddressButton = queryByTestId('collectible-address-copy');
fireEvent.click(copyAddressButton);
expect(copyToClipboard).toHaveBeenCalledWith(collectibles[5].address);
});
it('should navigate to draft transaction send route with ERC721 data', async () => {
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
mockStore,
);
const collectibleSendButton = queryByTestId('collectible-send-button');
fireEvent.click(collectibleSendButton);
await waitFor(() => {
expect(startNewDraftTransaction).toHaveBeenCalledWith({
type: AssetType.NFT,
details: collectibles[5],
});
expect(mockHistoryPush).toHaveBeenCalledWith(SEND_ROUTE);
});
});
it('should not render send button if isCurrentlyOwned is false', () => {
const sixthCollectibleProps = {
collectible: collectibles[6],
};
collectibles[6].isCurrentlyOwned = false;
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...sixthCollectibleProps} />,
mockStore,
);
const collectibleSendButton = queryByTestId('collectible-send-button');
expect(collectibleSendButton).not.toBeInTheDocument();
});
describe(`Alternative Networks' OpenSea Links`, () => {
it('should open opeasea link with goeli testnet chainId', async () => {
global.platform = { openTab: jest.fn() };
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
mockStore,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
fireEvent.click(openOptionMenuButton);
const openOpenSea = queryByTestId('collectible-options__view-on-opensea');
fireEvent.click(openOpenSea);
await waitFor(() => {
expect(global.platform.openTab).toHaveBeenCalledWith({
url: `https://testnets.opensea.io/assets/${collectibles[5].address}/${collectibles[5].tokenId}`,
});
});
});
it('should open tab to mainnet opensea url with collectible info', async () => {
global.platform = { openTab: jest.fn() };
const mainnetState = {
...mockState,
metamask: {
...mockState.metamask,
provider: {
chainId: '0x1',
},
},
};
const mainnetMockStore = configureMockStore([thunk])(mainnetState);
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
mainnetMockStore,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
fireEvent.click(openOptionMenuButton);
const openOpenSea = queryByTestId('collectible-options__view-on-opensea');
fireEvent.click(openOpenSea);
await waitFor(() => {
expect(global.platform.openTab).toHaveBeenCalledWith({
url: `https://opensea.io/assets/${collectibles[5].address}/${collectibles[5].tokenId}`,
});
});
});
it('should open tab to polygon opensea url with collectible info', async () => {
const polygonState = {
...mockState,
metamask: {
...mockState.metamask,
provider: {
chainId: '0x89',
},
},
};
const polygonMockStore = configureMockStore([thunk])(polygonState);
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
polygonMockStore,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
fireEvent.click(openOptionMenuButton);
const openOpenSea = queryByTestId('collectible-options__view-on-opensea');
fireEvent.click(openOpenSea);
await waitFor(() => {
expect(global.platform.openTab).toHaveBeenCalledWith({
url: `https://opensea.io/assets/matic/${collectibles[5].address}/${collectibles[5].tokenId}`,
});
});
});
it('should open tab to sepolia opensea url with collectible info', async () => {
const sepoliaState = {
...mockState,
metamask: {
...mockState.metamask,
provider: {
chainId: '0xaa36a7',
},
},
};
const sepoliaMockStore = configureMockStore([thunk])(sepoliaState);
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
sepoliaMockStore,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
fireEvent.click(openOptionMenuButton);
const openOpenSea = queryByTestId('collectible-options__view-on-opensea');
fireEvent.click(openOpenSea);
await waitFor(() => {
expect(global.platform.openTab).toHaveBeenCalledWith({
url: `https://testnets.opensea.io/assets/${collectibles[5].address}/${collectibles[5].tokenId}`,
});
});
});
it('should not render opensea redirect button', async () => {
const randomNetworkState = {
...mockState,
metamask: {
...mockState.metamask,
provider: {
chainId: '0x99',
},
},
};
const randomNetworkMockStore = configureMockStore([thunk])(
randomNetworkState,
);
const { queryByTestId } = renderWithProvider(
<CollectibleDetails {...props} />,
randomNetworkMockStore,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
fireEvent.click(openOptionMenuButton);
const openOpenSea = queryByTestId('collectible-options__view-on-opensea');
await waitFor(() => {
expect(openOpenSea).not.toBeInTheDocument();
});
});
});
});

View File

@ -22,6 +22,7 @@ const CollectibleOptions = ({ onRemove, onViewOnOpensea }) => {
{collectibleOptionsOpen ? (
<Menu
anchorElement={collectibleOptionsButtonElement}
data-testid="close-collectible-options-menu"
onHide={() => setCollectibleOptionsOpen(false)}
>
{onViewOnOpensea ? (
@ -38,7 +39,7 @@ const CollectibleOptions = ({ onRemove, onViewOnOpensea }) => {
) : null}
<MenuItem
iconName={ICON_NAMES.TRASH}
data-testid="collectible-options__hide"
data-testid="collectible-item-remove"
onClick={() => {
setCollectibleOptionsOpen(false);
onRemove();
@ -54,7 +55,7 @@ const CollectibleOptions = ({ onRemove, onViewOnOpensea }) => {
CollectibleOptions.propTypes = {
onRemove: PropTypes.func.isRequired,
onViewOnOpensea: PropTypes.func.isRequired,
onViewOnOpensea: PropTypes.func,
};
export default CollectibleOptions;

View File

@ -0,0 +1,80 @@
import { fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import CollectibleOptions from './collectible-options';
describe('Collectible Options Component', () => {
const props = {
onRemove: jest.fn(),
onViewOnOpensea: jest.fn(),
};
it('should expand collectible options menu`', async () => {
const { queryByTestId } = renderWithProvider(
<CollectibleOptions {...props} />,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
expect(queryByTestId('collectible-item-remove')).not.toBeInTheDocument();
fireEvent.click(openOptionMenuButton);
await waitFor(() => {
expect(queryByTestId('collectible-item-remove')).toBeInTheDocument();
});
});
it('should expand and close menu options when clicked`', async () => {
const { queryByTestId } = renderWithProvider(
<CollectibleOptions {...props} />,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
fireEvent.click(openOptionMenuButton);
const closeOptionMenuButton = queryByTestId(
'close-collectible-options-menu',
);
fireEvent.click(closeOptionMenuButton);
expect(closeOptionMenuButton).not.toBeInTheDocument();
});
it('should click onRemove handler and close option menu', () => {
const { queryByTestId } = renderWithProvider(
<CollectibleOptions {...props} />,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
fireEvent.click(openOptionMenuButton);
const removeCollectibleButton = queryByTestId('collectible-item-remove');
fireEvent.click(removeCollectibleButton);
expect(props.onRemove).toHaveBeenCalled();
expect(removeCollectibleButton).not.toBeInTheDocument();
});
it('should click onViewOnOpensea handler and close option menu', () => {
const { queryByTestId } = renderWithProvider(
<CollectibleOptions {...props} />,
);
const openOptionMenuButton = queryByTestId('collectible-options__button');
const removeCollectibleButton = queryByTestId('collectible-item-remove');
fireEvent.click(openOptionMenuButton);
const openOpenSea = queryByTestId('collectible-options__view-on-opensea');
fireEvent.click(openOpenSea);
expect(props.onViewOnOpensea).toHaveBeenCalled();
expect(removeCollectibleButton).not.toBeInTheDocument();
});
});

View File

@ -134,10 +134,11 @@ export default function CollectiblesItems({
return (
<div className="collectibles-items__collection" key={`collection-${key}`}>
<button
className="collectibles-items__collection-wrapper"
data-testid="collection-expander-button"
onClick={() => {
updateCollectibleDropDownStateKey(key, isExpanded);
}}
className="collectibles-items__collection-wrapper"
>
<Box
marginBottom={2}
@ -183,6 +184,7 @@ export default function CollectiblesItems({
return (
<Box
data-testid="collectible-wrapper"
width={width}
key={`collectible-${i}`}
className="collectibles-items__item-wrapper"
@ -202,6 +204,7 @@ export default function CollectiblesItems({
>
<img
className="collectibles-items__item-image"
data-testid="collectible-image"
src={collectibleImage}
alt={collectibleImageAlt}
/>

View File

@ -0,0 +1,86 @@
import React from 'react';
import { fireEvent } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import mockState from '../../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import { updateCollectibleDropDownState } from '../../../store/actions';
import CollectiblesItems from '.';
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(() => ({ search: '' })),
useHistory: () => ({
push: mockHistoryPush,
}),
}));
jest.mock('../../../store/actions.ts', () => ({
...jest.requireActual('../../../store/actions.ts'),
updateCollectibleDropDownState: jest.fn().mockReturnValue(jest.fn()),
}));
describe('Collectibles Item Component', () => {
const collectibles =
mockState.metamask.allNftContracts[mockState.metamask.selectedAddress][5];
const props = {
collections: {
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
collectibles,
collectionImage: '',
collectionName: 'Collectible Collection',
},
},
previouslyOwnedCollection: {
collectibles: [],
},
};
const mockStore = configureMockStore([thunk])(mockState);
it('should expand collectible collection showing individual collectibles', async () => {
const { queryByTestId, queryAllByTestId, rerender } = renderWithProvider(
<CollectiblesItems {...props} />,
mockStore,
);
const collectionExpanderButton = queryByTestId(
'collection-expander-button',
);
expect(queryAllByTestId('collectible-wrapper')).toHaveLength(0);
fireEvent.click(collectionExpanderButton);
expect(updateCollectibleDropDownState).toHaveBeenCalledWith({
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
'0x5': {
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': true,
'0x495f947276749Ce646f68AC8c248420045cb7b5e': false,
},
},
});
rerender(<CollectiblesItems {...props} />, mockStore);
expect(queryAllByTestId('collectible-wrapper')).toHaveLength(8);
});
it('should collectible click image', () => {
const { queryAllByTestId } = renderWithProvider(
<CollectiblesItems {...props} />,
mockStore,
);
const collectibleImages = queryAllByTestId('collectible-image');
fireEvent.click(collectibleImages[0]);
const firstCollectible = collectibles[0];
const collectibleRoute = `/asset/${firstCollectible.address}/${firstCollectible.tokenId}`;
expect(mockHistoryPush).toHaveBeenCalledWith(collectibleRoute);
});
});

View File

@ -8,6 +8,7 @@ const Menu = ({
anchorElement,
children,
className,
'data-testid': dataTestId,
onHide,
popperOptions,
}) => {
@ -24,7 +25,11 @@ const Menu = ({
return createPortal(
<>
<div className="menu__background" onClick={onHide} />
<div
className="menu__background"
data-testid={dataTestId}
onClick={onHide}
/>
<div
className={classnames('menu__container', className)}
data-testid={className}
@ -45,6 +50,7 @@ Menu.propTypes = {
className: PropTypes.string,
onHide: PropTypes.func.isRequired,
popperOptions: PropTypes.object,
dataTestId: PropTypes.string,
};
Menu.defaultProps = {

View File

@ -1,8 +1,17 @@
import React from 'react';
import { fireEvent } from '@testing-library/react';
import { fireEvent, waitFor } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { useHistory } from 'react-router-dom';
import { renderWithProvider } from '../../../test/jest/rendering';
import * as Actions from '../../store/actions';
import mockState from '../../../test/data/mock-state.json';
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
import {
addNftVerifyOwnership,
ignoreTokens,
setNewCollectibleAddedMessage,
updateCollectibleDropDownState,
} from '../../store/actions';
import AddCollectible from '.';
const VALID_ADDRESS = '0x312BE6a98441F9F6e3F6246B13CA19701e0AC3B9';
@ -10,9 +19,39 @@ const INVALID_ADDRESS = 'aoinsafasdfa';
const VALID_TOKENID = '1201';
const INVALID_TOKENID = 'abcde';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: jest.fn(
jest.fn().mockReturnValue({
push: jest.fn(),
location: {
state: {
tokenAddress: '0xTokenAddress',
},
},
}),
),
}));
jest.mock('../../store/actions.ts', () => ({
addNftVerifyOwnership: jest
.fn()
.mockReturnValue(jest.fn().mockResolvedValue()),
getTokenStandardAndDetails: jest.fn().mockResolvedValue(),
ignoreTokens: jest.fn().mockReturnValue(jest.fn().mockResolvedValue()),
setNewCollectibleAddedMessage: jest
.fn()
.mockReturnValue(jest.fn().mockResolvedValue()),
updateCollectibleDropDownState: jest
.fn()
.mockReturnValue(jest.fn().mockResolvedValue()),
}));
describe('AddCollectible', () => {
const store = configureMockStore([])({
metamask: { provider: { chainId: '0x1' } },
const store = configureMockStore([thunk])(mockState);
beforeEach(() => {
jest.restoreAllMocks();
});
it('should enable the "Add" button when valid entries are input into both Address and TokenId fields', () => {
@ -53,7 +92,7 @@ describe('AddCollectible', () => {
expect(getByText('Add')).not.toBeEnabled();
});
it('should call addNftVerifyOwnership action with correct values (tokenId should not be in scientific notation)', () => {
it('should call addNftVerifyOwnership, updateCollectibleDropDownState, setNewCollectibleAddedMessage, and ignoreTokens action with correct values (tokenId should not be in scientific notation)', async () => {
const { getByTestId, getByText } = renderWithProvider(
<AddCollectible />,
store,
@ -65,15 +104,76 @@ describe('AddCollectible', () => {
fireEvent.change(getByTestId('token-id'), {
target: { value: LARGE_TOKEN_ID },
});
const addCollectibleVerifyOwnershipSpy = jest.spyOn(
Actions,
'addNftVerifyOwnership',
);
fireEvent.click(getByText('Add'));
expect(addCollectibleVerifyOwnershipSpy).toHaveBeenCalledWith(
'0x312BE6a98441F9F6e3F6246B13CA19701e0AC3B9',
'9007199254740992',
await waitFor(() => {
expect(addNftVerifyOwnership).toHaveBeenCalledWith(
'0x312BE6a98441F9F6e3F6246B13CA19701e0AC3B9',
'9007199254740992',
);
expect(updateCollectibleDropDownState).toHaveBeenCalledWith({
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
'0x5': {
'0x312BE6a98441F9F6e3F6246B13CA19701e0AC3B9': true,
'0x495f947276749Ce646f68AC8c248420045cb7b5e': false,
},
},
});
expect(setNewCollectibleAddedMessage).toHaveBeenCalledWith('success');
expect(ignoreTokens).toHaveBeenCalledWith({
dontShowLoadingIndicator: true,
tokensToIgnore: '0xTokenAddress',
});
});
});
it('should throw error message and click close on failure message', async () => {
addNftVerifyOwnership.mockImplementation(() =>
jest.fn().mockRejectedValue(new Error('error')),
);
const { getByTestId, getByText, queryByTitle } = renderWithProvider(
<AddCollectible />,
store,
);
fireEvent.change(getByTestId('address'), {
target: { value: VALID_ADDRESS },
});
const LARGE_TOKEN_ID = Number.MAX_SAFE_INTEGER + 1;
fireEvent.change(getByTestId('token-id'), {
target: { value: LARGE_TOKEN_ID },
});
fireEvent.click(getByText('Add'));
await waitFor(() => {
expect(setNewCollectibleAddedMessage).toHaveBeenCalledWith('error');
});
const addCollectibleClose = queryByTitle('Close');
fireEvent.click(addCollectibleClose);
});
it('should route to default route when cancel button is clicked', () => {
const { queryByTestId } = renderWithProvider(<AddCollectible />, store);
const cancelButton = queryByTestId('page-container-footer-cancel');
fireEvent.click(cancelButton);
expect(useHistory().push).toHaveBeenCalledWith(DEFAULT_ROUTE);
});
it('should route to default route when close button is clicked', () => {
const { queryByLabelText } = renderWithProvider(<AddCollectible />, store);
const closeButton = queryByLabelText('close');
fireEvent.click(closeButton);
expect(useHistory().push).toHaveBeenCalledWith(DEFAULT_ROUTE);
});
});

View File

@ -406,7 +406,7 @@ exports[`Security Tab should match snapshot 1`] = `
class="MuiInputBase-input MuiInput-input MuiInputBase-inputMarginDense MuiInput-inputMarginDense"
dir="auto"
type="text"
value=""
value="dweb.link"
/>
</div>