import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' // Mock @solana/wallet-adapter-base with a proper base class vi.mock('@solana/wallet-adapter-base', () => { class MockBaseMessageSignerWalletAdapter { _evtListeners: Record void)[]> = {} get connected() { return !!(this as any)._publicKey } emit(event: string, ...args: any[]) { for (const fn of this._evtListeners[event] || []) fn(...args) return true } on(event: string, fn: (...args: any[]) => void) { if (!this._evtListeners[event]) this._evtListeners[event] = [] this._evtListeners[event].push(fn) return this } off(event: string, fn: (...args: any[]) => void) { if (this._evtListeners[event]) { this._evtListeners[event] = this._evtListeners[event].filter( f => f !== fn ) } return this } async prepareTransaction(tx: any) { return tx } } return { BaseMessageSignerWalletAdapter: MockBaseMessageSignerWalletAdapter, isVersionedTransaction: vi.fn(() => false), scopePollingDetectionStrategy: vi.fn(), WalletAccountError: class extends Error { override name = 'WalletAccountError' }, WalletConnectionError: class extends Error { override name = 'WalletConnectionError' constructor(msg?: string) { super(msg || 'WalletConnectionError') } }, WalletDisconnectedError: class extends Error { override name = 'WalletDisconnectedError' }, WalletDisconnectionError: class extends Error { override name = 'WalletDisconnectionError' constructor(msg?: string) { super(msg || 'WalletDisconnectionError') } }, WalletError: class extends Error {}, WalletNotConnectedError: class extends Error { override name = 'WalletNotConnectedError' }, WalletNotReadyError: class extends Error { override name = 'WalletNotReadyError' }, WalletPublicKeyError: class extends Error { override name = 'WalletPublicKeyError' constructor(msg?: string) { super(msg || 'WalletPublicKeyError') } }, WalletReadyState: { Installed: 'Installed', NotDetected: 'NotDetected', Loadable: 'Loadable', Unsupported: 'Unsupported' }, WalletSendTransactionError: class extends Error { override name = 'WalletSendTransactionError' constructor(msg?: string) { super(msg || 'WalletSendTransactionError') } }, WalletSignTransactionError: class extends Error { override name = 'WalletSignTransactionError' constructor(msg?: string) { super(msg || 'WalletSignTransactionError') } } } }) vi.mock('@solana/web3.js', () => ({ PublicKey: class { constructor(public _bytes: any) {} toBase58() { return 'mockBase58' } toBytes() { return this._bytes } toJSON() { return 'mockJSON' } toBuffer() { return new Uint8Array([]) } toString() { return 'mockString' } } })) describe('BaseWalletAdapter', () => { let mockWallet: any beforeEach(() => { vi.clearAllMocks() mockWallet = { publicKey: { toBytes: () => new Uint8Array([1, 2, 3]) }, connect: vi.fn().mockResolvedValue(undefined), disconnect: vi.fn().mockResolvedValue(undefined), signTransaction: vi.fn(async (tx: any) => tx), signAllTransactions: vi.fn(async (txs: any) => txs), signAndSendTransaction: vi .fn() .mockResolvedValue({ signature: 'mockSig' }), signMessage: vi .fn() .mockResolvedValue({ signature: new Uint8Array([4, 5, 6]) }), on: vi.fn(), off: vi.fn() } }) afterEach(() => { delete (window as any).coinbaseSolana vi.restoreAllMocks() }) async function createInstalledAdapter() { const { scopePollingDetectionStrategy } = await import('@solana/wallet-adapter-base') const { BaseWalletAdapter } = await import('./BaseWalletAdapter') ;(window as any).coinbaseSolana = mockWallet const adapter = new BaseWalletAdapter() const calls = vi.mocked(scopePollingDetectionStrategy).mock.calls const callback = calls[calls.length - 1]![0] as () => boolean callback() return adapter } describe('constructor', () => { it('should set correct name and properties', async () => { const { BaseWalletAdapter } = await import('./BaseWalletAdapter') const adapter = new BaseWalletAdapter() expect(adapter.name).toBe('Base Wallet') expect(adapter.url).toContain('chrome.google.com') expect(adapter.icon).toContain('data:image/svg+xml;base64,') expect(adapter.publicKey).toBeNull() expect(adapter.connecting).toBe(false) expect(adapter.readyState).toBe('NotDetected') }) it('should call scopePollingDetectionStrategy', async () => { const { scopePollingDetectionStrategy } = await import('@solana/wallet-adapter-base') const { BaseWalletAdapter } = await import('./BaseWalletAdapter') new BaseWalletAdapter() expect(scopePollingDetectionStrategy).toHaveBeenCalledWith( expect.any(Function) ) }) it('should detect wallet when window.coinbaseSolana exists', async () => { const { scopePollingDetectionStrategy } = await import('@solana/wallet-adapter-base') const { BaseWalletAdapter } = await import('./BaseWalletAdapter') const adapter = new BaseWalletAdapter() const mock = vi.mocked(scopePollingDetectionStrategy).mock const callback = mock.calls[mock.calls.length - 1]![0] as () => boolean expect(callback()).toBe(false) ;(window as any).coinbaseSolana = mockWallet expect(callback()).toBe(true) expect(adapter.readyState).toBe('Installed') }) }) describe('connect', () => { it('should throw WalletNotReadyError when not installed', async () => { const { BaseWalletAdapter } = await import('./BaseWalletAdapter') const adapter = new BaseWalletAdapter() await expect(adapter.connect()).rejects.toThrow() }) it('should connect successfully when installed', async () => { const adapter = await createInstalledAdapter() await adapter.connect() expect(mockWallet.connect).toHaveBeenCalled() expect(adapter.publicKey).toBeDefined() expect(adapter.connecting).toBe(false) }) it('should emit connect event on success', async () => { const adapter = await createInstalledAdapter() const connectHandler = vi.fn() adapter.on('connect', connectHandler) await adapter.connect() expect(connectHandler).toHaveBeenCalled() }) it('should throw when wallet.connect fails', async () => { mockWallet.connect.mockRejectedValue(new Error('Connection failed')) const adapter = await createInstalledAdapter() await expect(adapter.connect()).rejects.toThrow() }) it('should throw when no publicKey after connect', async () => { mockWallet.publicKey = null const adapter = await createInstalledAdapter() await expect(adapter.connect()).rejects.toThrow() }) it('should return early if already connected', async () => { const adapter = await createInstalledAdapter() await adapter.connect() await adapter.connect() expect(mockWallet.connect).toHaveBeenCalledTimes(1) }) }) describe('disconnect', () => { it('should clear state on disconnect', async () => { const adapter = await createInstalledAdapter() await adapter.connect() await adapter.disconnect() expect(adapter.publicKey).toBeNull() expect(mockWallet.disconnect).toHaveBeenCalled() }) it('should emit disconnect event', async () => { const adapter = await createInstalledAdapter() await adapter.connect() const handler = vi.fn() adapter.on('disconnect', handler) await adapter.disconnect() expect(handler).toHaveBeenCalled() }) it('should handle wallet.disconnect error gracefully', async () => { mockWallet.disconnect.mockRejectedValue(new Error('fail')) const adapter = await createInstalledAdapter() await adapter.connect() await expect(adapter.disconnect()).resolves.toBeUndefined() }) it('should emit disconnect even when no wallet connected', async () => { const { BaseWalletAdapter } = await import('./BaseWalletAdapter') const adapter = new BaseWalletAdapter() const handler = vi.fn() adapter.on('disconnect', handler) await adapter.disconnect() expect(handler).toHaveBeenCalled() }) }) describe('signTransaction', () => { it('should throw when not connected', async () => { const { BaseWalletAdapter } = await import('./BaseWalletAdapter') const adapter = new BaseWalletAdapter() await expect(adapter.signTransaction({} as any)).rejects.toThrow() }) it('should call wallet.signTransaction when connected', async () => { const adapter = await createInstalledAdapter() await adapter.connect() const mockTx = { mock: true } as any await adapter.signTransaction(mockTx) expect(mockWallet.signTransaction).toHaveBeenCalledWith(mockTx) }) }) describe('signAllTransactions', () => { it('should throw when not connected', async () => { const { BaseWalletAdapter } = await import('./BaseWalletAdapter') const adapter = new BaseWalletAdapter() await expect(adapter.signAllTransactions([{} as any])).rejects.toThrow() }) it('should call wallet.signAllTransactions when connected', async () => { const adapter = await createInstalledAdapter() await adapter.connect() const mockTxs = [{ mock: 1 }, { mock: 2 }] as any[] await adapter.signAllTransactions(mockTxs) expect(mockWallet.signAllTransactions).toHaveBeenCalledWith(mockTxs) }) }) describe('signMessage', () => { it('should throw when not connected', async () => { const { BaseWalletAdapter } = await import('./BaseWalletAdapter') const adapter = new BaseWalletAdapter() await expect(adapter.signMessage(new Uint8Array([1]))).rejects.toThrow() }) it('should return signature when connected', async () => { const adapter = await createInstalledAdapter() await adapter.connect() const result = await adapter.signMessage(new Uint8Array([1, 2, 3])) expect(mockWallet.signMessage).toHaveBeenCalled() expect(result).toEqual(new Uint8Array([4, 5, 6])) }) }) describe('sendTransaction', () => { it('should throw when not connected', async () => { const { BaseWalletAdapter } = await import('./BaseWalletAdapter') const adapter = new BaseWalletAdapter() await expect( adapter.sendTransaction({} as any, {} as any) ).rejects.toThrow() }) it('should return signature when connected', async () => { const adapter = await createInstalledAdapter() await adapter.connect() const mockTx = {} as any const mockConnection = { commitment: 'confirmed' } as any const result = await adapter.sendTransaction(mockTx, mockConnection) expect(mockWallet.signAndSendTransaction).toHaveBeenCalled() expect(result).toBe('mockSig') }) }) })