@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/{chunk-25CAMYCG.js → chunk-BB2PTJHI.js} +22 -20
  3. package/dist/chunk-BB2PTJHI.js.map +1 -0
  4. package/dist/{chunk-5ATGT5S4.js → chunk-EZFCQZHU.js} +14 -6
  5. package/dist/chunk-EZFCQZHU.js.map +1 -0
  6. package/dist/{chunk-DFI52A2E.js → chunk-KCLMSSPS.js} +364 -242
  7. package/dist/chunk-KCLMSSPS.js.map +1 -0
  8. package/dist/{chunk-XUNDLCEH.js → chunk-LDZZUYG7.js} +2 -2
  9. package/dist/{chunk-QTV77W42.js → chunk-SFSFIGHM.js} +45 -35
  10. package/dist/chunk-SFSFIGHM.js.map +1 -0
  11. package/dist/{chunk-FSJKN4YN.js → chunk-ZSCZLHKX.js} +194 -2
  12. package/dist/chunk-ZSCZLHKX.js.map +1 -0
  13. package/dist/{chunk-FH4TZRDV.js → chunk-ZVTG6US2.js} +2 -2
  14. package/dist/index.css +4 -4
  15. package/dist/index.css.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/{lowestListing-DUZ_nYml.d.ts → lowestListing-W7P4EkC3.d.ts} +34 -11
  18. package/dist/react/_internal/databeat/index.js +5 -5
  19. package/dist/react/_internal/index.d.ts +1 -1
  20. package/dist/react/_internal/index.js +3 -1
  21. package/dist/react/_internal/wagmi/index.d.ts +3 -2
  22. package/dist/react/_internal/wagmi/index.js +3 -1
  23. package/dist/react/hooks/index.d.ts +8 -5
  24. package/dist/react/hooks/index.js +6 -4
  25. package/dist/react/hooks/options/index.js +2 -2
  26. package/dist/react/index.d.ts +2 -2
  27. package/dist/react/index.js +9 -7
  28. package/dist/react/queries/index.d.ts +1 -1
  29. package/dist/react/queries/index.js +6 -2
  30. package/dist/react/ssr/index.js +1 -1
  31. package/dist/react/ui/components/collectible-card/index.d.ts +3 -2
  32. package/dist/react/ui/components/collectible-card/index.js +7 -7
  33. package/dist/react/ui/icons/index.js +1 -1
  34. package/dist/react/ui/index.js +7 -7
  35. package/dist/react/ui/modals/_internal/components/actionModal/index.js +5 -5
  36. package/dist/types/index.js +1 -1
  37. package/dist/utils/index.js +1 -1
  38. package/package.json +19 -19
  39. package/src/react/_internal/api/__mocks__/marketplace.msw.ts +35 -21
  40. package/src/react/_internal/wagmi/__tests__/create-config.test.ts +1 -11
  41. package/src/react/_internal/wagmi/get-connectors.ts +27 -24
  42. package/src/react/hooks/__tests__/useCancelTransactionSteps.test.tsx +4 -9
  43. package/src/react/hooks/__tests__/useGenerateCancelTransaction.test.tsx +5 -4
  44. package/src/react/hooks/__tests__/useGenerateListingTransaction.test.tsx +14 -10
  45. package/src/react/hooks/__tests__/useGenerateOfferTransaction.test.tsx +115 -65
  46. package/src/react/hooks/__tests__/useGenerateSellTransaction.test.tsx +10 -7
  47. package/src/react/hooks/__tests__/useInventory.test.tsx +294 -0
  48. package/src/react/hooks/index.ts +1 -0
  49. package/src/react/hooks/useAutoSelectFeeOption.tsx +10 -3
  50. package/src/react/hooks/useCancelOrder.tsx +1 -0
  51. package/src/react/hooks/useCancelTransactionSteps.tsx +18 -4
  52. package/src/react/hooks/useGenerateOfferTransaction.tsx +11 -1
  53. package/src/react/hooks/useInventory.tsx +15 -0
  54. package/src/react/hooks/util/optimisticCancelUpdates.ts +115 -0
  55. package/src/react/queries/index.ts +1 -0
  56. package/src/react/queries/inventory.ts +303 -0
  57. package/src/react/queries/listBalances.ts +1 -8
  58. package/src/react/queries/listCollectibles.ts +12 -3
  59. package/src/react/ui/components/_internals/action-button/__tests__/ActionButtonBody.test.tsx +27 -94
  60. package/src/react/ui/components/_internals/action-button/__tests__/NonOwnerActions.test.tsx +59 -0
  61. package/src/react/ui/components/_internals/action-button/__tests__/OwnerActions.test.tsx +73 -0
  62. package/src/react/ui/components/_internals/action-button/__tests__/useActionButtonLogic.test.tsx +77 -0
  63. package/src/react/ui/components/_internals/action-button/components/ActionButtonBody.tsx +3 -2
  64. package/src/react/ui/components/_internals/action-button/hooks/useActionButtonLogic.ts +4 -3
  65. package/src/react/ui/components/collectible-card/CollectibleAsset.tsx +1 -0
  66. package/src/react/ui/components/collectible-card/CollectibleCard.tsx +18 -12
  67. package/src/react/ui/components/collectible-card/__tests__/CollectibleAsset.test.tsx +200 -0
  68. package/src/react/ui/components/collectible-card/__tests__/CollectibleCard.test.tsx +92 -123
  69. package/src/react/ui/components/collectible-card/__tests__/Footer.test.tsx +136 -0
  70. package/src/react/ui/modals/BuyModal/__tests__/Modal.test.tsx +2 -8
  71. package/src/react/ui/modals/CreateListingModal/__tests__/Modal.test.tsx +74 -104
  72. package/src/react/ui/modals/MakeOfferModal/__tests__/Modal.test.tsx +108 -78
  73. package/src/react/ui/modals/SellModal/__tests__/Modal.test.tsx +72 -135
  74. package/src/react/ui/modals/_internal/components/actionModal/ActionModal.test.tsx +286 -0
  75. package/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx +16 -4
  76. package/src/react/ui/modals/_internal/components/currencyOptionsSelect/__tests__/index.test.tsx +35 -132
  77. package/src/react/ui/modals/_internal/components/floorPriceText/__tests__/FloorPriceText.test.tsx +199 -0
  78. package/src/react/ui/modals/_internal/components/priceInput/__tests__/PriceInput.test.tsx +55 -0
  79. package/src/react/ui/modals/_internal/components/priceInput/index.tsx +1 -1
  80. package/src/react/ui/modals/_internal/components/selectWaasFeeOptions/__tests__/ActionButtons.test.tsx +72 -0
  81. package/src/react/ui/modals/_internal/components/selectWaasFeeOptions/__tests__/BalanceIndicator.test.tsx +50 -0
  82. package/src/react/ui/modals/_internal/components/selectWaasFeeOptions/__tests__/SelectWaasFeeOptions.test.tsx +193 -0
  83. package/src/react/ui/modals/_internal/components/switchChainModal/index.tsx +2 -2
  84. package/test/const.ts +24 -0
  85. package/test/test-utils.tsx +85 -47
  86. package/.changeset/flat-parks-clean.md +0 -8
  87. package/.changeset/red-buckets-deny.md +0 -6
  88. package/.changeset/seven-doors-taste.md +0 -5
  89. package/dist/chunk-25CAMYCG.js.map +0 -1
  90. package/dist/chunk-5ATGT5S4.js.map +0 -1
  91. package/dist/chunk-DFI52A2E.js.map +0 -1
  92. package/dist/chunk-FSJKN4YN.js.map +0 -1
  93. package/dist/chunk-QTV77W42.js.map +0 -1
  94. package/src/react/ui/components/_internals/action-button/__tests__/ActionButton.test.tsx +0 -107
  95. package/src/react/ui/modals/_internal/components/priceInput/__tests__/index.test.tsx +0 -164
  96. /package/dist/{chunk-XUNDLCEH.js.map → chunk-LDZZUYG7.js.map} +0 -0
  97. /package/dist/{chunk-FH4TZRDV.js.map → chunk-ZVTG6US2.js.map} +0 -0
