Skip to content

Commit

Permalink
Enhance Testing and Refactor RedirectWithAnimation for Improved Testa…
Browse files Browse the repository at this point in the history
…bility (#7)

* test: enhance unit tests for Button, FlatList, and add BaseScreen.test

* feat: enhance RedirectWithAnimation template for better testability

- Extracted BackHandler logic to a custom hook: useBackHandler

* test: add unit test for RedirectWithAnimation template

* test: improve unit tests for Input molecule

- Added edge case coverage for `value` prop
  • Loading branch information
abdullahbayram authored Dec 25, 2024
1 parent 39b7a18 commit 42d613c
Show file tree
Hide file tree
Showing 19 changed files with 733 additions and 82 deletions.
1 change: 1 addition & 0 deletions __tests__/__snapshots__/App.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ exports[`<App /> Text renders correctly on App 1`] = `
"paddingTop": 10,
}
}
testID="base-screen"
>
<Text
style={
Expand Down
9 changes: 9 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ module.exports = {
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
coverageThreshold: { global: { branches: 50, functions: 65, lines: 70, statements: 70 } },
coveragePathIgnorePatterns: [
'/node_modules/',
'src/components/atoms/index.js',
'src/components/molecules/index.js',
'src/components/organisms/index.js',
'src/components/templates/index.js',
'src/screens/index.js',
'wdyr.js',
],
testPathIgnorePatterns: ['/__tests__/mocks/', '/__tests__/utils/'],
moduleNameMapper: {
'^@components/(.*)$': '<rootDir>/src/components/$1',
Expand Down
68 changes: 39 additions & 29 deletions src/components/atoms/Button/Button.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@ import Button from '.';
const BUTTON_TEXT_CLICK_ME = 'Click Me';
const BUTTON_TEXT_PRESS_ME = 'Press Me';

beforeEach(() => {
jest.clearAllMocks(); // Clears all mocks in the test suite
});

const mockOnPress = jest.fn();

const renderButton = (onPress, mode, children, icon, disabled) => {
const renderButton = ({ onPress = () => {}, mode, children, icon, disabled }) => {
return render(
<Button onPress={onPress} icon={icon} mode={mode} disabled={disabled}>
{children}
Expand All @@ -20,46 +16,60 @@ const renderButton = (onPress, mode, children, icon, disabled) => {
};

describe('<Button />', () => {
it('matches the snapshot', () => {
const { toJSON } = renderButton(() => {}, 'outlined', BUTTON_TEXT_CLICK_ME, 'camera');
expect(toJSON()).toMatchSnapshot();
afterEach(() => {
jest.clearAllMocks(); // Ensures mocks are cleared after each test
});
it('matches the snapshot(defaultProps)', () => {
const { toJSON } = renderButton(() => {}, undefined, BUTTON_TEXT_CLICK_ME);

it('matches the snapshot with custom props', () => {
const { toJSON } = renderButton({
onPress: mockOnPress,
mode: 'outlined',
children: BUTTON_TEXT_CLICK_ME,
icon: 'camera',
});
expect(toJSON()).toMatchSnapshot();
});
it('renders correctly with children and props', () => {
renderButton(() => {}, 'outlined', BUTTON_TEXT_CLICK_ME, 'camera');
expect(screen.getByText(BUTTON_TEXT_CLICK_ME)).toBeTruthy();
});

it('renders correctly with default props', () => {
renderButton(mockOnPress, undefined, BUTTON_TEXT_PRESS_ME);
const button = screen.getByText(BUTTON_TEXT_PRESS_ME);
expect(button).toBeTruthy();
fireEvent.press(button);
expect(mockOnPress).toHaveBeenCalledTimes(1);
it('matches the snapshot with default props', () => {
const { toJSON } = renderButton({
onPress: mockOnPress,
children: BUTTON_TEXT_CLICK_ME,
});
expect(toJSON()).toMatchSnapshot();
});

it('calls onPress when the button is pressed', () => {
renderButton(mockOnPress, undefined, BUTTON_TEXT_PRESS_ME);
const button = screen.getByText(BUTTON_TEXT_PRESS_ME);
fireEvent.press(button);
expect(mockOnPress).toHaveBeenCalledTimes(1);
it('renders button text correctly', () => {
renderButton({ onPress: mockOnPress, children: BUTTON_TEXT_CLICK_ME });
expect(screen.getByText(BUTTON_TEXT_CLICK_ME)).toBeTruthy();
});

it('renders correctly with a custom icon and mode', () => {
renderButton(mockOnPress, 'outlined', BUTTON_TEXT_PRESS_ME, 'camera');
it('calls onPress when the button is pressed', () => {
renderButton({ onPress: mockOnPress, children: BUTTON_TEXT_PRESS_ME });
const button = screen.getByText(BUTTON_TEXT_PRESS_ME);
expect(button).toBeTruthy();
fireEvent.press(button);
expect(mockOnPress).toHaveBeenCalledTimes(1);
});

it('does not call onPress when the button is disabled', () => {
renderButton(mockOnPress, undefined, BUTTON_TEXT_PRESS_ME, 'camera', true);
renderButton({
onPress: mockOnPress,
children: BUTTON_TEXT_PRESS_ME,
disabled: true,
});
const button = screen.getByText(BUTTON_TEXT_PRESS_ME);
fireEvent.press(button);
expect(mockOnPress).not.toHaveBeenCalled();
});

it('renders with a custom icon and mode', () => {
renderButton({
onPress: mockOnPress,
mode: 'outlined',
children: BUTTON_TEXT_PRESS_ME,
icon: 'camera',
});
expect(screen.getByText(BUTTON_TEXT_PRESS_ME)).toBeTruthy();
fireEvent.press(screen.getByText(BUTTON_TEXT_PRESS_ME));
expect(mockOnPress).toHaveBeenCalledTimes(1);
});
});
4 changes: 2 additions & 2 deletions src/components/atoms/Button/__snapshots__/Button.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Button /> matches the snapshot 1`] = `
exports[`<Button /> matches the snapshot with custom props 1`] = `
<View
collapsable={false}
style={
Expand Down Expand Up @@ -169,7 +169,7 @@ exports[`<Button /> matches the snapshot 1`] = `
</View>
`;

exports[`<Button /> matches the snapshot(defaultProps) 1`] = `
exports[`<Button /> matches the snapshot with default props 1`] = `
<View
collapsable={false}
style={
Expand Down
40 changes: 33 additions & 7 deletions src/components/atoms/FlatList/FlatList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { render, screen } from '@testing-library/react-native';
import FlatList from '.';
import styles from './FlatList.style';
import Text from '../Text';

jest.mock('./FlatList.style', () => ({
flatList: {
Expand All @@ -10,11 +11,19 @@ jest.mock('./FlatList.style', () => ({
}));

describe('FlatList Component', () => {
it('should render correctly with default props', () => {
render(<FlatList testID="custom-flatlist" data={[]} renderItem={() => null} />);
const mockData = [
{ id: '1', title: 'Test Item' },
{ id: '2', title: 'Test Item 2' },
];

it('renders correctly with default props (snapshot)', () => {
const { toJSON } = render(<FlatList testID="custom-flatlist" data={[]} renderItem={() => null} />);
expect(toJSON()).toMatchSnapshot();
});

it('renders correctly with default props', () => {
render(<FlatList testID="custom-flatlist" data={[]} renderItem={() => null} />);
const flatList = screen.getByTestId('custom-flatlist');

expect(flatList).toBeTruthy();
expect(flatList.props.style).toEqual(styles.flatList);
expect(flatList.props.initialNumToRender).toBe(10);
Expand All @@ -23,15 +32,32 @@ describe('FlatList Component', () => {
expect(flatList.props.removeClippedSubviews).toBe(true);
});

it('should pass additional props to the FlatList', () => {
const mockData = [{ id: '1', title: 'Test Item' }];
const mockRenderItem = jest.fn();
it('passes additional props to the FlatList', () => {
const mockRenderItem = jest.fn(({ item }) => <Text>{item.title}</Text>);

render(<FlatList testID="custom-flatlist" data={mockData} renderItem={mockRenderItem} horizontal />);

const flatList = screen.getByTestId('custom-flatlist');

expect(flatList.props.data).toEqual(mockData);
expect(flatList.props.horizontal).toBe(true);
});

it('renders data items correctly', () => {
const mockRenderItem = jest.fn(({ item }) => <Text>{item.title}</Text>);

render(<FlatList testID="custom-flatlist" data={mockData} renderItem={mockRenderItem} />);

const flatList = screen.getByTestId('custom-flatlist');
expect(flatList).toBeTruthy();

expect(mockRenderItem).toHaveBeenCalledTimes(mockData.length);

expect(screen.getByText('Test Item')).toBeTruthy();
});

it('handles an empty data array gracefully', () => {
render(<FlatList testID="custom-flatlist" data={[]} renderItem={() => null} />);
const flatList = screen.getByTestId('custom-flatlist');
expect(flatList.props.data).toEqual([]);
});
});
33 changes: 33 additions & 0 deletions src/components/atoms/FlatList/__snapshots__/FlatList.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`FlatList Component renders correctly with default props (snapshot) 1`] = `
<RCTScrollView
data={[]}
getItem={[Function]}
getItemCount={[Function]}
initialNumToRender={10}
keyExtractor={[Function]}
maxToRenderPerBatch={5}
onContentSizeChange={[Function]}
onLayout={[Function]}
onMomentumScrollBegin={[Function]}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollBeginDrag={[Function]}
onScrollEndDrag={[Function]}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={0.0001}
stickyHeaderIndices={[]}
style={
{
"backgroundColor": "white",
}
}
testID="custom-flatlist"
viewabilityConfigCallbackPairs={[]}
windowSize={10}
>
<View />
</RCTScrollView>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,6 @@ describe('ActivityOverlay Component', () => {
expect(activityIndicator.props.color).toBe('white');
expect(activityIndicator.props.size).toBe('large');
});

it('does not apply styles or render elements when isVisible is false', () => {
render(<ActivityOverlay isVisible={false} />);

expect(screen.queryByTestId('ActivityIndicator')).toBeNull();
});
});

describe('ActivityOverlay Component - Snapshot Tests', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/molecules/ActivityOverlay/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const ActivityOverlay = ({ isVisible = false, color = 'white', size = 'large', z
};

ActivityOverlay.propTypes = {
isVisible: PropTypes.bool,
isVisible: PropTypes.bool.isRequired,
color: PropTypes.string,
size: PropTypes.oneOf(['small', 'large']),
zIndex: PropTypes.number,
Expand Down
67 changes: 45 additions & 22 deletions src/components/molecules/Input/Input.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@ import TextInput from '../../atoms/TextInput';
import Input from '.';
import { lightColors } from '../../../constants/theme';

describe('<Input />', () => {
const mockOnChangeText = jest.fn();
const mockOnBlur = jest.fn();
const mockOnEndEditing = jest.fn();

const renderInput = ({ label = 'Default Label', icon = null, maxLength, value = '', errorObject = null, style }) => {
return render(
<Input
label={label}
icon={icon}
maxLength={maxLength}
value={value}
onChangeText={mockOnChangeText}
onBlur={mockOnBlur}
onEndEditing={mockOnEndEditing}
errorObject={errorObject}
style={style}
/>,
);
};
const mockOnChangeText = jest.fn();
const mockOnBlur = jest.fn();
const mockOnEndEditing = jest.fn();

const renderInput = ({ label = 'Default Label', icon = null, maxLength, value = '', errorObject = null, style }) => {
return render(
<Input
label={label}
icon={icon}
maxLength={maxLength}
value={value}
onChangeText={mockOnChangeText}
onBlur={mockOnBlur}
onEndEditing={mockOnEndEditing}
errorObject={errorObject}
style={style}
/>,
);
};

describe('<Input />', () => {
afterEach(() => {
jest.clearAllMocks();
});
Expand Down Expand Up @@ -100,11 +100,34 @@ describe('<Input />', () => {

expect(mockOnChangeText).toHaveBeenCalledWith('123456789012345'); // maxLength of 10
});
});

describe('<Input /> edge cases for value prop', () => {
it('handles null value gracefully', () => {
renderInput({ value: null });

const inputElement = screen.getByTestId('text-input-flat');
expect(inputElement.props.value).toBe(''); // Null should be converted to an empty string
});

it('handles empty or undefined value gracefully', () => {
it('handles undefined value gracefully', () => {
renderInput({ value: undefined });

const inputElement = screen.getByTestId('text-input-flat');
expect(inputElement.props.value).toBe('');
expect(inputElement.props.value).toBe(''); // Undefined should be converted to an empty string
});

it('handles empty string value gracefully', () => {
renderInput({ value: '' });

const inputElement = screen.getByTestId('text-input-flat');
expect(inputElement.props.value).toBe(''); // Empty string remains as is
});

it('handles numeric value gracefully', () => {
renderInput({ value: 123 });

const inputElement = screen.getByTestId('text-input-flat');
expect(inputElement.props.value).toBe('123'); // Number should be converted to a string
});
});
49 changes: 49 additions & 0 deletions src/components/templates/BaseScreen/BaseScreen.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import { screen } from '@testing-library/react-native';
import { renderInThemeProvider } from '../../../../__tests__/utils/renderInThemeProvider';
import Screen from '.';
import Text from '../../atoms/Text';

describe('<Screen />', () => {
it('renders correctly and matches the snapshot', () => {
const { toJSON } = renderInThemeProvider(
<Screen>
<Text>Test Content</Text>
</Screen>,
);

expect(toJSON()).toMatchSnapshot();
});

it('renders children correctly', () => {
renderInThemeProvider(
<Screen>
<Text>Test Content</Text>
</Screen>,
);

expect(screen.getByText('Test Content')).toBeTruthy();
});

it('applies styles from the theme correctly', () => {
const theme = {
colors: { background: 'blue' },
};

renderInThemeProvider(
<Screen testID="base-screen">
<Text>Styled Content</Text>
</Screen>,
theme,
);

const baseScreen = screen.getByTestId('base-screen');

expect(baseScreen.props.style).toEqual(
expect.objectContaining({
flex: 1,
backgroundColor: 'blue',
}),
);
});
});
Loading

0 comments on commit 42d613c

Please sign in to comment.