@0xsequence/marketplace-sdk 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/dist/{chunk-25CAMYCG.js → chunk-BB2PTJHI.js} +22 -20
- package/dist/chunk-BB2PTJHI.js.map +1 -0
- package/dist/{chunk-5ATGT5S4.js → chunk-EZFCQZHU.js} +14 -6
- package/dist/chunk-EZFCQZHU.js.map +1 -0
- package/dist/{chunk-DFI52A2E.js → chunk-KCLMSSPS.js} +364 -242
- package/dist/chunk-KCLMSSPS.js.map +1 -0
- package/dist/{chunk-XUNDLCEH.js → chunk-LDZZUYG7.js} +2 -2
- package/dist/{chunk-QTV77W42.js → chunk-SFSFIGHM.js} +45 -35
- package/dist/chunk-SFSFIGHM.js.map +1 -0
- package/dist/{chunk-FSJKN4YN.js → chunk-ZSCZLHKX.js} +194 -2
- package/dist/chunk-ZSCZLHKX.js.map +1 -0
- package/dist/{chunk-FH4TZRDV.js → chunk-ZVTG6US2.js} +2 -2
- package/dist/index.css +4 -4
- package/dist/index.css.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{lowestListing-DUZ_nYml.d.ts → lowestListing-W7P4EkC3.d.ts} +34 -11
- package/dist/react/_internal/databeat/index.js +5 -5
- package/dist/react/_internal/index.d.ts +1 -1
- package/dist/react/_internal/index.js +3 -1
- package/dist/react/_internal/wagmi/index.d.ts +3 -2
- package/dist/react/_internal/wagmi/index.js +3 -1
- package/dist/react/hooks/index.d.ts +8 -5
- package/dist/react/hooks/index.js +6 -4
- package/dist/react/hooks/options/index.js +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +9 -7
- package/dist/react/queries/index.d.ts +1 -1
- package/dist/react/queries/index.js +6 -2
- package/dist/react/ssr/index.js +1 -1
- package/dist/react/ui/components/collectible-card/index.d.ts +3 -2
- package/dist/react/ui/components/collectible-card/index.js +7 -7
- package/dist/react/ui/icons/index.js +1 -1
- package/dist/react/ui/index.js +7 -7
- package/dist/react/ui/modals/_internal/components/actionModal/index.js +5 -5
- package/dist/types/index.js +1 -1
- package/dist/utils/index.js +1 -1
- package/package.json +19 -19
- package/src/react/_internal/api/__mocks__/marketplace.msw.ts +35 -21
- package/src/react/_internal/wagmi/__tests__/create-config.test.ts +1 -11
- package/src/react/_internal/wagmi/get-connectors.ts +27 -24
- package/src/react/hooks/__tests__/useCancelTransactionSteps.test.tsx +4 -9
- package/src/react/hooks/__tests__/useGenerateCancelTransaction.test.tsx +5 -4
- package/src/react/hooks/__tests__/useGenerateListingTransaction.test.tsx +14 -10
- package/src/react/hooks/__tests__/useGenerateOfferTransaction.test.tsx +115 -65
- package/src/react/hooks/__tests__/useGenerateSellTransaction.test.tsx +10 -7
- package/src/react/hooks/__tests__/useInventory.test.tsx +294 -0
- package/src/react/hooks/index.ts +1 -0
- package/src/react/hooks/useAutoSelectFeeOption.tsx +10 -3
- package/src/react/hooks/useCancelOrder.tsx +1 -0
- package/src/react/hooks/useCancelTransactionSteps.tsx +18 -4
- package/src/react/hooks/useGenerateOfferTransaction.tsx +11 -1
- package/src/react/hooks/useInventory.tsx +15 -0
- package/src/react/hooks/util/optimisticCancelUpdates.ts +115 -0
- package/src/react/queries/index.ts +1 -0
- package/src/react/queries/inventory.ts +303 -0
- package/src/react/queries/listBalances.ts +1 -8
- package/src/react/queries/listCollectibles.ts +12 -3
- package/src/react/ui/components/_internals/action-button/__tests__/ActionButtonBody.test.tsx +27 -94
- package/src/react/ui/components/_internals/action-button/__tests__/NonOwnerActions.test.tsx +59 -0
- package/src/react/ui/components/_internals/action-button/__tests__/OwnerActions.test.tsx +73 -0
- package/src/react/ui/components/_internals/action-button/__tests__/useActionButtonLogic.test.tsx +77 -0
- package/src/react/ui/components/_internals/action-button/components/ActionButtonBody.tsx +3 -2
- package/src/react/ui/components/_internals/action-button/hooks/useActionButtonLogic.ts +4 -3
- package/src/react/ui/components/collectible-card/CollectibleAsset.tsx +1 -0
- package/src/react/ui/components/collectible-card/CollectibleCard.tsx +18 -12
- package/src/react/ui/components/collectible-card/__tests__/CollectibleAsset.test.tsx +200 -0
- package/src/react/ui/components/collectible-card/__tests__/CollectibleCard.test.tsx +92 -123
- package/src/react/ui/components/collectible-card/__tests__/Footer.test.tsx +136 -0
- package/src/react/ui/modals/BuyModal/__tests__/Modal.test.tsx +2 -8
- package/src/react/ui/modals/CreateListingModal/__tests__/Modal.test.tsx +74 -104
- package/src/react/ui/modals/MakeOfferModal/__tests__/Modal.test.tsx +108 -78
- package/src/react/ui/modals/SellModal/__tests__/Modal.test.tsx +72 -135
- package/src/react/ui/modals/_internal/components/actionModal/ActionModal.test.tsx +286 -0
- package/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx +16 -4
- package/src/react/ui/modals/_internal/components/currencyOptionsSelect/__tests__/index.test.tsx +35 -132
- package/src/react/ui/modals/_internal/components/floorPriceText/__tests__/FloorPriceText.test.tsx +199 -0
- package/src/react/ui/modals/_internal/components/priceInput/__tests__/PriceInput.test.tsx +55 -0
- package/src/react/ui/modals/_internal/components/priceInput/index.tsx +1 -1
- package/src/react/ui/modals/_internal/components/selectWaasFeeOptions/__tests__/ActionButtons.test.tsx +72 -0
- package/src/react/ui/modals/_internal/components/selectWaasFeeOptions/__tests__/BalanceIndicator.test.tsx +50 -0
- package/src/react/ui/modals/_internal/components/selectWaasFeeOptions/__tests__/SelectWaasFeeOptions.test.tsx +193 -0
- package/src/react/ui/modals/_internal/components/switchChainModal/index.tsx +2 -2
- package/test/const.ts +24 -0
- package/test/test-utils.tsx +85 -47
- package/.changeset/flat-parks-clean.md +0 -8
- package/.changeset/red-buckets-deny.md +0 -6
- package/.changeset/seven-doors-taste.md +0 -5
- package/dist/chunk-25CAMYCG.js.map +0 -1
- package/dist/chunk-5ATGT5S4.js.map +0 -1
- package/dist/chunk-DFI52A2E.js.map +0 -1
- package/dist/chunk-FSJKN4YN.js.map +0 -1
- package/dist/chunk-QTV77W42.js.map +0 -1
- package/src/react/ui/components/_internals/action-button/__tests__/ActionButton.test.tsx +0 -107
- package/src/react/ui/modals/_internal/components/priceInput/__tests__/index.test.tsx +0 -164
- /package/dist/{chunk-XUNDLCEH.js.map → chunk-LDZZUYG7.js.map} +0 -0
- /package/dist/{chunk-FH4TZRDV.js.map → chunk-ZVTG6US2.js.map} +0 -0
package/src/react/ui/components/_internals/action-button/__tests__/useActionButtonLogic.test.tsx
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { renderHook, waitFor } from '@test';
|
|
4
|
+
import { createMockWallet } from '@test/mocks/wallet';
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import * as walletModule from '../../../../../_internal/wallet/useWallet';
|
|
7
|
+
import { useActionButtonLogic } from '../hooks/useActionButtonLogic';
|
|
8
|
+
import { actionButtonStore, setPendingAction } from '../store';
|
|
9
|
+
import { CollectibleCardAction } from '../types';
|
|
10
|
+
|
|
11
|
+
describe('useActionButtonLogic', () => {
|
|
12
|
+
const onCannotPerformActionMock = vi.fn();
|
|
13
|
+
const defaultProps = {
|
|
14
|
+
tokenId: '123',
|
|
15
|
+
action:
|
|
16
|
+
(CollectibleCardAction.BUY as CollectibleCardAction.BUY) ||
|
|
17
|
+
CollectibleCardAction.OFFER,
|
|
18
|
+
owned: false,
|
|
19
|
+
onCannotPerformAction: onCannotPerformActionMock,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
actionButtonStore.pendingAction.set(null);
|
|
27
|
+
|
|
28
|
+
vi.spyOn(walletModule, 'useWallet').mockReturnValue({
|
|
29
|
+
wallet: createMockWallet(),
|
|
30
|
+
isError: false,
|
|
31
|
+
isLoading: false,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('restricts owners from performing buy/offer actions', async () => {
|
|
36
|
+
setPendingAction(defaultProps.action, vi.fn(), '123');
|
|
37
|
+
|
|
38
|
+
const { result } = renderHook(() =>
|
|
39
|
+
useActionButtonLogic({
|
|
40
|
+
...defaultProps,
|
|
41
|
+
owned: true,
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Callback should be called with the action
|
|
46
|
+
expect(result.current.isOwnerAction).toBe(false);
|
|
47
|
+
expect(onCannotPerformActionMock).toHaveBeenCalledWith(
|
|
48
|
+
CollectibleCardAction.BUY,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('executes pending action when user becomes connected', async () => {
|
|
53
|
+
const executePendingActionMock = vi.fn();
|
|
54
|
+
|
|
55
|
+
setPendingAction(
|
|
56
|
+
CollectibleCardAction.BUY,
|
|
57
|
+
executePendingActionMock,
|
|
58
|
+
defaultProps.tokenId,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
renderHook(() =>
|
|
62
|
+
useActionButtonLogic({
|
|
63
|
+
...defaultProps,
|
|
64
|
+
owned: false,
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
waitFor(
|
|
69
|
+
() => {
|
|
70
|
+
expect(executePendingActionMock).toHaveBeenCalled();
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
timeout: 1000,
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useOpenConnectModal } from '@0xsequence/connect';
|
|
4
4
|
import { Button, type IconProps } from '@0xsequence/design-system';
|
|
5
5
|
import type { ComponentType } from 'react';
|
|
6
|
-
import {
|
|
6
|
+
import { useWallet } from '../../../../../_internal/wallet/useWallet';
|
|
7
7
|
import { setPendingAction } from '../store';
|
|
8
8
|
import type { CollectibleCardAction } from '../types';
|
|
9
9
|
|
|
@@ -22,7 +22,8 @@ export function ActionButtonBody({
|
|
|
22
22
|
icon,
|
|
23
23
|
action,
|
|
24
24
|
}: ActionButtonBodyProps) {
|
|
25
|
-
const {
|
|
25
|
+
const { wallet } = useWallet();
|
|
26
|
+
const address = wallet?.address;
|
|
26
27
|
const { setOpenConnectModal } = useOpenConnectModal();
|
|
27
28
|
|
|
28
29
|
const handleClick = (e: React.MouseEvent) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useWallet } from '../../../../../_internal/wallet/useWallet';
|
|
5
5
|
import {
|
|
6
6
|
actionButtonStore,
|
|
7
7
|
clearPendingAction,
|
|
@@ -24,7 +24,8 @@ export const useActionButtonLogic = ({
|
|
|
24
24
|
action,
|
|
25
25
|
onCannotPerformAction,
|
|
26
26
|
}: UseActionButtonLogicProps) => {
|
|
27
|
-
const {
|
|
27
|
+
const { wallet } = useWallet();
|
|
28
|
+
const address = wallet?.address;
|
|
28
29
|
const actionsThatOwnersCannotPerform = [
|
|
29
30
|
CollectibleCardAction.BUY,
|
|
30
31
|
CollectibleCardAction.OFFER,
|
|
@@ -38,7 +39,7 @@ export const useActionButtonLogic = ({
|
|
|
38
39
|
owned &&
|
|
39
40
|
actionButtonStore.pendingAction.get() &&
|
|
40
41
|
address &&
|
|
41
|
-
|
|
42
|
+
actionsThatOwnersCannotPerform.includes(action) &&
|
|
42
43
|
actionButtonStore.pendingAction.get()?.collectibleId === tokenId
|
|
43
44
|
) {
|
|
44
45
|
onCannotPerformAction?.(
|
|
@@ -16,7 +16,10 @@ import { Footer } from './Footer';
|
|
|
16
16
|
|
|
17
17
|
function CollectibleSkeleton() {
|
|
18
18
|
return (
|
|
19
|
-
<div
|
|
19
|
+
<div
|
|
20
|
+
data-testid="collectible-card-skeleton"
|
|
21
|
+
className="w-card-width overflow-hidden rounded-xl border border-border-base focus-visible:border-border-focus focus-visible:shadow-none focus-visible:outline-focus active:border-border-focus active:shadow-none"
|
|
22
|
+
>
|
|
20
23
|
<div className="relative aspect-square overflow-hidden bg-background-secondary">
|
|
21
24
|
<Skeleton
|
|
22
25
|
size="lg"
|
|
@@ -40,7 +43,7 @@ type CollectibleCardProps = {
|
|
|
40
43
|
collectionAddress: Hex;
|
|
41
44
|
orderbookKind?: OrderbookKind;
|
|
42
45
|
collectionType?: ContractType;
|
|
43
|
-
|
|
46
|
+
collectible: CollectibleOrder | undefined;
|
|
44
47
|
onCollectibleClick?: (tokenId: string) => void;
|
|
45
48
|
onOfferClick?: ({
|
|
46
49
|
order,
|
|
@@ -51,6 +54,7 @@ type CollectibleCardProps = {
|
|
|
51
54
|
}) => void;
|
|
52
55
|
assetSrcPrefixUrl?: string;
|
|
53
56
|
balance?: string;
|
|
57
|
+
balanceIsLoading: boolean;
|
|
54
58
|
cardLoading?: boolean;
|
|
55
59
|
/**
|
|
56
60
|
* Callback function that is called when the user attempts to perform an action
|
|
@@ -77,22 +81,23 @@ export function CollectibleCard({
|
|
|
77
81
|
collectionAddress,
|
|
78
82
|
orderbookKind,
|
|
79
83
|
collectionType,
|
|
80
|
-
|
|
84
|
+
collectible,
|
|
81
85
|
onCollectibleClick,
|
|
82
86
|
onOfferClick,
|
|
83
87
|
balance,
|
|
88
|
+
balanceIsLoading,
|
|
84
89
|
cardLoading,
|
|
85
90
|
onCannotPerformAction,
|
|
86
91
|
assetSrcPrefixUrl,
|
|
87
92
|
}: CollectibleCardProps) {
|
|
88
|
-
const collectibleMetadata =
|
|
89
|
-
const highestOffer =
|
|
93
|
+
const collectibleMetadata = collectible?.metadata;
|
|
94
|
+
const highestOffer = collectible?.offer;
|
|
90
95
|
|
|
91
96
|
const { data: lowestListingCurrency } = useCurrency({
|
|
92
97
|
chainId,
|
|
93
|
-
currencyAddress:
|
|
98
|
+
currencyAddress: collectible?.listing?.priceCurrencyAddress,
|
|
94
99
|
query: {
|
|
95
|
-
enabled: !!
|
|
100
|
+
enabled: !!collectible?.listing?.priceCurrencyAddress,
|
|
96
101
|
},
|
|
97
102
|
});
|
|
98
103
|
|
|
@@ -103,14 +108,15 @@ export function CollectibleCard({
|
|
|
103
108
|
const action = (
|
|
104
109
|
balance
|
|
105
110
|
? (highestOffer && CollectibleCardAction.SELL) ||
|
|
106
|
-
(!
|
|
111
|
+
(!collectible?.listing && CollectibleCardAction.LIST) ||
|
|
107
112
|
CollectibleCardAction.TRANSFER
|
|
108
|
-
: (
|
|
113
|
+
: (collectible?.listing && CollectibleCardAction.BUY) ||
|
|
109
114
|
CollectibleCardAction.OFFER
|
|
110
115
|
) as CollectibleCardAction;
|
|
111
116
|
|
|
112
117
|
return (
|
|
113
118
|
<div
|
|
119
|
+
data-testid="collectible-card"
|
|
114
120
|
className="w-card-width overflow-hidden rounded-xl border border-border-base bg-background-primary focus-visible:border-border-focus focus-visible:shadow-focus-ring focus-visible:outline-focus active:border-border-focus active:shadow-active-ring"
|
|
115
121
|
onClick={() => onCollectibleClick?.(collectibleId)}
|
|
116
122
|
onKeyDown={(e) => {
|
|
@@ -132,13 +138,13 @@ export function CollectibleCard({
|
|
|
132
138
|
type={collectionType}
|
|
133
139
|
onOfferClick={(e) => onOfferClick?.({ order: highestOffer, e })}
|
|
134
140
|
highestOffer={highestOffer}
|
|
135
|
-
lowestListingPriceAmount={
|
|
141
|
+
lowestListingPriceAmount={collectible?.listing?.priceAmount}
|
|
136
142
|
lowestListingCurrency={lowestListingCurrency}
|
|
137
143
|
balance={balance}
|
|
138
144
|
decimals={collectibleMetadata?.decimals}
|
|
139
145
|
/>
|
|
140
146
|
|
|
141
|
-
{(highestOffer ||
|
|
147
|
+
{(highestOffer || collectible) && !balanceIsLoading && (
|
|
142
148
|
<div className="-bottom-action-offset absolute flex w-full items-center justify-center bg-overlay-light p-2 backdrop-blur transition-transform duration-200 ease-in-out group-hover:translate-y-[-44px]">
|
|
143
149
|
<ActionButton
|
|
144
150
|
chainId={chainId}
|
|
@@ -147,7 +153,7 @@ export function CollectibleCard({
|
|
|
147
153
|
orderbookKind={orderbookKind}
|
|
148
154
|
action={action}
|
|
149
155
|
highestOffer={highestOffer}
|
|
150
|
-
lowestListing={
|
|
156
|
+
lowestListing={collectible?.listing}
|
|
151
157
|
owned={!!balance}
|
|
152
158
|
onCannotPerformAction={onCannotPerformAction}
|
|
153
159
|
/>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { render, screen } from '@test/test-utils';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import type { TokenMetadata } from '../../../../_internal';
|
|
4
|
+
import { CollectibleAsset } from '../CollectibleAsset';
|
|
5
|
+
|
|
6
|
+
describe('CollectibleAsset', () => {
|
|
7
|
+
it('renders image content correctly with proper loading states and fallback', async () => {
|
|
8
|
+
const originalImage = window.Image;
|
|
9
|
+
|
|
10
|
+
// We need to use a proper constructor function to match the Image interface
|
|
11
|
+
const MockImage = function (this: HTMLImageElement) {
|
|
12
|
+
this.src = '';
|
|
13
|
+
this.alt = '';
|
|
14
|
+
this.className = '';
|
|
15
|
+
this.onload = null;
|
|
16
|
+
this.onerror = null;
|
|
17
|
+
return this;
|
|
18
|
+
} as unknown as typeof Image;
|
|
19
|
+
|
|
20
|
+
window.Image = MockImage;
|
|
21
|
+
|
|
22
|
+
const mockMetadata: Partial<TokenMetadata> = {
|
|
23
|
+
tokenId: '1',
|
|
24
|
+
name: 'Test Collectible',
|
|
25
|
+
image: 'https://example.com/test-image.png',
|
|
26
|
+
attributes: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Initial render should show the loading skeleton
|
|
30
|
+
const { rerender } = render(
|
|
31
|
+
<CollectibleAsset
|
|
32
|
+
name="Test Collectible"
|
|
33
|
+
collectibleMetadata={mockMetadata as TokenMetadata}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// check if skeleton is rendered during loading
|
|
38
|
+
const skeleton = screen.getByTestId('collectible-asset-skeleton');
|
|
39
|
+
expect(skeleton).toBeInTheDocument();
|
|
40
|
+
|
|
41
|
+
// trigger the image load event to simulate successful loading
|
|
42
|
+
const imgElement = document.querySelector('img');
|
|
43
|
+
expect(imgElement).not.toBeNull();
|
|
44
|
+
|
|
45
|
+
if (imgElement) {
|
|
46
|
+
expect(imgElement.getAttribute('src')).toBe(
|
|
47
|
+
'https://example.com/test-image.png',
|
|
48
|
+
);
|
|
49
|
+
expect(imgElement.getAttribute('alt')).toBe('Test Collectible');
|
|
50
|
+
|
|
51
|
+
// initial state should be invisible due to loading
|
|
52
|
+
expect(imgElement.className).toContain('invisible');
|
|
53
|
+
|
|
54
|
+
// successful image load
|
|
55
|
+
imgElement.dispatchEvent(new Event('load'));
|
|
56
|
+
|
|
57
|
+
// after loading, the image should be visible
|
|
58
|
+
expect(imgElement.className).toContain('visible');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// failing image that should use fallback
|
|
62
|
+
const mockMetadataWithBadImage: Partial<TokenMetadata> = {
|
|
63
|
+
tokenId: '1',
|
|
64
|
+
name: 'Test Collectible',
|
|
65
|
+
image: 'https://example.com/bad-image.png',
|
|
66
|
+
attributes: [],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
rerender(
|
|
70
|
+
<CollectibleAsset
|
|
71
|
+
name="Test Collectible"
|
|
72
|
+
collectibleMetadata={mockMetadataWithBadImage as TokenMetadata}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const updatedImgElement = document.querySelector('img');
|
|
77
|
+
if (updatedImgElement) {
|
|
78
|
+
// simulate image load error
|
|
79
|
+
updatedImgElement.dispatchEvent(new Event('error'));
|
|
80
|
+
|
|
81
|
+
// after error, the src should be changed to the placeholder
|
|
82
|
+
expect(updatedImgElement.className).toContain('visible');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// restore the original Image implementation
|
|
86
|
+
window.Image = originalImage;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('handles video content with appropriate controls and loading states', () => {
|
|
90
|
+
// Create a mock for the HTMLVideoElement addEventListener
|
|
91
|
+
const originalAddEventListener =
|
|
92
|
+
HTMLVideoElement.prototype.addEventListener;
|
|
93
|
+
HTMLVideoElement.prototype.addEventListener = vi.fn(
|
|
94
|
+
(event: string, handler: EventListenerOrEventListenerObject) => {
|
|
95
|
+
// Immediately call the loadedmetadata handler to simulate video loaded
|
|
96
|
+
if (event === 'loadedmetadata' && typeof handler === 'function') {
|
|
97
|
+
handler(new Event('loadedmetadata'));
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Mock browser detection for Safari
|
|
103
|
+
const originalUserAgent = navigator.userAgent;
|
|
104
|
+
Object.defineProperty(navigator, 'userAgent', {
|
|
105
|
+
value:
|
|
106
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15',
|
|
107
|
+
configurable: true,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Mock video metadata
|
|
111
|
+
const mockVideoMetadata: Partial<TokenMetadata> = {
|
|
112
|
+
tokenId: '1',
|
|
113
|
+
name: 'Video Collectible',
|
|
114
|
+
video: 'https://example.com/video.mp4',
|
|
115
|
+
attributes: [],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
render(
|
|
119
|
+
<CollectibleAsset
|
|
120
|
+
name="Video Collectible"
|
|
121
|
+
collectibleMetadata={mockVideoMetadata as TokenMetadata}
|
|
122
|
+
/>,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Check that video element is present with correct attributes
|
|
126
|
+
const videoElement = document.querySelector('video');
|
|
127
|
+
expect(videoElement).not.toBeNull();
|
|
128
|
+
|
|
129
|
+
if (videoElement) {
|
|
130
|
+
// Video source should be set correctly
|
|
131
|
+
const sourceElement = videoElement.querySelector('source');
|
|
132
|
+
expect(sourceElement).not.toBeNull();
|
|
133
|
+
expect(sourceElement?.getAttribute('src')).toBe(
|
|
134
|
+
'https://example.com/video.mp4',
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Video should have correct attributes for NFT display
|
|
138
|
+
expect(videoElement.autoplay).toBe(true);
|
|
139
|
+
expect(videoElement.loop).toBe(true);
|
|
140
|
+
expect(videoElement.controls).toBe(true);
|
|
141
|
+
expect(videoElement.playsInline).toBe(true);
|
|
142
|
+
expect(videoElement.muted).toBe(true);
|
|
143
|
+
|
|
144
|
+
// In Safari, pointer-events-none should be applied
|
|
145
|
+
expect(videoElement.className).toContain('pointer-events-none');
|
|
146
|
+
|
|
147
|
+
// After metadata loaded, video should be visible
|
|
148
|
+
expect(videoElement.className).toContain('visible');
|
|
149
|
+
expect(videoElement.className).not.toContain('invisible');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Clean up mocks
|
|
153
|
+
HTMLVideoElement.prototype.addEventListener = originalAddEventListener;
|
|
154
|
+
Object.defineProperty(navigator, 'userAgent', {
|
|
155
|
+
value: originalUserAgent,
|
|
156
|
+
configurable: true,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('handles HTML content in iframes with proper sandboxing', () => {
|
|
161
|
+
// Mock HTML content metadata
|
|
162
|
+
const mockHtmlMetadata: Partial<TokenMetadata> = {
|
|
163
|
+
tokenId: '1',
|
|
164
|
+
name: 'HTML Collectible',
|
|
165
|
+
animation_url: 'https://example.com/interactive.html',
|
|
166
|
+
attributes: [],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
render(
|
|
170
|
+
<CollectibleAsset
|
|
171
|
+
name="HTML Collectible"
|
|
172
|
+
collectibleMetadata={mockHtmlMetadata as TokenMetadata}
|
|
173
|
+
/>,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Check that iframe element is present with correct attributes
|
|
177
|
+
const iframeElement = document.querySelector('iframe');
|
|
178
|
+
expect(iframeElement).not.toBeNull();
|
|
179
|
+
|
|
180
|
+
if (iframeElement) {
|
|
181
|
+
// iframe source should be set correctly
|
|
182
|
+
expect(iframeElement.getAttribute('src')).toBe(
|
|
183
|
+
'https://example.com/interactive.html',
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// iframe should have appropriate attributes for security
|
|
187
|
+
expect(iframeElement.getAttribute('sandbox')).toBe('allow-scripts');
|
|
188
|
+
|
|
189
|
+
// iframe should have title for accessibility
|
|
190
|
+
expect(iframeElement.getAttribute('title')).toBe('HTML Collectible');
|
|
191
|
+
|
|
192
|
+
// iframe should have proper styling
|
|
193
|
+
expect(iframeElement.className).toContain('aspect-square');
|
|
194
|
+
expect(iframeElement.className).toContain('w-full');
|
|
195
|
+
|
|
196
|
+
// Verify border styling
|
|
197
|
+
expect(iframeElement.style.border).toBe('0px');
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -1,125 +1,94 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TEST_COLLECTIBLE, TEST_CURRENCY } from '@test/const';
|
|
2
|
+
import { fireEvent, render, screen } from '@test/test-utils';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
type CollectibleOrder,
|
|
6
|
+
ContractType,
|
|
7
|
+
OrderSide,
|
|
8
|
+
OrderbookKind,
|
|
9
|
+
} from '../../../../_internal';
|
|
10
|
+
import { mockTokenMetadata } from '../../../../_internal/api/__mocks__/indexer.msw';
|
|
11
|
+
import { mockOrder } from '../../../../_internal/api/__mocks__/marketplace.msw';
|
|
12
|
+
import * as hooks from '../../../../hooks';
|
|
13
|
+
import { CollectibleCard } from '../CollectibleCard';
|
|
2
14
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// .closest('article');
|
|
83
|
-
// if (collectibleElement) {
|
|
84
|
-
// fireEvent.click(collectibleElement);
|
|
85
|
-
// }
|
|
86
|
-
// expect(onCollectibleClick).toHaveBeenCalledWith(defaultProps.collectibleId);
|
|
87
|
-
// });
|
|
88
|
-
// it('should open external url in new tab when external link is clicked', () => {
|
|
89
|
-
// render(<CollectibleCard {...defaultProps} />);
|
|
90
|
-
// const externalLink = screen.getByRole('link');
|
|
91
|
-
// expect(externalLink).toHaveAttribute('href', 'https://example.com');
|
|
92
|
-
// expect(externalLink).toHaveAttribute('target', '_blank');
|
|
93
|
-
// expect(externalLink).toHaveAttribute('rel', 'noopener noreferrer');
|
|
94
|
-
// });
|
|
95
|
-
// it('should use fallback image when image load fails', async () => {
|
|
96
|
-
// render(<CollectibleCard {...defaultProps} />);
|
|
97
|
-
// const image = screen.getByAltText('Test Collectible');
|
|
98
|
-
// // Trigger error
|
|
99
|
-
// fireEvent.error(image);
|
|
100
|
-
// // Check if image source is changed to fallback
|
|
101
|
-
// expect(image).toHaveAttribute('src', expect.stringContaining('chess-tile'));
|
|
102
|
-
// });
|
|
103
|
-
// it('should show balance for ERC-1155 tokens', () => {
|
|
104
|
-
// const erc1155Props = {
|
|
105
|
-
// ...defaultProps,
|
|
106
|
-
// collectionType: ContractType.ERC1155,
|
|
107
|
-
// balance: '5',
|
|
108
|
-
// };
|
|
109
|
-
// render(<CollectibleCard {...erc1155Props} />);
|
|
110
|
-
// expect(screen.getByText('Owned: 5')).toBeInTheDocument();
|
|
111
|
-
// });
|
|
112
|
-
// it('should call onCannotPerformAction when action cannot be performed', async () => {
|
|
113
|
-
// const onCannotPerformAction = vi.fn();
|
|
114
|
-
// render(
|
|
115
|
-
// <CollectibleCard
|
|
116
|
-
// {...defaultProps}
|
|
117
|
-
// onCannotPerformAction={onCannotPerformAction}
|
|
118
|
-
// balance="1" // Make it owned to test owner actions
|
|
119
|
-
// />,
|
|
120
|
-
// );
|
|
121
|
-
// // We can't directly test this without mocking the useActionButtonLogic hook
|
|
122
|
-
// // Just testing the prop passes through
|
|
123
|
-
// expect(onCannotPerformAction).not.toHaveBeenCalled();
|
|
124
|
-
// });
|
|
15
|
+
const defaultProps = {
|
|
16
|
+
collectibleId: '1',
|
|
17
|
+
chainId: 1,
|
|
18
|
+
collectionAddress: TEST_COLLECTIBLE.collectionAddress,
|
|
19
|
+
collectible: {
|
|
20
|
+
order: mockOrder,
|
|
21
|
+
listing: { ...mockOrder, side: OrderSide.listing },
|
|
22
|
+
offer: { ...mockOrder, side: OrderSide.offer },
|
|
23
|
+
metadata: {
|
|
24
|
+
...mockTokenMetadata,
|
|
25
|
+
tokenId: mockTokenMetadata.tokenId as string,
|
|
26
|
+
assets: mockTokenMetadata.assets
|
|
27
|
+
? mockTokenMetadata.assets.map((asset) => ({
|
|
28
|
+
...asset,
|
|
29
|
+
tokenId: asset.tokenId || '',
|
|
30
|
+
}))
|
|
31
|
+
: undefined,
|
|
32
|
+
},
|
|
33
|
+
} as CollectibleOrder,
|
|
34
|
+
balance: '100',
|
|
35
|
+
balanceIsLoading: false,
|
|
36
|
+
cardLoading: false,
|
|
37
|
+
onCannotPerformAction: vi.fn(),
|
|
38
|
+
assetSrcPrefixUrl: 'https://example.com/',
|
|
39
|
+
orderbookKind: OrderbookKind.sequence_marketplace_v2,
|
|
40
|
+
collectionType: ContractType.ERC721,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('CollectibleCard', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
vi.restoreAllMocks();
|
|
47
|
+
|
|
48
|
+
const useCurrencySpy = vi.spyOn(hooks, 'useCurrency');
|
|
49
|
+
useCurrencySpy.mockReturnValue({
|
|
50
|
+
data: TEST_CURRENCY,
|
|
51
|
+
} as ReturnType<typeof hooks.useCurrency>);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('Renders correctly with valid props and shows proper collectible details', () => {
|
|
55
|
+
render(<CollectibleCard {...defaultProps} />);
|
|
56
|
+
|
|
57
|
+
expect(screen.getByText('Mock NFT')).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByText('1 TEST')).toBeInTheDocument();
|
|
59
|
+
// there is an offer
|
|
60
|
+
expect(screen.getByTitle('Notification Bell')).toBeInTheDocument();
|
|
61
|
+
expect(screen.getByRole('img', { name: 'Mock NFT' })).toHaveAttribute(
|
|
62
|
+
'src',
|
|
63
|
+
defaultProps.assetSrcPrefixUrl + defaultProps.collectible.metadata.image,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('Handles loading state by showing skeleton component', () => {
|
|
68
|
+
render(<CollectibleCard {...defaultProps} cardLoading={true} />);
|
|
69
|
+
expect(screen.getByTestId('collectible-card-skeleton')).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('Triggers appropriate callbacks when collectible or action buttons are clicked', () => {
|
|
73
|
+
const onCollectibleClick = vi.fn();
|
|
74
|
+
const onOfferClick = vi.fn();
|
|
75
|
+
|
|
76
|
+
render(
|
|
77
|
+
<CollectibleCard
|
|
78
|
+
{...defaultProps}
|
|
79
|
+
onCollectibleClick={onCollectibleClick}
|
|
80
|
+
onOfferClick={onOfferClick}
|
|
81
|
+
/>,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const notificationBell = screen.getByRole('button', {
|
|
85
|
+
name: 'Notification Bell',
|
|
86
|
+
});
|
|
87
|
+
fireEvent.click(notificationBell);
|
|
88
|
+
expect(onOfferClick).toHaveBeenCalled();
|
|
89
|
+
|
|
90
|
+
const collectibleCard = screen.getByTestId('collectible-card');
|
|
91
|
+
fireEvent.click(collectibleCard);
|
|
92
|
+
expect(onCollectibleClick).toHaveBeenCalled();
|
|
93
|
+
});
|
|
125
94
|
});
|