diff --git a/packages/website/src/features/Packages/AbiMethod/AbiContractMethodInputType.tsx b/packages/website/src/features/Packages/AbiMethod/AbiContractMethodInputType.tsx index a9904a3b0..e353b5183 100644 --- a/packages/website/src/features/Packages/AbiMethod/AbiContractMethodInputType.tsx +++ b/packages/website/src/features/Packages/AbiMethod/AbiContractMethodInputType.tsx @@ -31,7 +31,7 @@ const isBigInt = (value: unknown): value is bigint => { interface AbiMethodInputProps { input: AbiParameter; - handleUpdate: (value: any) => void; + handleUpdate: (value: any, error?: string) => void; value: any; error?: string; } @@ -43,16 +43,14 @@ export const AbiContractMethodInputType: FC = ({ error, }) => { switch (true) { - case input.type.endsWith('[][]'): - return ( - - ); - // handle tuples in arrays - case input.type.startsWith('tuple'): + // Handle tuples and arrays of tuples (complex data structures) + case input.type.startsWith('tuple') || input.type.endsWith('[][]'): + if (value !== undefined && typeof value !== 'object') { + throw new Error( + 'Expected object or array for tuple or nested arrays, got ' + + typeof value + ); + } return ( = ({ `Expected string, string array or undefined for address type, got ${typeof value}` ); } - return ; + return ( + + ); case input.type.startsWith('int') || input.type.startsWith('uint'): if (!isBigInt(value) && value !== undefined) { throw new Error( @@ -95,9 +95,9 @@ export const AbiContractMethodInputType: FC = ({ /> ); case input.type.startsWith('bytes'): { - if (!isString(value) && !isStringArray(value) && value !== undefined) { + if (!isString(value) && value !== undefined) { throw new Error( - `Expected string, string array or undefined for bytes type, got ${typeof value}` + `Expected string or undefined for bytes type, got ${typeof value}` ); } // Extract the number of bytes from the type string (e.g., 'bytes32' -> 32) @@ -106,6 +106,7 @@ export const AbiContractMethodInputType: FC = ({ ); @@ -116,6 +117,8 @@ export const AbiContractMethodInputType: FC = ({ `Expected string, string array or undefined for default type, got ${typeof value}` ); } - return ; + return ( + + ); } }; diff --git a/packages/website/src/features/Packages/AbiMethod/ByteInput.tsx b/packages/website/src/features/Packages/AbiMethod/ByteInput.tsx index 745ba7c77..2af65cc99 100644 --- a/packages/website/src/features/Packages/AbiMethod/ByteInput.tsx +++ b/packages/website/src/features/Packages/AbiMethod/ByteInput.tsx @@ -19,8 +19,15 @@ const validateByteInput = ( value: string, byte?: number ): string | undefined => { - // For dynamic bytes (bytes), no length validation needed + // For dynamic bytes (bytes), validate hex format and odd length if (!byte) { + if (value.startsWith('0x')) { + // Only check odd length if the hex part contains only valid hex characters + const hexPart = value.slice(2); // Remove '0x' prefix + if (/^[0-9a-fA-F]*$/.test(hexPart) && hexPart.length % 2 === 1) { + return 'Hex string must have even number of characters after 0x prefix'; + } + } return undefined; } @@ -53,35 +60,40 @@ export const ByteInput: FC = ({ // Create a dynamic regex based on the byte size const hexRegex = byte ? new RegExp(`^0x[0-9a-fA-F]{${byte * 2}}$`) // Fixed size: exact length - : /^0x[0-9a-fA-F]*$/; // Dynamic: any length hex string + : /^0x([0-9a-fA-F]{2})*$/; // Dynamic: any even length hex string - const handleChange = (inputValue: string) => { + const getHandleUpdateValues = ( + inputValue: string, + byte?: number + ): [string | undefined, string?] => { const validationError = validateByteInput(inputValue, byte); // Allow empty input if (!inputValue) { - handleUpdate(undefined); - setInputValue(''); - return; + return [undefined]; } if (!hexRegex.test(inputValue)) { if (inputValue.startsWith('0x')) { - // If it starts with 0x but isn't a valid hex, pass it through as is - // This allows partial hex inputs while typing - handleUpdate(undefined, validationError); + // Invalid hex format - only return error if it's a validation error (like odd length) + return validationError ? [undefined, validationError] : [undefined]; } else { - // If it's not a hex string at all, convert it to hex + // Convert string to hex const hexValue = stringToHex( inputValue, byte ? { size: byte } : undefined ); - handleUpdate(hexValue, validationError); + return validationError ? [hexValue, validationError] : [hexValue]; } } else { - // If it's already a valid hex string, pass it through - handleUpdate(inputValue, validationError); + // Valid hex string + return [inputValue]; } + }; + + const handleChange = (inputValue: string) => { + const updateValues = getHandleUpdateValues(inputValue, byte); + handleUpdate(...updateValues); setInputValue(inputValue); }; diff --git a/packages/website/src/features/Packages/AbiMethod/JsonInput.tsx b/packages/website/src/features/Packages/AbiMethod/JsonInput.tsx index 18712105b..bbe6f0e49 100644 --- a/packages/website/src/features/Packages/AbiMethod/JsonInput.tsx +++ b/packages/website/src/features/Packages/AbiMethod/JsonInput.tsx @@ -21,7 +21,9 @@ export const JsonInput: FC = ({ error, }) => { const [inputValue, setInputValue] = useState( - Array.isArray(value) ? JSON.stringify(value) : value || '' + Array.isArray(value) || (typeof value === 'object' && value !== null) + ? JSON.stringify(value) + : value || '' ); const handleChange = (e: ChangeEvent) => { diff --git a/packages/website/src/features/Packages/AbiMethod/NumberInput.tsx b/packages/website/src/features/Packages/AbiMethod/NumberInput.tsx index a133b2611..f6543a406 100644 --- a/packages/website/src/features/Packages/AbiMethod/NumberInput.tsx +++ b/packages/website/src/features/Packages/AbiMethod/NumberInput.tsx @@ -1,11 +1,11 @@ import { FC, useRef, ChangeEvent, useState } from 'react'; import { Input } from '@/components/ui/input'; import { cn } from '@/lib/utils'; -import { parseUnits } from 'viem'; +import { parseUnits, formatUnits } from 'viem'; interface NumberInputProps { - handleUpdate: (value: any, error?: string) => void; - value: any; + handleUpdate: (value: string | undefined, error?: string) => void; + value: bigint | undefined; error?: string; suffix?: string; showWeiValue?: boolean; @@ -20,7 +20,13 @@ export const NumberInput: FC = ({ showWeiValue = false, fixedDecimals = 0, }) => { - const [inputValue, setInputValue] = useState(value || ''); + const [inputValue, setInputValue] = useState(() => { + if (value === undefined) return ''; + if (fixedDecimals > 0) { + return formatUnits(value, fixedDecimals); + } + return value.toString(); + }); const decimals = fixedDecimals; const inputRef = useRef(null); @@ -36,26 +42,37 @@ export const NumberInput: FC = ({ const _value = event.target.value; if (_value === '') { - handleUpdate(undefined); setInputValue(''); + handleUpdate(undefined); return; } - // if (!checkDecimalPlaces(inputValue)) { - // handleUpdate({ - // inputValue, - // parsedValue: undefined, - // error: `Input has more decimal places than allowed (max: ${decimals})`, - // }); - // return; - // } - - try { - parseUnits(_value, decimals); - handleUpdate(_value); + // Si fixedDecimals > 0, permitimos decimales + if (fixedDecimals > 0) { setInputValue(_value); - } catch { - handleUpdate(undefined, 'Invalid number format'); + try { + // Convertir a wei + parseUnits(_value, decimals); + handleUpdate(_value); + } catch (error) { + handleUpdate(undefined, 'Invalid number format'); + } + return; + } else { + // Si fixedDecimals = 0, solo permitimos nĂºmeros enteros + if (!/^\d+$/.test(_value)) { + setInputValue(_value); + handleUpdate(undefined, 'Invalid number format'); + return; + } + + try { + setInputValue(_value); + handleUpdate(_value); + } catch { + setInputValue(_value); + handleUpdate(undefined, 'Invalid number format'); + } } }; @@ -151,13 +168,20 @@ export const NumberInput: FC = ({ )} */} {suffix && ( -
+
{suffix}
)}
{showWeiValue && inputValue && ( -

{inputValue} wei

+

+ {fixedDecimals > 0 + ? `${parseUnits(inputValue, decimals).toString()} wei` + : `${inputValue} wei`} +

)} ); diff --git a/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/address.test.tsx b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/address.test.tsx new file mode 100644 index 000000000..163627c25 --- /dev/null +++ b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/address.test.tsx @@ -0,0 +1,140 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { AbiContractMethodInputType } from '../../AbiContractMethodInputType'; +import { AbiParameter } from 'abitype'; + +describe('AbiContractMethodInputType - Address', () => { + // Mock AbiParameter for address type + const mockAddressInput: AbiParameter = { + name: 'recipient', + type: 'address', + internalType: 'address', + }; + + const validAddress = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'; + const invalidAddress = '0x123'; + + it('renders address input correctly', () => { + const handleUpdate = vi.fn(); + render( + + ); + + // Check if the input component is rendered + const inputElement = screen.getByTestId('address-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(validAddress); + }); + + it('handles valid address input', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('address-input'); + + // Type a valid address + fireEvent.change(inputElement, { target: { value: validAddress } }); + + // Check if handleUpdate was called with the valid address + expect(handleUpdate).toHaveBeenCalledWith(validAddress); + }); + + it('handles invalid address input', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('address-input'); + + // Type an invalid address + fireEvent.change(inputElement, { target: { value: invalidAddress } }); + + // Check if handleUpdate was called with undefined and error + expect(handleUpdate).toHaveBeenCalledWith(undefined, 'Invalid address'); + }); + + it('handles empty input', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('address-input'); + + // Clear the input + fireEvent.change(inputElement, { target: { value: '' } }); + + // Check if handleUpdate was called with undefined + expect(handleUpdate).toHaveBeenCalledWith(undefined); + }); + + it('handles undefined value', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('address-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(''); + }); + + it('displays error state correctly', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('address-input'); + expect(inputElement).toHaveClass('border-destructive'); + }); + + it('throws error for invalid value type', () => { + const handleUpdate = vi.fn(); + const consoleSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => vi.fn()); + + expect(() => + render( + + ) + ).toThrow( + 'Expected string, string array or undefined for address type, got number' + ); + + consoleSpy.mockRestore(); + }); +}); diff --git a/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/boolean.test.tsx b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/boolean.test.tsx new file mode 100644 index 000000000..af91f268c --- /dev/null +++ b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/boolean.test.tsx @@ -0,0 +1,99 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { AbiParameter } from 'abitype'; +import { AbiContractMethodInputType } from '@/features/Packages/AbiMethod/AbiContractMethodInputType'; + +describe('AbiContractMethodInputType - Boolean', () => { + // Mock AbiParameter for boolean type + const mockBooleanInput: AbiParameter = { + name: 'isActive', + type: 'bool', + internalType: 'bool', + }; + + it('renders boolean input correctly', () => { + const handleUpdate = vi.fn(); + render( + + ); + + // Check if the switch component is rendered + const switchElement = screen.getByTestId('bool-input'); + expect(switchElement).toBeInTheDocument(); + + // Check if the label shows "False" initially + expect(screen.getByText('False')).toBeInTheDocument(); + }); + + it('handles value updates correctly', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const switchElement = screen.getByTestId('bool-input'); + + // Click the switch to change value + fireEvent.click(switchElement); + + // Check if handleUpdate was called with true + expect(handleUpdate).toHaveBeenCalledWith(true); + + // Check if label updated to "True" + expect(screen.getByText('True')).toBeInTheDocument(); + }); + + it('handles undefined value as false and allows updates', () => { + const handleUpdate = vi.fn(); + render( + + ); + + // Check if the switch component is rendered + const switchElement = screen.getByTestId('bool-input'); + expect(switchElement).toBeInTheDocument(); + + // Check if the label shows "False" initially (undefined should be treated as false) + expect(screen.getByText('False')).toBeInTheDocument(); + + // Click the switch to change value + fireEvent.click(switchElement); + + // Check if handleUpdate was called with true + expect(handleUpdate).toHaveBeenCalledWith(true); + + // Check if label updated to "True" + expect(screen.getByText('True')).toBeInTheDocument(); + }); + + it('throws error for invalid boolean value', () => { + const handleUpdate = vi.fn(); + const consoleSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => vi.fn()); + + expect(() => + render( + + ) + ).toThrow('Expected boolean for bool type, got string'); + + consoleSpy.mockRestore(); + }); +}); diff --git a/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/bytes.test.tsx b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/bytes.test.tsx new file mode 100644 index 000000000..aa8eb17b6 --- /dev/null +++ b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/bytes.test.tsx @@ -0,0 +1,458 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { AbiParameter } from 'abitype'; +import { AbiContractMethodInputType } from '@/features/Packages/AbiMethod/AbiContractMethodInputType'; +import { TooltipProvider } from '@/components/ui/tooltip'; + +describe('AbiContractMethodInputType - Bytes', () => { + // Mock AbiParameter for different bytes types + const mockBytes32Input: AbiParameter = { + name: 'hash', + type: 'bytes32', + internalType: 'bytes32', + }; + + const mockBytesInput: AbiParameter = { + name: 'data', + type: 'bytes', + internalType: 'bytes', + }; + + const mockBytes1Input: AbiParameter = { + name: 'flag', + type: 'bytes1', + internalType: 'bytes1', + }; + + const validBytes32 = + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + const validBytes = '0x1234567890abcdef'; + const validBytes1 = '0x12'; + const invalidBytes = '0x123'; // Invalid length for bytes1 + + describe('Fixed-size bytes (bytes32)', () => { + it('renders bytes32 input correctly', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + // Check if the byte input is rendered + const inputElement = screen.getByTestId('byte32-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(validBytes32); + }); + + it('handles valid bytes32 input', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('byte32-input'); + + // Type a valid bytes32 value + fireEvent.change(inputElement, { target: { value: validBytes32 } }); + + // Check if handleUpdate was called with the valid bytes32 (no error parameter) + expect(handleUpdate).toHaveBeenCalledWith(validBytes32); + }); + + it('handles string conversion to hex for bytes32', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('byte32-input'); + + // Type a string that should be converted to hex + fireEvent.change(inputElement, { target: { value: 'Hello World' } }); + + // Check if handleUpdate was called with the hex conversion (no error parameter) + expect(handleUpdate).toHaveBeenCalledWith( + '0x48656c6c6f20576f726c64000000000000000000000000000000000000000000' + ); + }); + + it('handles empty input', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('byte32-input'); + + // Clear the input + fireEvent.change(inputElement, { target: { value: '' } }); + + // Check if handleUpdate was called with undefined + expect(handleUpdate).toHaveBeenCalledWith(undefined); + }); + + it('handles undefined value', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('byte32-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(''); + }); + + it('displays error state correctly', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('byte32-input'); + expect(inputElement).toHaveClass('border-destructive'); + }); + }); + + describe('Dynamic bytes (bytes)', () => { + it('renders dynamic bytes input correctly', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + // Check if the byte input is rendered + const inputElement = screen.getByTestId('bytedynamic-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(validBytes); + }); + + it('handles valid dynamic bytes input', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('bytedynamic-input'); + + // Type a valid bytes value + fireEvent.change(inputElement, { target: { value: validBytes } }); + + // Check if handleUpdate was called with the valid bytes (no error parameter) + expect(handleUpdate).toHaveBeenCalledWith(validBytes); + }); + + it('handles string conversion to hex for dynamic bytes', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('bytedynamic-input'); + + // Type a string that should be converted to hex + fireEvent.change(inputElement, { target: { value: 'Test' } }); + + // Check if handleUpdate was called with the hex conversion (no error parameter) + expect(handleUpdate).toHaveBeenCalledWith('0x54657374'); + }); + + it('handles partial hex input for dynamic bytes', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('bytedynamic-input'); + + // Type a partial hex string with odd length (invalid) + fireEvent.change(inputElement, { target: { value: '0x123' } }); + + // Check if handleUpdate was called with undefined and error for odd length + expect(handleUpdate).toHaveBeenCalledWith( + undefined, + 'Hex string must have even number of characters after 0x prefix' + ); + }); + + it('shows error for invalid hex format in dynamic bytes', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('bytedynamic-input'); + + // Type an invalid hex string (non-hex characters) + fireEvent.change(inputElement, { target: { value: '0x12g' } }); + + // Check if handleUpdate was called with undefined (no valid value) + expect(handleUpdate).toHaveBeenCalledWith(undefined); + }); + + it('shows error state for invalid hex input in dynamic bytes', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('bytedynamic-input'); + expect(inputElement).toHaveClass('border-destructive'); + }); + }); + + describe('Small fixed-size bytes (bytes1)', () => { + it('renders bytes1 input correctly', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + // Check if the byte input is rendered + const inputElement = screen.getByTestId('byte1-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(validBytes1); + }); + + it('handles valid bytes1 input', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('byte1-input'); + + // Type a valid bytes1 value + fireEvent.change(inputElement, { target: { value: validBytes1 } }); + + // Check if handleUpdate was called with the valid bytes1 (no error parameter) + expect(handleUpdate).toHaveBeenCalledWith(validBytes1); + }); + + it('handles string conversion to hex for bytes1', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('byte1-input'); + + // Type a single character that should be converted to hex + fireEvent.change(inputElement, { target: { value: 'A' } }); + + // Check if handleUpdate was called with the hex conversion (no error parameter) + expect(handleUpdate).toHaveBeenCalledWith('0x41'); + }); + + it('handles length validation for bytes1', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('byte1-input'); + + // Type a hex string that's too long for bytes1 + fireEvent.change(inputElement, { target: { value: '0x1234' } }); + + // Check if handleUpdate was called with undefined and length error + expect(handleUpdate).toHaveBeenCalledWith( + undefined, + 'Input is 2 too long. It must be 4 characters.' + ); + }); + }); + + describe('Error handling', () => { + it('throws error for invalid bytes value type', () => { + const handleUpdate = vi.fn(); + const consoleSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => vi.fn()); + + expect(() => + render( + + + + ) + ).toThrow('Expected string or undefined for bytes type, got number'); + + consoleSpy.mockRestore(); + }); + + it('throws error for array value type', () => { + const handleUpdate = vi.fn(); + const consoleSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => vi.fn()); + + expect(() => + render( + + + + ) + ).toThrow('Expected string or undefined for bytes type, got object'); + + consoleSpy.mockRestore(); + }); + }); + + describe('Placeholder and UI', () => { + it('shows correct placeholder for bytes32', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('byte32-input'); + expect(inputElement).toHaveAttribute( + 'placeholder', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + }); + + it('shows correct placeholder for dynamic bytes', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('bytedynamic-input'); + expect(inputElement).toHaveAttribute('placeholder', '0x...'); + }); + + it('shows correct placeholder for bytes1', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('byte1-input'); + expect(inputElement).toHaveAttribute('placeholder', '0x00'); + }); + }); +}); diff --git a/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/default.test.tsx b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/default.test.tsx new file mode 100644 index 000000000..d758808f3 --- /dev/null +++ b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/default.test.tsx @@ -0,0 +1,152 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { AbiContractMethodInputType } from '../../AbiContractMethodInputType'; +import { AbiParameter } from 'abitype'; + +describe('AbiContractMethodInputType - Default', () => { + // Mock AbiParameter for string type (falls to default case) + const mockStringInput: AbiParameter = { + name: 'message', + type: 'string', + internalType: 'string', + }; + + // Mock AbiParameter for function type (falls to default case) + const mockFunctionInput: AbiParameter = { + name: 'callback', + type: 'function', + internalType: 'function', + }; + + it('renders default input for string type correctly', () => { + const handleUpdate = vi.fn(); + const testValue = 'Hello World'; + + render( + + ); + + // Check if the default input component is rendered + const inputElement = screen.getByTestId('default-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(testValue); + }); + + it('handles string input changes', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('default-input'); + const newValue = 'New string value'; + + // Type a new value + fireEvent.change(inputElement, { target: { value: newValue } }); + + // Check if handleUpdate was called with the new value + expect(handleUpdate).toHaveBeenCalledWith(newValue); + }); + + it('handles function input changes', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('default-input'); + const newValue = '0xabcdef1234567890'; + + // Type a new value + fireEvent.change(inputElement, { target: { value: newValue } }); + + // Check if handleUpdate was called with the new value + expect(handleUpdate).toHaveBeenCalledWith(newValue); + }); + + it('handles empty input', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('default-input'); + + // Clear the input + fireEvent.change(inputElement, { target: { value: '' } }); + + // Check if handleUpdate was called with empty string + expect(handleUpdate).toHaveBeenCalledWith(''); + }); + + it('handles undefined value', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('default-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(''); + }); + + it('displays error state correctly', () => { + const handleUpdate = vi.fn(); + const errorMessage = 'Invalid input'; + + render( + + ); + + const inputElement = screen.getByTestId('default-input'); + const errorElement = screen.getByText(errorMessage); + + expect(inputElement).toHaveClass('border-red-500'); + expect(errorElement).toBeInTheDocument(); + }); + + it('handles string array input changes', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('default-input'); + const newValue = 'new,array,values'; + + // Type a new value + fireEvent.change(inputElement, { target: { value: newValue } }); + + // Check if handleUpdate was called with the new value + expect(handleUpdate).toHaveBeenCalledWith(newValue); + }); +}); diff --git a/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/nested-arrays.test.tsx b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/nested-arrays.test.tsx new file mode 100644 index 000000000..e1c5715a6 --- /dev/null +++ b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/nested-arrays.test.tsx @@ -0,0 +1,284 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { AbiParameter } from 'abitype'; +import { AbiContractMethodInputType } from '@/features/Packages/AbiMethod/AbiContractMethodInputType'; +import { TooltipProvider } from '@/components/ui/tooltip'; + +describe('AbiContractMethodInputType - Nested Arrays', () => { + // Mock AbiParameter for different nested array types + const mockUint256Array2D: AbiParameter = { + name: 'numbers', + type: 'uint256[][]', + internalType: 'uint256[][]', + }; + + const mockStringArray2D: AbiParameter = { + name: 'strings', + type: 'string[][]', + internalType: 'string[][]', + }; + + const mockTupleArray2D: AbiParameter = { + name: 'matrix', + type: 'tuple[][]', + internalType: 'struct Point[][]', + components: [ + { name: 'x', type: 'uint256', internalType: 'uint256' }, + { name: 'y', type: 'uint256', internalType: 'uint256' }, + ], + }; + + const validUint256Array2D = [ + ['1', '2', '3'], + ['4', '5', '6'], + ]; + + const validStringArray2D = [ + ['hello', 'world'], + ['test', 'array'], + ]; + + const validTupleArray2D = [ + [ + { x: '1', y: '2' }, + { x: '3', y: '4' }, + ], + [ + { x: '5', y: '6' }, + { x: '7', y: '8' }, + ], + ]; + + describe('Uint256 Nested Arrays', () => { + it('renders uint256 nested array input correctly', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(JSON.stringify(validUint256Array2D)); + }); + + it('handles valid uint256 nested array input', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + const newValue = JSON.stringify(validUint256Array2D); + + fireEvent.change(inputElement, { target: { value: newValue } }); + + expect(handleUpdate).toHaveBeenCalledWith(validUint256Array2D); + }); + + it('shows correct placeholder for uint256 nested array', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + expect(inputElement).toHaveAttribute( + 'placeholder', + '[[v1, v2], [v3, v4]]' + ); + }); + }); + + describe('String Nested Arrays', () => { + it('renders string nested array input correctly', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(JSON.stringify(validStringArray2D)); + }); + + it('handles valid string nested array input', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + const newValue = JSON.stringify(validStringArray2D); + + fireEvent.change(inputElement, { target: { value: newValue } }); + + expect(handleUpdate).toHaveBeenCalledWith(validStringArray2D); + }); + + it('shows correct placeholder for string nested array', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + expect(inputElement).toHaveAttribute( + 'placeholder', + '[[v1, v2], [v3, v4]]' + ); + }); + }); + + describe('Tuple Nested Arrays', () => { + it('renders tuple nested array input correctly', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(JSON.stringify(validTupleArray2D)); + }); + + it('handles valid tuple nested array input', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + const newValue = JSON.stringify(validTupleArray2D); + + fireEvent.change(inputElement, { target: { value: newValue } }); + + expect(handleUpdate).toHaveBeenCalledWith(validTupleArray2D); + }); + + it('shows correct placeholder for tuple nested array', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + expect(inputElement).toHaveAttribute( + 'placeholder', + '[[v1, v2], [v3, v4]]' + ); + }); + }); + + describe('Error Handling', () => { + it('throws error for non-array nested array value', () => { + const handleUpdate = vi.fn(); + + // Silence React error messages for this test + // eslint-disable-next-line no-console + const originalError = console.error; + // eslint-disable-next-line no-console + console.error = vi.fn(); + + expect(() => { + render( + + + + ); + }).toThrow( + 'Expected object or array for tuple or nested arrays, got string' + ); + + // Restore console.error + // eslint-disable-next-line no-console + console.error = originalError; + }); + + it('handles malformed JSON gracefully', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + + const malformedInputs = [ + '[[1, 2], [3,', // Incomplete array + '[[1, 2], [3, 4,', // Trailing comma + '[[1, 2], [3, 4]]', // Missing closing bracket + ]; + + malformedInputs.forEach((malformed) => { + fireEvent.change(inputElement, { target: { value: malformed } }); + expect(handleUpdate).toHaveBeenCalledWith( + undefined, + 'Invalid JSON format' + ); + }); + }); + }); +}); diff --git a/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/number.test.tsx b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/number.test.tsx new file mode 100644 index 000000000..2c62a2197 --- /dev/null +++ b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/number.test.tsx @@ -0,0 +1,225 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { AbiParameter } from 'abitype'; +import { AbiContractMethodInputType } from '@/features/Packages/AbiMethod/AbiContractMethodInputType'; +import { NumberInput } from '@/features/Packages/AbiMethod/NumberInput'; +import { parseEther } from 'viem'; + +describe('AbiContractMethodInputType - Number', () => { + // Mock AbiParameter for number types + const mockUintInput: AbiParameter = { + name: 'amount', + type: 'uint256', + internalType: 'uint256', + }; + + it('renders uint input correctly', () => { + const handleUpdate = vi.fn(); + render( + + ); + + // Check if the number input is rendered + const inputElement = screen.getByTestId('number-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(100); + expect(screen.getByText('100 wei')).toBeInTheDocument(); + }); + + it('handles value updates correctly', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('number-input'); + + // Change the input value + fireEvent.change(inputElement, { target: { value: '200' } }); + + // Check if handleUpdate was called with the new value + expect(handleUpdate).toHaveBeenCalledWith('200'); + // Check if wei value is updated + expect(screen.getByText('200 wei')).toBeInTheDocument(); + }); + + it('handles empty input correctly', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('number-input'); + + // Clear the input + fireEvent.change(inputElement, { target: { value: '' } }); + + // Check if handleUpdate was called with undefined + expect(handleUpdate).toHaveBeenCalledWith(undefined); + // Check that wei value is not displayed + expect(screen.queryByText(/wei/)).not.toBeInTheDocument(); + }); + + it('handles decimal numbers correctly', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('number-input'); + fireEvent.change(inputElement, { target: { value: '100.5' } }); + + expect(handleUpdate).toHaveBeenCalledWith( + undefined, + 'Invalid number format' + ); + }); + + it('throws error for invalid number value type', () => { + const handleUpdate = vi.fn(); + const consoleSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => vi.fn()); + + expect(() => + render( + + ) + ).toThrow('Expected bigint or undefined for number type, got string'); + + consoleSpy.mockRestore(); + }); +}); + +describe('NumberInput', () => { + describe('Wei mode (no fixedDecimals)', () => { + it('renders with wei value', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('number-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(100); + expect(screen.getByText('100 wei')).toBeInTheDocument(); + }); + + it('handles wei value updates', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('number-input'); + fireEvent.change(inputElement, { target: { value: '200' } }); + + expect(handleUpdate).toHaveBeenCalledWith('200'); + expect(screen.getByText('200 wei')).toBeInTheDocument(); + }); + + it('rejects decimal values in wei mode', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('number-input'); + fireEvent.change(inputElement, { target: { value: '100.5' } }); + + expect(handleUpdate).toHaveBeenCalledWith( + undefined, + 'Invalid number format' + ); + }); + }); + + describe('ETH mode (with fixedDecimals)', () => { + it('renders with ETH value', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('number-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(1.5); + expect(screen.getByText('1500000000000000000 wei')).toBeInTheDocument(); + }); + + it('handles ETH value updates', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('number-input'); + fireEvent.change(inputElement, { target: { value: '2.5' } }); + + expect(handleUpdate).toHaveBeenCalledWith('2.5'); + expect(screen.getByText('2500000000000000000 wei')).toBeInTheDocument(); + }); + + it('accepts decimal values in ETH mode', () => { + const handleUpdate = vi.fn(); + render( + + ); + + const inputElement = screen.getByTestId('number-input'); + fireEvent.change(inputElement, { target: { value: '1.234567' } }); + + expect(handleUpdate).toHaveBeenCalledWith('1.234567'); + }); + }); +}); diff --git a/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/tuple.test.tsx b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/tuple.test.tsx new file mode 100644 index 000000000..6e56ba90f --- /dev/null +++ b/packages/website/src/features/Packages/AbiMethod/__tests__/AbiContractMethodInputType/tuple.test.tsx @@ -0,0 +1,349 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { AbiParameter } from 'abitype'; +import { AbiContractMethodInputType } from '@/features/Packages/AbiMethod/AbiContractMethodInputType'; +import { TooltipProvider } from '@/components/ui/tooltip'; + +describe('AbiContractMethodInputType - Tuple', () => { + // Mock AbiParameter for different tuple types + const mockTupleInput: AbiParameter = { + name: 'person', + type: 'tuple', + internalType: 'struct Person', + components: [ + { name: 'name', type: 'string', internalType: 'string' }, + { name: 'age', type: 'uint256', internalType: 'uint256' }, + { name: 'active', type: 'bool', internalType: 'bool' }, + ], + }; + + const mockTupleArrayInput: AbiParameter = { + name: 'people', + type: 'tuple[]', + internalType: 'struct Person[]', + components: [ + { name: 'name', type: 'string', internalType: 'string' }, + { name: 'age', type: 'uint256', internalType: 'uint256' }, + { name: 'active', type: 'bool', internalType: 'bool' }, + ], + }; + + const validTuple = { + name: 'John Doe', + age: '30', + active: true, + }; + + const validTupleArray = [ + { name: 'John Doe', age: '30', active: true }, + { name: 'Jane Smith', age: '25', active: false }, + ]; + + describe('Simple Tuple', () => { + it('renders tuple input correctly', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + // Check if the json input is rendered + const inputElement = screen.getByTestId('json-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(JSON.stringify(validTuple)); + }); + + it('handles valid tuple input', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + const newValue = JSON.stringify(validTuple); + + // Type a valid tuple value + fireEvent.change(inputElement, { target: { value: newValue } }); + + // Check if handleUpdate was called with the parsed tuple + expect(handleUpdate).toHaveBeenCalledWith(validTuple); + }); + + it('handles empty input', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + + // Clear the input + fireEvent.change(inputElement, { target: { value: '' } }); + + // Check if handleUpdate was called with undefined + expect(handleUpdate).toHaveBeenCalledWith(undefined); + }); + + it('handles undefined value', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(''); + }); + + it('displays error state for invalid JSON', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + + // Type invalid JSON + fireEvent.change(inputElement, { target: { value: '{invalid json' } }); + + // Check if handleUpdate was called with error + expect(handleUpdate).toHaveBeenCalledWith( + undefined, + 'Invalid JSON format' + ); + }); + + it('shows correct placeholder for tuple', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + expect(inputElement).toHaveAttribute('placeholder', '[v1, v2]'); + }); + }); + + describe('Tuple Array', () => { + it('renders tuple array input correctly', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + // Check if the json input is rendered + const inputElement = screen.getByTestId('json-input'); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveValue(JSON.stringify(validTupleArray)); + }); + + it('handles valid tuple array input', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + const newValue = JSON.stringify(validTupleArray); + + // Type a valid tuple array value + fireEvent.change(inputElement, { target: { value: newValue } }); + + // Check if handleUpdate was called with the parsed tuple array + expect(handleUpdate).toHaveBeenCalledWith(validTupleArray); + }); + + it('handles empty tuple array', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + + // Clear the input + fireEvent.change(inputElement, { target: { value: '' } }); + + // Check if handleUpdate was called with undefined + expect(handleUpdate).toHaveBeenCalledWith(undefined); + }); + + it('shows correct placeholder for tuple array', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + expect(inputElement).toHaveAttribute( + 'placeholder', + '[[v1, v2], [v3, v4]]' + ); + }); + }); + + describe('Error Handling', () => { + it('handles malformed JSON gracefully', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + + // Test various malformed JSON inputs + const malformedInputs = [ + '{name: "John"}', // Missing quotes + '{"name": "John",}', // Trailing comma + '{"name": "John"', // Missing closing brace + '[1, 2, 3,', // Incomplete array + ]; + + malformedInputs.forEach((malformed) => { + fireEvent.change(inputElement, { target: { value: malformed } }); + expect(handleUpdate).toHaveBeenCalledWith( + undefined, + 'Invalid JSON format' + ); + }); + }); + + it('handles partial JSON input', () => { + const handleUpdate = vi.fn(); + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + + // Type partial JSON + fireEvent.change(inputElement, { target: { value: '{"name":' } }); + + // Should show error for incomplete JSON + expect(handleUpdate).toHaveBeenCalledWith( + undefined, + 'Invalid JSON format' + ); + }); + }); + + describe('Value Type Validation', () => { + it('throws error for non-object tuple value', () => { + const handleUpdate = vi.fn(); + + // Silence React error messages for this test + // eslint-disable-next-line no-console + const originalError = console.error; + // eslint-disable-next-line no-console + console.error = vi.fn(); + + // This should throw an error because the component expects an object for tuple type + expect(() => { + render( + + + + ); + }).toThrow( + 'Expected object or array for tuple or nested arrays, got string' + ); + + // Restore console.error + // eslint-disable-next-line no-console + console.error = originalError; + }); + + it('handles string array values correctly', () => { + const handleUpdate = vi.fn(); + const stringArrayValue = ['value1', 'value2']; + + render( + + + + ); + + const inputElement = screen.getByTestId('json-input'); + expect(inputElement).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/website/src/features/Packages/interact/AbiContractMethodInteraction.tsx b/packages/website/src/features/Packages/interact/AbiContractMethodInteraction.tsx index 8da7322b2..7ec7c05ec 100644 --- a/packages/website/src/features/Packages/interact/AbiContractMethodInteraction.tsx +++ b/packages/website/src/features/Packages/interact/AbiContractMethodInteraction.tsx @@ -483,7 +483,7 @@ export const AbiContractMethodInteraction: FC<{