import { describe, it, expect } from 'vitest' import { parseError } from './error-utils' import { WalletConnectorError } from '@meshconnect/uwc-types' describe('error-utils', () => { describe('parseError', () => { describe('expired proposal errors', () => { it('should detect "proposal expired" error', () => { const error = new Error('proposal expired') expect(() => parseError(error)).toThrow(WalletConnectorError) try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('expired') expect((err as WalletConnectorError).message).toBe('proposal expired') } }) it('should detect "expired proposal" error', () => { const error = new Error('expired proposal') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('expired') expect((err as WalletConnectorError).message).toBe('expired proposal') } }) it('should detect "pairing expired" error', () => { const error = new Error('pairing expired') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('expired') } }) it('should detect "expired pairing" error', () => { const error = new Error('expired pairing') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('expired') } }) it('should detect "proposal has expired" error', () => { const error = new Error('The proposal has expired') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('expired') } }) it('should detect "pairing has expired" error', () => { const error = new Error('The pairing has expired') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('expired') } }) it('should be case-insensitive for expired errors', () => { const error = new Error('PROPOSAL EXPIRED') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('expired') } }) it('should detect expired in longer error messages', () => { const error = new Error( 'Connection failed because the proposal expired after 5 minutes' ) try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('expired') } }) }) describe('rejection errors', () => { it('should detect EIP-1193 error code 4001', () => { const error = { code: 4001, message: 'User rejected the request' } try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') expect((err as WalletConnectorError).message).toBe( 'User rejected the request' ) } }) it('should detect ACTION_REJECTED error code', () => { const error = { code: 'ACTION_REJECTED', message: 'User rejected the action' } try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should detect "user rejected" in message', () => { const error = new Error('user rejected the transaction') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should detect "user denied" in message', () => { const error = new Error('user denied the request') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should detect "user cancelled" in message', () => { const error = new Error('user cancelled the operation') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should detect "user canceled" (American spelling) in message', () => { const error = new Error('user canceled the operation') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should detect "rejected by user" in message', () => { const error = new Error('Transaction was rejected by user') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should detect "denied by user" in message', () => { const error = new Error('Access denied by user') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should detect "cancelled by user" in message', () => { const error = new Error('Operation cancelled by user') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should detect "canceled by user" in message', () => { const error = new Error('Operation canceled by user') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should detect "user disapproved" in message', () => { const error = new Error('user disapproved the transaction') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should detect "user declined" in message', () => { const error = new Error('user declined the request') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should detect "action_rejected" in message', () => { const error = new Error('action_rejected by wallet') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should be case-insensitive for rejection errors', () => { const error = new Error('USER REJECTED THE REQUEST') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) }) describe('unknown errors', () => { it('should classify unrecognized errors as unknown', () => { const error = new Error('Network connection failed') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('unknown') expect((err as WalletConnectorError).message).toBe( 'Network connection failed' ) } }) it('should handle generic Error objects', () => { const error = new Error('Something went wrong') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('unknown') } }) it('should handle timeout errors as unknown', () => { const error = new Error('Request timed out') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('unknown') } }) }) describe('error message extraction', () => { it('should extract message from Error object', () => { const error = new Error('Custom error message') try { parseError(error) } catch (err) { expect((err as WalletConnectorError).message).toBe( 'Custom error message' ) } }) it('should extract message from error.message property', () => { const error = { message: 'Error from object with message' } try { parseError(error) } catch (err) { expect((err as WalletConnectorError).message).toBe( 'Error from object with message' ) } }) it('should extract message from error.error.message nested property', () => { const error = { error: { message: 'Nested error message' } } try { parseError(error) } catch (err) { expect((err as WalletConnectorError).message).toBe( 'Nested error message' ) } }) it('should convert non-object errors to string', () => { const error = 'String error message' try { parseError(error) } catch (err) { expect((err as WalletConnectorError).message).toBe( 'String error message' ) } }) it('should handle null error', () => { const error = null try { parseError(error) } catch (err) { expect((err as WalletConnectorError).message).toBe('null') } }) it('should handle undefined error', () => { const error = undefined try { parseError(error) } catch (err) { expect((err as WalletConnectorError).message).toBe('undefined') } }) it('should handle number as error', () => { const error = 404 try { parseError(error) } catch (err) { expect((err as WalletConnectorError).message).toBe('404') } }) }) describe('priority order', () => { it('should prioritize expired over rejection patterns', () => { const error = new Error('user rejected because the proposal expired') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('expired') } }) it('should prioritize EIP-1193 code 4001 for rejection', () => { const error = { code: 4001, message: 'Some other message without rejection keywords' } try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should check rejection patterns only if code check fails', () => { const error = { code: 5000, message: 'user rejected the transaction' } try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) }) describe('real-world error scenarios', () => { it('should handle MetaMask user rejection', () => { const error = { code: 4001, message: 'MetaMask Tx Signature: User denied transaction signature.' } try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('rejected') } }) it('should handle WalletConnect expired proposal', () => { const error = new Error( 'Proposal expired. Please try connecting again.' ) try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('expired') } }) it('should handle network errors as unknown', () => { const error = { message: 'Failed to fetch', name: 'NetworkError' } try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('unknown') expect((err as WalletConnectorError).message).toBe('Failed to fetch') } }) it('should handle JSON-RPC errors as unknown', () => { const error = { code: -32603, message: 'Internal JSON-RPC error' } try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('unknown') } }) }) describe('edge cases', () => { it('should handle empty string error', () => { const error = '' try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('unknown') expect((err as WalletConnectorError).message).toBe('') } }) it('should handle error with only whitespace', () => { const error = new Error(' ') try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('unknown') } }) it('should handle object without message property', () => { const error = { code: 123, data: 'some data' } try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).type).toBe('unknown') } }) it('should handle array as error', () => { const error = ['error1', 'error2'] try { parseError(error) } catch (err) { expect(err).toBeInstanceOf(WalletConnectorError) expect((err as WalletConnectorError).message).toBe('error1,error2') } }) it('should never return - always throws', () => { const error = new Error('test') expect(() => parseError(error)).toThrow() }) }) describe('numeric code preservation (MFS-667)', () => { // Capture the thrown error explicitly — a bare try/catch passes vacuously if // parseError ever stopped throwing. const thrownBy = (input: unknown): WalletConnectorError => { try { parseError(input) } catch (err) { return err as WalletConnectorError } throw new Error('parseError did not throw') } it('preserves a numeric provider code so the classifier can place it', () => { const err = thrownBy({ code: -32002, message: 'resource unavailable' }) expect(err.code).toBe(-32002) expect(err.type).toBe('unknown') }) it('preserves the code alongside a rejection type', () => { const err = thrownBy({ code: 4001, message: 'User rejected' }) expect(err.code).toBe(4001) expect(err.type).toBe('rejected') }) it('leaves code undefined for a string code (numeric-only slot)', () => { const err = thrownBy({ code: 'ACTION_REJECTED', message: 'x' }) expect(err.code).toBeUndefined() }) it('leaves code undefined when the source error has no code', () => { const err = thrownBy(new Error('Failed to fetch')) expect(err.code).toBeUndefined() }) it('passes an already-typed WalletConnectorError through unchanged (no double-wrap)', () => { // A deeper layer already classified this; re-processing would downgrade // type/code. Matches injected/tron behavior. const original = new WalletConnectorError({ type: 'expired', message: 'Proposal expired', code: 4900 }) const thrown = thrownBy(original) expect(thrown).toBe(original) expect(thrown.type).toBe('expired') expect(thrown.code).toBe(4900) }) }) }) })