@@ -1,166 +1,103 @@
1
- import { cleanup, fireEvent, render, screen } from '@test';
1
+ import { cleanup, render, renderHook, screen, waitFor } from '@test';
2
+ import { TEST_COLLECTIBLE } from '@test/const';
3
+ import { createMockWallet } from '@test/mocks/wallet';
2
4
  import { beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { useCollection, useCurrency } from '../../../../hooks';
5
+ import { useSellModal } from '..';
6
+ import { StepType, WalletKind } from '../../../../_internal';
7
+ import { createMockStep } from '../../../../_internal/api/__mocks__/marketplace.msw';
8
+ import { mockOrder } from '../../../../_internal/api/__mocks__/marketplace.msw';
9
+ import * as walletModule from '../../../../_internal/wallet/useWallet';
4
10
  import { SellModal } from '../Modal';
5
- import { type OpenSellModalArgs, sellModal$ } from '../store';
11
+ import * as useGetTokenApprovalDataModule from '../hooks/useGetTokenApproval';
6
12
 
7
- import { MarketplaceKind, type Order } from '../../../../_internal';
8
-
9
- import { server } from '@test';
10
- import { http, HttpResponse } from 'msw';
11
- import { mockMarketplaceEndpoint } from '../../../../_internal/api/__mocks__/marketplace.msw';
12
- import { useSell } from '../hooks/useSell';
13
-
14
- // Test data
15
- const mockOrder = {
16
- orderId: '1',
17
- priceAmount: '1000000000000000000',
18
- priceCurrencyAddress: '0x0',
19
- quantityRemaining: '1',
20
- createdAt: new Date().toISOString(),
21
- marketplace: MarketplaceKind.sequence_marketplace_v2,
22
- } as Order;
23
-
24
- const mockModalProps = {
25
- collectionAddress: '0x123',
26
- chainId: 1,
27
- tokenId: '1',
13
+ const defaultArgs = {
14
+ collectionAddress: TEST_COLLECTIBLE.collectionAddress,
15
+ chainId: TEST_COLLECTIBLE.chainId,
16
+ tokenId: TEST_COLLECTIBLE.collectibleId,
28
17
  order: mockOrder,
29
- } satisfies OpenSellModalArgs;
30
-
31
- // TODO: remove when there is mocks for more endpoints
32
- vi.mock(import('../../../../hooks'), async (importOriginal) => {
33
- const mod = await importOriginal();
34
- return {
35
- ...mod,
36
- useCollection: vi.fn().mockImplementation(mod.useCollection),
37
- useCurrency: vi.fn().mockImplementation(mod.useCurrency),
38
- };
39
- });
40
-
41
- vi.mock('@0xsequence/kit', () => ({
42
- useWaasFeeOptions: vi.fn().mockReturnValue([]),
43
- }));
44
-
45
- beforeEach(() => {
46
- cleanup();
47
- vi.clearAllMocks();
48
- vi.mock('../hooks/useSell', () => ({
49
- useSell: vi.fn().mockReturnValue({
50
- isLoading: false,
51
- executeApproval: vi.fn(),
52
- sell: vi.fn(),
53
- }),
54
- }));
55
- });
18
+ };
56
19
 
57
- describe.skip('SellModal', () => {
58
- it('should not render when modal is closed', () => {
59
- render(<SellModal />);
60
- expect(screen.queryByText('You have an offer')).toBeNull();
61
- });
20
+ describe('MakeOfferModal', () => {
21
+ const mockWallet = createMockWallet();
62
22
 
63
- it.skip('should render error state', async () => {
64
- // Override MSW to return error
65
- server.use(
66
- http.post(mockMarketplaceEndpoint('GetCollectible'), () => {
67
- return HttpResponse.error();
68
- }),
69
- );
70
- sellModal$.open(mockModalProps);
71
- render(<SellModal />);
72
- const errorModal = await screen.findByTestId('error-modal');
73
- expect(errorModal).toBeVisible();
23
+ beforeEach(() => {
24
+ cleanup();
25
+ // Reset all mocks
26
+ vi.clearAllMocks();
27
+ vi.resetAllMocks();
28
+ vi.restoreAllMocks();
74
29
  });
75
30
 
76
- it.skip('should render main modal when data is loaded', async () => {
77
- vi.mocked(useCollection).mockReturnValue({
78
- // @ts-expect-error - TODO: mock this better
79
- data: {},
31
+ it('should show main button if there is no approval step', async () => {
32
+ // Mock sequence wallet
33
+ const sequenceWallet = {
34
+ ...mockWallet,
35
+ walletKind: WalletKind.sequence,
36
+ };
37
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
38
+ wallet: sequenceWallet,
80
39
  isLoading: false,
81
40
  isError: false,
82
41
  });
83
-
84
- vi.mocked(useCurrency).mockReturnValue({
85
- // @ts-expect-error - TODO: mock this better
86
- data: {},
42
+ vi.spyOn(
43
+ useGetTokenApprovalDataModule,
44
+ 'useGetTokenApprovalData',
45
+ ).mockReturnValue({
46
+ data: {
47
+ step: null,
48
+ },
87
49
  isLoading: false,
88
- isError: false,
50
+ isSuccess: true,
89
51
  });
90
52
 
91
- sellModal$.open(mockModalProps);
53
+ // Render the modal
54
+ const { result } = renderHook(() => useSellModal());
55
+ result.current.show(defaultArgs);
56
+
92
57
  render(<SellModal />);
93
- const text = await screen.findByText('Offer received');
94
- expect(text).toBeInTheDocument();
58
+
59
+ // Wait for the component to update
60
+ await waitFor(() => {
61
+ // The Approve TOKEN button should not exist
62
+ expect(screen.queryByText('Approve TOKEN')).toBeNull();
63
+
64
+ // The Accept button should exist
65
+ expect(screen.getByRole('button', { name: 'Accept' })).toBeDefined();
66
+ });
95
67
  });
96
- });
97
68
 
98
- describe.skip('Modal Actions', () => {
99
- it('should handle approval step correctly', async () => {
100
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
101
- vi.mocked(useCollection as any).mockReturnValue({
102
- data: {
103
- name: 'Test Collection',
104
- decimals: 0,
105
- },
69
+ it('(non-sequence wallets) should show approve token button if there is an approval step, disable main button', async () => {
70
+ const nonSequenceWallet = {
71
+ ...mockWallet,
72
+ walletKind: 'unknown' as WalletKind,
73
+ };
74
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
75
+ wallet: nonSequenceWallet,
106
76
  isLoading: false,
107
77
  isError: false,
108
78
  });
109
-
110
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
111
- vi.mocked(useCurrency as any).mockReturnValue({
79
+ vi.spyOn(
80
+ useGetTokenApprovalDataModule,
81
+ 'useGetTokenApprovalData',
82
+ ).mockReturnValue({
112
83
  data: {
113
- name: 'Test Currency',
114
- imageUrl: 'test-url',
84
+ step: createMockStep(StepType.tokenApproval),
115
85
  },
116
86
  isLoading: false,
117
- isError: false,
87
+ isSuccess: true,
118
88
  });
119
89
 
120
- const mockExecuteApproval = vi.fn();
90
+ // Render the modal
91
+ const { result } = renderHook(() => useSellModal());
92
+ result.current.show(defaultArgs);
121
93
 
122
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
123
- (useSell as any).mockReturnValue({
124
- isLoading: false,
125
- executeApproval: mockExecuteApproval,
126
- sell: vi.fn(),
127
- });
128
-
129
- sellModal$.open({
130
- ...mockModalProps,
131
- order: {
132
- ...mockOrder,
133
- quantityRemaining: '1',
134
- },
135
- });
136
- sellModal$.steps.approval.exist.set(true);
137
- sellModal$.steps.approval.isExecuting.set(false);
138
- sellModal$.steps.transaction.isExecuting.set(false);
139
94
  render(<SellModal />);
140
95
 
141
- const approveButton = await screen.findByText('Approve TOKEN');
142
- expect(approveButton).not.toBeDisabled();
143
- fireEvent.click(approveButton);
144
- expect(mockExecuteApproval).toHaveBeenCalled();
145
- });
146
- });
96
+ await waitFor(() => {
97
+ expect(screen.getByText('Approve TOKEN')).toBeDefined();
147
98
 
148
- it.skip('should handle sell action correctly', async () => {
149
- const mockSell = vi.fn();
150
- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
151
- (useSell as any).mockReturnValue({
152
- isLoading: false,
153
- executeApproval: vi.fn(),
154
- sell: mockSell,
99
+ expect(screen.getByRole('button', { name: 'Accept' })).toBeDefined();
100
+ expect(screen.getByRole('button', { name: 'Accept' })).toBeDisabled();
101
+ });
155
102
  });
156
-
157
- sellModal$.open(mockModalProps);
158
- sellModal$.steps.approval.exist.set(false);
159
- sellModal$.steps.approval.isExecuting.set(false);
160
- sellModal$.steps.transaction.isExecuting.set(false);
161
- render(<SellModal />);
162
-
163
- const acceptButton = screen.getByText('Accept');
164
- fireEvent.click(acceptButton);
165
- expect(mockSell).toHaveBeenCalled();
166
103
  });
@@ -0,0 +1,286 @@
1
+ import { fireEvent, render, screen, waitFor } from '@test';
2
+ import { type Address, custom, zeroAddress } from 'viem';
3
+ import { mainnet, polygon } from 'viem/chains';
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { WalletKind } from '../../../../../_internal';
6
+ import * as walletModule from '../../../../../_internal/wallet/useWallet';
7
+ import { ActionModal } from './ActionModal';
8
+
9
+ const mockShowSwitchChainModal = vi.fn();
10
+ vi.mock('../switchChainModal', () => ({
11
+ useSwitchChainModal: () => ({
12
+ show: mockShowSwitchChainModal,
13
+ close: vi.fn(),
14
+ isSwitching$: { get: () => false },
15
+ }),
16
+ }));
17
+
18
+ describe('ActionModal', () => {
19
+ const mockOnClose = vi.fn();
20
+ const mockOnClick = vi.fn();
21
+
22
+ const defaultProps = {
23
+ isOpen: true,
24
+ onClose: mockOnClose,
25
+ title: 'Test Modal',
26
+ children: <div>Modal Content</div>,
27
+ ctas: [
28
+ {
29
+ label: 'Test Button',
30
+ onClick: mockOnClick,
31
+ testid: 'test-button',
32
+ },
33
+ ],
34
+ chainId: polygon.id,
35
+ };
36
+
37
+ beforeEach(() => {
38
+ vi.clearAllMocks();
39
+ });
40
+
41
+ describe('Loading states', () => {
42
+ it('should show a loading spinner when modalLoading prop is true', async () => {
43
+ render(<ActionModal {...defaultProps} modalLoading={true} />);
44
+
45
+ expect(screen.getByTestId('spinner')).toBeInTheDocument();
46
+ });
47
+
48
+ it('should show a loading spinner when isLoading from useWallet is true', async () => {
49
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
50
+ wallet: null,
51
+ isLoading: true,
52
+ isError: false,
53
+ });
54
+
55
+ render(<ActionModal {...defaultProps} />);
56
+
57
+ expect(screen.getByTestId('spinner')).toBeInTheDocument();
58
+ });
59
+
60
+ it('should show error message when useWallet returns an error', () => {
61
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
62
+ wallet: null,
63
+ isLoading: false,
64
+ isError: true,
65
+ });
66
+
67
+ render(<ActionModal {...defaultProps} />);
68
+
69
+ expect(screen.getByTestId('error-loading-text')).toBeInTheDocument();
70
+ expect(screen.getByText('Error loading modal')).toBeInTheDocument();
71
+ expect(screen.queryByText('Modal Content')).not.toBeInTheDocument();
72
+ expect(screen.queryByTestId('test-button')).not.toBeInTheDocument();
73
+ });
74
+
75
+ it('should show modal content if loading states is false and no error', async () => {
76
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
77
+ wallet: null,
78
+ isLoading: false,
79
+ isError: false,
80
+ });
81
+
82
+ render(<ActionModal {...defaultProps} modalLoading={false} />);
83
+
84
+ expect(screen.getByText('Modal Content')).toBeInTheDocument();
85
+ expect(screen.getByTestId('test-button')).toBeInTheDocument();
86
+ });
87
+ });
88
+
89
+ describe('Chain switching', () => {
90
+ it('should automatically switch chain for Sequence WaaS wallets', async () => {
91
+ const switchChainMock = vi.fn();
92
+
93
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
94
+ wallet: {
95
+ address: () => Promise.resolve(zeroAddress as Address),
96
+ getChainId: vi.fn().mockResolvedValue(mainnet.id),
97
+ switchChain: switchChainMock,
98
+ transport: custom({ request: vi.fn() }),
99
+ walletKind: WalletKind.sequence,
100
+ isWaaS: true,
101
+ handleConfirmTransactionStep: vi.fn(),
102
+ handleSendTransactionStep: vi.fn(),
103
+ handleSignMessageStep: vi.fn(),
104
+ hasTokenApproval: vi.fn(),
105
+ },
106
+ isLoading: false,
107
+ isError: false,
108
+ });
109
+
110
+ render(<ActionModal {...defaultProps} />);
111
+
112
+ expect(switchChainMock).toHaveBeenCalledWith(polygon.id);
113
+ });
114
+
115
+ it('should show switch chain modal when CTA is clicked with chain mismatch', async () => {
116
+ mockShowSwitchChainModal.mockClear();
117
+
118
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
119
+ wallet: {
120
+ address: () => Promise.resolve(zeroAddress as Address),
121
+ getChainId: vi.fn().mockResolvedValue(mainnet.id), // different from defaultProps.chainId
122
+ switchChain: vi.fn(),
123
+ transport: custom({ request: vi.fn() }),
124
+ walletKind: WalletKind.sequence,
125
+ isWaaS: false,
126
+ handleConfirmTransactionStep: vi.fn(),
127
+ handleSendTransactionStep: vi.fn(),
128
+ handleSignMessageStep: vi.fn(),
129
+ hasTokenApproval: vi.fn(),
130
+ },
131
+ isLoading: false,
132
+ isError: false,
133
+ });
134
+
135
+ render(<ActionModal {...defaultProps} />);
136
+
137
+ const button = screen.getByTestId('test-button');
138
+ fireEvent.click(button);
139
+
140
+ await waitFor(() => {
141
+ expect(mockShowSwitchChainModal).toHaveBeenCalledWith({
142
+ chainIdToSwitchTo: polygon.id,
143
+ onSuccess: expect.any(Function),
144
+ });
145
+ });
146
+ });
147
+
148
+ it('should directly execute callback when chain already matches', async () => {
149
+ mockOnClick.mockClear();
150
+
151
+ vi.spyOn(walletModule, 'useWallet').mockReturnValue({
152
+ wallet: {
153
+ address: () => Promise.resolve(zeroAddress as Address),
154
+ getChainId: vi.fn().mockResolvedValue(polygon.id), // Same as defaultProps.chainId
155
+ switchChain: vi.fn(),
156
+ transport: custom({ request: vi.fn() }),
157
+ walletKind: WalletKind.sequence,
158
+ isWaaS: false,
159
+ handleConfirmTransactionStep: vi.fn(),
160
+ handleSendTransactionStep: vi.fn(),
161
+ handleSignMessageStep: vi.fn(),
162
+ hasTokenApproval: vi.fn(),
163
+ },
164
+ isLoading: false,
165
+ isError: false,
166
+ });
167
+
168
+ render(<ActionModal {...defaultProps} />);
169
+
170
+ const button = screen.getByTestId('test-button');
171
+ fireEvent.click(button);
172
+
173
+ await waitFor(() => {
174
+ expect(mockOnClick).toHaveBeenCalled();
175
+ });
176
+ });
177
+ });
178
+
179
+ describe('CTA buttons', () => {
180
+ it('should call onClick when CTA button is clicked', async () => {
181
+ const onClick = vi.fn();
182
+ render(
183
+ <ActionModal
184
+ {...defaultProps}
185
+ ctas={[{ label: 'Click Me', onClick, testid: 'cta-button' }]}
186
+ />,
187
+ );
188
+
189
+ const button = screen.getByTestId('cta-button');
190
+ fireEvent.click(button);
191
+
192
+ await waitFor(() => {
193
+ expect(onClick).toHaveBeenCalled();
194
+ });
195
+ });
196
+
197
+ it('should disable the button when disabled prop is true', () => {
198
+ render(
199
+ <ActionModal
200
+ {...defaultProps}
201
+ ctas={[
202
+ {
203
+ label: 'Disabled Button',
204
+ onClick: mockOnClick,
205
+ disabled: true,
206
+ testid: 'disabled-button',
207
+ },
208
+ ]}
209
+ />,
210
+ );
211
+
212
+ const button = screen.getByTestId('disabled-button');
213
+ expect(button).toBeDisabled();
214
+
215
+ fireEvent.click(button);
216
+ expect(mockOnClick).not.toHaveBeenCalled();
217
+ });
218
+
219
+ it('should show spinner when pending prop is true', () => {
220
+ render(
221
+ <ActionModal
222
+ {...defaultProps}
223
+ ctas={[
224
+ {
225
+ label: 'Loading Button',
226
+ onClick: mockOnClick,
227
+ pending: true,
228
+ testid: 'pending-button',
229
+ },
230
+ ]}
231
+ />,
232
+ );
233
+
234
+ expect(screen.getByTestId('pending-button')).toBeInTheDocument();
235
+ expect(screen.getByTestId('pending-button-spinner')).toBeInTheDocument(); // wrapper of spinner has data-testid of {testid}-spinner
236
+ });
237
+
238
+ it('should not render hidden buttons', () => {
239
+ render(
240
+ <ActionModal
241
+ {...defaultProps}
242
+ ctas={[
243
+ {
244
+ label: 'Visible Button',
245
+ onClick: vi.fn(),
246
+ testid: 'visible-button',
247
+ },
248
+ {
249
+ label: 'Hidden Button',
250
+ onClick: vi.fn(),
251
+ hidden: true,
252
+ testid: 'hidden-button',
253
+ },
254
+ ]}
255
+ />,
256
+ );
257
+
258
+ expect(screen.getByTestId('visible-button')).toBeInTheDocument();
259
+ expect(screen.queryByTestId('hidden-button')).not.toBeInTheDocument();
260
+ });
261
+
262
+ it('should render multiple CTA buttons', () => {
263
+ render(
264
+ <ActionModal
265
+ {...defaultProps}
266
+ ctas={[
267
+ {
268
+ label: 'Primary CTA',
269
+ onClick: vi.fn(),
270
+ testid: 'primary-cta',
271
+ },
272
+ {
273
+ label: 'Secondary CTA',
274
+ onClick: vi.fn(),
275
+ variant: 'secondary',
276
+ testid: 'secondary-cta',
277
+ },
278
+ ]}
279
+ />,
280
+ );
281
+
282
+ expect(screen.getByTestId('primary-cta')).toBeInTheDocument();
283
+ expect(screen.getByTestId('secondary-cta')).toBeInTheDocument();
284
+ });
285
+ });
286
+ });
@@ -52,7 +52,7 @@ export const ActionModal = observer(
52
52
  const chainMismatch = walletChainId !== Number(chainId);
53
53
  if (chainMismatch) {
54
54
  showSwitchChainModal({
55
- chainIdToSwitchTo: Number(chainId),
55
+ chainIdToSwitchTo: chainId,
56
56
  onSuccess,
57
57
  });
58
58
  } else {
@@ -84,13 +84,21 @@ export const ActionModal = observer(
84
84
  {modalLoading || isLoading || isError ? (
85
85
  <div
86
86
  className={`flex ${spinnerContainerClassname} w-full items-center justify-center`}
87
+ data-testid="error-loading-wrapper"
87
88
  >
88
89
  {isError && (
89
- <Text className="text-center font-body text-error100 text-small">
90
+ <Text
91
+ data-testid="error-loading-text"
92
+ className="text-center font-body text-error100 text-small"
93
+ >
90
94
  Error loading modal
91
95
  </Text>
92
96
  )}
93
- {isLoading && <Spinner size="lg" />}
97
+ {(isLoading || modalLoading) && (
98
+ <div data-testid="spinner">
99
+ <Spinner size="lg" />
100
+ </div>
101
+ )}
94
102
  </div>
95
103
  ) : (
96
104
  children
@@ -118,7 +126,11 @@ export const ActionModal = observer(
118
126
  data-testid={cta.testid}
119
127
  label={
120
128
  <div className="flex items-center justify-center gap-2">
121
- {cta.pending && <Spinner size="sm" />}
129
+ {cta.pending && (
130
+ <div data-testid={`${cta.testid}-spinner`}>
131
+ <Spinner size="sm" />
132
+ </div>
133
+ )}
122
134
 
123
135
  {cta.label}
124
136
  </div>