diff --git a/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.spec.tsx b/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.spec.tsx new file mode 100644 index 0000000000..5f4d40add1 --- /dev/null +++ b/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.spec.tsx @@ -0,0 +1,205 @@ +import { + Bid, + ChainId, + Item, + ListingStatus, + Network, + NFTCategory, + Order, + Rarity +} from '@dcl/schemas' +import { waitFor } from '@testing-library/react' +import React, { RefObject } from 'react' +import { t } from 'decentraland-dapps/dist/modules/translation/utils' + +import * as bidAPI from '../../../modules/vendor/decentraland/bid/api' +import * as orderAPI from '../../../modules/vendor/decentraland/order/api' +import { renderWithProviders } from '../../../utils/tests' +import BestBuyingOption from './BestBuyingOption' +import { formatWeiMANA } from '../../../lib/mana' + +jest.mock('../../../modules/vendor/decentraland/nft/api') +jest.mock('../../../modules/vendor/decentraland/order/api') +jest.mock('../../../modules/vendor/decentraland/bid/api') +jest.mock('decentraland-dapps/dist/containers', () => { + const module = jest.requireActual('decentraland-dapps/dist/containers') + return { + ...module, + Profile: () =>
+ } +}) + +describe('Best Buying Option', () => { + let asset: Item = { + contractAddress: '0xaddress', + itemId: '1', + id: '1', + name: 'asset name', + thumbnail: '', + url: '', + category: NFTCategory.WEARABLE, + rarity: Rarity.UNIQUE, + price: '10', + available: 2, + isOnSale: false, + creator: '0xcreator', + beneficiary: null, + createdAt: 1671033414000, + updatedAt: 1671033414000, + reviewedAt: 1671033414000, + soldAt: 1671033414000, + data: { + parcel: undefined, + estate: undefined, + wearable: undefined, + ens: undefined, + emote: undefined + }, + network: Network.MATIC, + chainId: ChainId.ETHEREUM_GOERLI, + firstListedAt: null + } + + let orderResponse: Order = { + id: '1', + marketplaceAddress: '0xmarketplace', + contractAddress: '0xaddress', + tokenId: '1', + owner: '0x92712b730b9a474f99a47bb8b1750190d5959a2b', + buyer: null, + price: '10', + status: ListingStatus.OPEN, + expiresAt: 1671033414000, + createdAt: 1671033414000, + updatedAt: 0, + network: Network.MATIC, + chainId: ChainId.ETHEREUM_GOERLI + } + + let bid: Bid = { + id: '1', + bidAddress: '0xbid', + bidder: 'bidder', + seller: 'seller', + price: '2', + fingerprint: '', + status: ListingStatus.OPEN, + blockchainId: '1', + blockNumber: '', + expiresAt: 1671033414000, + createdAt: 1671033414000, + updatedAt: 0, + contractAddress: '0xaddress', + tokenId: '', + network: Network.MATIC, + chainId: ChainId.ETHEREUM_GOERLI + } + + afterEach(() => { + jest.resetAllMocks() + }) + + describe('Mint option', () => { + it('should render the mint option', async () => { + const reference: RefObject = React.createRef() + const { getByText } = renderWithProviders( + + ) + + expect( + getByText(t('best_buying_option.minting.title')) + ).toBeInTheDocument() + }) + + it('should render the mint price', async () => { + const reference: RefObject = React.createRef() + const { getByText } = renderWithProviders( + + ) + + const price = formatWeiMANA(asset.price) + + expect(getByText(price)).toBeInTheDocument() + }) + }) + + describe('Listing option', () => { + beforeEach(() => { + asset.available = 0 + ;(orderAPI.orderAPI.fetchOrders as jest.Mock).mockResolvedValueOnce({ + data: [orderResponse], + total: 1 + }) + ;(bidAPI.bidAPI.fetchByNFT as jest.Mock).mockResolvedValueOnce({ + data: [bid], + total: 1 + }) + }) + + it('should render the listing option', async () => { + const reference: RefObject = React.createRef() + const { getByText, findByTestId } = renderWithProviders( + + ) + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + expect( + getByText(t('best_buying_option.buy_listing.title')) + ).toBeInTheDocument() + }) + + it('should render the listing price and de highest offer for that NFT', async () => { + const reference: RefObject = React.createRef() + const { getByText, findByTestId } = renderWithProviders( + + ) + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + const price = formatWeiMANA(orderResponse.price) + + const highestOffer = formatWeiMANA(bid.price) + + expect(getByText(price)).toBeInTheDocument() + expect(getByText(highestOffer)).toBeInTheDocument() + }) + }) + + describe('No available options', () => { + beforeEach(() => { + asset.available = 0 + ;(orderAPI.orderAPI.fetchOrders as jest.Mock).mockResolvedValueOnce({ + data: [], + total: 0 + }) + ;(bidAPI.bidAPI.fetchByNFT as jest.Mock).mockResolvedValueOnce({ + data: [bid], + total: 0 + }) + }) + + it('should render no options available', async () => { + const reference: RefObject = React.createRef() + const { getByText, findByTestId } = renderWithProviders( + + ) + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + expect(getByText(t('best_buying_option.empty.title'))).toBeInTheDocument() + }) + }) +}) diff --git a/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.tsx b/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.tsx index cfa24ae4f3..273711e4e4 100644 --- a/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.tsx +++ b/webapp/src/components/AssetPage/BestBuyingOption/BestBuyingOption.tsx @@ -101,7 +101,7 @@ const BestBuyingOption = ({ asset, tableRef }: Props) => {
{isLoading ? (
- +
) : buyOption === BuyOptions.MINT && asset && !isNFT(asset) ? (
diff --git a/webapp/src/components/AssetPage/ListingsTable/ListingsTable.spec.tsx b/webapp/src/components/AssetPage/ListingsTable/ListingsTable.spec.tsx new file mode 100644 index 0000000000..cffa86f3d8 --- /dev/null +++ b/webapp/src/components/AssetPage/ListingsTable/ListingsTable.spec.tsx @@ -0,0 +1,226 @@ +import { + ChainId, + Item, + ListingStatus, + Network, + NFTCategory, + Order, + Rarity +} from '@dcl/schemas' +import { waitFor, within } from '@testing-library/react' +import { t } from 'decentraland-dapps/dist/modules/translation/utils' +import { formatDistanceToNow, getDateAndMonthName } from '../../../lib/date' +import { formatWeiMANA } from '../../../lib/mana' +import { OwnersResponse } from '../../../modules/vendor/decentraland' +import * as nftAPI from '../../../modules/vendor/decentraland/nft/api' +import * as orderAPI from '../../../modules/vendor/decentraland/order/api' +import { renderWithProviders } from '../../../utils/tests' +import ListingsTable, { ROWS_PER_PAGE } from './ListingsTable' + +jest.mock('../../../modules/vendor/decentraland/nft/api') +jest.mock('../../../modules/vendor/decentraland/order/api') +jest.mock('decentraland-dapps/dist/containers', () => { + const module = jest.requireActual('decentraland-dapps/dist/containers') + return { + ...module, + Profile: () =>
+ } +}) + +describe('Listings Table', () => { + let asset: Item = { + contractAddress: '0xaddress', + itemId: '1', + id: '1', + name: 'asset name', + thumbnail: '', + url: '', + category: NFTCategory.WEARABLE, + rarity: Rarity.UNIQUE, + price: '10', + available: 2, + isOnSale: false, + creator: '0xcreator', + beneficiary: null, + createdAt: 1671033414000, + updatedAt: 1671033414000, + reviewedAt: 1671033414000, + soldAt: 1671033414000, + data: { + parcel: undefined, + estate: undefined, + wearable: undefined, + ens: undefined, + emote: undefined + }, + network: Network.MATIC, + chainId: ChainId.ETHEREUM_GOERLI, + firstListedAt: null + } + + let ownersResponse: OwnersResponse = { + issuedId: 1, + ownerId: '0x92712b730b9a474f99a47bb8b1750190d5959a2b', + orderStatus: 'open', + orderExpiresAt: '1671033414000', + tokenId: '1' + } + + let orderResponse: Order = { + id: '1', + marketplaceAddress: '0xmarketplace', + contractAddress: '0xaddress', + tokenId: '1', + owner: '0x92712b730b9a474f99a47bb8b1750190d5959a2b', + buyer: null, + price: '10', + status: ListingStatus.OPEN, + expiresAt: 1671033414000, + createdAt: 1671033414000, + updatedAt: 0, + network: Network.MATIC, + chainId: ChainId.ETHEREUM_GOERLI + } + + afterEach(() => { + jest.resetAllMocks() + }) + + describe('Empty table', () => { + beforeEach(() => { + ;(nftAPI.nftAPI.getOwners as jest.Mock).mockResolvedValueOnce({ + data: [], + total: 0 + }) + ;(orderAPI.orderAPI.fetchOrders as jest.Mock).mockResolvedValueOnce({ + data: [], + total: 0 + }) + }) + + it('should render the empty table message', async () => { + const { getByText, findByTestId } = renderWithProviders( + + ) + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + expect( + getByText(t('listings_table.there_are_no_listings')) + ).toBeInTheDocument() + }) + }) + + describe('Should render the table correctly', () => { + beforeEach(() => { + ;(nftAPI.nftAPI.getOwners as jest.Mock).mockResolvedValueOnce({ + data: [ownersResponse], + total: 1 + }) + ;(orderAPI.orderAPI.fetchOrders as jest.Mock).mockResolvedValueOnce({ + data: [orderResponse], + total: 1 + }) + }) + + it('should render the table', async () => { + const screen = renderWithProviders() + + const { findByTestId, getByTestId } = screen + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + expect(getByTestId('listings-table')).not.toBe(null) + }) + + it('should render the table data correctly', async () => { + const screen = renderWithProviders() + + const { findByTestId, getByText } = screen + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + const created = getDateAndMonthName(orderResponse.createdAt) + const expires = formatDistanceToNow(+orderResponse.expiresAt, { + addSuffix: true + }) + const price = formatWeiMANA(orderResponse.price) + + expect(getByText(orderResponse.tokenId)).not.toBe(null) + expect(getByText(created)).not.toBe(null) + expect(getByText(expires)).not.toBe(null) + expect(getByText(price)).not.toBe(null) + }) + }) + + describe('Pagination', () => { + describe('Should have pagination', () => { + beforeEach(() => { + ;(nftAPI.nftAPI.getOwners as jest.Mock).mockResolvedValueOnce({ + data: Array(ROWS_PER_PAGE + 1).fill(ownersResponse), + total: 7 + }) + ;(orderAPI.orderAPI.fetchOrders as jest.Mock).mockResolvedValueOnce({ + data: Array(ROWS_PER_PAGE + 1).fill(orderResponse), + total: 7 + }) + }) + + it('should render the pagination correctly', async () => { + const screen = renderWithProviders() + + const { findByTestId, getByRole } = screen + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + const navigation = getByRole('navigation') + + expect(within(navigation).getByText('1')).toBeInTheDocument() + expect(within(navigation).getByText('2')).toBeInTheDocument() + }) + }) + + describe('Should not have pagination', () => { + beforeEach(() => { + ;(nftAPI.nftAPI.getOwners as jest.Mock).mockResolvedValueOnce({ + data: Array(ROWS_PER_PAGE - 1).fill(ownersResponse), + total: 5 + }) + ;(orderAPI.orderAPI.fetchOrders as jest.Mock).mockResolvedValueOnce({ + data: Array(ROWS_PER_PAGE - 1).fill(orderResponse), + total: 5 + }) + }) + + it('should not render pagination as there is no need', async () => { + const screen = renderWithProviders() + + const { findByTestId, queryByRole } = screen + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + expect(queryByRole('navigation')).not.toBeInTheDocument() + }) + }) + }) +}) diff --git a/webapp/src/components/AssetPage/ListingsTable/ListingsTable.tsx b/webapp/src/components/AssetPage/ListingsTable/ListingsTable.tsx index c309dbcf4b..8fc46f86e9 100644 --- a/webapp/src/components/AssetPage/ListingsTable/ListingsTable.tsx +++ b/webapp/src/components/AssetPage/ListingsTable/ListingsTable.tsx @@ -12,7 +12,7 @@ import { LinkedProfile } from '../../LinkedProfile' import { Props } from './ListingsTable.types' import styles from './ListingsTable.module.css' -const ROWS_PER_PAGE = 6 +export const ROWS_PER_PAGE = 6 const INITIAL_PAGE = 1 const ListingsTable = (props: Props) => { @@ -75,7 +75,7 @@ const ListingsTable = (props: Props) => {
{isLoading ? (
- +
) : orders.length === 0 ? (
@@ -83,7 +83,7 @@ const ListingsTable = (props: Props) => {
) : ( <> - +
diff --git a/webapp/src/components/AssetPage/OwnersTable/OwnersTable.spec.tsx b/webapp/src/components/AssetPage/OwnersTable/OwnersTable.spec.tsx new file mode 100644 index 0000000000..38deab5a06 --- /dev/null +++ b/webapp/src/components/AssetPage/OwnersTable/OwnersTable.spec.tsx @@ -0,0 +1,175 @@ +import { ChainId, Item, Network, NFTCategory, Rarity } from '@dcl/schemas' +import { queryByRole, waitFor, within } from '@testing-library/react' +import { t } from 'decentraland-dapps/dist/modules/translation/utils' +import { OwnersResponse } from '../../../modules/vendor/decentraland' +import * as nftAPI from '../../../modules/vendor/decentraland/nft/api' +import { renderWithProviders } from '../../../utils/tests' +import OwnersTable, { ROWS_PER_PAGE } from './OwnersTable' + +const ownerIdMock = '0x92712b730b9a474f99a47bb8b1750190d5959a2b' + +jest.mock('../../../modules/vendor/decentraland/nft/api') +jest.mock('decentraland-dapps/dist/containers', () => { + const module = jest.requireActual('decentraland-dapps/dist/containers') + return { + ...module, + Profile: () =>
{ownerIdMock}
+ } +}) + +describe('Owners Table', () => { + let asset: Item = { + contractAddress: '0xaddress', + itemId: '1', + id: '1', + name: 'asset name', + thumbnail: '', + url: '', + category: NFTCategory.WEARABLE, + rarity: Rarity.UNIQUE, + price: '10', + available: 2, + isOnSale: false, + creator: '0xcreator', + beneficiary: null, + createdAt: 1671033414000, + updatedAt: 1671033414000, + reviewedAt: 1671033414000, + soldAt: 1671033414000, + data: { + parcel: undefined, + estate: undefined, + wearable: undefined, + ens: undefined, + emote: undefined + }, + network: Network.MATIC, + chainId: ChainId.ETHEREUM_GOERLI, + firstListedAt: null + } + let ownersResponse: OwnersResponse = { + issuedId: 1, + ownerId: ownerIdMock, + orderStatus: 'open', + orderExpiresAt: '1671033414000', + tokenId: '1' + } + + afterEach(() => { + jest.resetAllMocks() + }) + + describe('Empty table', () => { + beforeEach(() => { + ;(nftAPI.nftAPI.getOwners as jest.Mock).mockResolvedValueOnce({ + data: [], + total: 0 + }) + }) + + it('should render the empty table message', async () => { + const { getByText, findByTestId } = renderWithProviders( + + ) + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + expect( + getByText(t('owners_table.there_are_no_owners')) + ).toBeInTheDocument() + }) + }) + + describe('Should render the table correctly', () => { + beforeEach(() => { + ;(nftAPI.nftAPI.getOwners as jest.Mock).mockResolvedValueOnce({ + data: [ownersResponse], + total: 1 + }) + }) + + it('should render the table', async () => { + const screen = renderWithProviders() + + const { findByTestId, container } = screen + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + expect(container.getElementsByClassName('OwnersTable')).not.toBe(null) + }) + + it('should render the table data correctly', async () => { + const screen = renderWithProviders() + + const { findByTestId, getByText } = screen + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + expect(getByText(ownersResponse.ownerId)).not.toBe(null) + expect(getByText(ownersResponse.issuedId)).not.toBe(null) + }) + }) + + describe('Pagination', () => { + describe('Should have pagination', () => { + beforeEach(() => { + ;(nftAPI.nftAPI.getOwners as jest.Mock).mockResolvedValueOnce({ + data: Array(ROWS_PER_PAGE + 2).fill(ownersResponse), + total: 7 + }) + }) + + it('should render the pagination correctly', async () => { + const screen = renderWithProviders() + + const { findByTestId, getByRole } = screen + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + const navigation = getByRole('navigation') + + expect(within(navigation).getByText('1')).toBeInTheDocument() + expect(within(navigation).getByText('2')).toBeInTheDocument() + }) + }) + + describe('Should not have pagination', () => { + beforeEach(() => { + ;(nftAPI.nftAPI.getOwners as jest.Mock).mockResolvedValueOnce({ + data: Array(ROWS_PER_PAGE - 1).fill(ownersResponse), + total: 5 + }) + }) + + it('should not render pagination as there is no need', async () => { + const screen = renderWithProviders() + + const { findByTestId, queryByRole } = screen + + const loader = await findByTestId('loader') + + await waitFor(() => { + expect(loader).not.toBeInTheDocument() + }) + + expect(queryByRole('navigation')).not.toBeInTheDocument() + }) + }) + }) +}) diff --git a/webapp/src/components/AssetPage/OwnersTable/OwnersTable.tsx b/webapp/src/components/AssetPage/OwnersTable/OwnersTable.tsx index 5914012777..3844b4fbb2 100644 --- a/webapp/src/components/AssetPage/OwnersTable/OwnersTable.tsx +++ b/webapp/src/components/AssetPage/OwnersTable/OwnersTable.tsx @@ -15,7 +15,7 @@ import ListedBadge from '../../ListedBadge' import { OrderDirection, Props } from './OwnersTable.types' import styles from './OwnersTable.module.css' -const ROWS_PER_PAGE = 6 +export const ROWS_PER_PAGE = 6 const INITIAL_PAGE = 1 const OwnersTable = (props: Props) => { @@ -57,7 +57,7 @@ const OwnersTable = (props: Props) => {
{isLoading ? (
- +
) : owners.length === 0 ? (
@@ -87,7 +87,7 @@ const OwnersTable = (props: Props) => { - + {owners?.map(owner => ( @@ -107,11 +107,11 @@ const OwnersTable = (props: Props) => { {owner.orderStatus === ListingStatus.OPEN && owner.orderExpiresAt && - +owner.orderExpiresAt >= Date.now() ? ( + Number(owner.orderExpiresAt) >= Date.now() ? ( ) : null}
- {asset && ( + {asset?.contractAddress && owner.tokenId && ( { } }) +jest.mock('decentraland-dapps/dist/modules/translation/utils', () => { + const module = jest.requireActual( + 'decentraland-dapps/dist/modules/translation/utils' + ) + return { + ...module, + T: ({ id, values }: typeof module['T']) => module.t(id, values) + } +}) + config({ path: path.resolve(process.cwd(), '.env.example') }) global.TextEncoder = TextEncoder global.TextDecoder = TextDecoder as any diff --git a/webapp/src/utils/tests.tsx b/webapp/src/utils/tests.tsx new file mode 100644 index 0000000000..0e16c19f4a --- /dev/null +++ b/webapp/src/utils/tests.tsx @@ -0,0 +1,84 @@ +import { render } from '@testing-library/react' +import createSagasMiddleware from 'redux-saga' +import { createMemoryHistory } from 'history' +import { Provider } from 'react-redux' +import { applyMiddleware, compose, createStore, Store } from 'redux' +import { ConnectedRouter, routerMiddleware } from 'connected-react-router' +import { createStorageMiddleware } from 'decentraland-dapps/dist/modules/storage/middleware' +import { storageReducerWrapper } from 'decentraland-dapps/dist/modules/storage/reducer' +import { createTransactionMiddleware } from 'decentraland-dapps/dist/modules/transaction/middleware' +import { CLEAR_TRANSACTIONS } from 'decentraland-dapps/dist/modules/transaction/actions' +import TranslationProvider from 'decentraland-dapps/dist/providers/TranslationProvider' +import { createRootReducer, RootState } from '../modules/reducer' +import * as locales from '../modules/translation/locales' +import { ARCHIVE_BID, UNARCHIVE_BID } from '../modules/bid/actions' +import { GENERATE_IDENTITY_SUCCESS } from '../modules/identity/actions' +import { SET_IS_TRYING_ON } from '../modules/ui/preview/actions' +import { rootSaga } from '../modules/sagas' +import { fetchTilesRequest } from '../modules/tile/actions' + +export const history = require('history').createBrowserHistory() + +export function initTestStore(preloadedState = {}) { + const rootReducer = storageReducerWrapper(createRootReducer(history)) + const sagasMiddleware = createSagasMiddleware() + const transactionMiddleware = createTransactionMiddleware() + const { storageMiddleware, loadStorageMiddleware } = createStorageMiddleware({ + storageKey: 'marketplace-v2', // this is the key used to save the state in localStorage (required) + paths: [ + ['ui', 'archivedBidIds'], + ['ui', 'preview', 'isTryingOn'], + ['identity', 'data'] + ], // array of paths from state to be persisted (optional) + actions: [ + CLEAR_TRANSACTIONS, + ARCHIVE_BID, + UNARCHIVE_BID, + GENERATE_IDENTITY_SUCCESS, + SET_IS_TRYING_ON + ], // array of actions types that will trigger a SAVE (optional) + migrations: {} // migration object that will migrate your localstorage (optional) + }) + + const middleware = applyMiddleware( + sagasMiddleware, + routerMiddleware(history), + transactionMiddleware, + storageMiddleware + ) + const enhancer = compose(middleware) + const store = createStore(rootReducer, preloadedState, enhancer) + + sagasMiddleware.run(rootSaga) + loadStorageMiddleware(store) + store.dispatch(fetchTilesRequest()) + + return store +} + +export function renderWithProviders( + component: JSX.Element, + { preloadedState, store }: { preloadedState?: RootState; store?: Store } = {} +) { + const initializedStore = + store || + initTestStore({ + ...(preloadedState || {}), + storage: { loading: false }, + translation: { data: locales, locale: 'en' } + }) + + const history = createMemoryHistory() + + function AppProviders({ children }: { children: JSX.Element }) { + return ( + + + {children} + + + ) + } + + return render(component, { wrapper: AppProviders }) +}