Skip to content

Commit

Permalink
feat(components): view-pager - add meaningful tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur Yorsh committed Feb 28, 2020
1 parent 170e5bb commit 3e38df8
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 61 deletions.
66 changes: 38 additions & 28 deletions src/components/ui/viewPager/viewPager.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ import {
ViewProps,
ViewStyle,
} from 'react-native';
import { I18nLayoutService } from '../support/services';

type ChildElement = React.ReactElement;
type ChildrenProp = ChildElement | ChildElement[];
import {
ChildrenWithProps,
RTLService,
} from '../../devsupport';

export interface ViewPagerProps extends ViewProps {
children: ChildrenProp;
export interface ViewPagerProps<ChildrenProps = {}> extends ViewProps {
selectedIndex?: number;
onSelect?: (index: number) => void;
children?: ChildrenWithProps<ChildrenProps>;
shouldLoadComponent?: (index: number) => boolean;
onOffsetChange?: (offset: number) => void;
onSelect?: (index: number) => void;
}

export type ViewPagerElement = React.ReactElement<ViewPagerProps>;
Expand All @@ -38,17 +38,17 @@ export type ViewPagerElement = React.ReactElement<ViewPagerProps>;
* `ViewPager` allows flipping through the "pages".
*
* @extends React.Component
**
* @property {number} selectedIndex - Determines index of the selected page.
*
* @property {ReactElement | ReactElement[]} children - Determines children of the component.
*
* @property {number} selectedIndex - Determines the index of selected "page".
* @property {(index: number) => void} onSelect - Called when page become visible.
*
* @property {(index: number) => boolean} shouldLoadComponent - Determines loading behavior particular page and can be
* used for lazy loading.
* @property {ReactElement | ReactElement[]} children - Components to render within the view.
*
* @property {(offset: number) => void} onOffsetChange - Fires on scroll event with current scroll offset.
* @property {(index: number) => boolean} shouldLoadComponent - Determines loading behavior for particular page.
* Can be used for lazy loading.
*
* @property {(index: number) => void} onSelect - Fires on "page" select with corresponding index.
* @property {(offset: number) => void} onOffsetChange - Called when scroll offset changes.
*
* @property {ViewProps} ...ViewProps - Any props applied to View component.
*
Expand All @@ -58,7 +58,8 @@ export type ViewPagerElement = React.ReactElement<ViewPagerProps>;
*
* @example ViewPagerInlineStyling
*/
export class ViewPager extends React.Component<ViewPagerProps> implements PanResponderCallbacks {
export class ViewPager<ChildrenProps = {}> extends React.Component<ViewPagerProps<ChildrenProps>>
implements PanResponderCallbacks {

static defaultProps: Partial<ViewPagerProps> = {
selectedIndex: 0,
Expand All @@ -78,7 +79,10 @@ export class ViewPager extends React.Component<ViewPagerProps> implements PanRes
public componentDidUpdate(prevProps: ViewPagerProps): void {
if (prevProps.selectedIndex !== this.props.selectedIndex) {
const index: number = this.props.selectedIndex;
this.scrollToIndex({ index, animated: true });
this.scrollToIndex({
index,
animated: true,
});
}
}

Expand All @@ -90,7 +94,7 @@ export class ViewPager extends React.Component<ViewPagerProps> implements PanRes
const isHorizontalMove: boolean = Math.abs(state.dx) > 0 && Math.abs(state.dx) > Math.abs(state.dy);

if (isHorizontalMove) {
const i18nOffset: number = I18nLayoutService.select(state.dx, -state.dx);
const i18nOffset: number = RTLService.select(state.dx, -state.dx);
const nextSelectedIndex: number = this.props.selectedIndex - Math.sign(i18nOffset);

return nextSelectedIndex >= 0 && nextSelectedIndex < this.getChildCount();
Expand All @@ -100,20 +104,26 @@ export class ViewPager extends React.Component<ViewPagerProps> implements PanRes
};

public onPanResponderMove = (event: GestureResponderEvent, state: PanResponderGestureState): void => {
const i18nOffset: number = I18nLayoutService.select(this.contentWidth, -this.contentWidth);
const i18nOffset: number = RTLService.select(this.contentWidth, -this.contentWidth);
const selectedPageOffset: number = this.props.selectedIndex * i18nOffset;

this.contentOffset.setValue(state.dx - selectedPageOffset);
};

public onPanResponderRelease = (event: GestureResponderEvent, state: PanResponderGestureState) => {
if (Math.abs(state.vx) >= 0.5 || Math.abs(state.dx) >= 0.5 * this.contentWidth) {
const i18nOffset: number = I18nLayoutService.select(state.dx, -state.dx);
const i18nOffset: number = RTLService.select(state.dx, -state.dx);
const index: number = i18nOffset > 0 ? this.props.selectedIndex - 1 : this.props.selectedIndex + 1;
this.scrollToIndex({ index, animated: true });
this.scrollToIndex({
index,
animated: true,
});
} else {
const index: number = this.props.selectedIndex;
this.scrollToIndex({ index, animated: true });
this.scrollToIndex({
index,
animated: true,
});
}
};

Expand All @@ -138,7 +148,7 @@ export class ViewPager extends React.Component<ViewPagerProps> implements PanRes
};

private onContentOffsetAnimationStateChanged = (state: { value: number }): void => {
this.contentOffsetValue = I18nLayoutService.select(-state.value, state.value);
this.contentOffsetValue = RTLService.select(-state.value, state.value);

if (this.props.onOffsetChange) {
this.props.onOffsetChange(this.contentOffsetValue);
Expand All @@ -157,14 +167,14 @@ export class ViewPager extends React.Component<ViewPagerProps> implements PanRes
const animationDuration: number = params.animated ? 300 : 0;

return Animated.timing(this.contentOffset, {
toValue: I18nLayoutService.select(-params.offset, params.offset),
toValue: RTLService.select(-params.offset, params.offset),
easing: Easing.linear,
duration: animationDuration,
});
};

private renderComponentChild = (source: ChildElement, index: number): ChildElement => {
const contentView: ChildElement | null = this.props.shouldLoadComponent(index) ? source : null;
private renderComponentChild = (source: React.ReactElement<ChildrenProps>, index: number): React.ReactElement => {
const contentView = this.props.shouldLoadComponent(index) ? source : null;

return (
<View style={styles.contentContainer}>
Expand All @@ -173,7 +183,7 @@ export class ViewPager extends React.Component<ViewPagerProps> implements PanRes
);
};

private renderComponentChildren = (source: ChildrenProp): ChildElement[] => {
private renderComponentChildren = (source: ChildrenWithProps<ChildrenProps>): React.ReactElement[] => {
return React.Children.map(source, this.renderComponentChild);
};

Expand All @@ -191,11 +201,11 @@ export class ViewPager extends React.Component<ViewPagerProps> implements PanRes
};

public render(): React.ReactElement<ViewProps> {
const { style, children, ...restProps } = this.props;
const { style, children, ...viewProps } = this.props;

return (
<Animated.View
{...restProps}
{...viewProps}
style={[styles.container, style, this.getContainerStyle()]}
onLayout={this.onLayout}
{...this.panResponder.panHandlers}
Expand Down
64 changes: 31 additions & 33 deletions src/components/ui/viewPager/viewPager.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,62 @@
/**
* @license
* Copyright Akveo. All Rights Reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import React from 'react';
import { Text } from 'react-native';
import {
View,
ScrollView,
ViewProps,
Animated,
} from 'react-native';
import {
fireEvent,
render,
RenderAPI,
} from 'react-native-testing-library';
import { ReactTestInstance } from 'react-test-renderer';
import {
ViewPager,
ViewPagerProps,
} from './viewPager.component';

describe('@view-pager: component checks', () => {

const Mock = (props?: ViewPagerProps): React.ReactElement<ViewPagerProps> => (
const TestViewPager = (props?: ViewPagerProps): React.ReactElement<ViewPagerProps> => (
<ViewPager {...props}/>
);

const ChildMock = (props?: ViewProps): React.ReactElement<ViewProps> => (
<View {...props}/>
);
it('should render two tabs', () => {
const component = render(
<TestViewPager>
<Text>Tab 0</Text>
<Text>Tab 1</Text>
</TestViewPager>,
);

it('* shouldLoadComponent called for each child', () => {
expect(component.getByText('Tab 0')).toBeTruthy();
expect(component.getByText('Tab 1')).toBeTruthy();
});

it('should call shouldLoadComponent for each child', () => {
const shouldLoadComponent = jest.fn();

render(
<Mock shouldLoadComponent={shouldLoadComponent}>
<ChildMock/>
<ChildMock/>
</Mock>,
<TestViewPager shouldLoadComponent={shouldLoadComponent}>
<Text>Tab 0</Text>
<Text>Tab 1</Text>
</TestViewPager>,
);

expect(shouldLoadComponent).toBeCalledTimes(2);
});

it('* shouldLoadComponent disables child loading', () => {
const disabledComponentIndex: number = 1;

const shouldLoadComponent = jest.fn((...args: any[]) => {
const index: number = args[0];
return index !== disabledComponentIndex;
});
it('should not render child if disabled by shouldLoadComponent', () => {

const component: RenderAPI = render(
<Mock shouldLoadComponent={shouldLoadComponent}>
<ChildMock/>
<ChildMock/>
</Mock>,
<TestViewPager shouldLoadComponent={index => index !== 1}>
<Text>Tab 0</Text>
<Text>Tab 1</Text>
</TestViewPager>,
);

const scrollView: ReactTestInstance = component.getByType(Animated.View);

const unloadedChild = scrollView.props.children[disabledComponentIndex];

expect(unloadedChild.props.children).toBeNull();
expect(component.queryByText('Tab 0')).toBeTruthy();
expect(component.queryByText('Tab 1')).toBeFalsy();
});

});
Expand Down

0 comments on commit 3e38df8

Please sign in to comment.