@0xsequence/marketplace-sdk 0.8.4 → 0.8.6

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/{chunk-VF3LWBQB.js → chunk-6SEJI7YS.js} +170 -9
  3. package/dist/chunk-6SEJI7YS.js.map +1 -0
  4. package/dist/{chunk-25CAMYCG.js → chunk-BB2PTJHI.js} +22 -20
  5. package/dist/chunk-BB2PTJHI.js.map +1 -0
  6. package/dist/{chunk-XUNDLCEH.js → chunk-LDZZUYG7.js} +2 -2
  7. package/dist/{chunk-44YGZVBS.js → chunk-QMWMJVTX.js} +2 -2
  8. package/dist/{chunk-HRL2TMXU.js → chunk-TGFX3TMV.js} +44 -34
  9. package/dist/{chunk-HRL2TMXU.js.map → chunk-TGFX3TMV.js.map} +1 -1
  10. package/dist/{chunk-VBRJ2OPM.js → chunk-V3NVAVHV.js} +2 -2
  11. package/dist/index.css +171 -36
  12. package/dist/index.css.map +1 -1
  13. package/dist/index.js +6 -6
  14. package/dist/react/_internal/databeat/index.js +5 -5
  15. package/dist/react/_internal/index.d.ts +1 -1
  16. package/dist/react/_internal/index.js +3 -1
  17. package/dist/react/_internal/wagmi/index.d.ts +3 -2
  18. package/dist/react/_internal/wagmi/index.js +3 -1
  19. package/dist/react/hooks/index.d.ts +246 -29
  20. package/dist/react/hooks/index.js +8 -4
  21. package/dist/react/hooks/options/index.js +2 -2
  22. package/dist/react/index.d.ts +2 -1
  23. package/dist/react/index.js +11 -7
  24. package/dist/react/queries/index.js +1 -1
  25. package/dist/react/ssr/index.js +1 -1
  26. package/dist/react/ui/components/collectible-card/index.d.ts +3 -2
  27. package/dist/react/ui/components/collectible-card/index.js +7 -7
  28. package/dist/react/ui/icons/index.js +2 -2
  29. package/dist/react/ui/index.js +7 -7
  30. package/dist/react/ui/modals/_internal/components/actionModal/index.js +5 -5
  31. package/dist/types/index.js +1 -1
  32. package/dist/utils/abi/index.js +5 -5
  33. package/dist/utils/index.js +6 -6
  34. package/package.json +20 -19
  35. package/src/react/_internal/wagmi/__tests__/create-config.test.ts +1 -11
  36. package/src/react/_internal/wagmi/get-connectors.ts +27 -24
  37. package/src/react/hooks/__tests__/useAutoSelectFeeOption.test.tsx +21 -75
  38. package/src/react/hooks/__tests__/useCurrencyBalance.test.tsx +4 -25
  39. package/src/react/hooks/index.ts +1 -0
  40. package/src/react/hooks/useFilterState.tsx +181 -0
  41. package/src/react/hooks/useFilters.tsx +24 -0
  42. package/src/react/ui/components/_internals/action-button/__tests__/ActionButtonBody.test.tsx +27 -94
  43. package/src/react/ui/components/_internals/action-button/__tests__/NonOwnerActions.test.tsx +59 -0
  44. package/src/react/ui/components/_internals/action-button/__tests__/OwnerActions.test.tsx +73 -0
  45. package/src/react/ui/components/_internals/action-button/__tests__/useActionButtonLogic.test.tsx +77 -0
  46. package/src/react/ui/components/_internals/action-button/components/ActionButtonBody.tsx +3 -2
  47. package/src/react/ui/components/_internals/action-button/hooks/useActionButtonLogic.ts +4 -3
  48. package/src/react/ui/components/collectible-card/CollectibleAsset.tsx +1 -0
  49. package/src/react/ui/components/collectible-card/CollectibleCard.tsx +18 -12
  50. package/src/react/ui/components/collectible-card/__tests__/CollectibleAsset.test.tsx +200 -0
  51. package/src/react/ui/components/collectible-card/__tests__/CollectibleCard.test.tsx +92 -123
  52. package/src/react/ui/components/collectible-card/__tests__/Footer.test.tsx +136 -0
  53. package/src/react/ui/modals/BuyModal/__tests__/Modal.test.tsx +2 -8
  54. package/src/react/ui/modals/SellModal/__tests__/Modal.test.tsx +72 -135
  55. package/src/react/ui/modals/_internal/components/selectWaasFeeOptions/__tests__/ActionButtons.test.tsx +72 -0
  56. package/src/react/ui/modals/_internal/components/selectWaasFeeOptions/__tests__/BalanceIndicator.test.tsx +50 -0
  57. package/src/react/ui/modals/_internal/components/selectWaasFeeOptions/__tests__/SelectWaasFeeOptions.test.tsx +193 -0
  58. package/src/react/ui/modals/_internal/components/switchChainModal/__tests__/SwitchChainModal.test.tsx +38 -58
  59. package/src/react/ui/modals/_internal/components/switchChainModal/index.tsx +3 -1
  60. package/test/test-utils.tsx +12 -22
  61. package/tsconfig.tsbuildinfo +1 -1
  62. package/dist/chunk-25CAMYCG.js.map +0 -1
  63. package/dist/chunk-VF3LWBQB.js.map +0 -1
  64. package/src/react/ui/components/_internals/action-button/__tests__/ActionButton.test.tsx +0 -107
  65. /package/dist/{chunk-XUNDLCEH.js.map → chunk-LDZZUYG7.js.map} +0 -0
  66. /package/dist/{chunk-44YGZVBS.js.map → chunk-QMWMJVTX.js.map} +0 -0
  67. /package/dist/{chunk-VBRJ2OPM.js.map → chunk-V3NVAVHV.js.map} +0 -0
