Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions app/components/UI/BrowserUrlBar/BrowserUrlBar.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,25 @@ const styleSheet = ({
textInputWrapper: {
flex: 1,
},
hidden: {
position: 'absolute',
opacity: 0,
},
textInput: {
flex: 1,
height: 44,
paddingVertical: 0,
margin: 0,
paddingLeft: isUrlBarFocused ? 16 : 0,
...fontStyles.normal,
fontSize: Device.isAndroid() ? 16 : 14,
color: colors.text.default,
},
urlBarText: {
...fontStyles.normal,
fontSize: Device.isAndroid() ? 16 : 14,
color: colors.text.default,
position: isUrlBarFocused ? 'absolute' : 'relative',
opacity: isUrlBarFocused ? 0 : 1,
},
browserUrlBarWrapper: {
flexDirection: 'row',
alignItems: 'center',
Expand Down
217 changes: 206 additions & 11 deletions app/components/UI/BrowserUrlBar/BrowserUrlBar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React from 'react';
import React, { createRef } from 'react';
import renderWithProvider from '../../../util/test/renderWithProvider';
import BrowserUrlBar from './BrowserUrlBar';
import { ConnectionType } from './BrowserUrlBar.types';
import { ConnectionType, BrowserUrlBarRef } from './BrowserUrlBar.types';
import { useMetrics } from '../../../components/hooks/useMetrics';
import { backgroundState } from '../../../util/test/initial-root-state';
import { selectAccountsLength } from '../../../selectors/accountTrackerController';
import {
selectNetworkConfigurations,
selectProviderConfig,
} from '../../../selectors/networkController';
import { fireEvent } from '@testing-library/react-native';
import { fireEvent, act } from '@testing-library/react-native';
import { BrowserURLBarSelectorsIDs } from '../../../../e2e/selectors/Browser/BrowserURLBar.selectors';
import { AccountOverviewSelectorsIDs } from '../../../../e2e/selectors/Browser/AccountOverview.selectors';
import Routes from '../../../constants/navigation/Routes';
Expand Down Expand Up @@ -95,14 +95,14 @@ describe('BrowserUrlBar', () => {
});
});

it('should render correctly', () => {
it('render matches snapshot when focused', () => {
const { toJSON } = renderWithProvider(<BrowserUrlBar {...defaultProps} />, {
state: mockInitialState,
});
expect(toJSON()).toMatchSnapshot();
});

it('should render correctly when url bar is not focused', () => {
it('render matches snapshot when not focused', () => {
const { toJSON } = renderWithProvider(
<BrowserUrlBar {...propsWithoutUrlBarFocused} />,
{
Expand All @@ -112,7 +112,7 @@ describe('BrowserUrlBar', () => {
expect(toJSON()).toMatchSnapshot();
});

it('should handle text input changes', () => {
it('calls onChangeText when text input changes', () => {
const { getByTestId } = renderWithProvider(
<BrowserUrlBar {...defaultProps} />,
{
Expand All @@ -126,7 +126,7 @@ describe('BrowserUrlBar', () => {
expect(defaultProps.onChangeText).toHaveBeenCalledWith('test.com');
});

it('should handle submit editing', () => {
it('trims whitespace and calls onSubmitEditing when text is submitted', () => {
const { getByTestId } = renderWithProvider(
<BrowserUrlBar {...defaultProps} />,
{
Expand All @@ -142,7 +142,7 @@ describe('BrowserUrlBar', () => {
expect(defaultProps.onSubmitEditing).toHaveBeenCalledWith('test.com');
});

it('should handle clear input button press', () => {
it('clears input and calls onChangeText when clear button is pressed', () => {
const { getByTestId } = renderWithProvider(
<BrowserUrlBar {...defaultProps} />,
{
Expand All @@ -156,7 +156,7 @@ describe('BrowserUrlBar', () => {
expect(defaultProps.onChangeText).toHaveBeenCalledWith('');
});

it('should handle cancel button press', () => {
it('calls onCancel and sets focus state to false when cancel button is pressed', () => {
const { getByTestId } = renderWithProvider(
<BrowserUrlBar {...defaultProps} />,
{
Expand All @@ -173,7 +173,7 @@ describe('BrowserUrlBar', () => {
expect(defaultProps.setIsUrlBarFocused).toHaveBeenCalledWith(false);
});

it('should handle account right button press', () => {
it('tracks analytics events and navigates to account permissions when account button is pressed', () => {
const { getByTestId } = renderWithProvider(
<BrowserUrlBar {...propsWithoutUrlBarFocused} />,
{ state: mockInitialState },
Expand All @@ -197,7 +197,7 @@ describe('BrowserUrlBar', () => {
});
});

it('should handle focus and blur events', () => {
it('updates focus state and calls focus/blur callbacks when input receives focus events', () => {
const { getByTestId } = renderWithProvider(
<BrowserUrlBar {...defaultProps} />,
{
Expand All @@ -215,4 +215,199 @@ describe('BrowserUrlBar', () => {
expect(defaultProps.setIsUrlBarFocused).toHaveBeenCalledWith(false);
expect(defaultProps.onBlur).toHaveBeenCalled();
});

describe('useImperativeHandle methods', () => {
let urlBarRef: React.RefObject<BrowserUrlBarRef>;

beforeEach(() => {
// Arrange - Create ref for each test
urlBarRef = createRef<BrowserUrlBarRef>();
jest.clearAllMocks();
(useMetrics as jest.Mock).mockReturnValue({
trackEvent: mockTrackEvent,
createEventBuilder: mockCreateEventBuilder,
});

mockUseSelector.mockImplementation((selector) => {
if (selector === selectAccountsLength) return 1;
if (selector === selectNetworkConfigurations) return {};
if (selector === selectProviderConfig) return { chainId: '0x1' };
return null;
});
});

describe('hide method', () => {
it('calls onCancel prop when hide is invoked', () => {
// Arrange
const onCancelMock = jest.fn();
const props = { ...defaultProps, onCancel: onCancelMock };

renderWithProvider(<BrowserUrlBar {...props} ref={urlBarRef} />, {
state: mockInitialState,
});

// Act
act(() => {
urlBarRef.current?.hide();
});

// Assert
expect(onCancelMock).toHaveBeenCalledTimes(1);
});

it('sets URL bar focused state to false when hide is invoked', () => {
// Arrange
const setIsUrlBarFocusedMock = jest.fn();
const props = {
...defaultProps,
setIsUrlBarFocused: setIsUrlBarFocusedMock,
isUrlBarFocused: true,
};

renderWithProvider(<BrowserUrlBar {...props} ref={urlBarRef} />, {
state: mockInitialState,
});

// Act
act(() => {
urlBarRef.current?.hide();
});

// Assert
expect(setIsUrlBarFocusedMock).toHaveBeenCalledWith(false);
});
});

describe('focus method', () => {
it('exposes focus method through imperative handle', () => {
// Arrange
renderWithProvider(
<BrowserUrlBar {...defaultProps} ref={urlBarRef} />,
{ state: mockInitialState },
);

// Act & Assert - Should not throw errors and should be callable
expect(() => urlBarRef.current?.focus()).not.toThrow();
expect(typeof urlBarRef.current?.focus).toBe('function');
});
});

describe('blur method', () => {
it('exposes blur method through imperative handle', () => {
// Arrange
renderWithProvider(
<BrowserUrlBar {...defaultProps} ref={urlBarRef} />,
{ state: mockInitialState },
);

// Act & Assert - Should not throw errors and should be callable
expect(() => urlBarRef.current?.blur()).not.toThrow();
expect(typeof urlBarRef.current?.blur).toBe('function');
});
});

describe('setNativeProps method', () => {
it('delegates to underlying TextInput setNativeProps when called', () => {
// Arrange
renderWithProvider(
<BrowserUrlBar {...propsWithoutUrlBarFocused} ref={urlBarRef} />,
{ state: mockInitialState },
);

// Act & Assert - Should not throw errors when calling setNativeProps
expect(() => {
act(() => {
urlBarRef.current?.setNativeProps({ text: 'newsite.com' });
});
}).not.toThrow();

// Additional test: setNativeProps should be a function
expect(typeof urlBarRef.current?.setNativeProps).toBe('function');
});

it('exposes setNativeProps method through imperative handle', () => {
// Arrange
renderWithProvider(
<BrowserUrlBar {...defaultProps} ref={urlBarRef} />,
{ state: mockInitialState },
);
const testProps = { text: 'test.com', placeholder: 'test placeholder' };

// Act & Assert - Should not throw errors and should be callable
expect(() => {
act(() => {
urlBarRef.current?.setNativeProps(testProps);
});
}).not.toThrow();
expect(typeof urlBarRef.current?.setNativeProps).toBe('function');
});

it('handles non-string text values without throwing errors', () => {
// Arrange
renderWithProvider(
<BrowserUrlBar {...propsWithoutUrlBarFocused} ref={urlBarRef} />,
{ state: mockInitialState },
);

// Act & Assert - Should not throw errors when passing non-string text
expect(() => {
act(() => {
urlBarRef.current?.setNativeProps({ text: 123 as number });
});
}).not.toThrow();
});

it('handles calls without text property without throwing errors', () => {
// Arrange
renderWithProvider(
<BrowserUrlBar {...propsWithoutUrlBarFocused} ref={urlBarRef} />,
{ state: mockInitialState },
);

// Act & Assert - Should not throw errors when called without text property
expect(() => {
act(() => {
urlBarRef.current?.setNativeProps({ placeholder: 'test' });
});
}).not.toThrow();
});
});

describe('ref interface completeness', () => {
it('exposes all required imperative methods when ref is attached', () => {
// Arrange & Act
renderWithProvider(
<BrowserUrlBar {...defaultProps} ref={urlBarRef} />,
{ state: mockInitialState },
);

// Assert
expect(urlBarRef.current).toBeTruthy();
expect(typeof urlBarRef.current?.hide).toBe('function');
expect(typeof urlBarRef.current?.focus).toBe('function');
expect(typeof urlBarRef.current?.blur).toBe('function');
expect(typeof urlBarRef.current?.setNativeProps).toBe('function');
});

it('handles method calls gracefully without throwing errors', () => {
// Arrange
renderWithProvider(
<BrowserUrlBar {...defaultProps} ref={urlBarRef} />,
{ state: mockInitialState },
);

// Act & Assert - Should not throw errors
expect(() => {
urlBarRef.current?.focus();
urlBarRef.current?.blur();
act(() => {
urlBarRef.current?.setNativeProps({ text: 'test' });
});
act(() => {
urlBarRef.current?.hide();
});
}).not.toThrow();
});
});
});
});
22 changes: 19 additions & 3 deletions app/components/UI/BrowserUrlBar/BrowserUrlBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
TextInput,
TextInputSubmitEditingEventData,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native';
import { useStyles } from '../../../component-library/hooks';
Expand Down Expand Up @@ -82,8 +83,12 @@ const BrowserUrlBar = forwardRef<BrowserUrlBarRef, BrowserUrlBarProps>(
hide: () => onCancelInput(),
blur: () => inputRef?.current?.blur(),
focus: () => inputRef?.current?.focus(),
setNativeProps: (props: object) =>
inputRef?.current?.setNativeProps(props),
setNativeProps: (props: { text?: string }) => {
if (props.text) {
inputValueRef.current = props.text;
}
inputRef?.current?.setNativeProps(props);
},
}));

/**
Expand Down Expand Up @@ -201,12 +206,23 @@ const BrowserUrlBar = forwardRef<BrowserUrlBarRef, BrowserUrlBarProps>(
returnKeyType={'go'}
selectTextOnFocus
keyboardAppearance={themeAppearance}
style={styles.textInput}
style={[styles.textInput, !isUrlBarFocused && styles.hidden]}
onChangeText={onChangeTextInput}
onSubmitEditing={onSubmitEditingInput}
onBlur={onBlurInput}
onFocus={onFocusInput}
/>
<TouchableWithoutFeedback
onPress={() => inputRef?.current?.focus()}
>
<Text
style={styles.urlBarText}
numberOfLines={1}
ellipsizeMode="head"
>
{inputValueRef.current || activeUrl}
</Text>
</TouchableWithoutFeedback>
</View>
{isUrlBarFocused ? (
<ButtonIcon
Expand Down
Loading
Loading