) : 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 })
+}