Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ module.exports = {
},
},
],
ignorePatterns: ['coverage/**/*', 'lib/**/*', 'docs/**/*'],
};
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ Iterable's React Native SDK relies on:
_UI Components require additional peer dependencies_
- [React Navigation 6+](https://github.com/react-navigation/react-navigation)
- [React Native Safe Area Context 4+](https://github.com/th3rdwave/react-native-safe-area-context)
- [React Native Vector Icons 10+](https://github.com/oblador/react-native-vector-icons)
- [React Native WebView 13+](https://github.com/react-native-webview/react-native-webview)

- **iOS**
Expand Down
2 changes: 0 additions & 2 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,3 @@ dependencies {
implementation jscFlavor
}
}

apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")
1 change: 0 additions & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
"react-native-gesture-handler": "^2.24.0",
"react-native-safe-area-context": "^5.1.0",
"react-native-screens": "^4.9.1",
"react-native-vector-icons": "^10.2.0",
"react-native-webview": "^13.13.1"
},
"devDependencies": {
Expand Down
18 changes: 15 additions & 3 deletions example/src/components/App/App.constants.ts

Large diffs are not rendered by default.

27 changes: 23 additions & 4 deletions example/src/components/App/App.utils.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import Icon from 'react-native-vector-icons/Ionicons';
import { Image, View } from 'react-native';
import type { Route } from '../../constants/routes';

export const getIcon = (name: string, props: Record<string, unknown>) => (
<Icon name={name} {...props} />
);
export const getIcon = (name: Route, props: Record<string, unknown>) => {
const { color, size = 25 } = props;

return (
<View style={{ height: size as number, width: size as number }}>
<Image
source={{ width: size as number, height: size as number, uri: name }}
tintColor={color as string}
resizeMode="contain"
style={{
width: size as number,
height: size as number,
}}
fadeDuration={0}
height={size as number}
width={size as number}
resizeMethod="scale"
/>
</View>
);
};
2 changes: 1 addition & 1 deletion example/src/components/App/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const Main = () => {
screenOptions={({ route }) => {
const iconName = routeIcon[route.name];
return {
tabBarIcon: (props) => getIcon(iconName, props),
tabBarIcon: (props) => getIcon(iconName as Route, props),
tabBarActiveTintColor: colors.brandPurple,
tabBarInactiveTintColor: colors.textSecondary,
headerShown: false,
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = {
],
testMatch: ['<rootDir>/src/**/*.(test|spec).[jt]s?(x)'],
transformIgnorePatterns: [
'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)/)',
'node_modules/(?!(react-native|@react-native|@react-navigation|react-native-screens|react-native-safe-area-context|react-native-gesture-handler|react-native-webview)/)',
],
collectCoverageFrom: [
'src/**/*.{cjs,js,jsx,mjs,ts,tsx}',
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@
"@testing-library/react-native": "^13.3.3",
"@types/jest": "^29.5.5",
"@types/react": "^19.0.0",
"@types/react-native-vector-icons": "^6.4.18",
"@typescript-eslint/eslint-plugin": "^8.13.0",
"@typescript-eslint/parser": "^8.13.0",
"commitlint": "^19.6.1",
Expand All @@ -89,13 +88,13 @@
"eslint-plugin-tsdoc": "^0.3.0",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"prettier-eslint": "^16.4.2",
"react": "19.0.0",
"react-native": "0.79.3",
"react-native-builder-bob": "^0.40.4",
"react-native-gesture-handler": "^2.26.0",
"react-native-safe-area-context": "^5.4.0",
"react-native-screens": "^4.10.0",
"react-native-vector-icons": "^10.2.0",
"react-native-webview": "^13.14.1",
"react-test-renderer": "19.0.0",
"release-it": "^17.10.0",
Expand All @@ -113,7 +112,6 @@
"react": "*",
"react-native": "*",
"react-native-safe-area-context": "*",
"react-native-vector-icons": "*",
"react-native-webview": "*"
},
"peerDependenciesMeta": {
Expand Down
243 changes: 243 additions & 0 deletions src/inbox/components/HeaderBackButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { fireEvent, render } from '@testing-library/react-native';
import { PixelRatio } from 'react-native';
import { HeaderBackButton, ICON_MARGIN, ICON_SIZE } from './HeaderBackButton';

describe('HeaderBackButton', () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe('Rendering', () => {
it('should render without crashing', () => {
const { getByTestId } = render(<HeaderBackButton testID="back-button" />);
expect(getByTestId('back-button')).toBeTruthy();
});

it('should render with default back arrow image', () => {
const { UNSAFE_getByType } = render(<HeaderBackButton />);
const image = UNSAFE_getByType('Image' as any);
expect(image).toBeTruthy();
expect(image.props.source).toMatchObject({
uri: expect.stringContaining('data:image/png;base64'),
width: PixelRatio.getPixelSizeForLayoutSize(ICON_SIZE),
height: PixelRatio.getPixelSizeForLayoutSize(ICON_SIZE),
});
});

it('should render without label by default', () => {
const { queryByText } = render(<HeaderBackButton />);
expect(queryByText(/./)).toBeNull();
});

it('should render with label when provided', () => {
const label = 'Back';
const { getByText } = render(<HeaderBackButton label={label} />);
expect(getByText(label)).toBeTruthy();
});

it('should render with custom label text', () => {
const customLabel = 'Go Back to Home';
const { getByText } = render(<HeaderBackButton label={customLabel} />);
expect(getByText(customLabel)).toBeTruthy();
});
});

describe('Custom Image Props', () => {
it('should render with custom imageUri', () => {
const customUri = 'https://example.com/custom-back-icon.png';
const { UNSAFE_getByType } = render(
<HeaderBackButton imageUri={customUri} />
);
const image = UNSAFE_getByType('Image' as any);
expect(image.props.source).toMatchObject({
uri: customUri,
});
});

it('should render with custom imageSource', () => {
const customSource = { uri: 'https://example.com/icon.png' };
const { UNSAFE_getByType } = render(
<HeaderBackButton imageSource={customSource} />
);
const image = UNSAFE_getByType('Image' as any);
expect(image.props.source).toEqual(customSource);
});

it('should prioritize imageSource over imageUri when both are provided', () => {
const customUri = 'https://example.com/custom-back-icon.png';
const customSource = { uri: 'https://example.com/icon.png' };
const { UNSAFE_getByType } = render(
<HeaderBackButton imageUri={customUri} imageSource={customSource} />
);
const image = UNSAFE_getByType('Image' as any);
expect(image.props.source).toEqual(customSource);
});
});

describe('Image Properties', () => {
it('should render image with correct properties', () => {
const { UNSAFE_getByType } = render(<HeaderBackButton />);
const image = UNSAFE_getByType('Image' as any);

expect(image.props.resizeMode).toBe('contain');
expect(image.props.fadeDuration).toBe(0);
expect(image.props.height).toBe(ICON_SIZE);
expect(image.props.width).toBe(ICON_SIZE);
expect(image.props.resizeMethod).toBe('scale');
expect(image.props.tintColor).toBeTruthy();
});

it('should apply correct style to image', () => {
const { UNSAFE_getByType } = render(<HeaderBackButton />);
const image = UNSAFE_getByType('Image' as any);

expect(image.props.style).toMatchObject({
height: ICON_SIZE,
margin: ICON_MARGIN,
width: ICON_SIZE,
});
});
});

describe('Touch Interaction', () => {
it('should call onPress when button is pressed', () => {
const onPressMock = jest.fn();
const { getByTestId } = render(
<HeaderBackButton testID="back-button" onPress={onPressMock} />
);

fireEvent.press(getByTestId('back-button'));
expect(onPressMock).toHaveBeenCalledTimes(1);
});

it('should call onPressIn when touch starts', () => {
const onPressInMock = jest.fn();
const { getByTestId } = render(
<HeaderBackButton testID="back-button" onPressIn={onPressInMock} />
);

fireEvent(getByTestId('back-button'), 'pressIn');
expect(onPressInMock).toHaveBeenCalledTimes(1);
});

it('should call onPressOut when touch ends', () => {
const onPressOutMock = jest.fn();
const { getByTestId } = render(
<HeaderBackButton testID="back-button" onPressOut={onPressOutMock} />
);

fireEvent(getByTestId('back-button'), 'pressOut');
expect(onPressOutMock).toHaveBeenCalledTimes(1);
});

it('should not trigger onPress when disabled', () => {
const onPressMock = jest.fn();
const { getByTestId } = render(
<HeaderBackButton
testID="back-button"
onPress={onPressMock}
disabled={true}
/>
);

fireEvent.press(getByTestId('back-button'));
expect(onPressMock).not.toHaveBeenCalled();
});
});

describe('Platform-specific behavior', () => {
it('should export correct icon size constant', () => {
// ICON_SIZE is evaluated at module load time based on Platform.OS
expect(ICON_SIZE).toBeDefined();
expect([21, 24]).toContain(ICON_SIZE);
});

it('should export correct icon margin constant', () => {
// ICON_MARGIN is evaluated at module load time based on Platform.OS
expect(ICON_MARGIN).toBeDefined();
expect([3, 8]).toContain(ICON_MARGIN);
});

it('should use consistent icon size in image props', () => {
const { UNSAFE_getByType } = render(<HeaderBackButton />);
const image = UNSAFE_getByType('Image' as any);

expect(image.props.height).toBe(ICON_SIZE);
expect(image.props.width).toBe(ICON_SIZE);
});
});

describe('Accessibility', () => {
it('should accept accessibility props', () => {
const { getByTestId } = render(
<HeaderBackButton
testID="back-button"
accessible={true}
accessibilityLabel="Navigate back"
accessibilityHint="Returns to previous screen"
/>
);

const button = getByTestId('back-button');
expect(button.props.accessible).toBe(true);
expect(button.props.accessibilityLabel).toBe('Navigate back');
expect(button.props.accessibilityHint).toBe('Returns to previous screen');
});
});

describe('Component Structure', () => {
it('should render View with correct flex direction', () => {
const { UNSAFE_getAllByType } = render(<HeaderBackButton label="Back" />);
const views = UNSAFE_getAllByType('View' as any);

// Find the view with returnButton style
const returnButtonView = views.find(
(view) =>
view.props.style?.flexDirection === 'row' &&
view.props.style?.alignItems === 'center'
);
expect(returnButtonView).toBeTruthy();
});

it('should render label text with correct style when provided', () => {
const { getByText } = render(<HeaderBackButton label="Back" />);
const labelElement = getByText('Back');

expect(labelElement.props.style).toMatchObject({
fontSize: 20,
});
});
});

describe('Edge Cases', () => {
it('should handle empty string label', () => {
const { queryByText } = render(<HeaderBackButton label="" />);
// Empty string should not render text
expect(queryByText('')).toBeNull();
});

it('should handle multiple props correctly', () => {
const onPressMock = jest.fn();
const label = 'Custom Back';
const customUri = 'https://example.com/icon.png';

const { getByText, getByTestId } = render(
<HeaderBackButton
testID="back-button"
label={label}
imageUri={customUri}
onPress={onPressMock}
accessible={true}
accessibilityLabel="Go back"
/>
);

expect(getByText(label)).toBeTruthy();
expect(getByTestId('back-button').props.accessible).toBe(true);

fireEvent.press(getByTestId('back-button'));
expect(onPressMock).toHaveBeenCalledTimes(1);
});
});
});
Loading