import React, { act } from 'react' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { createRoot } from 'react-dom/client' import { ConnectionContext } from '../providers/ConnectionProvider' import { useSignTypedData } from './useSignTypedData' import type { EIP712TypedData, UseSignTypedDataReturn } from '@meshconnect/uwc-types' // --------------------------------------------------------------------------- // Fixtures + helpers // --------------------------------------------------------------------------- const USDC_TYPED_DATA: EIP712TypedData = { domain: { name: 'USD Coin', version: '2', chainId: 1, verifyingContract: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' }, types: { TransferWithAuthorization: [ { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'validAfter', type: 'uint256' }, { name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' } ] }, primaryType: 'TransferWithAuthorization', message: { from: '0xabc', to: '0xdef', value: '1000000', validAfter: 0, validBefore: 9999999999, nonce: '0xdeadbeef' } } /** Minimal stub that satisfies ConnectionContextValue's connector shape. */ function makeConnector( signTypedData: (typedData: EIP712TypedData) => Promise ) { return { signTypedData, getSession: () => ({ isConnected: false }), isReady: () => true, subscribe: () => () => {}, getWallets: () => [], getNetworks: () => [] } } /** * Minimal context value that wraps a connector stub. Pass `{ activeAddress }` * as an object (not a bare arg) so an explicit `undefined` survives. */ function makeContextValue( connector: ReturnType, { activeAddress }: { activeAddress: string | undefined } = { activeAddress: '0xUserAddress' } ) { return { connector, session: { isConnected: activeAddress !== undefined, walletId: undefined, networkId: undefined, activeAddress }, wallets: [], networks: [], isReady: true } } /** * Renders `useSignTypedData` inside a component that exposes its return value * via a captured ref. Mirrors useSignSolanaTransaction.test.tsx: createRoot + * act, no @testing-library/react (not installed in this package). */ function renderHookInContext( contextValue: ReturnType ) { const captured: { current: UseSignTypedDataReturn | null } = { current: null } function Capture() { captured.current = useSignTypedData() return null } const container = document.createElement('div') document.body.appendChild(container) const root = createRoot(container) async function render() { await act(async () => { root.render( ) }) } async function unmount() { await act(async () => { root.unmount() }) document.body.removeChild(container) } return { captured, render, unmount } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('useSignTypedData', () => { let container: HTMLDivElement let root: ReturnType beforeEach(() => { container = document.createElement('div') document.body.appendChild(container) root = createRoot(container) }) afterEach(async () => { await act(async () => { root.unmount() }) if (document.body.contains(container)) { document.body.removeChild(container) } }) it('throws when rendered outside ConnectionProvider', () => { function BareHook() { useSignTypedData() return null } expect(() => { const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) try { act(() => { root.render() }) } finally { errorSpy.mockRestore() } }).toThrowError('useSignTypedData must be used within a ConnectionProvider') }) it('returns the signature from connector.signTypedData on success', async () => { const mockSign = vi.fn().mockResolvedValue('0xtypedsig') const ctxValue = makeContextValue(makeConnector(mockSign)) const { captured, render, unmount } = renderHookInContext(ctxValue) await render() expect(captured.current).not.toBeNull() let result: string | undefined await act(async () => { result = await captured.current!.signTypedData(USDC_TYPED_DATA) }) expect(mockSign).toHaveBeenCalledOnce() expect(mockSign).toHaveBeenCalledWith(USDC_TYPED_DATA) expect(result).toBe('0xtypedsig') expect(captured.current!.signature).toBe('0xtypedsig') await unmount() }) it('sets error state and re-throws when connector rejects', async () => { const walletError = { code: 4001, message: 'User rejected' } const mockSign = vi.fn().mockRejectedValue(walletError) const ctxValue = makeContextValue(makeConnector(mockSign)) const { captured, render, unmount } = renderHookInContext(ctxValue) await render() let thrownError: unknown await act(async () => { try { await captured.current!.signTypedData(USDC_TYPED_DATA) } catch (err) { thrownError = err } }) expect(thrownError).toBe(walletError) expect(captured.current!.error).toBe(walletError) await unmount() }) it('sets isLoading=true during call and false after success', async () => { let resolveSign!: (v: string) => void const pendingSign = new Promise(res => { resolveSign = res }) const mockSign = vi.fn().mockReturnValue(pendingSign) const ctxValue = makeContextValue(makeConnector(mockSign)) const { captured, render, unmount } = renderHookInContext(ctxValue) await render() let callPromise: Promise await act(async () => { callPromise = captured.current!.signTypedData(USDC_TYPED_DATA) }) expect(captured.current!.isLoading).toBe(true) await act(async () => { resolveSign('0xsig') await callPromise! }) expect(captured.current!.isLoading).toBe(false) await unmount() }) it('throws "No wallet connected" when there is no active address', async () => { const mockSign = vi.fn() const ctxValue = makeContextValue(makeConnector(mockSign), { activeAddress: undefined }) const { captured, render, unmount } = renderHookInContext(ctxValue) await render() let thrownError: unknown await act(async () => { try { await captured.current!.signTypedData(USDC_TYPED_DATA) } catch (err) { thrownError = err } }) expect((thrownError as Error).message).toBe('No wallet connected') expect(mockSign).not.toHaveBeenCalled() await unmount() }) })