Skip to content

Commit 6614c24

Browse files
authored
Merge pull request #760 from Iterable/MOB-12247-remove-dependency-on-react-native-vector-icons
[MOB-12247] remove-dependency-on-react-native-vector-icons
2 parents 0091a8f + c1f3dec commit 6614c24

File tree

13 files changed

+635
-116
lines changed

13 files changed

+635
-116
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ module.exports = {
4848
},
4949
},
5050
],
51+
ignorePatterns: ['coverage/**/*', 'lib/**/*', 'docs/**/*'],
5152
};

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ Iterable's React Native SDK relies on:
4343
_UI Components require additional peer dependencies_
4444
- [React Navigation 6+](https://github.com/react-navigation/react-navigation)
4545
- [React Native Safe Area Context 4+](https://github.com/th3rdwave/react-native-safe-area-context)
46-
- [React Native Vector Icons 10+](https://github.com/oblador/react-native-vector-icons)
4746
- [React Native WebView 13+](https://github.com/react-native-webview/react-native-webview)
4847

4948
- **iOS**

example/android/app/build.gradle

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,5 +117,3 @@ dependencies {
117117
implementation jscFlavor
118118
}
119119
}
120-
121-
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")

example/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"react-native-gesture-handler": "^2.24.0",
2020
"react-native-safe-area-context": "^5.1.0",
2121
"react-native-screens": "^4.9.1",
22-
"react-native-vector-icons": "^10.2.0",
2322
"react-native-webview": "^13.13.1"
2423
},
2524
"devDependencies": {

example/src/components/App/App.constants.ts

Lines changed: 15 additions & 3 deletions
Large diffs are not rendered by default.
Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
1-
import Icon from 'react-native-vector-icons/Ionicons';
1+
import { Image, View } from 'react-native';
2+
import type { Route } from '../../constants/routes';
23

3-
export const getIcon = (name: string, props: Record<string, unknown>) => (
4-
<Icon name={name} {...props} />
5-
);
4+
export const getIcon = (name: Route, props: Record<string, unknown>) => {
5+
const { color, size = 25 } = props;
6+
7+
return (
8+
<View style={{ height: size as number, width: size as number }}>
9+
<Image
10+
source={{ width: size as number, height: size as number, uri: name }}
11+
tintColor={color as string}
12+
resizeMode="contain"
13+
style={{
14+
width: size as number,
15+
height: size as number,
16+
}}
17+
fadeDuration={0}
18+
height={size as number}
19+
width={size as number}
20+
resizeMethod="scale"
21+
/>
22+
</View>
23+
);
24+
};

example/src/components/App/Main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const Main = () => {
2525
screenOptions={({ route }) => {
2626
const iconName = routeIcon[route.name];
2727
return {
28-
tabBarIcon: (props) => getIcon(iconName, props),
28+
tabBarIcon: (props) => getIcon(iconName as Route, props),
2929
tabBarActiveTintColor: colors.brandPurple,
3030
tabBarInactiveTintColor: colors.textSecondary,
3131
headerShown: false,

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module.exports = {
66
],
77
testMatch: ['<rootDir>/src/**/*.(test|spec).[jt]s?(x)'],
88
transformIgnorePatterns: [
9-
'node_modules/(?!(react-native|@react-native|@react-navigation|react-native-screens|react-native-safe-area-context|react-native-gesture-handler|react-native-webview|react-native-vector-icons)/)',
9+
'node_modules/(?!(react-native|@react-native|@react-navigation|react-native-screens|react-native-safe-area-context|react-native-gesture-handler|react-native-webview)/)',
1010
],
1111
collectCoverageFrom: [
1212
'src/**/*.{cjs,js,jsx,mjs,ts,tsx}',

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@
7777
"@testing-library/react-native": "^13.3.3",
7878
"@types/jest": "^29.5.5",
7979
"@types/react": "^19.0.0",
80-
"@types/react-native-vector-icons": "^6.4.18",
8180
"@typescript-eslint/eslint-plugin": "^8.13.0",
8281
"@typescript-eslint/parser": "^8.13.0",
8382
"commitlint": "^19.6.1",
@@ -89,13 +88,13 @@
8988
"eslint-plugin-tsdoc": "^0.3.0",
9089
"jest": "^29.7.0",
9190
"prettier": "^3.0.3",
91+
"prettier-eslint": "^16.4.2",
9292
"react": "19.0.0",
9393
"react-native": "0.79.3",
9494
"react-native-builder-bob": "^0.40.4",
9595
"react-native-gesture-handler": "^2.26.0",
9696
"react-native-safe-area-context": "^5.4.0",
9797
"react-native-screens": "^4.10.0",
98-
"react-native-vector-icons": "^10.2.0",
9998
"react-native-webview": "^13.14.1",
10099
"react-test-renderer": "19.0.0",
101100
"release-it": "^17.10.0",
@@ -113,7 +112,6 @@
113112
"react": "*",
114113
"react-native": "*",
115114
"react-native-safe-area-context": "*",
116-
"react-native-vector-icons": "*",
117115
"react-native-webview": "*"
118116
},
119117
"peerDependenciesMeta": {
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { fireEvent, render } from '@testing-library/react-native';
3+
import { PixelRatio } from 'react-native';
4+
import { HeaderBackButton, ICON_MARGIN, ICON_SIZE } from './HeaderBackButton';
5+
6+
describe('HeaderBackButton', () => {
7+
beforeEach(() => {
8+
jest.clearAllMocks();
9+
});
10+
11+
describe('Rendering', () => {
12+
it('should render without crashing', () => {
13+
const { getByTestId } = render(<HeaderBackButton testID="back-button" />);
14+
expect(getByTestId('back-button')).toBeTruthy();
15+
});
16+
17+
it('should render with default back arrow image', () => {
18+
const { UNSAFE_getByType } = render(<HeaderBackButton />);
19+
const image = UNSAFE_getByType('Image' as any);
20+
expect(image).toBeTruthy();
21+
expect(image.props.source).toMatchObject({
22+
uri: expect.stringContaining('data:image/png;base64'),
23+
width: PixelRatio.getPixelSizeForLayoutSize(ICON_SIZE),
24+
height: PixelRatio.getPixelSizeForLayoutSize(ICON_SIZE),
25+
});
26+
});
27+
28+
it('should render without label by default', () => {
29+
const { queryByText } = render(<HeaderBackButton />);
30+
expect(queryByText(/./)).toBeNull();
31+
});
32+
33+
it('should render with label when provided', () => {
34+
const label = 'Back';
35+
const { getByText } = render(<HeaderBackButton label={label} />);
36+
expect(getByText(label)).toBeTruthy();
37+
});
38+
39+
it('should render with custom label text', () => {
40+
const customLabel = 'Go Back to Home';
41+
const { getByText } = render(<HeaderBackButton label={customLabel} />);
42+
expect(getByText(customLabel)).toBeTruthy();
43+
});
44+
});
45+
46+
describe('Custom Image Props', () => {
47+
it('should render with custom imageUri', () => {
48+
const customUri = 'https://example.com/custom-back-icon.png';
49+
const { UNSAFE_getByType } = render(
50+
<HeaderBackButton imageUri={customUri} />
51+
);
52+
const image = UNSAFE_getByType('Image' as any);
53+
expect(image.props.source).toMatchObject({
54+
uri: customUri,
55+
});
56+
});
57+
58+
it('should render with custom imageSource', () => {
59+
const customSource = { uri: 'https://example.com/icon.png' };
60+
const { UNSAFE_getByType } = render(
61+
<HeaderBackButton imageSource={customSource} />
62+
);
63+
const image = UNSAFE_getByType('Image' as any);
64+
expect(image.props.source).toEqual(customSource);
65+
});
66+
67+
it('should prioritize imageSource over imageUri when both are provided', () => {
68+
const customUri = 'https://example.com/custom-back-icon.png';
69+
const customSource = { uri: 'https://example.com/icon.png' };
70+
const { UNSAFE_getByType } = render(
71+
<HeaderBackButton imageUri={customUri} imageSource={customSource} />
72+
);
73+
const image = UNSAFE_getByType('Image' as any);
74+
expect(image.props.source).toEqual(customSource);
75+
});
76+
});
77+
78+
describe('Image Properties', () => {
79+
it('should render image with correct properties', () => {
80+
const { UNSAFE_getByType } = render(<HeaderBackButton />);
81+
const image = UNSAFE_getByType('Image' as any);
82+
83+
expect(image.props.resizeMode).toBe('contain');
84+
expect(image.props.fadeDuration).toBe(0);
85+
expect(image.props.height).toBe(ICON_SIZE);
86+
expect(image.props.width).toBe(ICON_SIZE);
87+
expect(image.props.resizeMethod).toBe('scale');
88+
expect(image.props.tintColor).toBeTruthy();
89+
});
90+
91+
it('should apply correct style to image', () => {
92+
const { UNSAFE_getByType } = render(<HeaderBackButton />);
93+
const image = UNSAFE_getByType('Image' as any);
94+
95+
expect(image.props.style).toMatchObject({
96+
height: ICON_SIZE,
97+
margin: ICON_MARGIN,
98+
width: ICON_SIZE,
99+
});
100+
});
101+
});
102+
103+
describe('Touch Interaction', () => {
104+
it('should call onPress when button is pressed', () => {
105+
const onPressMock = jest.fn();
106+
const { getByTestId } = render(
107+
<HeaderBackButton testID="back-button" onPress={onPressMock} />
108+
);
109+
110+
fireEvent.press(getByTestId('back-button'));
111+
expect(onPressMock).toHaveBeenCalledTimes(1);
112+
});
113+
114+
it('should call onPressIn when touch starts', () => {
115+
const onPressInMock = jest.fn();
116+
const { getByTestId } = render(
117+
<HeaderBackButton testID="back-button" onPressIn={onPressInMock} />
118+
);
119+
120+
fireEvent(getByTestId('back-button'), 'pressIn');
121+
expect(onPressInMock).toHaveBeenCalledTimes(1);
122+
});
123+
124+
it('should call onPressOut when touch ends', () => {
125+
const onPressOutMock = jest.fn();
126+
const { getByTestId } = render(
127+
<HeaderBackButton testID="back-button" onPressOut={onPressOutMock} />
128+
);
129+
130+
fireEvent(getByTestId('back-button'), 'pressOut');
131+
expect(onPressOutMock).toHaveBeenCalledTimes(1);
132+
});
133+
134+
it('should not trigger onPress when disabled', () => {
135+
const onPressMock = jest.fn();
136+
const { getByTestId } = render(
137+
<HeaderBackButton
138+
testID="back-button"
139+
onPress={onPressMock}
140+
disabled={true}
141+
/>
142+
);
143+
144+
fireEvent.press(getByTestId('back-button'));
145+
expect(onPressMock).not.toHaveBeenCalled();
146+
});
147+
});
148+
149+
describe('Platform-specific behavior', () => {
150+
it('should export correct icon size constant', () => {
151+
// ICON_SIZE is evaluated at module load time based on Platform.OS
152+
expect(ICON_SIZE).toBeDefined();
153+
expect([21, 24]).toContain(ICON_SIZE);
154+
});
155+
156+
it('should export correct icon margin constant', () => {
157+
// ICON_MARGIN is evaluated at module load time based on Platform.OS
158+
expect(ICON_MARGIN).toBeDefined();
159+
expect([3, 8]).toContain(ICON_MARGIN);
160+
});
161+
162+
it('should use consistent icon size in image props', () => {
163+
const { UNSAFE_getByType } = render(<HeaderBackButton />);
164+
const image = UNSAFE_getByType('Image' as any);
165+
166+
expect(image.props.height).toBe(ICON_SIZE);
167+
expect(image.props.width).toBe(ICON_SIZE);
168+
});
169+
});
170+
171+
describe('Accessibility', () => {
172+
it('should accept accessibility props', () => {
173+
const { getByTestId } = render(
174+
<HeaderBackButton
175+
testID="back-button"
176+
accessible={true}
177+
accessibilityLabel="Navigate back"
178+
accessibilityHint="Returns to previous screen"
179+
/>
180+
);
181+
182+
const button = getByTestId('back-button');
183+
expect(button.props.accessible).toBe(true);
184+
expect(button.props.accessibilityLabel).toBe('Navigate back');
185+
expect(button.props.accessibilityHint).toBe('Returns to previous screen');
186+
});
187+
});
188+
189+
describe('Component Structure', () => {
190+
it('should render View with correct flex direction', () => {
191+
const { UNSAFE_getAllByType } = render(<HeaderBackButton label="Back" />);
192+
const views = UNSAFE_getAllByType('View' as any);
193+
194+
// Find the view with returnButton style
195+
const returnButtonView = views.find(
196+
(view) =>
197+
view.props.style?.flexDirection === 'row' &&
198+
view.props.style?.alignItems === 'center'
199+
);
200+
expect(returnButtonView).toBeTruthy();
201+
});
202+
203+
it('should render label text with correct style when provided', () => {
204+
const { getByText } = render(<HeaderBackButton label="Back" />);
205+
const labelElement = getByText('Back');
206+
207+
expect(labelElement.props.style).toMatchObject({
208+
fontSize: 20,
209+
});
210+
});
211+
});
212+
213+
describe('Edge Cases', () => {
214+
it('should handle empty string label', () => {
215+
const { queryByText } = render(<HeaderBackButton label="" />);
216+
// Empty string should not render text
217+
expect(queryByText('')).toBeNull();
218+
});
219+
220+
it('should handle multiple props correctly', () => {
221+
const onPressMock = jest.fn();
222+
const label = 'Custom Back';
223+
const customUri = 'https://example.com/icon.png';
224+
225+
const { getByText, getByTestId } = render(
226+
<HeaderBackButton
227+
testID="back-button"
228+
label={label}
229+
imageUri={customUri}
230+
onPress={onPressMock}
231+
accessible={true}
232+
accessibilityLabel="Go back"
233+
/>
234+
);
235+
236+
expect(getByText(label)).toBeTruthy();
237+
expect(getByTestId('back-button').props.accessible).toBe(true);
238+
239+
fireEvent.press(getByTestId('back-button'));
240+
expect(onPressMock).toHaveBeenCalledTimes(1);
241+
});
242+
});
243+
});

0 commit comments

Comments
 (0)