@@ -16,7 +16,10 @@ import { Footer } from './Footer';
16
16
 
17
17
  function CollectibleSkeleton() {
18
18
  return (
19
- <div 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">
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
- lowestListing: CollectibleOrder | undefined;
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
- lowestListing,
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 = lowestListing?.metadata;
89
- const highestOffer = lowestListing?.offer;
93
+ const collectibleMetadata = collectible?.metadata;
94
+ const highestOffer = collectible?.offer;
90
95
 
91
96
  const { data: lowestListingCurrency } = useCurrency({
92
97
  chainId,
93
- currencyAddress: lowestListing?.order?.priceCurrencyAddress,
98
+ currencyAddress: collectible?.listing?.priceCurrencyAddress,
94
99
  query: {
95
- enabled: !!lowestListing?.order?.priceCurrencyAddress,
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
- (!lowestListing?.order && CollectibleCardAction.LIST) ||
111
+ (!collectible?.listing && CollectibleCardAction.LIST) ||
107
112
  CollectibleCardAction.TRANSFER
108
- : (lowestListing?.order && CollectibleCardAction.BUY) ||
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={lowestListing?.order?.priceAmount}
141
+ lowestListingPriceAmount={collectible?.listing?.priceAmount}
136
142
  lowestListingCurrency={lowestListingCurrency}
137
143
  balance={balance}
138
144
  decimals={collectibleMetadata?.decimals}
139
145
  />
140
146
 
141
- {(highestOffer || lowestListing) && (
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={lowestListing?.order}
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 { describe } from 'vitest';
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
- describe.skip('CollectibleCard', () => {
4
- // // Create a mock order that follows the Order interface
5
- // const mockOrder: Order = {
6
- // orderId: '1',
7
- // marketplace: MarketplaceKind.sequence_marketplace_v2,
8
- // side: OrderSide.listing,
9
- // status: OrderStatus.active,
10
- // chainId: 1,
11
- // originName: 'test marketplace',
12
- // collectionContractAddress: '0x1234567890123456789012345678901234567890',
13
- // tokenId: '123',
14
- // createdBy: '0x0000000000000000000000000000000000000000',
15
- // priceAmount: '1000000',
16
- // priceAmountFormatted: '1',
17
- // priceAmountNet: '1000000',
18
- // priceAmountNetFormatted: '1',
19
- // priceCurrencyAddress: USDC_ADDRESS,
20
- // priceDecimals: 6,
21
- // priceUSD: 1,
22
- // priceUSDFormatted: '1',
23
- // quantityInitial: '1',
24
- // quantityInitialFormatted: '1',
25
- // quantityRemaining: '1',
26
- // quantityRemainingFormatted: '1',
27
- // quantityAvailable: '1',
28
- // quantityAvailableFormatted: '1',
29
- // quantityDecimals: 0,
30
- // feeBps: 250,
31
- // feeBreakdown: [],
32
- // validFrom: new Date().toISOString(),
33
- // validUntil: new Date(Date.now() + 86400000).toISOString(),
34
- // blockNumber: 1,
35
- // createdAt: new Date().toISOString(),
36
- // updatedAt: new Date().toISOString(),
37
- // };
38
- // const defaultProps = {
39
- // collectibleId: '123',
40
- // chainId: 1,
41
- // collectionAddress:
42
- // '0x1234567890123456789012345678901234567890' as `0x${string}`,
43
- // lowestListing: {
44
- // metadata: {
45
- // tokenId: '123',
46
- // name: 'Test Collectible',
47
- // description: 'Test Description',
48
- // image: 'https://example.com/image.png',
49
- // external_url: 'https://example.com',
50
- // decimals: 0,
51
- // attributes: [],
52
- // } as TokenMetadata,
53
- // order: mockOrder,
54
- // },
55
- // orderbookKind: OrderbookKind.sequence_marketplace_v2,
56
- // collectionType: ContractType.ERC721,
57
- // };
58
- // it('should render loading state when cardLoading is true', () => {
59
- // render(<CollectibleCard {...defaultProps} cardLoading={true} />);
60
- // expect(screen.getByTestId('loading-skeleton')).toBeInTheDocument();
61
- // });
62
- // it('should render collectible with image and metadata', async () => {
63
- // render(<CollectibleCard {...defaultProps} />);
64
- // // Check if name is displayed
65
- // expect(screen.getByText('Test Collectible')).toBeInTheDocument();
66
- // // Check price
67
- // expect(screen.getByText(/1 USDC/)).toBeInTheDocument();
68
- // // Check if token type is displayed
69
- // expect(screen.getByText('ERC-721')).toBeInTheDocument();
70
- // });
71
- // it('should call onCollectibleClick when clicked', () => {
72
- // const onCollectibleClick = vi.fn();
73
- // render(
74
- // <CollectibleCard
75
- // {...defaultProps}
76
- // onCollectibleClick={onCollectibleClick}
77
- // />,
78
- // );
79
- // // Click on the collectible
80
- // const collectibleElement = screen
81
- // .getByText('Test Collectible')
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
  });
@@ -0,0 +1,136 @@
1
+ import { TEST_CURRENCY } from '@test/const';
2
+ import { render, screen } from '@test/test-utils';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { ContractType } from '../../../../_internal';
5
+ import { mockOrder } from '../../../../_internal/api/__mocks__/marketplace.msw';
6
+ import { Footer } from '../Footer';
7
+
8
+ const defaultProps = {
9
+ name: 'Test',
10
+ type: ContractType.ERC721,
11
+ decimals: 18,
12
+ onOfferClick: vi.fn(),
13
+ highestOffer: mockOrder,
14
+ lowestOffer: mockOrder,
15
+ lowestListingPriceAmount: '100',
16
+ lowestListingCurrency: TEST_CURRENCY,
17
+ balance: '100',
18
+ };
19
+
20
+ describe('Footer', () => {
21
+ it('Renders with basic props (name, type) correctly', () => {
22
+ render(<Footer {...defaultProps} />);
23
+
24
+ expect(screen.getByText('Test')).toBeInTheDocument();
25
+ expect(screen.getByText('0.0001 TEST')).toBeInTheDocument();
26
+
27
+ const notificationBell = screen.getByRole('button', {
28
+ name: 'Notification Bell',
29
+ });
30
+ expect(notificationBell).toBeInTheDocument();
31
+ });
32
+
33
+ it('Truncates long names appropriately based on presence of offers', () => {
34
+ const longName =
35
+ 'This is a very long collectible name that needs truncation';
36
+
37
+ // Test truncation with offer present (truncates to 13 chars + "...")
38
+ render(
39
+ <Footer {...defaultProps} name={longName} highestOffer={mockOrder} />,
40
+ );
41
+ expect(screen.getByText('This is a ver...')).toBeInTheDocument();
42
+
43
+ // Test truncation without offer present (truncates to 17 chars + "...")
44
+ render(
45
+ <Footer {...defaultProps} name={longName} highestOffer={undefined} />,
46
+ );
47
+ expect(screen.getByText('This is a very lo...')).toBeInTheDocument();
48
+
49
+ // Test short name with offer (no truncation needed)
50
+ render(
51
+ <Footer {...defaultProps} name="Short Name" highestOffer={mockOrder} />,
52
+ );
53
+ expect(screen.getByText('Short Name')).toBeInTheDocument();
54
+ });
55
+
56
+ it('Formats prices correctly for different scenarios (normal, overflow, underflow)', () => {
57
+ // Test normal price formatting
58
+ render(
59
+ <Footer
60
+ {...defaultProps}
61
+ lowestListingPriceAmount="1000000000000000000" // 1 TOKEN in wei
62
+ />,
63
+ );
64
+ expect(screen.getByText('1 TEST')).toBeInTheDocument();
65
+
66
+ // Test small number formatting (shows more decimals)
67
+ render(
68
+ <Footer
69
+ {...defaultProps}
70
+ lowestListingPriceAmount="5000000000000000" // 0.005 TOKEN in wei
71
+ />,
72
+ );
73
+ expect(screen.getByText('0.005 TEST')).toBeInTheDocument();
74
+
75
+ // Test underflow price (< 0.0001)
76
+ render(
77
+ <Footer
78
+ {...defaultProps}
79
+ lowestListingPriceAmount="10000000000000" // 0.00001 TOKEN in wei
80
+ />,
81
+ );
82
+ // Should display minimum price with chevron icon
83
+ expect(screen.getByText('0.0001 TEST')).toBeInTheDocument();
84
+
85
+ // Test overflow price (> 100,000,000)
86
+ render(
87
+ <Footer
88
+ {...defaultProps}
89
+ lowestListingPriceAmount="100000000000000000000000000" // 100M+ TOKEN in wei
90
+ />,
91
+ );
92
+ // Should display maximum price with chevron icon
93
+ expect(screen.getByText('100,000,000 TEST')).toBeInTheDocument();
94
+ });
95
+
96
+ it("Displays 'Not listed yet' when no listing price is provided", () => {
97
+ // Create props without listing price information
98
+ const propsWithoutListing = {
99
+ ...defaultProps,
100
+ lowestListingPriceAmount: undefined,
101
+ lowestListingCurrency: undefined,
102
+ };
103
+
104
+ render(<Footer {...propsWithoutListing} />);
105
+
106
+ // Verify "Not listed yet" text is displayed
107
+ expect(screen.getByText('Not listed yet')).toBeInTheDocument();
108
+ });
109
+
110
+ it('Shows proper token balance information for ERC721 vs ERC1155 tokens', () => {
111
+ // Test ERC721 token display
112
+ render(<Footer {...defaultProps} type={ContractType.ERC721} />);
113
+ expect(screen.getByText('ERC-721')).toBeInTheDocument();
114
+
115
+ // Test ERC1155 token display without balance
116
+ render(
117
+ <Footer
118
+ {...defaultProps}
119
+ type={ContractType.ERC1155}
120
+ balance={undefined}
121
+ />,
122
+ );
123
+ expect(screen.getByText('ERC-1155')).toBeInTheDocument();
124
+
125
+ // Test ERC1155 token display with balance
126
+ render(
127
+ <Footer
128
+ {...defaultProps}
129
+ type={ContractType.ERC1155}
130
+ balance="1000000000000000000"
131
+ decimals={18}
132
+ />,
133
+ );
134
+ expect(screen.getByText('Owned: 1')).toBeInTheDocument();
135
+ });
136
+ });