diff --git a/demo/examples/src/appkit/actions/jettons/jettons.test.ts b/demo/examples/src/appkit/actions/jettons/jettons.test.ts new file mode 100644 index 000000000..d98d140e6 --- /dev/null +++ b/demo/examples/src/appkit/actions/jettons/jettons.test.ts @@ -0,0 +1,276 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { AppKit } from '@ton/appkit'; +import { Network } from '@ton/walletkit'; +import type { WalletInterface } from '@ton/appkit'; +import { Address, beginCell } from '@ton/core'; + +import { createTransferJettonTransactionExample } from './create-transfer-jetton-transaction'; +import { getJettonBalanceExample } from './get-jetton-balance'; +import { getJettonInfoExample } from './get-jetton-info'; +import { getJettonWalletAddressExample } from './get-jetton-wallet-address'; +import { getJettonsByAddressExample } from './get-jettons-by-address'; +import { getJettonsExample } from './get-jettons'; +import { transferJettonExample } from './transfer-jetton'; + +describe('Jetton Actions Examples (Integration)', () => { + let appKit: AppKit; + let mockClient: { + jettonsByAddress: ReturnType; + jettonsByOwnerAddress: ReturnType; + runGetMethod: ReturnType; + }; + let consoleSpy: ReturnType; + + const JETTON_ADDRESS = 'EQDBE420tTQIkoWcZ9pEOTKY63WVmwyIl3hH6yWl0r_h51Tl'; + // Real Zero Address + const VALID_ADDRESS = 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c'; + const JETTON_WALLET_ADDRESS = VALID_ADDRESS; + + // Helper to mock address in stack + const mockAddressStack = (address: string) => { + const cell = beginCell().storeAddress(Address.parse(address)).endCell(); + return [{ type: 'cell', value: cell.toBoc().toString('base64') }]; + }; + + beforeEach(() => { + vi.clearAllMocks(); + consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + // Initialize real AppKit + appKit = new AppKit({ + networks: { + [Network.mainnet().chainId]: {}, + }, + }); + + // Mock the ApiClient + mockClient = { + jettonsByAddress: vi.fn(), + jettonsByOwnerAddress: vi.fn(), + runGetMethod: vi.fn(), + }; + + // Default runGetMethod mock to prevent errors in shared actions + mockClient.runGetMethod.mockImplementation((_addr: string, method: string) => { + if (method === 'get_wallet_address') { + return Promise.resolve({ + stack: mockAddressStack(JETTON_WALLET_ADDRESS), + }); + } + if (method === 'get_wallet_data') { + return Promise.resolve({ + stack: [{ type: 'num', value: '100000000' }], + }); + } + return Promise.reject(new Error(`Method ${method} not mocked`)); + }); + + // Spy on networkManager.getClient to return our mock client + // @ts-expect-error - exploiting internal access for testing + vi.spyOn(appKit.networkManager, 'getClient').mockReturnValue(mockClient); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + describe('getJettonInfoExample', () => { + it('should get and log jetton info', async () => { + mockClient.jettonsByAddress.mockResolvedValue({ + jetton_masters: [{ address: JETTON_ADDRESS, jetton: JETTON_ADDRESS }], + metadata: { + [JETTON_ADDRESS]: { + token_info: [ + { + valid: true, + type: 'jetton_masters', + name: 'Test Jetton', + symbol: 'TEST', + description: 'Test Description', + image: 'test-image-url', + extra: { decimals: 6, uri: 'test-uri' }, + }, + ], + }, + }, + }); + + await getJettonInfoExample(appKit); + + expect(mockClient.jettonsByAddress).toHaveBeenCalledWith({ + address: JETTON_ADDRESS, + offset: 0, + limit: 1, + }); + expect(consoleSpy).toHaveBeenCalledWith( + 'Jetton Info:', + expect.objectContaining({ + name: 'Test Jetton', + symbol: 'TEST', + decimals: 6, + }), + ); + }); + }); + + describe('getJettonWalletAddressExample', () => { + it('should log jetton wallet address when wallet is selected', async () => { + const mockWallet = { + getAddress: () => VALID_ADDRESS, + getWalletId: () => 'mock-wallet-id', + getNetwork: () => 'mainnet', + } as unknown as WalletInterface; + appKit.walletsManager.setWallets([mockWallet]); + + await getJettonWalletAddressExample(appKit); + + expect(mockClient.runGetMethod).toHaveBeenCalledWith( + JETTON_ADDRESS, + 'get_wallet_address', + expect.any(Array), + ); + expect(consoleSpy).toHaveBeenCalledWith( + 'Jetton Wallet Address:', + Address.parse(JETTON_WALLET_ADDRESS).toString(), + ); + }); + + it('should log message if no wallet selected', async () => { + appKit.walletsManager.setWallets([]); + await getJettonWalletAddressExample(appKit); + expect(consoleSpy).toHaveBeenCalledWith('No wallet selected'); + }); + }); + + describe('getJettonBalanceExample', () => { + it('should log jetton balance when wallet is selected', async () => { + const mockWallet = { + getAddress: () => VALID_ADDRESS, + getWalletId: () => 'mock-wallet-id', + getNetwork: () => 'mainnet', + } as unknown as WalletInterface; + appKit.walletsManager.setWallets([mockWallet]); + + await getJettonBalanceExample(appKit); + + expect(consoleSpy).toHaveBeenCalledWith('Jetton Balance:', '100'); + }); + }); + + describe('getJettonsByAddressExample', () => { + it('should log jettons count and details', async () => { + const mockWallet = { + getAddress: () => VALID_ADDRESS, + getWalletId: () => 'mock-wallet-id', + getNetwork: () => 'mainnet', + } as unknown as WalletInterface; + appKit.walletsManager.setWallets([mockWallet]); + + mockClient.jettonsByOwnerAddress.mockResolvedValue({ + jettons: [ + { + balance: '100000000', + decimalsNumber: 6, + info: { name: 'Test Jetton' }, + }, + ], + }); + + await getJettonsByAddressExample(appKit); + + expect(mockClient.jettonsByOwnerAddress).toHaveBeenCalledWith({ + ownerAddress: VALID_ADDRESS, + offset: undefined, + limit: undefined, + }); + expect(consoleSpy).toHaveBeenCalledWith('Jettons by Address:', 1); + expect(consoleSpy).toHaveBeenCalledWith('- Test Jetton: 100'); + }); + }); + + describe('getJettonsExample', () => { + it('should log jettons count and details when wallet selected', async () => { + const mockWallet = { + getAddress: () => VALID_ADDRESS, + getWalletId: () => 'mock-wallet-id', + getNetwork: () => 'mainnet', + } as unknown as WalletInterface; + appKit.walletsManager.setWallets([mockWallet]); + + mockClient.jettonsByOwnerAddress.mockResolvedValue({ + jettons: [ + { + balance: '500000000', + decimalsNumber: 9, + info: { name: 'Grams' }, + }, + ], + }); + + await getJettonsExample(appKit); + + expect(consoleSpy).toHaveBeenCalledWith('Jettons:', 1); + expect(consoleSpy).toHaveBeenCalledWith('- Grams: 0.5'); + }); + }); + + describe('createTransferJettonTransactionExample', () => { + it('should log transfer transaction', async () => { + const mockWallet = { + getAddress: () => VALID_ADDRESS, + getWalletId: () => 'mock-wallet-id', + getNetwork: () => 'mainnet', + } as unknown as WalletInterface; + appKit.walletsManager.setWallets([mockWallet]); + + await createTransferJettonTransactionExample(appKit); + expect(consoleSpy).toHaveBeenCalledWith( + 'Transfer Transaction:', + expect.objectContaining({ + fromAddress: VALID_ADDRESS, + messages: [ + expect.objectContaining({ + address: JETTON_WALLET_ADDRESS, + amount: '50000000', // DEFAULT_JETTON_GAS_FEE + }), + ], + }), + ); + }); + }); + + describe('transferJettonExample', () => { + it('should call sendTransaction with transfer data', async () => { + const mockWallet = { + getAddress: () => VALID_ADDRESS, + getWalletId: () => 'mock-wallet-id', + getNetwork: () => 'mainnet', + sendTransaction: vi.fn().mockResolvedValue({ hash: 'mock-hash' }), + } as unknown as WalletInterface; + appKit.walletsManager.setWallets([mockWallet]); + + await transferJettonExample(appKit); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + fromAddress: VALID_ADDRESS, + messages: [ + expect.objectContaining({ + address: JETTON_WALLET_ADDRESS, + amount: '50000000', + }), + ], + }), + ); + expect(consoleSpy).toHaveBeenCalledWith('Transfer Result:', { hash: 'mock-hash' }); + }); + }); +}); diff --git a/demo/examples/src/appkit/actions/nft/create-transfer-nft-transaction.ts b/demo/examples/src/appkit/actions/nft/create-transfer-nft-transaction.ts index 1fdd4c5d2..062472428 100644 --- a/demo/examples/src/appkit/actions/nft/create-transfer-nft-transaction.ts +++ b/demo/examples/src/appkit/actions/nft/create-transfer-nft-transaction.ts @@ -12,7 +12,7 @@ import { createTransferNftTransaction } from '@ton/appkit'; export const createTransferNftTransactionExample = async (appKit: AppKit) => { // SAMPLE_START: CREATE_TRANSFER_NFT_TRANSACTION const tx = await createTransferNftTransaction(appKit, { - nftAddress: 'EQCA14o1-VWhS29szfbpmbu_m7A_9S4m_Ba6sAyALH_mU68j', + nftAddress: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', recipientAddress: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', comment: 'Gift NFT', }); diff --git a/demo/examples/src/appkit/actions/nft/get-nft.ts b/demo/examples/src/appkit/actions/nft/get-nft.ts index 5fbb6fe98..48a1cb6fc 100644 --- a/demo/examples/src/appkit/actions/nft/get-nft.ts +++ b/demo/examples/src/appkit/actions/nft/get-nft.ts @@ -12,7 +12,7 @@ import { getNft } from '@ton/appkit'; export const getNftExample = async (appKit: AppKit) => { // SAMPLE_START: GET_NFT const nft = await getNft(appKit, { - address: 'EQCA14o1-VWhS29szfbpmbu_m7A_9S4m_Ba6sAyALH_mU68j', + address: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', }); if (nft) { diff --git a/demo/examples/src/appkit/actions/nft/nfts.test.ts b/demo/examples/src/appkit/actions/nft/nfts.test.ts new file mode 100644 index 000000000..3dbd0234b --- /dev/null +++ b/demo/examples/src/appkit/actions/nft/nfts.test.ts @@ -0,0 +1,179 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { AppKit } from '@ton/appkit'; +import { Network } from '@ton/walletkit'; +import type { WalletInterface } from '@ton/appkit'; + +import { createTransferNftTransactionExample } from './create-transfer-nft-transaction'; +import { getNftExample } from './get-nft'; +import { getNftsByAddressExample } from './get-nfts-by-address'; +import { getNftsExample } from './get-nfts'; +import { transferNftExample } from './transfer-nft'; + +describe('NFT Actions Examples (Integration)', () => { + let appKit: AppKit; + let mockClient: { + nftItemsByAddress: ReturnType; + nftItemsByOwner: ReturnType; + runGetMethod: ReturnType; + }; + let consoleSpy: ReturnType; + + // Use zero address which is guaranteed to be valid + const VALID_ADDRESS = 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c'; + const NFT_ADDRESS = VALID_ADDRESS; + const OWNER_ADDRESS = VALID_ADDRESS; + + beforeEach(() => { + vi.clearAllMocks(); + consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + // Initialize real AppKit + appKit = new AppKit({ + networks: { + [Network.mainnet().chainId]: {}, + }, + }); + + // Mock the ApiClient + mockClient = { + nftItemsByAddress: vi.fn(), + nftItemsByOwner: vi.fn(), + runGetMethod: vi.fn(), + }; + + // Spy on networkManager.getClient to return our mock client + // @ts-expect-error - exploiting internal access for testing + vi.spyOn(appKit.networkManager, 'getClient').mockReturnValue(mockClient); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + describe('getNftExample', () => { + it('should get and log NFT info', async () => { + mockClient.nftItemsByAddress.mockResolvedValue({ + nfts: [ + { + address: NFT_ADDRESS, + info: { name: 'Test NFT' }, + collection: { name: 'Test Collection' }, + }, + ], + }); + + await getNftExample(appKit); + + expect(mockClient.nftItemsByAddress).toHaveBeenCalledWith({ + address: NFT_ADDRESS, + }); + expect(consoleSpy).toHaveBeenCalledWith('NFT Name:', 'Test NFT'); + expect(consoleSpy).toHaveBeenCalledWith('NFT Collection:', 'Test Collection'); + }); + }); + + describe('getNftsByAddressExample', () => { + it('should log count of NFTs by address', async () => { + mockClient.nftItemsByOwner.mockResolvedValue({ + nfts: [ + { address: NFT_ADDRESS, info: { name: 'NFT 1' } }, + { + address: 'EQBvW8Z9l8-z_oP_x2J4Cj9v9-y_X--8_e_v_y_f_v_8_e_'.slice(0, 48), + info: { name: 'NFT 2' }, + }, + ], + }); + + await getNftsByAddressExample(appKit); + + expect(mockClient.nftItemsByOwner).toHaveBeenCalledWith({ + ownerAddress: OWNER_ADDRESS, + pagination: { + limit: undefined, + offset: undefined, + }, + }); + expect(consoleSpy).toHaveBeenCalledWith('NFTs by address:', 2); + }); + }); + + describe('getNftsExample', () => { + it('should log count and names of NFTs for selected wallet', async () => { + const mockWallet = { + getAddress: () => OWNER_ADDRESS, + getWalletId: () => 'mock-wallet-id', + getNetwork: () => 'mainnet', + } as unknown as WalletInterface; + appKit.walletsManager.setWallets([mockWallet]); + + mockClient.nftItemsByOwner.mockResolvedValue({ + nfts: [{ address: NFT_ADDRESS, info: { name: 'Cool NFT' } }], + }); + + await getNftsExample(appKit); + + expect(consoleSpy).toHaveBeenCalledWith('Total NFTs:', 1); + expect(consoleSpy).toHaveBeenCalledWith('- Cool NFT'); + }); + }); + + describe('createTransferNftTransactionExample', () => { + it('should log NFT transfer transaction', async () => { + const mockWallet = { + getAddress: () => OWNER_ADDRESS, + getWalletId: () => 'mock-wallet-id', + getNetwork: () => 'mainnet', + } as unknown as WalletInterface; + appKit.walletsManager.setWallets([mockWallet]); + + await createTransferNftTransactionExample(appKit); + + expect(consoleSpy).toHaveBeenCalledWith( + 'NFT Transfer Transaction:', + expect.objectContaining({ + fromAddress: OWNER_ADDRESS, + messages: [ + expect.objectContaining({ + address: NFT_ADDRESS, + amount: '100000000', // DEFAULT_NFT_GAS_FEE + }), + ], + }), + ); + }); + }); + + describe('transferNftExample', () => { + it('should call sendTransaction and log result', async () => { + const mockWallet = { + getAddress: () => OWNER_ADDRESS, + getWalletId: () => 'mock-wallet-id', + getNetwork: () => 'mainnet', + sendTransaction: vi.fn().mockResolvedValue({ hash: 'nft-mock-hash' }), + } as unknown as WalletInterface; + appKit.walletsManager.setWallets([mockWallet]); + + await transferNftExample(appKit); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + fromAddress: OWNER_ADDRESS, + messages: [ + expect.objectContaining({ + address: NFT_ADDRESS, + }), + ], + }), + ); + expect(consoleSpy).toHaveBeenCalledWith('NFT Transfer Result:', { hash: 'nft-mock-hash' }); + }); + }); +}); diff --git a/demo/examples/src/appkit/actions/nft/transfer-nft.ts b/demo/examples/src/appkit/actions/nft/transfer-nft.ts index 274eb79a2..7e53a94de 100644 --- a/demo/examples/src/appkit/actions/nft/transfer-nft.ts +++ b/demo/examples/src/appkit/actions/nft/transfer-nft.ts @@ -12,7 +12,7 @@ import { transferNft } from '@ton/appkit'; export const transferNftExample = async (appKit: AppKit) => { // SAMPLE_START: TRANSFER_NFT const result = await transferNft(appKit, { - nftAddress: 'EQCA14o1-VWhS29szfbpmbu_m7A_9S4m_Ba6sAyALH_mU68j', + nftAddress: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', recipientAddress: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', }); diff --git a/demo/examples/src/appkit/actions/wallets/wallets.test.ts b/demo/examples/src/appkit/actions/wallets/wallets.test.ts new file mode 100644 index 000000000..004732047 --- /dev/null +++ b/demo/examples/src/appkit/actions/wallets/wallets.test.ts @@ -0,0 +1,128 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { AppKit } from '@ton/appkit'; +import { Network } from '@ton/walletkit'; +import type { WalletInterface } from '@ton/appkit'; + +import { getConnectedWalletsExample } from './get-connected-wallets'; +import { getSelectedWalletExample } from './get-selected-wallet'; +import { setSelectedWalletIdExample } from './set-selected-wallet-id'; +import { watchConnectedWalletsExample } from './watch-connected-wallets'; +import { watchSelectedWalletExample } from './watch-selected-wallet'; + +describe('Wallet Actions Examples (Integration)', () => { + let appKit: AppKit; + let consoleSpy: ReturnType; + + const MOCK_WALLET_1 = { + getWalletId: () => 'wallet-1', + getAddress: () => 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', + getNetwork: () => 'mainnet', + } as unknown as WalletInterface; + + const MOCK_WALLET_2 = { + getWalletId: () => 'wallet-2', + getAddress: () => 'EQBvW8Z9l8-z_oP_x2J4Cj9v9-y_X--8_e_v_y_f_v_8_e_'.slice(0, 48), + getNetwork: () => 'testnet', + } as unknown as WalletInterface; + + beforeEach(() => { + vi.clearAllMocks(); + consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + appKit = new AppKit({ + networks: { + [Network.mainnet().chainId]: {}, + [Network.testnet().chainId]: {}, + }, + }); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + describe('getConnectedWalletsExample', () => { + it('should log connected wallets', () => { + appKit.walletsManager.setWallets([MOCK_WALLET_1, MOCK_WALLET_2]); + + getConnectedWalletsExample(appKit); + + expect(consoleSpy).toHaveBeenCalledWith('Connected wallets:', [MOCK_WALLET_1, MOCK_WALLET_2]); + }); + }); + + describe('getSelectedWalletExample', () => { + it('should log selected wallet info', () => { + appKit.walletsManager.setWallets([MOCK_WALLET_1]); + appKit.walletsManager.setSelectedWalletId('wallet-1'); + + getSelectedWalletExample(appKit); + + expect(consoleSpy).toHaveBeenCalledWith('Selected wallet:', 'wallet-1'); + expect(consoleSpy).toHaveBeenCalledWith('Address:', MOCK_WALLET_1.getAddress()); + }); + + it('should not log if no wallet is selected', () => { + appKit.walletsManager.setWallets([MOCK_WALLET_1]); + appKit.walletsManager.setSelectedWalletId(null); + + getSelectedWalletExample(appKit); + + expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining('Selected wallet:'), expect.anything()); + }); + }); + + describe('setSelectedWalletIdExample', () => { + it('should set selected wallet id on walletsManager', () => { + const spy = vi.spyOn(appKit.walletsManager, 'setSelectedWalletId'); + + setSelectedWalletIdExample(appKit); + + expect(spy).toHaveBeenCalledWith('my-wallet-id'); + }); + }); + + describe('watchConnectedWalletsExample', () => { + it('should call onChange when wallets are updated', () => { + const unsubscribe = watchConnectedWalletsExample(appKit); + expect(unsubscribe).toBeTypeOf('function'); + + // Manually emit the event as expected by the action + appKit.emitter.emit('wallets:updated', { wallets: [MOCK_WALLET_1] }, 'test'); + + expect(consoleSpy).toHaveBeenCalledWith('Connected wallets updated:', 1); + + unsubscribe(); + }); + }); + + describe('watchSelectedWalletExample', () => { + it('should call onChange when selected wallet changes', () => { + const unsubscribe = watchSelectedWalletExample(appKit); + + // 1. Select a wallet + appKit.walletsManager.setWallets([MOCK_WALLET_1]); + appKit.walletsManager.setSelectedWalletId('wallet-1'); + // Manually emit selection changed event + // @ts-expect-error - testing internal event emission + appKit.emitter.emit('wallets:selection-changed', {}, 'test'); + expect(consoleSpy).toHaveBeenCalledWith('Selected wallet changed:', 'wallet-1'); + + // 2. Deselect + appKit.walletsManager.setSelectedWalletId(null); + // @ts-expect-error - testing internal event emission + appKit.emitter.emit('wallets:selection-changed', {}, 'test'); + expect(consoleSpy).toHaveBeenCalledWith('Wallet deselected'); + + unsubscribe(); + }); + }); +}); diff --git a/demo/examples/src/appkit/hooks/jettons/jettons.test.tsx b/demo/examples/src/appkit/hooks/jettons/jettons.test.tsx new file mode 100644 index 000000000..f19e947ed --- /dev/null +++ b/demo/examples/src/appkit/hooks/jettons/jettons.test.tsx @@ -0,0 +1,182 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import * as AppKitReact from '@ton/appkit-react'; + +import { UseJettonInfoExample } from './use-jetton-info'; +import { UseJettonBalanceByAddressExample } from './use-jetton-balance-by-address'; +import { UseJettonWalletAddressExample } from './use-jetton-wallet-address'; +import { UseJettonsByAddressExample } from './use-jettons-by-address'; +import { UseJettonsExample } from './use-jettons'; +import { UseTransferJettonExample } from './use-transfer-jetton'; + +// Mock the whole module +vi.mock('@ton/appkit-react', async () => { + const actual = await vi.importActual('@ton/appkit-react'); + return { + ...actual, + useJettonInfo: vi.fn(), + useJettonBalanceByAddress: vi.fn(), + useJettonWalletAddress: vi.fn(), + useJettonsByAddress: vi.fn(), + useJettons: vi.fn(), + useTransferJetton: vi.fn(), + }; +}); + +describe('Jetton Hooks Examples', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('UseJettonInfoExample', () => { + it('should render loading state', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useJettonInfo).mockReturnValue({ + isLoading: true, + data: undefined, + error: null, + }); + + render(); + expect(screen.getByText('Loading...')).toBeDefined(); + }); + + it('should render error state', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useJettonInfo).mockReturnValue({ + isLoading: false, + data: undefined, + error: new Error('Failed to fetch'), + }); + + render(); + expect(screen.getByText('Error: Failed to fetch')).toBeDefined(); + }); + + it('should render jetton info', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useJettonInfo).mockReturnValue({ + isLoading: false, + data: { + name: 'Test Jetton', + symbol: 'TEST', + decimals: 9, + }, + error: null, + }); + + render(); + expect(screen.getByText('Name: Test Jetton')).toBeDefined(); + expect(screen.getByText('Symbol: TEST')).toBeDefined(); + expect(screen.getByText('Decimals: 9')).toBeDefined(); + }); + }); + + describe('UseJettonBalanceByAddressExample', () => { + it('should render balance', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useJettonBalanceByAddress).mockReturnValue({ + isLoading: false, + data: '1000000', + error: null, + }); + + render(); + expect(screen.getByText('Jetton Balance: 1000000')).toBeDefined(); + }); + }); + + describe('UseJettonWalletAddressExample', () => { + it('should render wallet address', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useJettonWalletAddress).mockReturnValue({ + isLoading: false, + data: 'EQB-mock-address', + error: null, + }); + + render(); + expect(screen.getByText('Jetton Wallet Address: EQB-mock-address')).toBeDefined(); + }); + }); + + describe('UseJettonsByAddressExample', () => { + it('should render list of jettons', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useJettonsByAddress).mockReturnValue({ + isLoading: false, + data: { + jettons: [ + { walletAddress: 'addr1', info: { name: 'Jetton 1' }, balance: '10' }, + { walletAddress: 'addr2', info: { name: 'Jetton 2' }, balance: '20' }, + ], + }, + error: null, + }); + + render(); + expect(screen.getByText('Jetton 1: 10')).toBeDefined(); + expect(screen.getByText('Jetton 2: 20')).toBeDefined(); + }); + }); + + describe('UseJettonsExample', () => { + it('should render list of jettons for current wallet', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useJettons).mockReturnValue({ + isLoading: false, + data: { + jettons: [{ walletAddress: 'addr1', info: { name: 'My Jetton' }, balance: '100' }], + }, + error: null, + }); + + render(); + expect(screen.getByText('My Jetton: 100')).toBeDefined(); + }); + }); + + describe('UseTransferJettonExample', () => { + it('should call transfer mutation on button click', () => { + const mockMutate = vi.fn(); + // @ts-expect-error - mock + vi.mocked(AppKitReact.useTransferJetton).mockReturnValue({ + mutate: mockMutate, + isPending: false, + error: null, + }); + + render(); + const button = screen.getByText('Transfer Jetton'); + fireEvent.click(button); + + expect(mockMutate).toHaveBeenCalledWith({ + recipientAddress: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', + amount: '100', + jettonAddress: 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs', + jettonDecimals: 6, + }); + }); + + it('should disable button when loading', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useTransferJetton).mockReturnValue({ + mutate: vi.fn(), + isPending: true, + error: null, + }); + + render(); + const button = screen.getByText('Transferring...'); + expect(button.closest('button')?.disabled).toBe(true); + }); + }); +}); diff --git a/demo/examples/src/appkit/hooks/nft/nfts.test.tsx b/demo/examples/src/appkit/hooks/nft/nfts.test.tsx new file mode 100644 index 000000000..249f640b9 --- /dev/null +++ b/demo/examples/src/appkit/hooks/nft/nfts.test.tsx @@ -0,0 +1,161 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/** @vitest-environment happy-dom */ + +import React from 'react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import * as AppKitReact from '@ton/appkit-react'; +import { Address } from '@ton/core'; + +import { UseNftExample } from './use-nft'; +import { UseNftsByAddressExample } from './use-nfts-by-address'; +import { UseNftsExample } from './use-nfts'; +import { UseTransferNftExample } from './use-transfer-nft'; + +// Mock the whole module +vi.mock('@ton/appkit-react', async () => { + const actual = await vi.importActual('@ton/appkit-react'); + return { + ...actual, + useNft: vi.fn(), + useNftsByAddress: vi.fn(), + useNfts: vi.fn(), + useTransferNft: vi.fn(), + }; +}); + +describe('NFT Hooks Examples', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('UseNftExample', () => { + it('should render loading state', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useNft).mockReturnValue({ + isLoading: true, + data: undefined, + error: null, + }); + + render(); + expect(screen.getByText('Loading...')).toBeDefined(); + }); + + it('should render error state', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useNft).mockReturnValue({ + isLoading: false, + data: undefined, + error: new Error('Failed to fetch'), + }); + + render(); + expect(screen.getByText('Error: Failed to fetch')).toBeDefined(); + }); + + it('should render NFT details', () => { + vi.mocked(AppKitReact.useNft).mockReturnValue({ + isLoading: false, + data: { + info: { name: 'Epic NFT' }, + collection: { name: 'Epic Collection' }, + ownerAddress: Address.parse('EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c'), + }, + error: null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + render(); + expect(screen.getByText('Name: Epic NFT')).toBeDefined(); + expect(screen.getByText('Collection: Epic Collection')).toBeDefined(); + expect(screen.getByText('Owner: EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c')).toBeDefined(); + }); + }); + + describe('UseNftsByAddressExample', () => { + it('should render list of NFTs', () => { + vi.mocked(AppKitReact.useNftsByAddress).mockReturnValue({ + isLoading: false, + data: { + nfts: [ + { + address: Address.parse('EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c'), + info: { name: 'NFT 1' }, + collection: { name: 'Coll 1' }, + }, + ], + }, + error: null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + render(); + expect(screen.getByText('NFT 1 (Coll 1)')).toBeDefined(); + }); + }); + + describe('UseNftsExample', () => { + it('should render my NFTs', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useNfts).mockReturnValue({ + isLoading: false, + data: { + nfts: [ + { + address: Address.parse('EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c'), + info: { name: 'My NFT' }, + collection: { name: 'My Coll' }, + }, + ], + }, + error: null, + }); + + render(); + expect(screen.getByText('My NFT (My Coll)')).toBeDefined(); + }); + }); + + describe('UseTransferNftExample', () => { + it('should call transfer mutation on button click', () => { + const mockMutate = vi.fn(); + // @ts-expect-error - mock + vi.mocked(AppKitReact.useTransferNft).mockReturnValue({ + mutate: mockMutate, + isPending: false, + error: null, + }); + + render(); + const button = screen.getByText('Transfer NFT'); + fireEvent.click(button); + + expect(mockMutate).toHaveBeenCalledWith({ + nftAddress: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', + recipientAddress: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', + comment: 'Gift for you', + }); + }); + + it('should disable button when loading', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useTransferNft).mockReturnValue({ + mutate: vi.fn(), + isPending: true, + error: null, + }); + + render(); + const button = screen.getByText('Transferring...'); + expect(button.closest('button')?.disabled).toBe(true); + }); + }); +}); diff --git a/demo/examples/src/appkit/hooks/swap/swap.test.tsx b/demo/examples/src/appkit/hooks/swap/swap.test.tsx new file mode 100644 index 000000000..6a1e5ff47 --- /dev/null +++ b/demo/examples/src/appkit/hooks/swap/swap.test.tsx @@ -0,0 +1,141 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import * as AppKitReact from '@ton/appkit-react'; + +import { UseSwapQuoteExample } from './use-swap-quote'; +import { UseBuildSwapTransactionExample } from './use-build-swap-transaction'; + +// Mock the whole module +vi.mock('@ton/appkit-react', async () => { + const actual = await vi.importActual('@ton/appkit-react'); + return { + ...actual, + useSwapQuote: vi.fn(), + useBuildSwapTransaction: vi.fn(), + useSendTransaction: vi.fn(), + }; +}); + +describe('Swap Hooks Examples', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('UseSwapQuoteExample', () => { + it('should render loading state', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useSwapQuote).mockReturnValue({ + isLoading: true, + data: undefined, + error: null, + }); + + render(); + expect(screen.getByText('Loading quote...')).toBeDefined(); + }); + + it('should render error state', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useSwapQuote).mockReturnValue({ + isLoading: false, + data: undefined, + error: new Error('Quote failed'), + }); + + render(); + expect(screen.getByText('Error: Quote failed')).toBeDefined(); + }); + + it('should render quote details', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useSwapQuote).mockReturnValue({ + isLoading: false, + data: { + toAmount: '0.99', + priceImpact: '0.01', + }, + error: null, + }); + + render(); + expect(screen.getByText('Expected Output: 0.99')).toBeDefined(); + expect(screen.getByText('Price Impact: 0.01')).toBeDefined(); + }); + }); + + describe('UseBuildSwapTransactionExample', () => { + it('should call buildTx and sendTx on button click', async () => { + const mockQuote = { toAmount: '0.99' }; + const mockTransaction = { to: 'address', value: '100' }; + const mockBuildTx = vi.fn().mockResolvedValue(mockTransaction); + const mockSendTx = vi.fn().mockResolvedValue(true); + + // @ts-expect-error - mock + vi.mocked(AppKitReact.useSwapQuote).mockReturnValue({ + data: mockQuote, + isLoading: false, + error: null, + }); + + // @ts-expect-error - mock + vi.mocked(AppKitReact.useBuildSwapTransaction).mockReturnValue({ + mutateAsync: mockBuildTx, + isPending: false, + }); + + // @ts-expect-error - mock + vi.mocked(AppKitReact.useSendTransaction).mockReturnValue({ + mutateAsync: mockSendTx, + isPending: false, + }); + + render(); + const button = screen.getByText('Swap'); + fireEvent.click(button); + + await waitFor(() => { + expect(mockBuildTx).toHaveBeenCalledWith({ + quote: mockQuote, + userAddress: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', + slippageBps: 100, + }); + }); + + await waitFor(() => { + expect(mockSendTx).toHaveBeenCalledWith(mockTransaction); + }); + }); + + it('should disable button when processing', () => { + // @ts-expect-error - mock + vi.mocked(AppKitReact.useSwapQuote).mockReturnValue({ + data: { toAmount: '0.99' }, + isLoading: false, + }); + + // @ts-expect-error - mock + vi.mocked(AppKitReact.useBuildSwapTransaction).mockReturnValue({ + mutateAsync: vi.fn(), + isPending: true, + }); + + // @ts-expect-error - mock + vi.mocked(AppKitReact.useSendTransaction).mockReturnValue({ + mutateAsync: vi.fn(), + isPending: false, + }); + + render(); + const button = screen.getByText('Processing...'); + expect(button.closest('button')?.disabled).toBe(true); + }); + }); +});