diff --git a/packages/beacon-ui/src/ui/alert/hooks/useWallets.test.tsx b/packages/beacon-ui/src/ui/alert/hooks/useWallets.test.tsx new file mode 100644 index 000000000..ecf472d30 --- /dev/null +++ b/packages/beacon-ui/src/ui/alert/hooks/useWallets.test.tsx @@ -0,0 +1,243 @@ +import { renderHook, act, waitFor } from '@testing-library/react' +import useWallets from './useWallets' +import { PostMessageTransport } from '@airgap/beacon-transport-postmessage' +import { NetworkType } from '@airgap/beacon-types' + +// ===================================================================== +// Mock the wallet utilities with a simple transformation so that the final +// wallet objects match the MergedWallet interface. +jest.mock('../../../utils/wallets', () => ({ + parseWallets: jest.fn((wallets) => + wallets.map((wallet: any) => ({ + id: wallet.id, + key: wallet.key, + name: wallet.name, + image: wallet.image, + // Transform singular fields into arrays. + descriptions: wallet.description ? [wallet.description] : [], + types: wallet.type ? [wallet.type] : [], + links: wallet.link ? [wallet.link] : [], + deepLink: wallet.deepLink, + supportedInteractionStandards: wallet.supportedInteractionStandards + })) + ), + mergeWallets: jest.fn((wallets: any) => wallets), + arrangeTopWallets: jest.fn((wallets: any, featuredWallets: any) => wallets) +})) + +// ===================================================================== +// Provide dummy wallet lists for testing. +const dummyDesktopList = [ + { + key: 'desktopWallet', + name: 'Desktop Wallet', + shortName: 'Desktop', + logo: 'desktop.png', + supportedInteractionStandards: [], + downloadLink: 'http://download.desktop', + deepLink: 'desktop://app' + } +] + +const dummyExtensionList = [ + { + id: 'extWallet', + key: 'extWallet', + shortName: 'Extension', + logo: 'ext.png', + supportedInteractionStandards: [], + link: 'http://extension.link' + } +] + +const dummyIOSList = [ + { + key: 'iosWallet', + shortName: 'iOS', + logo: 'ios.png', + supportedInteractionStandards: [], + universalLink: 'http://ios.universal', + deepLink: 'ios://app' + } +] + +const dummyWebList = [ + { + key: 'webWallet', + shortName: 'Web', + logo: 'web.png', + supportedInteractionStandards: [], + links: { + mainnet: 'http://web.mainnet', + ghostnet: 'http://web.testnet' + } + } +] + +// Mock the module that exports the wallet lists. +jest.mock('../wallet-lists', () => ({ + desktopList: dummyDesktopList, + extensionList: dummyExtensionList, + iOSList: dummyIOSList, + webList: dummyWebList +})) + +// ===================================================================== +// Mock PostMessageTransport so we can control the returned available extensions. +jest.mock('@airgap/beacon-transport-postmessage', () => ({ + PostMessageTransport: { + getAvailableExtensions: jest.fn() + } +})) + +// ===================================================================== +// Create a controlled mock for windowRef event listeners. +let messageHandler: any = null +const mockAddEventListener = jest.fn((event, handler) => { + if (event === 'message') { + messageHandler = handler + } +}) +const mockRemoveEventListener = jest.fn((event, handler) => { + if (event === 'message' && messageHandler === handler) { + messageHandler = null + } +}) + +// Renamed variables starting with "mock" are allowed in module factory. +jest.mock('@airgap/beacon-core', () => ({ + windowRef: { + addEventListener: (event: any, handler: any) => mockAddEventListener(event, handler), + removeEventListener: (event: any, handler: any) => mockRemoveEventListener(event, handler) + } +})) + +// ===================================================================== +// Begin tests +describe('useWallets hook', () => { + beforeEach(() => { + jest.clearAllMocks() + messageHandler = null + }) + + test('fetches available extensions on mount and returns a wallet map', async () => { + // Set up the mock to return an extension that is not already in extensionList. + const fakeExtension = { + id: 'newExt', + name: 'New Extension', + shortName: 'NewExt', + iconUrl: 'new.png', + link: 'http://new.link' + } + + ;(PostMessageTransport.getAvailableExtensions as any).mockResolvedValueOnce([fakeExtension]) + + // Render the hook. + const { result } = renderHook(() => useWallets()) + + // Wait for the async effect (fetching available extensions) to complete. + await waitFor(() => { + const walletMap = result.current + return walletMap instanceof Map && walletMap.size === 5 + }) + + const walletMap = result.current + expect(walletMap).toBeInstanceOf(Map) + expect(walletMap.size).toBe(5) + expect(walletMap.has('desktopWallet')).toBe(true) + expect(walletMap.has('extWallet')).toBe(true) + expect(walletMap.has('iosWallet')).toBe(true) + expect(walletMap.has('webWallet')).toBe(true) + expect(walletMap.has('newExt')).toBe(true) + + // Check one wallet's properties. + const desktopWallet = walletMap.get('desktopWallet') + expect(desktopWallet).toMatchObject({ + id: 'desktopWallet', + name: 'Desktop', + descriptions: ['Desktop App'], + types: ['desktop'], + links: ['http://download.desktop'], + deepLink: 'desktop://app' + }) + }) + + test('updates available extensions on "extensionsUpdated" message event', async () => { + // Initially return an empty array. + ;(PostMessageTransport.getAvailableExtensions as any).mockResolvedValueOnce([]) + + const { result } = renderHook(() => useWallets()) + + // Wait for the initial effect. + await waitFor(() => result.current instanceof Map) + + // Initially, the wallet map should not include any extra extension. + expect(result.current.has('updatedExt')).toBe(false) + + // Now, simulate receiving an "extensionsUpdated" message with new extension data. + const updatedExtension = { + id: 'updatedExt', + name: 'Updated Extension', + shortName: 'UpdatedExt', + iconUrl: 'updated.png', + link: 'http://updated.link' + } + // Set up the mock to return the updated extension. + ;(PostMessageTransport.getAvailableExtensions as any).mockResolvedValueOnce([updatedExtension]) + + // Simulate dispatching a message event. + await act(async () => { + if (messageHandler) { + await messageHandler({ data: 'extensionsUpdated' }) + } + }) + + // Wait for the state update after handling the message. + await waitFor(() => result.current.has('updatedExt')) + expect(result.current.has('updatedExt')).toBe(true) + }) + + test('respects networkType parameter for web wallets', async () => { + // For this test, pass networkType as 'testnet' to match our dummy webList. + ;(PostMessageTransport.getAvailableExtensions as any).mockResolvedValueOnce([]) + + const { result } = renderHook(() => useWallets(NetworkType.GHOSTNET)) + + await waitFor(() => { + const walletMap = result.current + const webWallet = walletMap.get('webWallet') + return Boolean(webWallet && webWallet.links[0] === 'http://web.testnet') + }) + + const walletMap = result.current + const webWallet = walletMap.get('webWallet') + expect(webWallet).toBeDefined() + expect(webWallet!.links[0]).toBe('http://web.testnet') + }) + + test('removes event listener on unmount', async () => { + ;(PostMessageTransport.getAvailableExtensions as any).mockResolvedValueOnce([]) + + const { unmount, result } = renderHook(() => useWallets()) + await waitFor(() => result.current instanceof Map) + + unmount() + expect(mockRemoveEventListener).toHaveBeenCalledWith('message', expect.any(Function)) + }) + + test('passes featuredWallets parameter to arrangeTopWallets', async () => { + // This test checks that the featuredWallets parameter is passed along. + const featuredWallets = ['custom1', 'custom2'] + ;(PostMessageTransport.getAvailableExtensions as any).mockResolvedValueOnce([]) + + renderHook(() => useWallets(undefined, featuredWallets)) + // Wait for the effect to run. + await waitFor(() => { + const { arrangeTopWallets } = require('../../../utils/wallets') + return arrangeTopWallets.mock.calls.length > 0 + }) + + const { arrangeTopWallets } = require('../../../utils/wallets') + expect(arrangeTopWallets).toHaveBeenCalledWith(expect.any(Array), featuredWallets) + }) +}) diff --git a/packages/beacon-ui/src/ui/alert/hooks/useWallets.tsx b/packages/beacon-ui/src/ui/alert/hooks/useWallets.tsx index 345b2302f..c7248c511 100644 --- a/packages/beacon-ui/src/ui/alert/hooks/useWallets.tsx +++ b/packages/beacon-ui/src/ui/alert/hooks/useWallets.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react' import { PostMessageTransport } from '@airgap/beacon-transport-postmessage' -import { arrangeTopWallets, mergeWallets, parseWallets } from 'src/utils/wallets' +import { arrangeTopWallets, mergeWallets, parseWallets } from '../../../utils/wallets' import { Extension, NetworkType } from '@airgap/beacon-types' import { desktopList, extensionList, iOSList, webList } from '../wallet-lists' import { windowRef } from '@airgap/beacon-core'