import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' // Mock @solana/wallet-adapter-base with a minimal base class (mirrors // BaseWalletAdapter.test.ts in uwc-bridge-parent). 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 } } const err = (name: string) => class extends Error { override name = name constructor(msg?: string) { super(msg || name) } } return { BaseMessageSignerWalletAdapter: MockBaseMessageSignerWalletAdapter, isVersionedTransaction: vi.fn(() => false), scopePollingDetectionStrategy: vi.fn(), WalletAccountError: err('WalletAccountError'), WalletConnectionError: err('WalletConnectionError'), WalletDisconnectedError: err('WalletDisconnectedError'), WalletDisconnectionError: err('WalletDisconnectionError'), WalletError: class extends Error {}, WalletNotConnectedError: err('WalletNotConnectedError'), WalletNotReadyError: err('WalletNotReadyError'), WalletPublicKeyError: err('WalletPublicKeyError'), WalletReadyState: { Installed: 'Installed', NotDetected: 'NotDetected', Loadable: 'Loadable', Unsupported: 'Unsupported' }, WalletSendTransactionError: err('WalletSendTransactionError'), WalletSignTransactionError: err('WalletSignTransactionError'), WalletSignMessageError: err('WalletSignMessageError') } }) vi.mock('@solana/web3.js', () => ({ PublicKey: class { constructor(public _bytes: any) {} toBase58() { return 'mockBase58' } toBytes() { return this._bytes } } })) const INJECTED_ID = 'coinbaseSolana' const WALLET_NAME = 'Base Wallet' describe('LegacyInjectedSolanaWalletAdapter (generic, metadata-driven)', () => { 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)[INJECTED_ID] delete (window as any).nested vi.restoreAllMocks() }) async function createInstalledAdapter(injectedId = INJECTED_ID) { const { LegacyInjectedSolanaWalletAdapter } = await import('./legacy-injected-solana-adapter') // Place the provider at the (possibly nested) injectedId path. if (injectedId.includes('.')) { const [head, tail] = injectedId.split('.') ;(window as any)[head!] = { [tail!]: mockWallet } } else { ;(window as any)[injectedId] = mockWallet } // Provider present at construction → the adapter is Installed synchronously, // no polling tick required. return new LegacyInjectedSolanaWalletAdapter(injectedId, WALLET_NAME) } describe('provider resolution helpers', () => { it('getInjectedSolanaProvider returns undefined when absent', async () => { const { getInjectedSolanaProvider } = await import('./legacy-injected-solana-adapter') expect(getInjectedSolanaProvider(INJECTED_ID)).toBeUndefined() }) it('getInjectedSolanaProvider returns the provider when present + valid', async () => { const { getInjectedSolanaProvider } = await import('./legacy-injected-solana-adapter') ;(window as any)[INJECTED_ID] = mockWallet expect(getInjectedSolanaProvider(INJECTED_ID)).toBe(mockWallet) }) it('getInjectedSolanaProvider rejects a global that is not a Solana provider', async () => { const { getInjectedSolanaProvider } = await import('./legacy-injected-solana-adapter') ;(window as any)[INJECTED_ID] = { foo: 'bar' } // no connect/signTransaction expect(getInjectedSolanaProvider(INJECTED_ID)).toBeUndefined() }) it('resolves a dot-nested injectedId path', async () => { const { getInjectedSolanaProvider } = await import('./legacy-injected-solana-adapter') ;(window as any).nested = { solana: mockWallet } expect(getInjectedSolanaProvider('nested.solana')).toBe(mockWallet) }) it('isLegacyInjectedSolanaProvider requires the full invoked method surface', async () => { const { isLegacyInjectedSolanaProvider } = await import('./legacy-injected-solana-adapter') // mockWallet implements connect/disconnect/on/off/sign* → valid expect(isLegacyInjectedSolanaProvider(mockWallet)).toBe(true) expect(isLegacyInjectedSolanaProvider({ connect: () => {} })).toBe(false) // Partial provider (connect + signTransaction only) is now rejected — it // would throw at disconnect/signMessage/lifecycle time otherwise. expect( isLegacyInjectedSolanaProvider({ connect: () => {}, signTransaction: () => {} }) ).toBe(false) // Missing just one method (off) → rejected. const missingOff = { ...mockWallet } delete missingOff.off expect(isLegacyInjectedSolanaProvider(missingOff)).toBe(false) expect(isLegacyInjectedSolanaProvider(null)).toBe(false) }) }) describe('constructor', () => { it('uses the name passed in (no hardcoded wallet name)', async () => { const { LegacyInjectedSolanaWalletAdapter } = await import('./legacy-injected-solana-adapter') const adapter = new LegacyInjectedSolanaWalletAdapter( INJECTED_ID, 'Some Other Wallet' ) expect(adapter.name).toBe('Some Other Wallet') expect(adapter.publicKey).toBeNull() expect(adapter.readyState).toBe('NotDetected') expect(adapter.supportedTransactionVersions).toEqual( new Set(['legacy', 0]) ) }) it('is Installed synchronously when the provider is already present (no poll tick)', async () => { const { scopePollingDetectionStrategy } = await import('@solana/wallet-adapter-base') const { LegacyInjectedSolanaWalletAdapter } = await import('./legacy-injected-solana-adapter') // Provider present before construction (the discovery path guarantees this): // readyState must be Installed immediately so the synchronous auto-reconnect // connect() doesn't race the first polling tick and throw WalletNotReadyError. ;(window as any)[INJECTED_ID] = mockWallet const adapter = new LegacyInjectedSolanaWalletAdapter( INJECTED_ID, WALLET_NAME ) expect(adapter.readyState).toBe('Installed') expect(scopePollingDetectionStrategy).not.toHaveBeenCalled() }) it('flips readyState to Installed only once its injectedId global appears', async () => { const { scopePollingDetectionStrategy } = await import('@solana/wallet-adapter-base') const { LegacyInjectedSolanaWalletAdapter } = await import('./legacy-injected-solana-adapter') const adapter = new LegacyInjectedSolanaWalletAdapter( INJECTED_ID, WALLET_NAME ) const mock = vi.mocked(scopePollingDetectionStrategy).mock const cb = mock.calls[mock.calls.length - 1]![0] as () => boolean expect(cb()).toBe(false) ;(window as any)[INJECTED_ID] = mockWallet expect(cb()).toBe(true) expect(adapter.readyState).toBe('Installed') }) }) describe('connect', () => { it('throws when not installed', async () => { const { LegacyInjectedSolanaWalletAdapter } = await import('./legacy-injected-solana-adapter') const adapter = new LegacyInjectedSolanaWalletAdapter( INJECTED_ID, WALLET_NAME ) await expect(adapter.connect()).rejects.toThrow() expect(mockWallet.connect).not.toHaveBeenCalled() }) it('connects and exposes the publicKey', async () => { const adapter = await createInstalledAdapter() await adapter.connect() expect(mockWallet.connect).toHaveBeenCalled() expect(adapter.publicKey?.toBase58()).toBe('mockBase58') }) it('resolves the provider at a nested injectedId', async () => { const adapter = await createInstalledAdapter('nested.solana') await adapter.connect() expect(mockWallet.connect).toHaveBeenCalled() expect(adapter.publicKey).not.toBeNull() }) it('wraps a provider connect rejection', async () => { mockWallet.connect.mockRejectedValue(new Error('user rejected')) const adapter = await createInstalledAdapter() await expect(adapter.connect()).rejects.toThrow() expect(adapter.publicKey).toBeNull() }) it('throws when the provider returns no publicKey', async () => { mockWallet.publicKey = null const adapter = await createInstalledAdapter() await expect(adapter.connect()).rejects.toThrow() }) it('is idempotent while connected', async () => { const adapter = await createInstalledAdapter() await adapter.connect() await adapter.connect() expect(mockWallet.connect).toHaveBeenCalledTimes(1) }) it('a concurrent connect() early-returns without clearing the in-flight connecting state', async () => { const adapter = await createInstalledAdapter() let resolveConnect!: () => void mockWallet.connect.mockReturnValue( new Promise(r => { resolveConnect = r }) ) const inflight = adapter.connect() expect(adapter.connecting).toBe(true) // Second call while the first is in flight must early-return and NOT reset // the shared connecting state (pre-fix the finally flipped it to false). await adapter.connect() expect(adapter.connecting).toBe(true) resolveConnect() await inflight expect(adapter.connecting).toBe(false) expect(mockWallet.connect).toHaveBeenCalledTimes(1) }) }) describe('disconnect + signing', () => { it('clears state and emits disconnect', async () => { const adapter = await createInstalledAdapter() await adapter.connect() const handler = vi.fn() adapter.on('disconnect', handler) await adapter.disconnect() expect(adapter.publicKey).toBeNull() expect(mockWallet.disconnect).toHaveBeenCalled() expect(handler).toHaveBeenCalled() }) it('signTransaction throws when not connected', async () => { const { LegacyInjectedSolanaWalletAdapter } = await import('./legacy-injected-solana-adapter') const adapter = new LegacyInjectedSolanaWalletAdapter( INJECTED_ID, WALLET_NAME ) await expect(adapter.signTransaction({} as any)).rejects.toThrow() }) it('signTransaction delegates to the provider', async () => { const adapter = await createInstalledAdapter() await adapter.connect() const tx = { id: 1 } as any await adapter.signTransaction(tx) expect(mockWallet.signTransaction).toHaveBeenCalledWith(tx) }) it('signMessage returns the provider signature', async () => { const adapter = await createInstalledAdapter() await adapter.connect() const sig = await adapter.signMessage(new Uint8Array([1])) expect(sig).toEqual(new Uint8Array([4, 5, 6])) }) it('sendTransaction returns the provider signature', async () => { const adapter = await createInstalledAdapter() await adapter.connect() const result = await adapter.sendTransaction( {} as any, { commitment: 'confirmed' } as any ) expect(mockWallet.signAndSendTransaction).toHaveBeenCalled() expect(result).toBe('mockSig') }) }) })