import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' // Mock Comlink. windowEndpoint mirrors the real one (comlink 4.4.2): its // add/removeEventListener are window's own methods bound to window. This lets the // destroy tests assert real attachment/detachment on window via dispatchEvent, // rather than against a decoupled spy. vi.mock('comlink', () => ({ expose: vi.fn(), windowEndpoint: vi.fn(() => ({ addEventListener: window.addEventListener.bind(window), removeEventListener: window.removeEventListener.bind(window), postMessage: vi.fn() })), proxy: vi.fn((obj: unknown) => obj) })) // Mock @wallet-standard/app vi.mock('@wallet-standard/app', () => ({ getWallets: vi.fn().mockReturnValue({ get: () => [] }) })) // Mock @solana/wallet-adapter-base vi.mock('@solana/wallet-adapter-base', () => ({ isWalletAdapterCompatibleStandardWallet: vi.fn().mockReturnValue(false), WalletReadyState: { Installed: 'Installed', NotDetected: 'NotDetected', Loadable: 'Loadable', Unsupported: 'Unsupported' } })) // Mock @solana/wallet-standard-wallet-adapter-base vi.mock('@solana/wallet-standard-wallet-adapter-base', () => ({ StandardWalletAdapter: vi.fn() })) // Mock @solana/web3.js vi.mock('@solana/web3.js', () => ({ Connection: vi.fn(), Transaction: { from: vi.fn() }, VersionedTransaction: { deserialize: vi.fn() } })) // Hoisted mock for BaseWalletAdapter - direct reference avoids restoreAllMocks issues const baseAdapterMock = vi.hoisted(() => ({ readyState: 'NotDetected' as string, shouldThrow: false, fn: null as ReturnType | null })) baseAdapterMock.fn = vi.fn() // Mock ./BaseWalletAdapter vi.mock('./BaseWalletAdapter', () => ({ BaseWalletAdapter: baseAdapterMock.fn })) describe('BridgeParent', () => { let mockIframe: HTMLIFrameElement beforeEach(() => { vi.clearAllMocks() vi.useFakeTimers() mockIframe = { contentWindow: {}, src: 'https://child.example.com/page' } as any }) afterEach(() => { vi.useRealTimers() vi.restoreAllMocks() }) describe('constructor', () => { it('should throw if no iframe provided', async () => { const { BridgeParent } = await import('./BridgeParent') expect(() => new BridgeParent(null as any)).toThrow( 'BridgeParent requires an iframe element' ) }) it('should accept a valid iframe', async () => { const { BridgeParent } = await import('./BridgeParent') const bridge = new BridgeParent(mockIframe) expect(bridge).toBeDefined() }) it('should call Comlink.expose after initialization', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') new BridgeParent(mockIframe) // Allow the async initializeConnection to run await vi.advanceTimersByTimeAsync(200) expect(Comlink.expose).toHaveBeenCalled() }) it('should call Comlink.windowEndpoint with iframe contentWindow', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) expect(Comlink.windowEndpoint).toHaveBeenCalledWith( mockIframe.contentWindow, window, 'https://child.example.com' ) }) it('should not initialize if iframe has no contentWindow', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') const iframeNoContent = { src: 'https://example.com' } as any new BridgeParent(iframeNoContent) await vi.advanceTimersByTimeAsync(200) expect(Comlink.expose).not.toHaveBeenCalled() }) }) describe('inbound origin/source hardening', () => { it('restricts comlink expose() to the iframe origin (not the "*" default)', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) // mockIframe.src is https://child.example.com/page → origin only. expect(Comlink.expose).toHaveBeenCalledWith( expect.anything(), expect.anything(), ['https://child.example.com'] ) }) it('drops a message whose source is not the iframe window', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') // Mirror how real comlink registers its endpoint listener. const exposedListener = vi.fn() vi.mocked(Comlink.expose).mockImplementationOnce((_api, endpoint) => { const ep = endpoint as { addEventListener: (type: string, listener: unknown) => void } ep.addEventListener('message', exposedListener) }) new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) // A foreign window (different reference than mockIframe.contentWindow). const foreignWindow = {} as Window const evt = new MessageEvent('message', { data: { id: 1 } }) Object.defineProperty(evt, 'source', { value: foreignWindow }) window.dispatchEvent(evt) expect(exposedListener).not.toHaveBeenCalled() }) it('delivers a message that comes from the iframe window', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') const exposedListener = vi.fn() vi.mocked(Comlink.expose).mockImplementationOnce((_api, endpoint) => { const ep = endpoint as { addEventListener: (type: string, listener: unknown) => void } ep.addEventListener('message', exposedListener) }) new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const evt = new MessageEvent('message', { data: { id: 1 } }) Object.defineProperty(evt, 'source', { value: mockIframe.contentWindow }) window.dispatchEvent(evt) expect(exposedListener).toHaveBeenCalledTimes(1) }) }) describe('destroy', () => { it('detaches from window the listener expose() registered (no zombie across sessions)', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') // Real Comlink registers its endpoint listener via // ep.addEventListener('message', cb); simulate that so we can prove destroy() // detaches it. windowEndpoint's add/removeEventListener are window's own // methods (see the comlink mock), so this exercises real window attachment. const exposedListener = vi.fn() vi.mocked(Comlink.expose).mockImplementationOnce((_api, endpoint) => { const ep = endpoint as { addEventListener: (type: string, listener: unknown) => void } ep.addEventListener('message', exposedListener) }) const bridge = new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) // Live before teardown: a window 'message' from the iframe reaches the // listener (source must match — the guard drops anything else). const liveEvt = new MessageEvent('message', { data: { id: 1 } }) Object.defineProperty(liveEvt, 'source', { value: mockIframe.contentWindow }) window.dispatchEvent(liveEvt) expect(exposedListener).toHaveBeenCalledTimes(1) bridge.destroy() // Gone after teardown: the zombie no longer answers. const deadEvt = new MessageEvent('message', { data: { id: 2 } }) Object.defineProperty(deadEvt, 'source', { value: mockIframe.contentWindow }) window.dispatchEvent(deadEvt) expect(exposedListener).toHaveBeenCalledTimes(1) }) it('detaches the guarded wrapper when comlink removes its ORIGINAL listener (RELEASE teardown)', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') // We register a source-guarded wrapper, not comlink's own callback. On a // child RELEASE comlink calls removeEventListener('message', ), // so removeEventListener must translate the original back to the wrapper — // otherwise the wrapper stays attached as a zombie. const exposedListener = vi.fn() let ep!: { addEventListener: (type: string, listener: unknown) => void removeEventListener: (type: string, listener: unknown) => void } vi.mocked(Comlink.expose).mockImplementationOnce((_api, endpoint) => { ep = endpoint as typeof ep ep.addEventListener('message', exposedListener) }) new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) // comlink's RELEASE removal uses its ORIGINAL callback reference. ep.removeEventListener('message', exposedListener) const evt = new MessageEvent('message', { data: { id: 1 } }) Object.defineProperty(evt, 'source', { value: mockIframe.contentWindow }) window.dispatchEvent(evt) expect(exposedListener).not.toHaveBeenCalled() }) it('resets readiness flags so a stale read cannot report ready', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') const bridge = new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any // Solana readiness is true right after init — a real precondition, not a setup. expect(exposedAPI.walletStandardWalletsReady).toBe(true) exposedAPI.tronWalletsReady = true exposedAPI.tonWalletsReady = true exposedAPI.eip6963WalletsReady = true bridge.destroy() expect(exposedAPI.tronWalletsReady).toBe(false) expect(exposedAPI.tonWalletsReady).toBe(false) expect(exposedAPI.eip6963WalletsReady).toBe(false) expect(exposedAPI.walletStandardWalletsReady).toBe(false) }) it('is a safe no-op when the iframe never had a contentWindow', async () => { const { BridgeParent } = await import('./BridgeParent') const iframeNoContent = { src: 'https://child.example.com' } as any const bridge = new BridgeParent(iframeNoContent) await vi.advanceTimersByTimeAsync(200) expect(() => bridge.destroy()).not.toThrow() }) it('clears state and tolerates a second destroy()', async () => { const { BridgeParent } = await import('./BridgeParent') const bridge = new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) bridge.destroy() expect(() => bridge.destroy()).not.toThrow() }) }) describe('EIP6963 wallet discovery', () => { it('should dispatch eip6963:requestProvider event', async () => { const dispatchSpy = vi.spyOn(window, 'dispatchEvent') const { BridgeParent } = await import('./BridgeParent') new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) expect(dispatchSpy).toHaveBeenCalledWith( expect.objectContaining({ type: 'eip6963:requestProvider' }) ) }) it('should collect announced wallets', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') // Set up a listener that announces a wallet when requestProvider fires const mockProvider = { request: vi.fn() } window.addEventListener('eip6963:requestProvider', () => { window.dispatchEvent( new CustomEvent('eip6963:announceProvider', { detail: { info: { uuid: 'wallet-1', name: 'TestWallet', icon: 'test-icon', rdns: 'com.test.wallet' }, provider: mockProvider } }) ) }) new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) // The exposed parentAPI should contain the discovered wallet const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any expect(exposedAPI).toBeDefined() expect(exposedAPI.eip6963Wallets).toHaveLength(1) expect(exposedAPI.eip6963Wallets[0].uuid).toBe('wallet-1') expect(exposedAPI.eip6963Wallets[0].name).toBe('TestWallet') expect(exposedAPI.eip6963WalletsReady).toBe(true) }) it('should deduplicate wallets by uuid', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') const mockProvider = { request: vi.fn() } window.addEventListener('eip6963:requestProvider', () => { // Announce same wallet twice const event = new CustomEvent('eip6963:announceProvider', { detail: { info: { uuid: 'wallet-1', name: 'TestWallet', icon: 'icon', rdns: 'com.test' }, provider: mockProvider } }) window.dispatchEvent(event) window.dispatchEvent(event) }) new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any expect(exposedAPI.eip6963Wallets).toHaveLength(1) }) }) describe('Solana wallet discovery', () => { it('should call getWallets and filter Solana wallets', async () => { const { getWallets } = await import('@wallet-standard/app') const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') const mockSolanaWallet = { name: 'Phantom', chains: ['solana:mainnet'], features: { 'standard:connect': {} }, accounts: [] } const mockNonSolanaWallet = { name: 'SomeWallet', chains: ['ethereum:1'], features: {}, accounts: [] } vi.mocked(getWallets).mockReturnValue({ get: () => [mockSolanaWallet, mockNonSolanaWallet] as any, on: vi.fn(), register: vi.fn() } as any) new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any expect(exposedAPI.walletStandardWallets).toHaveLength(1) expect(exposedAPI.walletStandardWallets[0].name).toBe('Phantom') expect(exposedAPI.walletStandardWalletsReady).toBe(true) }) it('should return empty array when no Solana wallets found', async () => { const { getWallets } = await import('@wallet-standard/app') const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') vi.mocked(getWallets).mockReturnValue({ get: () => [], on: vi.fn(), register: vi.fn() } as any) new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any expect(exposedAPI.walletStandardWallets).toEqual([]) }) }) describe('Tron wallet discovery', () => { it('should start with empty tron wallets and not ready', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any expect(exposedAPI.tronWallets).toEqual([]) expect(exposedAPI.tronWalletsReady).toBe(false) }) it('should discover tron wallets when discoverTronWallets is called', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') // Set up a tron provider on window ;(window as any).tronLink = { ready: true, request: vi.fn(), tronWeb: { defaultAddress: { base58: 'TAddr1', hex: '0x1' }, trx: { sign: vi.fn(), signMessageV2: vi.fn(), sendRawTransaction: vi.fn() }, transactionBuilder: { sendTrx: vi.fn(), triggerSmartContract: vi.fn() }, toHex: vi.fn() } } new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any // Simulate child calling discoverTronWallets exposedAPI.discoverTronWallets(['tronLink']) expect(exposedAPI.tronWalletsReady).toBe(true) expect(exposedAPI.tronWallets).toHaveLength(1) expect(exposedAPI.tronWallets[0].uuid).toBe('tron-tronlink') expect(exposedAPI.tronWallets[0].name).toBe('tronLink') expect(exposedAPI.tronWallets[0].injectedId).toBe('tronLink') // Cleanup delete (window as any).tronLink }) it('should handle nested tron provider paths', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') ;(window as any).tokenpocket = { tron: { ready: true, request: vi.fn(), tronWeb: { defaultAddress: { base58: 'TAddr2' }, trx: { sign: vi.fn(), signMessageV2: vi.fn(), sendRawTransaction: vi.fn() }, transactionBuilder: { sendTrx: vi.fn(), triggerSmartContract: vi.fn() }, toHex: vi.fn() } } } new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any exposedAPI.discoverTronWallets(['tokenpocket.tron']) expect(exposedAPI.tronWallets).toHaveLength(1) expect(exposedAPI.tronWallets[0].uuid).toBe('tron-tokenpocket-tron') delete (window as any).tokenpocket }) it('should return empty when injectedIds is empty', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any exposedAPI.discoverTronWallets([]) expect(exposedAPI.tronWallets).toEqual([]) expect(exposedAPI.tronWalletsReady).toBe(true) }) it('should skip providers without valid tronWeb.trx', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') ;(window as any).badWallet = { ready: true, tronWeb: { notTrx: true } } new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any exposedAPI.discoverTronWallets(['badWallet']) expect(exposedAPI.tronWallets).toEqual([]) delete (window as any).badWallet }) it('should deduplicate tron wallets by injectedId', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') ;(window as any).tronLink = { ready: true, request: vi.fn(), tronWeb: { defaultAddress: { base58: 'TAddr1' }, trx: { sign: vi.fn(), signMessageV2: vi.fn(), sendRawTransaction: vi.fn() }, transactionBuilder: { sendTrx: vi.fn(), triggerSmartContract: vi.fn() }, toHex: vi.fn() } } new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any exposedAPI.discoverTronWallets(['tronLink', 'tronLink']) expect(exposedAPI.tronWallets).toHaveLength(1) delete (window as any).tronLink }) }) describe('TON wallet discovery', () => { it('should start with empty ton wallets and not ready', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any expect(exposedAPI.tonWallets).toEqual([]) expect(exposedAPI.tonWalletsReady).toBe(false) }) it('should discover TON wallets when discoverTonWallets is called', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') ;(window as any).trustwalletTon = { tonconnect: { deviceInfo: { platform: 'chrome', appName: 'Trust Wallet', appVersion: '1.0', maxProtocolVersion: 2, features: [] }, walletInfo: { name: 'Trust Wallet', app_name: 'trustwallet', image: 'https://example.com/icon.png', about_url: 'https://example.com', tondns: '', platforms: ['chrome'], features: [] }, protocolVersion: 2, isWalletBrowser: false, connect: vi.fn(), restoreConnection: vi.fn(), send: vi.fn(), listen: vi.fn(() => vi.fn()), disconnect: vi.fn() } } new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any exposedAPI.discoverTonWallets(['trustwalletTon']) expect(exposedAPI.tonWalletsReady).toBe(true) expect(exposedAPI.tonWallets).toHaveLength(1) expect(exposedAPI.tonWallets[0].jsBridgeKey).toBe('trustwalletTon') expect(exposedAPI.tonWallets[0].uuid).toBe('ton-trustwalletton') delete (window as any).trustwalletTon }) it('should proxy walletInfo for isJSBridgeWithMetadata compatibility', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') ;(window as any).trustwalletTon = { tonconnect: { deviceInfo: { platform: 'chrome', appName: 'Trust Wallet', appVersion: '1.0', maxProtocolVersion: 2, features: [] }, walletInfo: { name: 'Trust Wallet', app_name: 'trustwallet', image: 'https://icon.png', about_url: 'https://tw.com', tondns: '', platforms: ['chrome'], features: [] }, protocolVersion: 2, isWalletBrowser: false, connect: vi.fn(), restoreConnection: vi.fn(), send: vi.fn(), listen: vi.fn(() => vi.fn()), disconnect: vi.fn() } } new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any exposedAPI.discoverTonWallets(['trustwalletTon']) const provider = exposedAPI.tonWallets[0].provider // Comlink.proxy mock returns the object as-is expect(provider.walletInfo).toBeDefined() expect(provider.walletInfo.name).toBe('Trust Wallet') expect(provider.walletInfo.app_name).toBe('trustwallet') expect(provider.walletInfo.platforms).toEqual(['chrome']) expect(provider.protocolVersion).toBe(2) expect(provider.isWalletBrowser).toBe(false) expect(typeof provider.restoreConnection).toBe('function') delete (window as any).trustwalletTon }) it('should skip wallets without valid tonconnect bridge', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') ;(window as any).fakeTon = { tonconnect: { notABridge: true } } new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any exposedAPI.discoverTonWallets(['fakeTon']) expect(exposedAPI.tonWallets).toEqual([]) delete (window as any).fakeTon }) it('should deduplicate TON wallets by jsBridgeKey', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') ;(window as any).trustwalletTon = { tonconnect: { connect: vi.fn(), send: vi.fn(), listen: vi.fn(() => vi.fn()), deviceInfo: { appName: 'Trust Wallet' } } } new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any exposedAPI.discoverTonWallets(['trustwalletTon', 'trustwalletTon']) expect(exposedAPI.tonWallets).toHaveLength(1) delete (window as any).trustwalletTon }) }) describe('Base Wallet detection', () => { beforeEach(async () => { baseAdapterMock.readyState = 'NotDetected' baseAdapterMock.shouldThrow = false // Re-set mocks that vi.restoreAllMocks() may have cleared const { getWallets } = await import('@wallet-standard/app') vi.mocked(getWallets).mockReturnValue({ get: () => [], on: vi.fn(), register: vi.fn() } as any) // Re-set BaseWalletAdapter mock - must use 'function' (not arrow) for 'new' compatibility baseAdapterMock.fn!.mockImplementation(function () { if (baseAdapterMock.shouldThrow) throw new Error('init failed') return { readyState: baseAdapterMock.readyState, name: 'Base Wallet', icon: 'data:image/svg+xml;base64,test', url: 'https://example.com', publicKey: null, connecting: false, connected: false, supportedTransactionVersions: new Set(['legacy', 0]), connect: vi.fn(), disconnect: vi.fn(), sendTransaction: vi.fn(), signTransaction: vi.fn(), signAllTransactions: vi.fn(), signMessage: vi.fn() } }) }) it('should include Base Wallet when detected as Installed', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') baseAdapterMock.readyState = 'Installed' new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any const baseWallet = exposedAPI.walletStandardWallets.find( (w: any) => w.name === 'Base Wallet' ) expect(baseWallet).toBeDefined() expect(baseWallet.uuid).toBe('base-wallet-traditional') expect(baseWallet.chains).toEqual(['solana:mainnet']) expect(baseWallet.features).toEqual(['traditional-adapter']) expect(baseWallet.adapter).toBeDefined() }) it('should include Base Wallet when detected as Loadable', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') baseAdapterMock.readyState = 'Loadable' new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any const baseWallet = exposedAPI.walletStandardWallets.find( (w: any) => w.name === 'Base Wallet' ) expect(baseWallet).toBeDefined() }) it('should not include Base Wallet when not detected', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any const baseWallet = exposedAPI.walletStandardWallets.find( (w: any) => w.name === 'Base Wallet' ) expect(baseWallet).toBeUndefined() }) it('should skip Base Wallet if name already in Wallet Standard wallets', async () => { const { getWallets } = await import('@wallet-standard/app') const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') const existingWallet = { name: 'Base Wallet', chains: ['solana:mainnet'], features: { 'standard:connect': {} }, accounts: [] } vi.mocked(getWallets).mockReturnValue({ get: () => [existingWallet] as any, on: vi.fn(), register: vi.fn() } as any) baseAdapterMock.readyState = 'Installed' new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any const baseWallets = exposedAPI.walletStandardWallets.filter( (w: any) => w.name === 'Base Wallet' ) expect(baseWallets).toHaveLength(1) expect(baseWallets[0].features).not.toContain('traditional-adapter') }) it('should handle BaseWalletAdapter constructor errors gracefully', async () => { const Comlink = await import('comlink') const { BridgeParent } = await import('./BridgeParent') baseAdapterMock.shouldThrow = true new BridgeParent(mockIframe) await vi.advanceTimersByTimeAsync(200) const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0] as any expect(exposedAPI).toBeDefined() expect(exposedAPI.walletStandardWallets).toEqual([]) }) }) })