From 147edd54228f206fefc06275222da076ae8542c6 Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Wed, 26 Mar 2025 11:35:14 +0100 Subject: [PATCH 01/22] changed SnapUIButton component to a custom component that works with snaps --- .../Snaps/SnapUIButton/SnapUIButton.tsx | 110 +++++++++++++----- 1 file changed, 80 insertions(+), 30 deletions(-) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx index e4729cbe6232..1bbb0e62a76f 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx @@ -1,13 +1,13 @@ import React, { FunctionComponent } from 'react'; import { ButtonType, UserInputEventType } from '@metamask/snaps-sdk'; -import ButtonLink from '../../../component-library/components/Buttons/Button/variants/ButtonLink'; -import { ButtonLinkProps } from '../../../component-library/components/Buttons/Button/variants/ButtonLink/ButtonLink.types'; +import { TouchableOpacity, StyleSheet, View } from 'react-native'; import { useSnapInterfaceContext } from '../SnapInterfaceContext'; import Text, { TextColor, TextVariant, } from '../../../component-library/components/Texts/Text'; import AnimatedLottieView from 'lottie-react-native'; +import { useTheme } from '../../../util/theme'; export interface SnapUIButtonProps { name?: string; @@ -16,6 +16,10 @@ export interface SnapUIButtonProps { form?: string; variant: keyof typeof COLORS; textVariant?: TextVariant; + style?: any; + disabled?: boolean; + onPress?: () => void; + children?: React.ReactNode; } const COLORS = { @@ -24,9 +28,7 @@ const COLORS = { disabled: TextColor.Muted, }; -export const SnapUIButton: FunctionComponent< - SnapUIButtonProps & ButtonLinkProps -> = ({ +export const SnapUIButton: FunctionComponent = ({ name, children, form, @@ -34,18 +36,22 @@ export const SnapUIButton: FunctionComponent< variant = 'primary', disabled = false, loading = false, - textVariant, + textVariant = TextVariant.BodyMDMedium, + style, + onPress, ...props }) => { const { handleEvent } = useSnapInterfaceContext(); + const { colors } = useTheme(); const handlePress = () => { + onPress?.(); + handleEvent({ event: UserInputEventType.ButtonClickEvent, name, }); - // Since we don't have onSubmit on mobile, the button submits the form. if (type === ButtonType.Submit) { handleEvent({ event: UserInputEventType.FormSubmitEvent, @@ -55,33 +61,77 @@ export const SnapUIButton: FunctionComponent< }; const overriddenVariant = disabled ? 'disabled' : variant; - const color = COLORS[overriddenVariant as keyof typeof COLORS]; + const isIcon = + React.isValidElement(children) && + (children.props?.type === 'Icon' || + (children.type as any)?.displayName === 'Icon'); + + const styles = StyleSheet.create({ + button: { + backgroundColor: colors.background.default, + borderRadius: 8, + paddingVertical: 8, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + ...style, + }, + content: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + icon: { + marginRight: 8, + }, + lastIcon: { + marginRight: 0, + }, + }); + + const renderContent = () => { + if (loading) { + return ( + + ); + } + + if (isIcon) { + return {children}; + } + + if (typeof children === 'string') { + return ( + + {children} + + ); + } + + return children; + }; + return ( - - ) : ( - - {children} - - ) - } - /> + accessible + accessibilityRole="button" + accessibilityLabel={typeof children === 'string' ? children : name} + {...props} + > + {renderContent()} + ); }; From 9d280d9503fbe6d58ec7c01802f4a7f7cf6be44e Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Wed, 26 Mar 2025 11:54:21 +0100 Subject: [PATCH 02/22] fixed lint issues --- .../Snaps/SnapUIButton/SnapUIButton.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx index 1bbb0e62a76f..837757dcd653 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx @@ -1,6 +1,12 @@ import React, { FunctionComponent } from 'react'; import { ButtonType, UserInputEventType } from '@metamask/snaps-sdk'; -import { TouchableOpacity, StyleSheet, View } from 'react-native'; +import { + TouchableOpacity, + StyleSheet, + View, + ViewStyle, + StyleProp, +} from 'react-native'; import { useSnapInterfaceContext } from '../SnapInterfaceContext'; import Text, { TextColor, @@ -16,7 +22,7 @@ export interface SnapUIButtonProps { form?: string; variant: keyof typeof COLORS; textVariant?: TextVariant; - style?: any; + style?: StyleProp; disabled?: boolean; onPress?: () => void; children?: React.ReactNode; @@ -66,7 +72,8 @@ export const SnapUIButton: FunctionComponent = ({ const isIcon = React.isValidElement(children) && (children.props?.type === 'Icon' || - (children.type as any)?.displayName === 'Icon'); + (children.type as React.ComponentType & { displayName?: string }) + ?.displayName === 'Icon'); const styles = StyleSheet.create({ button: { @@ -76,7 +83,7 @@ export const SnapUIButton: FunctionComponent = ({ flexDirection: 'row', justifyContent: 'center', alignItems: 'center', - ...style, + ...(style as ViewStyle), }, content: { flexDirection: 'row', @@ -98,6 +105,7 @@ export const SnapUIButton: FunctionComponent = ({ source={{ uri: './loading.json' }} autoPlay loop + // eslint-disable-next-line react-native/no-inline-styles style={{ width: 24, height: 24, From 252dee3e24e6992dc2d5d55d9262575fad2bcb41 Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Wed, 26 Mar 2025 14:13:02 +0100 Subject: [PATCH 03/22] updated snapshots --- .../SnapUIRenderer.test.tsx.snap | 84 ++++++------------- 1 file changed, 24 insertions(+), 60 deletions(-) diff --git a/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap b/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap index ae797d0a5874..d3b27363e467 100644 --- a/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap +++ b/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap @@ -1627,48 +1627,30 @@ exports[`SnapUIRenderer supports fields with multiple components 1`] = ` } testID="textfield-endacccessory" > - - - Submit - + Submit - + @@ -2668,47 +2650,29 @@ exports[`SnapUIRenderer supports forms with fields 1`] = ` - - - Submit - + Submit - + From 48bbe09f3af2ec930bf54a11ae7f22c229d35284 Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Thu, 27 Mar 2025 15:05:09 +0100 Subject: [PATCH 04/22] added tests --- .../Snaps/SnapUIButton/SnapUIButton.test.tsx | 193 ++++++++++++++++++ .../Snaps/SnapUIButton/SnapUIButton.tsx | 6 +- 2 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx new file mode 100644 index 000000000000..7d4746e9ca00 --- /dev/null +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx @@ -0,0 +1,193 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; +import { SnapUIButton } from './SnapUIButton'; +import { useSnapInterfaceContext } from '../SnapInterfaceContext'; +import { ButtonType, UserInputEventType } from '@metamask/snaps-sdk'; +import Text from '../../../component-library/components/Texts/Text'; +import { View } from 'react-native'; +import AnimatedLottieView from 'lottie-react-native'; + +jest.mock('../SnapInterfaceContext', () => ({ + useSnapInterfaceContext: jest.fn(), +})); + +jest.mock('../../../util/theme', () => ({ + useTheme: jest.fn().mockReturnValue({ + colors: { + background: { default: '#FFFFFF' }, + border: { muted: '#CCCCCC', default: '#DDDDDD' }, + text: { default: '#000000' }, + info: { default: '#0376C9' }, + error: { default: '#D73A49' }, + }, + }), +})); + +describe('SnapUIButton', () => { + const mockHandleEvent = jest.fn(); + const mockOnPress = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (useSnapInterfaceContext as jest.Mock).mockReturnValue({ + handleEvent: mockHandleEvent, + }); + }); + + it('renders correctly with text children', () => { + const { getByText } = render( + Test Button, + ); + + expect(getByText('Test Button')).toBeTruthy(); + }); + + it('renders correctly when disabled', () => { + const { getByText, getByTestId } = render( + + Disabled Button + , + ); + + expect(getByText('Disabled Button')).toBeTruthy(); + expect(getByTestId('disabled-button').props.disabled).toBe(true); + }); + + it('renders with loading state', () => { + const { UNSAFE_getByType } = render( + + Loading Button + , + ); + + expect(UNSAFE_getByType(AnimatedLottieView)).toBeTruthy(); + }); + + it('calls onPress and handles ButtonClickEvent when pressed', () => { + const { getByText } = render( + + Click Me + , + ); + + fireEvent.press(getByText('Click Me')); + + expect(mockOnPress).toHaveBeenCalledTimes(1); + expect(mockHandleEvent).toHaveBeenCalledWith({ + event: UserInputEventType.ButtonClickEvent, + name: 'test-button', + }); + }); + + it('handles FormSubmitEvent when button type is Submit', () => { + const { getByText } = render( + + Submit Form + , + ); + + fireEvent.press(getByText('Submit Form')); + + expect(mockHandleEvent).toHaveBeenCalledWith({ + event: UserInputEventType.ButtonClickEvent, + name: 'test-button', + }); + expect(mockHandleEvent).toHaveBeenCalledWith({ + event: UserInputEventType.FormSubmitEvent, + name: 'test-form', + }); + }); + + it('renders icon component correctly without wrapping in Text', () => { + const MockIcon = () => ; + MockIcon.displayName = 'Icon'; + + const { getByTestId } = render( + + + , + ); + + expect(getByTestId('icon-button')).toBeTruthy(); + expect(getByTestId('mock-icon')).toBeTruthy(); + }); + + it('renders destructive variant with correct color', () => { + const { UNSAFE_getAllByType } = render( + Destructive Button, + ); + + const textComponents = UNSAFE_getAllByType(Text); + expect(textComponents.length).toBeGreaterThan(0); + expect(textComponents[0].props.color).toBe('Error'); + }); + + it('renders with custom style', () => { + const customStyle = { backgroundColor: '#FF0000', borderRadius: 16 }; + const { getByTestId } = render( + + Styled Button + , + ); + + const button = getByTestId('styled-button'); + expect(button.props.style.backgroundColor).toBe('#FF0000'); + expect(button.props.style.borderRadius).toBe(16); + }); + + it('handles non-string, non-icon children properly', () => { + const NonStringComponent = () => ; + + const { getByTestId } = render( + + + , + ); + + expect(getByTestId('custom-children-button')).toBeTruthy(); + expect(getByTestId('non-string-component')).toBeTruthy(); + }); + + it('sets proper accessibility properties', () => { + const { getByTestId } = render( + + Accessible Button + , + ); + + const button = getByTestId('accessible-button'); + expect(button.props.accessible).toBe(true); + expect(button.props.accessibilityRole).toBe('button'); + expect(button.props.accessibilityLabel).toBe('Accessible Button'); + }); + + it('provides fallback to name for accessibilityLabel when children is not a string', () => { + const NonStringComponent = () => ; + + const { getByTestId } = render( + + + , + ); + + const button = getByTestId('accessible-button'); + expect(button.props.accessibilityLabel).toBe('button-name'); + }); +}); diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx index 837757dcd653..c953cdc2bc3c 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx @@ -6,6 +6,7 @@ import { View, ViewStyle, StyleProp, + TouchableOpacityProps, } from 'react-native'; import { useSnapInterfaceContext } from '../SnapInterfaceContext'; import Text, { @@ -15,7 +16,7 @@ import Text, { import AnimatedLottieView from 'lottie-react-native'; import { useTheme } from '../../../util/theme'; -export interface SnapUIButtonProps { +export interface SnapUIButtonProps extends TouchableOpacityProps { name?: string; loading?: boolean; type?: ButtonType; @@ -26,6 +27,7 @@ export interface SnapUIButtonProps { disabled?: boolean; onPress?: () => void; children?: React.ReactNode; + testID?: string; } const COLORS = { @@ -45,6 +47,7 @@ export const SnapUIButton: FunctionComponent = ({ textVariant = TextVariant.BodyMDMedium, style, onPress, + testID, ...props }) => { const { handleEvent } = useSnapInterfaceContext(); @@ -137,6 +140,7 @@ export const SnapUIButton: FunctionComponent = ({ accessible accessibilityRole="button" accessibilityLabel={typeof children === 'string' ? children : name} + testID={testID} {...props} > {renderContent()} From 586276549045f831445f7015de399b29ef3f2dcf Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Fri, 28 Mar 2025 15:17:01 +0100 Subject: [PATCH 05/22] made changes to address the alignment and colour inheritance --- .../Snaps/SnapUIButton/SnapUIButton.tsx | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx index c953cdc2bc3c..e45385307b78 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx @@ -3,7 +3,6 @@ import { ButtonType, UserInputEventType } from '@metamask/snaps-sdk'; import { TouchableOpacity, StyleSheet, - View, ViewStyle, StyleProp, TouchableOpacityProps, @@ -117,10 +116,6 @@ export const SnapUIButton: FunctionComponent = ({ ); } - if (isIcon) { - return {children}; - } - if (typeof children === 'string') { return ( @@ -129,6 +124,36 @@ export const SnapUIButton: FunctionComponent = ({ ); } + if (React.isValidElement(children) && children.props?.sections) { + try { + const modifiedSections = children.props.sections.map((section: any) => { + if (section.element === 'RNText') { + return { + ...section, + props: { + ...section.props, + style: { + ...section.props?.style, + color: colors.primary.default, + }, + }, + }; + } + return section; + }); + + return React.cloneElement( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + children as React.ReactElement<{ sections: any }>, + { + sections: modifiedSections, + }, + ); + } catch (error) { + console.log('Error modifying sections:', error); + } + } + return children; }; From dc24a00d49c94a718a5b8737209ae24dc6080d77 Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Fri, 28 Mar 2025 15:55:23 +0100 Subject: [PATCH 06/22] updated types --- app/components/Snaps/SnapUIButton/SnapUIButton.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx index e45385307b78..4ca0b309c95b 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx @@ -71,12 +71,6 @@ export const SnapUIButton: FunctionComponent = ({ const overriddenVariant = disabled ? 'disabled' : variant; const color = COLORS[overriddenVariant as keyof typeof COLORS]; - const isIcon = - React.isValidElement(children) && - (children.props?.type === 'Icon' || - (children.type as React.ComponentType & { displayName?: string }) - ?.displayName === 'Icon'); - const styles = StyleSheet.create({ button: { backgroundColor: colors.background.default, @@ -124,7 +118,7 @@ export const SnapUIButton: FunctionComponent = ({ ); } - if (React.isValidElement(children) && children.props?.sections) { + if (React.isValidElement(children) && 'sections' in children.props) { try { const modifiedSections = children.props.sections.map((section: any) => { if (section.element === 'RNText') { @@ -142,12 +136,12 @@ export const SnapUIButton: FunctionComponent = ({ return section; }); + type SectionProps = { sections: Array }; return React.cloneElement( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - children as React.ReactElement<{ sections: any }>, + children as React.ReactElement, { sections: modifiedSections, - }, + } as Partial, ); } catch (error) { console.log('Error modifying sections:', error); From 3289ecdea45a37a147cd7058c359c7b41e6399af Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Fri, 28 Mar 2025 16:05:04 +0100 Subject: [PATCH 07/22] changed type to interface --- app/components/Snaps/SnapUIButton/SnapUIButton.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx index 4ca0b309c95b..a3230e3c35ee 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx @@ -136,7 +136,10 @@ export const SnapUIButton: FunctionComponent = ({ return section; }); - type SectionProps = { sections: Array }; + interface SectionProps { + sections: Array; + } + return React.cloneElement( children as React.ReactElement, { From 2ef9b13a08a0523bfb547d61ba80dd7d331b9249 Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Fri, 28 Mar 2025 16:17:32 +0100 Subject: [PATCH 08/22] removed log and changed any --- app/components/Snaps/SnapUIButton/SnapUIButton.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx index a3230e3c35ee..1fb92c7e2d8c 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx @@ -120,6 +120,7 @@ export const SnapUIButton: FunctionComponent = ({ if (React.isValidElement(children) && 'sections' in children.props) { try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const modifiedSections = children.props.sections.map((section: any) => { if (section.element === 'RNText') { return { @@ -137,7 +138,8 @@ export const SnapUIButton: FunctionComponent = ({ }); interface SectionProps { - sections: Array; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sections: any[]; } return React.cloneElement( @@ -147,7 +149,7 @@ export const SnapUIButton: FunctionComponent = ({ } as Partial, ); } catch (error) { - console.log('Error modifying sections:', error); + console.error('Error modifying sections:', error); } } From cf494ed42847d59336ac32eeea7f0a2fb90c4589 Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Mon, 31 Mar 2025 09:04:05 +0200 Subject: [PATCH 09/22] snapshot update --- .../SnapUIRenderer.test.tsx.snap | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap b/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap index 81d962e585b0..bba8d7291fe5 100644 --- a/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap +++ b/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap @@ -1636,26 +1636,20 @@ exports[`SnapUIRenderer supports fields with multiple components 1`] = ` padding={0} style={ { - "backgroundColor": "transparent", - "color": "#121314", - "fontFamily": "EuclidCircularB-Regular", - "fontSize": 14, - "fontWeight": "400", - "letterSpacing": 0, - "lineHeight": 22, + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderRadius": 8, + "flexDirection": "row", + "justifyContent": "center", + "paddingVertical": 8, } } > @@ -2669,26 +2663,20 @@ exports[`SnapUIRenderer supports forms with fields 1`] = ` onPress={[Function]} style={ { - "backgroundColor": "transparent", - "color": "#121314", - "fontFamily": "EuclidCircularB-Regular", - "fontSize": 14, - "fontWeight": "400", - "letterSpacing": 0, - "lineHeight": 22, + "alignItems": "center", + "backgroundColor": "#ffffff", + "borderRadius": 8, + "flexDirection": "row", + "justifyContent": "center", + "paddingVertical": 8, } } > From 8b8c459ff67fbcdda08ce4e1b6f12cb70479e9d2 Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Mon, 31 Mar 2025 09:31:15 +0200 Subject: [PATCH 10/22] reverted comment --- app/components/Snaps/SnapUIButton/SnapUIButton.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx index 1fb92c7e2d8c..869250e48631 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx @@ -60,6 +60,7 @@ export const SnapUIButton: FunctionComponent = ({ name, }); + // Since we don't have onSubmit on mobile, the button submits the form. if (type === ButtonType.Submit) { handleEvent({ event: UserInputEventType.FormSubmitEvent, From 5779a822e98f0f8a4d6bf45056d696b859c8b95b Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Mon, 31 Mar 2025 10:55:14 +0200 Subject: [PATCH 11/22] removed centre alignment --- app/components/Snaps/SnapUIButton/SnapUIButton.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx index 869250e48631..0c22b35f94f6 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx @@ -78,7 +78,6 @@ export const SnapUIButton: FunctionComponent = ({ borderRadius: 8, paddingVertical: 8, flexDirection: 'row', - justifyContent: 'center', alignItems: 'center', ...(style as ViewStyle), }, From 66287582a20fe515dab8f167a8010c6a3574e5a3 Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Mon, 31 Mar 2025 13:36:26 +0200 Subject: [PATCH 12/22] updated snapshot --- .../SnapUIRenderer.test.tsx.snap | 12 +- .../Snaps/SnapUIRenderer/components/button.ts | 171 +++++++++++++++--- 2 files changed, 153 insertions(+), 30 deletions(-) diff --git a/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap b/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap index bba8d7291fe5..9698f7abeff9 100644 --- a/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap +++ b/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap @@ -1180,7 +1180,7 @@ exports[`SnapUIRenderer renders complex nested components 1`] = ` } > Foo @@ -1409,7 +1409,7 @@ exports[`SnapUIRenderer renders footers 1`] = ` } > Foo @@ -1640,13 +1640,12 @@ exports[`SnapUIRenderer supports fields with multiple components 1`] = ` "backgroundColor": "#ffffff", "borderRadius": 8, "flexDirection": "row", - "justifyContent": "center", "paddingVertical": 8, } } > Foo diff --git a/app/components/Snaps/SnapUIRenderer/components/button.ts b/app/components/Snaps/SnapUIRenderer/components/button.ts index ce8093e4f226..a5968b7a1d60 100644 --- a/app/components/Snaps/SnapUIRenderer/components/button.ts +++ b/app/components/Snaps/SnapUIRenderer/components/button.ts @@ -6,8 +6,12 @@ import { import { getJsxChildren } from '@metamask/snaps-utils'; import { NonEmptyArray } from '@metamask/utils'; import { mapTextToTemplate } from '../utils'; -import { UIComponentFactory } from './types'; -import { TextVariant } from '../../../../component-library/components/Texts/Text'; +import { UIComponent, UIComponentFactory } from './types'; +import { + TextVariant, + TextColor, +} from '../../../../component-library/components/Texts/Text'; +import { Theme } from '../../../../util/theme/models'; interface ButtonElementProps extends ButtonElement { props: ButtonProps & { @@ -16,26 +20,147 @@ interface ButtonElementProps extends ButtonElement { }; } -export const button: UIComponentFactory = ({ - element: e, - ...params -}) => ({ - element: 'SnapUIButton', - props: { - type: e.props.type, - // This differs from the extension implementation because we don't have proper form support on RN - form: e.props.form ?? params.form, - variant: e.props.variant, - name: e.props.name, - disabled: e.props.disabled, - loading: e.props.loading ?? false, - textVariant: - e.props.size === 'sm' - ? TextVariant.BodySMMedium - : TextVariant.BodyMDMedium, - }, - children: mapTextToTemplate( +const COLORS = { + primary: TextColor.Info, + destructive: TextColor.Error, + disabled: TextColor.Muted, +}; + +// Map TextColor enum to actual theme colors +const getThemeColor = (textColor: TextColor, theme: Theme) => { + switch (textColor) { + case TextColor.Info: + return theme.colors.primary.default; + case TextColor.Error: + return theme.colors.error.default; + case TextColor.Muted: + return theme.colors.text.muted; + default: + return theme.colors.text.default; + } +}; + +// Process children to handle TemplateRenderer component with proper theme colors +function processButtonChildren( + children: NonEmptyArray, + variant: string = 'primary', + disabled: boolean = false, + theme: Theme, +): NonEmptyArray { + const overriddenVariant = disabled ? 'disabled' : variant; + const textColor = + COLORS[overriddenVariant as keyof typeof COLORS] || TextColor.Info; + const themeColor = getThemeColor(textColor, theme); + + return children.map((child) => { + if (typeof child === 'string') { + return child; + } + + // Handle TemplateRenderer components with sections + if (child.element === 'TemplateRenderer') { + if ( + child.props && + 'sections' in child.props && + Array.isArray(child.props.sections) + ) { + const sections = child.props.sections; + const modifiedSections = sections.map((section: any) => { + if (section.element === 'RNText') { + // Use a direct approach to minimize type issues + return { + ...section, + props: { + ...section.props, + color: textColor, // Use the enum value which Text component expects + }, + }; + } + return section; + }); + + return { + ...child, + props: { + ...child.props, + sections: modifiedSections, + }, + }; + } + + // Handle TemplateRenderer with nested children (but no sections) + if (child.children && Array.isArray(child.children)) { + return { + ...child, + children: processButtonChildren( + child.children as NonEmptyArray, + variant, + disabled, + theme, + ), + }; + } + } + + // If the element is a Text/RNText component, apply color directly + if (child.element === 'Text' || child.element === 'RNText') { + return { + ...child, + props: { + ...child.props, + color: textColor, // Use the enum value which Text component expects + }, + }; + } + + // Handle any other component with children recursively + if (child.children && Array.isArray(child.children)) { + return { + ...child, + children: processButtonChildren( + child.children as NonEmptyArray, + variant, + disabled, + theme, + ), + }; + } + + return child; + }) as NonEmptyArray; +} + +export const button: UIComponentFactory = (params) => { + const { element: e, theme } = params; + + // The theme should already be available in params + const mappedChildren = mapTextToTemplate( getJsxChildren(e) as NonEmptyArray, params, - ), -}); + ); + + const processedChildren = processButtonChildren( + mappedChildren, + e.props.variant, + e.props.disabled, + theme, + ); + + return { + element: 'SnapUIButton', + props: { + type: e.props.type, + // This differs from the extension implementation because we don't have proper form support on RN + form: e.props.form ?? params.form, + variant: e.props.variant, + name: e.props.name, + disabled: e.props.disabled, + loading: e.props.loading ?? false, + textVariant: + e.props.size === 'sm' + ? TextVariant.BodySMMedium + : TextVariant.BodyMDMedium, + }, + children: processedChildren, + }; +}; From 87af0668c1592f2a0e69df5e36d0fb29f26a97ba Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Mon, 31 Mar 2025 17:03:12 +0200 Subject: [PATCH 13/22] revert button while fix --- .../Snaps/SnapUIRenderer/components/button.ts | 171 +++--------------- 1 file changed, 23 insertions(+), 148 deletions(-) diff --git a/app/components/Snaps/SnapUIRenderer/components/button.ts b/app/components/Snaps/SnapUIRenderer/components/button.ts index a5968b7a1d60..ce8093e4f226 100644 --- a/app/components/Snaps/SnapUIRenderer/components/button.ts +++ b/app/components/Snaps/SnapUIRenderer/components/button.ts @@ -6,12 +6,8 @@ import { import { getJsxChildren } from '@metamask/snaps-utils'; import { NonEmptyArray } from '@metamask/utils'; import { mapTextToTemplate } from '../utils'; -import { UIComponent, UIComponentFactory } from './types'; -import { - TextVariant, - TextColor, -} from '../../../../component-library/components/Texts/Text'; -import { Theme } from '../../../../util/theme/models'; +import { UIComponentFactory } from './types'; +import { TextVariant } from '../../../../component-library/components/Texts/Text'; interface ButtonElementProps extends ButtonElement { props: ButtonProps & { @@ -20,147 +16,26 @@ interface ButtonElementProps extends ButtonElement { }; } -const COLORS = { - primary: TextColor.Info, - destructive: TextColor.Error, - disabled: TextColor.Muted, -}; - -// Map TextColor enum to actual theme colors -const getThemeColor = (textColor: TextColor, theme: Theme) => { - switch (textColor) { - case TextColor.Info: - return theme.colors.primary.default; - case TextColor.Error: - return theme.colors.error.default; - case TextColor.Muted: - return theme.colors.text.muted; - default: - return theme.colors.text.default; - } -}; - -// Process children to handle TemplateRenderer component with proper theme colors -function processButtonChildren( - children: NonEmptyArray, - variant: string = 'primary', - disabled: boolean = false, - theme: Theme, -): NonEmptyArray { - const overriddenVariant = disabled ? 'disabled' : variant; - const textColor = - COLORS[overriddenVariant as keyof typeof COLORS] || TextColor.Info; - const themeColor = getThemeColor(textColor, theme); - - return children.map((child) => { - if (typeof child === 'string') { - return child; - } - - // Handle TemplateRenderer components with sections - if (child.element === 'TemplateRenderer') { - if ( - child.props && - 'sections' in child.props && - Array.isArray(child.props.sections) - ) { - const sections = child.props.sections; - const modifiedSections = sections.map((section: any) => { - if (section.element === 'RNText') { - // Use a direct approach to minimize type issues - return { - ...section, - props: { - ...section.props, - color: textColor, // Use the enum value which Text component expects - }, - }; - } - return section; - }); - - return { - ...child, - props: { - ...child.props, - sections: modifiedSections, - }, - }; - } - - // Handle TemplateRenderer with nested children (but no sections) - if (child.children && Array.isArray(child.children)) { - return { - ...child, - children: processButtonChildren( - child.children as NonEmptyArray, - variant, - disabled, - theme, - ), - }; - } - } - - // If the element is a Text/RNText component, apply color directly - if (child.element === 'Text' || child.element === 'RNText') { - return { - ...child, - props: { - ...child.props, - color: textColor, // Use the enum value which Text component expects - }, - }; - } - - // Handle any other component with children recursively - if (child.children && Array.isArray(child.children)) { - return { - ...child, - children: processButtonChildren( - child.children as NonEmptyArray, - variant, - disabled, - theme, - ), - }; - } - - return child; - }) as NonEmptyArray; -} - -export const button: UIComponentFactory = (params) => { - const { element: e, theme } = params; - - // The theme should already be available in params - const mappedChildren = mapTextToTemplate( +export const button: UIComponentFactory = ({ + element: e, + ...params +}) => ({ + element: 'SnapUIButton', + props: { + type: e.props.type, + // This differs from the extension implementation because we don't have proper form support on RN + form: e.props.form ?? params.form, + variant: e.props.variant, + name: e.props.name, + disabled: e.props.disabled, + loading: e.props.loading ?? false, + textVariant: + e.props.size === 'sm' + ? TextVariant.BodySMMedium + : TextVariant.BodyMDMedium, + }, + children: mapTextToTemplate( getJsxChildren(e) as NonEmptyArray, params, - ); - - const processedChildren = processButtonChildren( - mappedChildren, - e.props.variant, - e.props.disabled, - theme, - ); - - return { - element: 'SnapUIButton', - props: { - type: e.props.type, - // This differs from the extension implementation because we don't have proper form support on RN - form: e.props.form ?? params.form, - variant: e.props.variant, - name: e.props.name, - disabled: e.props.disabled, - loading: e.props.loading ?? false, - textVariant: - e.props.size === 'sm' - ? TextVariant.BodySMMedium - : TextVariant.BodyMDMedium, - }, - children: processedChildren, - }; -}; + ), +}); From afe910ea4ef273492f19c215647bebf77f7561f4 Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Mon, 31 Mar 2025 22:06:21 +0200 Subject: [PATCH 14/22] updated tests to support new structure --- .../Snaps/SnapUIButton/SnapUIButton.test.tsx | 57 +++-- .../Snaps/SnapUIButton/SnapUIButton.tsx | 91 ++------ .../SnapUIRenderer.test.tsx.snap | 15 ++ .../Snaps/SnapUIRenderer/components/button.ts | 221 ++++++++++++++++-- .../SnapUIRenderer/components/footer.test.ts | 168 ++++++++----- 5 files changed, 390 insertions(+), 162 deletions(-) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx index 7d4746e9ca00..3741ab5ecd7b 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx @@ -3,7 +3,9 @@ import { render, fireEvent } from '@testing-library/react-native'; import { SnapUIButton } from './SnapUIButton'; import { useSnapInterfaceContext } from '../SnapInterfaceContext'; import { ButtonType, UserInputEventType } from '@metamask/snaps-sdk'; -import Text from '../../../component-library/components/Texts/Text'; +import Text, { + TextColor, +} from '../../../component-library/components/Texts/Text'; import { View } from 'react-native'; import AnimatedLottieView from 'lottie-react-native'; @@ -17,7 +19,7 @@ jest.mock('../../../util/theme', () => ({ background: { default: '#FFFFFF' }, border: { muted: '#CCCCCC', default: '#DDDDDD' }, text: { default: '#000000' }, - info: { default: '#0376C9' }, + primary: { default: '#0376C9' }, error: { default: '#D73A49' }, }, }), @@ -34,9 +36,30 @@ describe('SnapUIButton', () => { }); }); + const createStyledText = ( + text: string, + color: TextColor = TextColor.Info, + ) => ( + + {text} + + ); + it('renders correctly with text children', () => { const { getByText } = render( - Test Button, + + {createStyledText('Test Button')} + , ); expect(getByText('Test Button')).toBeTruthy(); @@ -45,7 +68,7 @@ describe('SnapUIButton', () => { it('renders correctly when disabled', () => { const { getByText, getByTestId } = render( - Disabled Button + {createStyledText('Disabled Button', TextColor.Muted)} , ); @@ -56,7 +79,7 @@ describe('SnapUIButton', () => { it('renders with loading state', () => { const { UNSAFE_getByType } = render( - Loading Button + {createStyledText('Loading Button')} , ); @@ -66,7 +89,7 @@ describe('SnapUIButton', () => { it('calls onPress and handles ButtonClickEvent when pressed', () => { const { getByText } = render( - Click Me + {createStyledText('Click Me')} , ); @@ -87,7 +110,7 @@ describe('SnapUIButton', () => { name="test-button" form="test-form" > - Submit Form + {createStyledText('Submit Form')} , ); @@ -103,7 +126,7 @@ describe('SnapUIButton', () => { }); }); - it('renders icon component correctly without wrapping in Text', () => { + it('renders icon component correctly', () => { const MockIcon = () => ; MockIcon.displayName = 'Icon'; @@ -117,14 +140,14 @@ describe('SnapUIButton', () => { expect(getByTestId('mock-icon')).toBeTruthy(); }); - it('renders destructive variant with correct color', () => { - const { UNSAFE_getAllByType } = render( - Destructive Button, + it('renders destructive variant button correctly', () => { + const { getByTestId } = render( + + {createStyledText('Destructive Button', TextColor.Error)} + , ); - const textComponents = UNSAFE_getAllByType(Text); - expect(textComponents.length).toBeGreaterThan(0); - expect(textComponents[0].props.color).toBe('Error'); + expect(getByTestId('destructive-button')).toBeTruthy(); }); it('renders with custom style', () => { @@ -135,7 +158,7 @@ describe('SnapUIButton', () => { style={customStyle} testID="styled-button" > - Styled Button + {createStyledText('Styled Button')} , ); @@ -164,14 +187,14 @@ describe('SnapUIButton', () => { testID="accessible-button" name="button-name" > - Accessible Button + {createStyledText('Accessible Button')} , ); const button = getByTestId('accessible-button'); expect(button.props.accessible).toBe(true); expect(button.props.accessibilityRole).toBe('button'); - expect(button.props.accessibilityLabel).toBe('Accessible Button'); + expect(button.props.accessibilityLabel).toBe('button-name'); }); it('provides fallback to name for accessibilityLabel when children is not a string', () => { diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx index 0c22b35f94f6..51a7e71e26e4 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx @@ -8,7 +8,7 @@ import { TouchableOpacityProps, } from 'react-native'; import { useSnapInterfaceContext } from '../SnapInterfaceContext'; -import Text, { +import { TextColor, TextVariant, } from '../../../component-library/components/Texts/Text'; @@ -69,9 +69,6 @@ export const SnapUIButton: FunctionComponent = ({ } }; - const overriddenVariant = disabled ? 'disabled' : variant; - const color = COLORS[overriddenVariant as keyof typeof COLORS]; - const styles = StyleSheet.create({ button: { backgroundColor: colors.background.default, @@ -81,80 +78,32 @@ export const SnapUIButton: FunctionComponent = ({ alignItems: 'center', ...(style as ViewStyle), }, - content: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - }, - icon: { - marginRight: 8, - }, - lastIcon: { - marginRight: 0, + loadingAnimation: { + width: 24, + height: 24, }, }); - const renderContent = () => { - if (loading) { - return ( + if (loading) { + return ( + - ); - } - - if (typeof children === 'string') { - return ( - - {children} - - ); - } - - if (React.isValidElement(children) && 'sections' in children.props) { - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const modifiedSections = children.props.sections.map((section: any) => { - if (section.element === 'RNText') { - return { - ...section, - props: { - ...section.props, - style: { - ...section.props?.style, - color: colors.primary.default, - }, - }, - }; - } - return section; - }); - - interface SectionProps { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sections: any[]; - } - - return React.cloneElement( - children as React.ReactElement, - { - sections: modifiedSections, - } as Partial, - ); - } catch (error) { - console.error('Error modifying sections:', error); - } - } - - return children; - }; + + ); + } return ( = ({ testID={testID} {...props} > - {renderContent()} + {children} ); }; diff --git a/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap b/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap index 9698f7abeff9..44ec38bb05cf 100644 --- a/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap +++ b/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap @@ -1181,6 +1181,11 @@ exports[`SnapUIRenderer renders complex nested components 1`] = ` > Foo @@ -1410,6 +1415,11 @@ exports[`SnapUIRenderer renders footers 1`] = ` > Foo @@ -3028,6 +3038,11 @@ exports[`SnapUIRenderer supports the onCancel prop 1`] = ` > Foo diff --git a/app/components/Snaps/SnapUIRenderer/components/button.ts b/app/components/Snaps/SnapUIRenderer/components/button.ts index ce8093e4f226..7f46d61d2adc 100644 --- a/app/components/Snaps/SnapUIRenderer/components/button.ts +++ b/app/components/Snaps/SnapUIRenderer/components/button.ts @@ -6,8 +6,13 @@ import { import { getJsxChildren } from '@metamask/snaps-utils'; import { NonEmptyArray } from '@metamask/utils'; import { mapTextToTemplate } from '../utils'; -import { UIComponentFactory } from './types'; -import { TextVariant } from '../../../../component-library/components/Texts/Text'; +import { UIComponent, UIComponentFactory } from './types'; +import { + TextColor, + TextVariant, +} from '../../../../component-library/components/Texts/Text'; +import { Theme } from '@metamask/design-tokens'; +import { StyleProp, TextStyle } from 'react-native'; interface ButtonElementProps extends ButtonElement { props: ButtonProps & { @@ -15,27 +20,201 @@ interface ButtonElementProps extends ButtonElement { size?: 'sm' | 'md'; }; } +interface CommonProps { + style?: StyleProp; + color?: TextColor | string; + variant?: TextVariant; + [key: string]: unknown; +} + +interface TextComponentProps extends CommonProps { + color?: TextColor; + variant: TextVariant; +} + +interface IconComponentProps extends CommonProps { + color?: string; + name: string; + size: string | number; +} + +// Section component types +interface SectionComponent { + element: string; + props: CommonProps; +} + +interface TextSectionComponent extends SectionComponent { + element: 'RNText' | 'Text'; + props: TextComponentProps; +} + +interface IconSectionComponent extends SectionComponent { + element: 'SnapUIIcon'; + props: IconComponentProps; +} + +type ProcessedSectionComponent = + | TextSectionComponent + | IconSectionComponent + | SectionComponent; + +interface ComponentWithSections extends UIComponent { + props: { + sections: ProcessedSectionComponent[]; + [key: string]: unknown; + }; +} + +const COLORS = { + primary: TextColor.Info, + destructive: TextColor.Error, + disabled: TextColor.Muted, +}; + +/** + * Applies styling to button content elements based on the button variant + */ +function processButtonContent( + content: NonEmptyArray, + variant: string, + disabled: boolean, + textVariant: TextVariant, + theme: Theme, +): NonEmptyArray { + const overriddenVariant = disabled ? 'disabled' : variant; + const enumColor = COLORS[overriddenVariant as keyof typeof COLORS]; + + const themeColor = + enumColor === TextColor.Info + ? theme.colors.primary.default + : enumColor === TextColor.Error + ? theme.colors.error.default + : theme.colors.text.muted; + + /** + * Apply styles to a component based on its type + */ + function styleComponent(component: UIComponent): UIComponent { + const result: UIComponent = { + ...component, + }; + + if (component.element === 'Text' || component.element === 'RNText') { + result.props = { + ...component.props, + color: enumColor, + style: { + ...((component.props?.style as Record) || {}), + color: themeColor, + }, + }; + } else if (component.element === 'SnapUIIcon') { + result.props = { + ...component.props, + color: themeColor, + }; + } + + if (component.children && Array.isArray(component.children)) { + result.children = component.children + .map((child) => { + if (typeof child === 'string') return child; + if (!child || typeof child !== 'object') return null; + return styleComponent(child as UIComponent); + }) + .filter(Boolean) as NonEmptyArray; + } + + return result; + } + + return content.map((child) => { + if (typeof child === 'string') { + return { + element: 'Text', + props: { + color: enumColor, + variant: textVariant, + style: { color: themeColor }, + }, + children: [child], + }; + } + + if (child.props && 'sections' in child.props) { + const componentWithSections = child as ComponentWithSections; + const modifiedSections = componentWithSections.props.sections.map( + (section) => { + const result = { ...section }; + + if (section.element === 'RNText') { + result.props = { + ...section.props, + style: { + ...((section.props.style as Record) || {}), + color: themeColor, + }, + }; + } else if (section.element === 'SnapUIIcon') { + result.props = { + ...section.props, + color: themeColor, + }; + } + + return result; + }, + ); + + return { + ...componentWithSections, + props: { + ...componentWithSections.props, + sections: modifiedSections, + }, + }; + } + + return styleComponent(child); + }) as NonEmptyArray; +} export const button: UIComponentFactory = ({ element: e, + theme, ...params -}) => ({ - element: 'SnapUIButton', - props: { - type: e.props.type, - // This differs from the extension implementation because we don't have proper form support on RN - form: e.props.form ?? params.form, - variant: e.props.variant, - name: e.props.name, - disabled: e.props.disabled, - loading: e.props.loading ?? false, - textVariant: - e.props.size === 'sm' - ? TextVariant.BodySMMedium - : TextVariant.BodyMDMedium, - }, - children: mapTextToTemplate( +}) => { + const textVariant = + e.props.size === 'sm' ? TextVariant.BodySMMedium : TextVariant.BodyMDMedium; + + const mappedChildren = mapTextToTemplate( getJsxChildren(e) as NonEmptyArray, - params, - ), -}); + { + ...params, + theme, + }, + ); + + const processedChildren = processButtonContent( + mappedChildren, + e.props.variant || 'primary', + !!e.props.disabled, + textVariant, + theme, + ); + + return { + element: 'SnapUIButton', + props: { + type: e.props.type, + form: e.props.form ?? params.form, + variant: e.props.variant || 'primary', + name: e.props.name, + disabled: e.props.disabled, + loading: e.props.loading ?? false, + textVariant, + }, + children: processedChildren, + }; +}; diff --git a/app/components/Snaps/SnapUIRenderer/components/footer.test.ts b/app/components/Snaps/SnapUIRenderer/components/footer.test.ts index 74b3710bddff..c43df2a7f8e9 100644 --- a/app/components/Snaps/SnapUIRenderer/components/footer.test.ts +++ b/app/components/Snaps/SnapUIRenderer/components/footer.test.ts @@ -2,6 +2,7 @@ import { ButtonElement, FooterElement } from '@metamask/snaps-sdk/jsx'; import { footer, DEFAULT_FOOTER } from './footer'; import { mockTheme } from '../../../../util/theme'; import { ButtonVariants } from '../../../../component-library/components/Buttons/Button'; +import { UIComponent } from './types'; describe('footer', () => { const mockT = (value: string) => `translated_${value}`; @@ -46,34 +47,34 @@ describe('footer', () => { theme: mockTheme, }); - expect(result).toEqual({ - ...DEFAULT_FOOTER, - children: [ - { - element: 'SnapUIFooterButton', - key: 'snap-footer-button-0', - props: { - disabled: undefined, - form: undefined, - isSnapAction: true, - loading: false, - name: undefined, - onCancel: undefined, - textVariant: 'sBodyMDMedium', - type: undefined, - variant: 'Primary', - }, - children: [ - { - key: '57fd48ba929aa415dc4c3996c826a75f8686418c77765eb14fad2658efa73d87_1', - element: 'RNText', - children: 'Button', - props: { color: 'inherit' }, - }, - ], - }, - ], + expect(result.element).toBe(DEFAULT_FOOTER.element); + + const children = result.children as UIComponent[]; + expect(children).toBeTruthy(); + expect(Array.isArray(children)).toBe(true); + + const buttonComponent = children[0]; + expect(buttonComponent.element).toBe('SnapUIFooterButton'); + expect(buttonComponent.props).toMatchObject({ + disabled: undefined, + form: undefined, + isSnapAction: true, + loading: false, + name: undefined, + onCancel: undefined, + textVariant: 'sBodyMDMedium', + type: undefined, + variant: 'Primary', }); + + const buttonChildren = buttonComponent.children as UIComponent[]; + expect(buttonChildren).toBeTruthy(); + expect(Array.isArray(buttonChildren)).toBe(true); + + const textComponent = buttonChildren[0]; + expect(textComponent.element).toBe('RNText'); + expect(textComponent.children).toBe('Button'); + expect(textComponent.props?.color).toBe('Info'); }); it('add cancel button when onCancel is provided and only one child', () => { @@ -89,18 +90,42 @@ describe('footer', () => { theme: mockTheme, }); - expect(Array.isArray(result.children)).toBe(true); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((result.children as any[])[0]).toEqual({ - element: 'SnapUIFooterButton', - key: 'default-button', - props: { - isSnapAction: false, - onCancel: mockOnCancel, - variant: 'Secondary', - }, - children: 'translated_template_confirmation.cancel', + const children = result.children as UIComponent[]; + expect(children).toBeTruthy(); + expect(Array.isArray(children)).toBe(true); + + const cancelButton = children[0]; + expect(cancelButton.element).toBe('SnapUIFooterButton'); + expect(cancelButton.props).toMatchObject({ + isSnapAction: false, + onCancel: mockOnCancel, + variant: 'Secondary', }); + + if (typeof cancelButton.children === 'string') { + expect(cancelButton.children).toBe( + 'translated_template_confirmation.cancel', + ); + } else { + const buttonChildren = cancelButton.children as (string | UIComponent)[]; + + if (Array.isArray(buttonChildren) && buttonChildren.length > 0) { + const content = buttonChildren[0]; + + if (typeof content === 'string') { + expect(content).toBe('translated_template_confirmation.cancel'); + } else if ( + content && + typeof content === 'object' && + 'children' in content + ) { + const textContent = content.children; + if (typeof textContent === 'string') { + expect(textContent).toBe('translated_template_confirmation.cancel'); + } + } + } + } }); it('handle multiple buttons with correct variants', () => { @@ -116,19 +141,52 @@ describe('footer', () => { theme: mockTheme, }); - expect(Array.isArray(result.children)).toBe(true); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((result.children as any[])[0].props.variant).toBe( - ButtonVariants.Secondary, - ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((result.children as any[])[1].props.variant).toBe( - ButtonVariants.Primary, - ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((result.children as any[])[0].props.isSnapAction).toBe(true); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((result.children as any[])[1].props.isSnapAction).toBe(true); + const children = result.children as UIComponent[]; + expect(children).toBeTruthy(); + expect(Array.isArray(children)).toBe(true); + expect(children.length).toBeGreaterThanOrEqual(2); + + const rejectButton = children[0]; + expect(rejectButton.props?.variant).toBe(ButtonVariants.Secondary); + expect(rejectButton.props?.isSnapAction).toBe(true); + + const confirmButton = children[1]; + expect(confirmButton.props?.variant).toBe(ButtonVariants.Primary); + expect(confirmButton.props?.isSnapAction).toBe(true); + + const rejectButtonChildren = rejectButton.children as ( + | string + | UIComponent + )[]; + if ( + Array.isArray(rejectButtonChildren) && + rejectButtonChildren.length > 0 + ) { + const textContent = rejectButtonChildren[0]; + if (typeof textContent === 'object' && textContent.element === 'RNText') { + const text = textContent.children; + if (typeof text === 'string') { + expect(text).toBe('Reject'); + } + } + } + + const confirmButtonChildren = confirmButton.children as ( + | string + | UIComponent + )[]; + if ( + Array.isArray(confirmButtonChildren) && + confirmButtonChildren.length > 0 + ) { + const textContent = confirmButtonChildren[0]; + if (typeof textContent === 'object' && textContent.element === 'RNText') { + const text = textContent.children; + if (typeof text === 'string') { + expect(text).toBe('Confirm'); + } + } + } }); it('use index as key when button name is not provided', () => { @@ -143,7 +201,11 @@ describe('footer', () => { theme: mockTheme, }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((result.children as any[])[0].key).toBe('snap-footer-button-0'); + const children = result.children as UIComponent[]; + expect(children).toBeTruthy(); + expect(Array.isArray(children)).toBe(true); + + const button = children[0]; + expect(button.key).toBe('snap-footer-button-0'); }); }); From c29d582a75a5abbc1f9de0ec012700ad5652f21e Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Mon, 31 Mar 2025 22:14:42 +0200 Subject: [PATCH 15/22] removed colours from snapuibutton as it's now been moved to button.ts --- .../Snaps/SnapUIButton/SnapUIButton.tsx | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx index 51a7e71e26e4..afd8508a1eb0 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.tsx @@ -8,10 +8,6 @@ import { TouchableOpacityProps, } from 'react-native'; import { useSnapInterfaceContext } from '../SnapInterfaceContext'; -import { - TextColor, - TextVariant, -} from '../../../component-library/components/Texts/Text'; import AnimatedLottieView from 'lottie-react-native'; import { useTheme } from '../../../util/theme'; @@ -20,8 +16,6 @@ export interface SnapUIButtonProps extends TouchableOpacityProps { loading?: boolean; type?: ButtonType; form?: string; - variant: keyof typeof COLORS; - textVariant?: TextVariant; style?: StyleProp; disabled?: boolean; onPress?: () => void; @@ -29,21 +23,13 @@ export interface SnapUIButtonProps extends TouchableOpacityProps { testID?: string; } -const COLORS = { - primary: TextColor.Info, - destructive: TextColor.Error, - disabled: TextColor.Muted, -}; - export const SnapUIButton: FunctionComponent = ({ name, children, form, type = ButtonType.Button, - variant = 'primary', disabled = false, loading = false, - textVariant = TextVariant.BodyMDMedium, style, onPress, testID, @@ -88,7 +74,7 @@ export const SnapUIButton: FunctionComponent = ({ return ( Date: Mon, 31 Mar 2025 22:19:41 +0200 Subject: [PATCH 16/22] addressed lint issues in test --- .../Snaps/SnapUIButton/SnapUIButton.test.tsx | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx index 3741ab5ecd7b..b3539adb7035 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx @@ -57,9 +57,7 @@ describe('SnapUIButton', () => { it('renders correctly with text children', () => { const { getByText } = render( - - {createStyledText('Test Button')} - , + {createStyledText('Test Button')}, ); expect(getByText('Test Button')).toBeTruthy(); @@ -67,7 +65,7 @@ describe('SnapUIButton', () => { it('renders correctly when disabled', () => { const { getByText, getByTestId } = render( - + {createStyledText('Disabled Button', TextColor.Muted)} , ); @@ -78,9 +76,7 @@ describe('SnapUIButton', () => { it('renders with loading state', () => { const { UNSAFE_getByType } = render( - - {createStyledText('Loading Button')} - , + {createStyledText('Loading Button')}, ); expect(UNSAFE_getByType(AnimatedLottieView)).toBeTruthy(); @@ -88,7 +84,7 @@ describe('SnapUIButton', () => { it('calls onPress and handles ButtonClickEvent when pressed', () => { const { getByText } = render( - + {createStyledText('Click Me')} , ); @@ -105,7 +101,6 @@ describe('SnapUIButton', () => { it('handles FormSubmitEvent when button type is Submit', () => { const { getByText } = render( { MockIcon.displayName = 'Icon'; const { getByTestId } = render( - + , ); @@ -142,7 +137,7 @@ describe('SnapUIButton', () => { it('renders destructive variant button correctly', () => { const { getByTestId } = render( - + {createStyledText('Destructive Button', TextColor.Error)} , ); @@ -153,11 +148,7 @@ describe('SnapUIButton', () => { it('renders with custom style', () => { const customStyle = { backgroundColor: '#FF0000', borderRadius: 16 }; const { getByTestId } = render( - + {createStyledText('Styled Button')} , ); @@ -171,7 +162,7 @@ describe('SnapUIButton', () => { const NonStringComponent = () => ; const { getByTestId } = render( - + , ); @@ -182,11 +173,7 @@ describe('SnapUIButton', () => { it('sets proper accessibility properties', () => { const { getByTestId } = render( - + {createStyledText('Accessible Button')} , ); @@ -201,11 +188,7 @@ describe('SnapUIButton', () => { const NonStringComponent = () => ; const { getByTestId } = render( - + , ); From 9fcc492ba9ef142a3ba7c4952b060e79edabc045 Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Mon, 31 Mar 2025 22:26:08 +0200 Subject: [PATCH 17/22] linting on tests --- app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx index b3539adb7035..05009e0496be 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx @@ -42,6 +42,8 @@ describe('SnapUIButton', () => { ) => ( Date: Mon, 31 Mar 2025 22:32:32 +0200 Subject: [PATCH 18/22] linting issue --- .../Snaps/SnapUIButton/SnapUIButton.test.tsx | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx b/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx index 05009e0496be..e9c23f24873f 100644 --- a/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx +++ b/app/components/Snaps/SnapUIButton/SnapUIButton.test.tsx @@ -36,23 +36,24 @@ describe('SnapUIButton', () => { }); }); + const getColorForTextVariant = (color: TextColor): string => { + switch (color) { + case TextColor.Info: + return '#0376C9'; + case TextColor.Error: + return '#D73A49'; + case TextColor.Muted: + return '#000000'; + default: + return '#000000'; + } + }; + const createStyledText = ( text: string, color: TextColor = TextColor.Info, ) => ( - + {text} ); From 97b23b58197c872cdba523299ec1a35691892e9e Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Mon, 31 Mar 2025 22:52:27 +0200 Subject: [PATCH 19/22] snapshot update --- .../SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap b/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap index dcdc74d5d8c7..adcc9ae14e0d 100644 --- a/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap +++ b/app/components/Snaps/SnapUIRenderer/__snapshots__/SnapUIRenderer.test.tsx.snap @@ -1655,6 +1655,8 @@ exports[`SnapUIRenderer supports fields with multiple components 1`] = ` "paddingVertical": 8, } } + textVariant="sBodyMDMedium" + variant="primary" > Date: Tue, 1 Apr 2025 08:33:39 +0200 Subject: [PATCH 20/22] added tests --- .../SnapUIRenderer/components/button.test.ts | 356 ++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 app/components/Snaps/SnapUIRenderer/components/button.test.ts diff --git a/app/components/Snaps/SnapUIRenderer/components/button.test.ts b/app/components/Snaps/SnapUIRenderer/components/button.test.ts new file mode 100644 index 000000000000..b5ee12a1a49d --- /dev/null +++ b/app/components/Snaps/SnapUIRenderer/components/button.test.ts @@ -0,0 +1,356 @@ +import { ButtonType } from '@metamask/snaps-sdk'; +import { button } from './button'; +import { mockTheme } from '../../../../util/theme'; +import { + TextColor, + TextVariant, +} from '../../../../component-library/components/Texts/Text'; +import { UIComponent } from './types'; + +interface MockUIComponent { + element: string; + props?: Record; + children?: Array; +} + +jest.mock('@metamask/snaps-utils', () => ({ + getJsxChildren: jest.fn((element) => element.children || []), +})); + +jest.mock('../utils', () => ({ + mapTextToTemplate: jest.fn((children, params) => { + return children.map((child: string | MockUIComponent) => { + if (typeof child === 'string') { + return child; + } + return { + element: child.element || 'Text', + props: child.props || {}, + children: child.children || [], + }; + }); + }), +})); + +describe('button component factory', () => { + const mockT = (key: string) => key; + + const createButtonElement = ( + props: Record = {}, + children: Array = ['Button Text'], + ): any => ({ + type: 'Button', + props: { + variant: 'primary', + disabled: false, + loading: false, + ...props, + }, + key: 'test-key', + children, + }); + + it('creates a basic button with text children', () => { + const buttonElement = createButtonElement(); + + const result = button({ + element: buttonElement, + theme: mockTheme, + form: undefined, + map: {}, + t: mockT, + }); + + expect(result.element).toBe('SnapUIButton'); + expect(result.props?.variant).toBe('primary'); + expect(result.props?.disabled).toBe(false); + expect(result.props?.loading).toBe(false); + + const children = result.children as UIComponent[]; + expect(children).toHaveLength(1); + + const textChild = children[0] as UIComponent; + expect(textChild.element).toBe('Text'); + expect(textChild.props?.color).toBe(TextColor.Info); + + const styleObj = textChild.props?.style as Record; + expect(styleObj?.color).toBe(mockTheme.colors.primary.default); + }); + + it('sets button props correctly based on input', () => { + const buttonElement = createButtonElement({ + variant: 'destructive', + disabled: true, + loading: true, + name: 'test-button', + type: ButtonType.Submit, + form: 'test-form', + }); + + const result = button({ + element: buttonElement, + theme: mockTheme, + form: 'parent-form', + map: {}, + t: mockT, + }); + + expect(result.props?.variant).toBe('destructive'); + expect(result.props?.disabled).toBe(true); + expect(result.props?.loading).toBe(true); + expect(result.props?.name).toBe('test-button'); + expect(result.props?.type).toBe(ButtonType.Submit); + expect(result.props?.form).toBe('test-form'); + }); + + it('uses parent form when button form is not provided', () => { + const buttonElement = createButtonElement(); + + const result = button({ + element: buttonElement, + theme: mockTheme, + form: 'parent-form', + map: {}, + t: mockT, + }); + + expect(result.props?.form).toBe('parent-form'); + }); + + it('handles size property and sets text variant correctly', () => { + const smallButtonElement = createButtonElement({ size: 'sm' }); + const mediumButtonElement = createButtonElement({ size: 'md' }); + + const smallResult = button({ + element: smallButtonElement, + theme: mockTheme, + form: undefined, + map: {}, + t: mockT, + }); + + const mediumResult = button({ + element: mediumButtonElement, + theme: mockTheme, + form: undefined, + map: {}, + t: mockT, + }); + + expect(smallResult.props?.textVariant).toBe(TextVariant.BodySMMedium); + expect(mediumResult.props?.textVariant).toBe(TextVariant.BodyMDMedium); + }); + + it('applies disabled styling correctly', () => { + const buttonElement = createButtonElement({ disabled: true }); + + const result = button({ + element: buttonElement, + theme: mockTheme, + form: undefined, + map: {}, + t: mockT, + }); + + const textChild = (result.children as UIComponent[])[0] as UIComponent; + expect(textChild.props?.color).toBe(TextColor.Muted); + + const styleObj = textChild.props?.style as Record; + expect(styleObj?.color).toBe(mockTheme.colors.text.muted); + }); + + it('applies destructive variant styling correctly', () => { + const buttonElement = createButtonElement({ variant: 'destructive' }); + + const result = button({ + element: buttonElement, + theme: mockTheme, + form: undefined, + map: {}, + t: mockT, + }); + + const textChild = (result.children as UIComponent[])[0] as UIComponent; + expect(textChild.props?.color).toBe(TextColor.Error); + + const styleObj = textChild.props?.style as Record; + expect(styleObj?.color).toBe(mockTheme.colors.error.default); + }); + + it('styles complex children recursively', () => { + const nestedTextComponent: MockUIComponent = { + element: 'Text', + props: {}, + children: ['Nested Text'], + }; + + const complexTextComponent: MockUIComponent = { + element: 'Text', + props: {}, + children: ['Parent Text', nestedTextComponent], + }; + + const buttonElement = createButtonElement({}, [complexTextComponent]); + + const result = button({ + element: buttonElement, + theme: mockTheme, + form: undefined, + map: {}, + t: mockT, + }); + + const parent = (result.children as UIComponent[])[0] as UIComponent; + expect(parent.element).toBe('Text'); + expect(parent.props?.color).toBe(TextColor.Info); + + const styleObj = parent.props?.style as Record; + expect(styleObj?.color).toBe(mockTheme.colors.primary.default); + }); + + it('styles components with sections correctly', () => { + const sectionContainer: MockUIComponent = { + element: 'SectionContainer', + props: { + sections: [ + { + element: 'RNText', + props: {}, + }, + { + element: 'SnapUIIcon', + props: { + name: 'test-icon', + size: 24, + }, + }, + ], + }, + }; + + const buttonElement = createButtonElement({}, [sectionContainer]); + + const result = button({ + element: buttonElement, + theme: mockTheme, + form: undefined, + map: {}, + t: mockT, + }); + + const container = (result.children as UIComponent[])[0] as UIComponent; + expect(container.element).toBe('SectionContainer'); + + const sections = container.props?.sections as Array<{ + element: string; + props: Record; + }>; + expect(sections).toHaveLength(2); + + const textSection = sections[0]; + expect(textSection.element).toBe('RNText'); + const textSectionStyle = textSection.props.style as Record; + expect(textSectionStyle.color).toBe(mockTheme.colors.primary.default); + + const iconSection = sections[1]; + expect(iconSection.element).toBe('SnapUIIcon'); + expect(iconSection.props.color).toBe(mockTheme.colors.primary.default); + }); + + it('handles icon components correctly', () => { + const iconComponent: MockUIComponent = { + element: 'SnapUIIcon', + props: { + name: 'test-icon', + size: 24, + }, + }; + + const buttonElement = createButtonElement({}, [iconComponent]); + + const result = button({ + element: buttonElement, + theme: mockTheme, + form: undefined, + map: {}, + t: mockT, + }); + + const icon = (result.children as UIComponent[])[0] as UIComponent; + expect(icon.element).toBe('SnapUIIcon'); + expect(icon.props?.color).toBe(mockTheme.colors.primary.default); + }); + + it('handles mixed text and icon children', () => { + const iconComponent: MockUIComponent = { + element: 'SnapUIIcon', + props: { + name: 'test-icon', + size: 24, + }, + }; + + const buttonElement = createButtonElement({}, [ + 'Text Content', + iconComponent, + ]); + + const result = button({ + element: buttonElement, + theme: mockTheme, + form: undefined, + map: {}, + t: mockT, + }); + + const children = result.children as UIComponent[]; + expect(children).toHaveLength(2); + + const textChild = children[0] as UIComponent; + expect(textChild.element).toBe('Text'); + expect(textChild.props?.color).toBe(TextColor.Info); + + const iconChild = children[1] as UIComponent; + expect(iconChild.element).toBe('SnapUIIcon'); + expect(iconChild.props?.color).toBe(mockTheme.colors.primary.default); + }); + + it('applies the same color to both text and icons', () => { + const textComponent: MockUIComponent = { + element: 'Text', + props: {}, + children: ['Text'], + }; + + const iconComponent: MockUIComponent = { + element: 'SnapUIIcon', + props: { + name: 'test-icon', + size: 24, + }, + }; + + const buttonElement = createButtonElement({}, [ + textComponent, + iconComponent, + ]); + + const result = button({ + element: buttonElement, + theme: mockTheme, + form: undefined, + map: {}, + t: mockT, + }); + + const children = result.children as UIComponent[]; + expect(children).toHaveLength(2); + + const textChild = children[0] as UIComponent; + const iconChild = children[1] as UIComponent; + + const textStyle = textChild.props?.style as Record; + expect(textStyle?.color).toBe(mockTheme.colors.primary.default); + expect(iconChild.props?.color).toBe(mockTheme.colors.primary.default); + }); +}); From b03005d7da7dc02b88930d295ffb168b0801b417 Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Tue, 1 Apr 2025 09:18:59 +0200 Subject: [PATCH 21/22] linting issue --- .../SnapUIRenderer/components/button.test.ts | 71 +++++++++++++------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/app/components/Snaps/SnapUIRenderer/components/button.test.ts b/app/components/Snaps/SnapUIRenderer/components/button.test.ts index b5ee12a1a49d..675726ce7d94 100644 --- a/app/components/Snaps/SnapUIRenderer/components/button.test.ts +++ b/app/components/Snaps/SnapUIRenderer/components/button.test.ts @@ -7,19 +7,32 @@ import { } from '../../../../component-library/components/Texts/Text'; import { UIComponent } from './types'; +type TestButtonElement = { + type: string; + props: { + variant: string; + disabled: boolean; + loading: boolean; + children?: (string | MockUIComponent)[]; + [key: string]: unknown; + }; + key: string; + children: (string | MockUIComponent)[]; +}; + interface MockUIComponent { element: string; props?: Record; - children?: Array; + children?: (string | MockUIComponent)[]; } jest.mock('@metamask/snaps-utils', () => ({ - getJsxChildren: jest.fn((element) => element.children || []), + getJsxChildren: jest.fn((elem) => elem.children || []), })); jest.mock('../utils', () => ({ - mapTextToTemplate: jest.fn((children, params) => { - return children.map((child: string | MockUIComponent) => { + mapTextToTemplate: jest.fn((children) => + children.map((child: string | MockUIComponent) => { if (typeof child === 'string') { return child; } @@ -28,22 +41,24 @@ jest.mock('../utils', () => ({ props: child.props || {}, children: child.children || [], }; - }); - }), + }), + ), })); describe('button component factory', () => { const mockT = (key: string) => key; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const createButtonElement = ( props: Record = {}, - children: Array = ['Button Text'], - ): any => ({ + children: (string | MockUIComponent)[] = ['Button Text'], + ): TestButtonElement => ({ type: 'Button', props: { variant: 'primary', disabled: false, loading: false, + children, ...props, }, key: 'test-key', @@ -54,7 +69,8 @@ describe('button component factory', () => { const buttonElement = createButtonElement(); const result = button({ - element: buttonElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: buttonElement as any, theme: mockTheme, form: undefined, map: {}, @@ -88,7 +104,8 @@ describe('button component factory', () => { }); const result = button({ - element: buttonElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: buttonElement as any, theme: mockTheme, form: 'parent-form', map: {}, @@ -107,7 +124,8 @@ describe('button component factory', () => { const buttonElement = createButtonElement(); const result = button({ - element: buttonElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: buttonElement as any, theme: mockTheme, form: 'parent-form', map: {}, @@ -122,7 +140,8 @@ describe('button component factory', () => { const mediumButtonElement = createButtonElement({ size: 'md' }); const smallResult = button({ - element: smallButtonElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: smallButtonElement as any, theme: mockTheme, form: undefined, map: {}, @@ -130,7 +149,8 @@ describe('button component factory', () => { }); const mediumResult = button({ - element: mediumButtonElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: mediumButtonElement as any, theme: mockTheme, form: undefined, map: {}, @@ -145,7 +165,8 @@ describe('button component factory', () => { const buttonElement = createButtonElement({ disabled: true }); const result = button({ - element: buttonElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: buttonElement as any, theme: mockTheme, form: undefined, map: {}, @@ -163,7 +184,8 @@ describe('button component factory', () => { const buttonElement = createButtonElement({ variant: 'destructive' }); const result = button({ - element: buttonElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: buttonElement as any, theme: mockTheme, form: undefined, map: {}, @@ -193,7 +215,8 @@ describe('button component factory', () => { const buttonElement = createButtonElement({}, [complexTextComponent]); const result = button({ - element: buttonElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: buttonElement as any, theme: mockTheme, form: undefined, map: {}, @@ -231,7 +254,8 @@ describe('button component factory', () => { const buttonElement = createButtonElement({}, [sectionContainer]); const result = button({ - element: buttonElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: buttonElement as any, theme: mockTheme, form: undefined, map: {}, @@ -241,10 +265,10 @@ describe('button component factory', () => { const container = (result.children as UIComponent[])[0] as UIComponent; expect(container.element).toBe('SectionContainer'); - const sections = container.props?.sections as Array<{ + const sections = container.props?.sections as { element: string; props: Record; - }>; + }[]; expect(sections).toHaveLength(2); const textSection = sections[0]; @@ -269,7 +293,8 @@ describe('button component factory', () => { const buttonElement = createButtonElement({}, [iconComponent]); const result = button({ - element: buttonElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: buttonElement as any, theme: mockTheme, form: undefined, map: {}, @@ -296,7 +321,8 @@ describe('button component factory', () => { ]); const result = button({ - element: buttonElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: buttonElement as any, theme: mockTheme, form: undefined, map: {}, @@ -336,7 +362,8 @@ describe('button component factory', () => { ]); const result = button({ - element: buttonElement, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: buttonElement as any, theme: mockTheme, form: undefined, map: {}, From 82004b0afcd4afdc588435d9f6f5d868122813ce Mon Sep 17 00:00:00 2001 From: Daniel-Cross Date: Tue, 1 Apr 2025 09:24:12 +0200 Subject: [PATCH 22/22] changed type to interface --- app/components/Snaps/SnapUIRenderer/components/button.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/Snaps/SnapUIRenderer/components/button.test.ts b/app/components/Snaps/SnapUIRenderer/components/button.test.ts index 675726ce7d94..12c0ab8b0e62 100644 --- a/app/components/Snaps/SnapUIRenderer/components/button.test.ts +++ b/app/components/Snaps/SnapUIRenderer/components/button.test.ts @@ -7,7 +7,7 @@ import { } from '../../../../component-library/components/Texts/Text'; import { UIComponent } from './types'; -type TestButtonElement = { +interface TestButtonElement { type: string; props: { variant: string; @@ -18,7 +18,7 @@ type TestButtonElement = { }; key: string; children: (string | MockUIComponent)[]; -}; +} interface MockUIComponent { element: string;