import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import type { WalletMetadata } from '@meshconnect/uwc-types' function makeBridge(appName = 'TestWallet') { return { deviceInfo: { platform: 'browser', appName }, connect: vi.fn().mockResolvedValue(''), send: vi.fn().mockResolvedValue(''), listen: vi.fn().mockReturnValue(() => {}), disconnect: vi.fn().mockResolvedValue(undefined) } } function makeWalletMeta(jsBridgeKey: string): WalletMetadata { return { extensionInjectedProvider: { namespaceMetaData: { tvm: { jsBridgeKey } } } } as unknown as WalletMetadata } const mockWindow: Record = { UWCBridgeChildInitialized: undefined, tonkeeper: undefined, mytonwallet: undefined, okxTonWallet: undefined, tonWallets: undefined } Object.defineProperty(globalThis, 'window', { value: mockWindow, writable: true }) describe('getTonWallets / discoverTonProviders', () => { beforeEach(() => { vi.resetModules() mockWindow.UWCBridgeChildInitialized = undefined mockWindow.tonkeeper = undefined mockWindow.mytonwallet = undefined mockWindow.okxTonWallet = undefined mockWindow.tonWallets = undefined }) afterEach(() => { vi.restoreAllMocks() }) it('finds wallet at window[jsBridgeKey].tonconnect per spec', async () => { const bridge = makeBridge('Tonkeeper') mockWindow.tonkeeper = { tonconnect: bridge } const { getTonWallets } = await import('./ton-discovery') const wallets = await getTonWallets([makeWalletMeta('tonkeeper')]) expect(wallets).toHaveLength(1) expect(wallets[0].jsBridgeKey).toBe('tonkeeper') expect(wallets[0].name).toBe('Tonkeeper') expect(wallets[0].uuid).toBe('ton-tonkeeper') expect(wallets[0].provider).toBe(bridge) }) it('finds OKX wallet at window[jsBridgeKey].tonconnect', async () => { const bridge = makeBridge('OKX Wallet') mockWindow.okxTonWallet = { tonconnect: bridge } const { getTonWallets } = await import('./ton-discovery') const wallets = await getTonWallets([makeWalletMeta('okxTonWallet')]) expect(wallets).toHaveLength(1) expect(wallets[0].jsBridgeKey).toBe('okxTonWallet') expect(wallets[0].name).toBe('OKX Wallet') expect(wallets[0].provider).toBe(bridge) }) it('returns empty array when no wallets are found', async () => { const { getTonWallets } = await import('./ton-discovery') const wallets = await getTonWallets([makeWalletMeta('nonexistent')]) expect(wallets).toEqual([]) }) it('deduplicates keys', async () => { const bridge = makeBridge('Tonkeeper') mockWindow.tonkeeper = { tonconnect: bridge } const { getTonWallets } = await import('./ton-discovery') const wallets = await getTonWallets([ makeWalletMeta('tonkeeper'), makeWalletMeta('tonkeeper') ]) expect(wallets).toHaveLength(1) }) it('returns empty array in SSR (window undefined)', async () => { const savedWindow = globalThis.window // @ts-expect-error -- simulate SSR delete globalThis.window const { getTonWallets } = await import('./ton-discovery') const wallets = await getTonWallets([makeWalletMeta('tonkeeper')]) expect(wallets).toEqual([]) globalThis.window = savedWindow }) it('skips objects where .tonconnect is not a valid bridge (missing methods)', async () => { mockWindow.tonkeeper = { tonconnect: { deviceInfo: { platform: 'browser', appName: 'Fake' }, connect: vi.fn() // missing send and listen } } const { getTonWallets } = await import('./ton-discovery') const wallets = await getTonWallets([makeWalletMeta('tonkeeper')]) expect(wallets).toEqual([]) }) it('skips objects without .tonconnect property', async () => { mockWindow.tonkeeper = { someOtherProp: true } const { getTonWallets } = await import('./ton-discovery') const wallets = await getTonWallets([makeWalletMeta('tonkeeper')]) expect(wallets).toEqual([]) }) it('uses native discovery when not in bridge mode', async () => { const bridge = makeBridge('Tonkeeper') mockWindow.tonkeeper = { tonconnect: bridge } const { getTonWallets } = await import('./ton-discovery') const wallets = await getTonWallets([makeWalletMeta('tonkeeper')]) expect(wallets).toHaveLength(1) expect(wallets[0].jsBridgeKey).toBe('tonkeeper') }) }) describe('getTonWallets / bridge polling', () => { beforeEach(() => { vi.resetModules() vi.useFakeTimers() mockWindow.UWCBridgeChildInitialized = undefined mockWindow.tonkeeper = undefined mockWindow.mytonwallet = undefined mockWindow.okxTonWallet = undefined mockWindow.tonWallets = undefined }) afterEach(() => { vi.useRealTimers() vi.restoreAllMocks() }) it('prefers the in-frame (local) provider over the bridge when both are present', async () => { mockWindow.UWCBridgeChildInitialized = true // Parent-proxied wallet reported by the bridge... const bridgeWallet = { uuid: 'ton-tonkeeper', name: 'Tonkeeper', icon: '', jsBridgeKey: 'tonkeeper', provider: makeBridge('Tonkeeper') } mockWindow.tonWallets = [bridgeWallet] // ...and the extension's real provider injected into THIS frame. The local // one must win so the connection attributes to the iframe origin. const localBridge = makeBridge('Tonkeeper') mockWindow.tonkeeper = { tonconnect: localBridge } const { getTonWallets } = await import('./ton-discovery') const wallets = await getTonWallets([makeWalletMeta('tonkeeper')]) expect(wallets).toHaveLength(1) expect(wallets[0].jsBridgeKey).toBe('tonkeeper') expect(wallets[0].provider).toBe(localBridge) expect(wallets[0].provider).not.toBe(bridgeWallet.provider) }) it('returns empty array when bridge polling times out', async () => { mockWindow.UWCBridgeChildInitialized = true // No tonWallets set — polling will time out const { getTonWallets } = await import('./ton-discovery') const resultPromise = getTonWallets([makeWalletMeta('tonkeeper')]) await vi.advanceTimersByTimeAsync(5400) const wallets = await resultPromise expect(wallets).toEqual([]) }) it('uses the in-frame (local) provider in bridge mode without waiting for the parent', async () => { mockWindow.UWCBridgeChildInitialized = true // In-frame provider present; parent never reports any wallets. const localBridge = makeBridge('Tonkeeper') mockWindow.tonkeeper = { tonconnect: localBridge } const { getTonWallets } = await import('./ton-discovery') const resultPromise = getTonWallets([makeWalletMeta('tonkeeper')]) await vi.advanceTimersByTimeAsync(5400) const wallets = await resultPromise expect(wallets).toHaveLength(1) expect(wallets[0].jsBridgeKey).toBe('tonkeeper') expect(wallets[0].provider).toBe(localBridge) }) it('prefers the in-frame OKX provider in bridge mode (detection is generic, not Tonkeeper-only)', async () => { mockWindow.UWCBridgeChildInitialized = true const localBridge = makeBridge('OKX Wallet') mockWindow.okxTonWallet = { tonconnect: localBridge } const { getTonWallets } = await import('./ton-discovery') const wallets = await getTonWallets([makeWalletMeta('okxTonWallet')]) expect(wallets).toHaveLength(1) expect(wallets[0].jsBridgeKey).toBe('okxTonWallet') expect(wallets[0].provider).toBe(localBridge) }) it('falls back to the parent-proxied list when nothing is present at window[jsBridgeKey] yet', async () => { mockWindow.UWCBridgeChildInitialized = true // Nothing at window.tonkeeper yet (before BridgeChild shims, or an old // bridge-parent without TON support). const bridgeWallet = { uuid: 'ton-tonkeeper', name: 'Tonkeeper', icon: '', jsBridgeKey: 'tonkeeper', provider: makeBridge('Tonkeeper') } mockWindow.tonWallets = [bridgeWallet] const { getTonWallets } = await import('./ton-discovery') const wallets = await getTonWallets([makeWalletMeta('tonkeeper')]) expect(wallets).toEqual([bridgeWallet]) }, 3000) it('picks up bridge wallets that appear after a delay', async () => { mockWindow.UWCBridgeChildInitialized = true const bridgeWallet = { uuid: 'ton-tonkeeper', name: 'Tonkeeper', icon: '', jsBridgeKey: 'tonkeeper', provider: makeBridge('Tonkeeper') } // Simulate bridge wallets appearing after 200ms setTimeout(() => { mockWindow.tonWallets = [bridgeWallet] }, 200) const { getTonWallets } = await import('./ton-discovery') const resultPromise = getTonWallets([makeWalletMeta('tonkeeper')]) await vi.advanceTimersByTimeAsync(400) const wallets = await resultPromise expect(wallets).toEqual([bridgeWallet]) }) it('handles invalid bridge wallets array', async () => { mockWindow.UWCBridgeChildInitialized = true mockWindow.tonWallets = 'not-an-array' const { getTonWallets } = await import('./ton-discovery') const resultPromise = getTonWallets([makeWalletMeta('tonkeeper')]) await vi.advanceTimersByTimeAsync(5400) const wallets = await resultPromise expect(wallets).toEqual([]) }) it('picks up a wallet the child writes inside the grace window (t≈5050ms)', async () => { // The child can marshal on its final tick, writing window.tonWallets just // after its own 5000ms budget. The consumer's BRIDGE_WALLET_POLL_MS (5300ms) // grace must still observe it — an equal 5000ms deadline would miss it. mockWindow.UWCBridgeChildInitialized = true const bridgeWallet = { uuid: 'ton-tonkeeper', name: 'Tonkeeper', icon: '', jsBridgeKey: 'tonkeeper', provider: makeBridge('Tonkeeper') } setTimeout(() => { mockWindow.tonWallets = [bridgeWallet] }, 5050) const { getTonWallets } = await import('./ton-discovery') const resultPromise = getTonWallets([makeWalletMeta('tonkeeper')]) await vi.advanceTimersByTimeAsync(5200) const wallets = await resultPromise expect(wallets).toEqual([bridgeWallet]) }) })