diff --git a/packages/lib/src/spatial-navigation/components/ScrollView/AnyScrollView.tsx b/packages/lib/src/spatial-navigation/components/ScrollView/AnyScrollView.tsx index 9ed1e741..bcc16d47 100644 --- a/packages/lib/src/spatial-navigation/components/ScrollView/AnyScrollView.tsx +++ b/packages/lib/src/spatial-navigation/components/ScrollView/AnyScrollView.tsx @@ -12,6 +12,7 @@ type Props = { contentContainerStyle?: ViewStyle; scrollDuration?: number; onScroll?: (event: { nativeEvent: { contentOffset: { y: number; x: number } } }) => void; + testID?: string; }; export const AnyScrollView = React.forwardRef( diff --git a/packages/lib/src/spatial-navigation/components/ScrollView/CustomScrollView/CustomScrollView.test.tsx b/packages/lib/src/spatial-navigation/components/ScrollView/CustomScrollView/CustomScrollView.test.tsx new file mode 100644 index 00000000..3c76759f --- /dev/null +++ b/packages/lib/src/spatial-navigation/components/ScrollView/CustomScrollView/CustomScrollView.test.tsx @@ -0,0 +1,103 @@ +import { View } from 'react-native'; +import { RenderResult, act, fireEvent, render, screen } from '@testing-library/react-native'; +import { ReactTestInstance } from 'react-test-renderer'; +import '../../tests/helpers/configureTestRemoteControl'; +import { SpatialNavigationScrollView } from '../ScrollView'; +import { SpatialNavigationView } from '../../View'; +import { TestButton } from '../../tests/TestButton'; +import { SpatialNavigationRoot } from '../../Root'; +import testRemoteControlManager from '../../tests/helpers/testRemoteControlManager'; +import { DefaultFocus } from '../../../context/DefaultFocusContext'; + +const MOCKED_BUTTON_HEIGHT = 100; + +export const setComponentLayoutSize = ( + component: ReactTestInstance, + size: { width: number; height: number; x: number; y: number }, +) => { + fireEvent(component, 'layout', { + nativeEvent: { layout: { width: size.width, height: size.height, x: size.x, y: size.y } }, + }); +}; + +export const expectButtonToHaveFocus = (component: RenderResult, text: string) => { + const element = component.getByRole('button', { name: text }); + expect(element).toBeSelected(); +}; + +const TestPage = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; + +jest.spyOn(View.prototype, 'measureLayout').mockImplementation(function (node, callback) { + // @ts-expect-error it's weird but that's fine, it's our only way to get the button's context + const buttonLabel = this?.props?.accessibilityLabel; + const buttonNumber = parseInt(buttonLabel, 10) - 1; + + callback(0, buttonNumber * MOCKED_BUTTON_HEIGHT, 0, 0); +}); + +const expectViewToHaveScroll = (element: ReactTestInstance, scrollValue: number) => + expect(element).toHaveStyle({ transform: [{ translateY: scrollValue }] }); + +describe('CustomScrollView', () => { + const scrollViewTestId = 'scrollview'; + const innerScrollViewTestId = scrollViewTestId + '-content'; + + it('scrolls properly upon focus and stops when overflowing', async () => { + const MOCK_SCREEN_SIZE = 300; + const MOCK_TOTAL_CONTENT_SIZE = 800; + + const component = render(); + act(() => jest.runAllTimers()); + + const scrollViewRoot = component.getByTestId(scrollViewTestId); + setComponentLayoutSize(scrollViewRoot, { width: 0, height: MOCK_SCREEN_SIZE, x: 0, y: 0 }); + const scrollViewInner = component.getByTestId(innerScrollViewTestId); + setComponentLayoutSize(scrollViewInner, { + width: 0, + height: MOCK_TOTAL_CONTENT_SIZE, + x: 0, + y: 0, + }); + + testRemoteControlManager.handleDown(); + expectViewToHaveScroll(scrollViewInner, -100); + testRemoteControlManager.handleDown(); + expectViewToHaveScroll(scrollViewInner, -200); + testRemoteControlManager.handleDown(); + expectViewToHaveScroll(scrollViewInner, -300); + testRemoteControlManager.handleDown(); + expectViewToHaveScroll(scrollViewInner, -400); + testRemoteControlManager.handleDown(); + expectViewToHaveScroll(scrollViewInner, -500); + testRemoteControlManager.handleDown(); + + // Once the view is going to over-scroll, it should stop scrolling + expectViewToHaveScroll(scrollViewInner, -500); + testRemoteControlManager.handleDown(); + expectViewToHaveScroll(scrollViewInner, -500); + testRemoteControlManager.handleDown(); + expectViewToHaveScroll(scrollViewInner, -500); + testRemoteControlManager.handleDown(); + + expect(screen).toMatchSnapshot(); + }); +}); diff --git a/packages/lib/src/spatial-navigation/components/ScrollView/CustomScrollView/CustomScrollView.test.tsx.snap b/packages/lib/src/spatial-navigation/components/ScrollView/CustomScrollView/CustomScrollView.test.tsx.snap new file mode 100644 index 00000000..dfeb299a --- /dev/null +++ b/packages/lib/src/spatial-navigation/components/ScrollView/CustomScrollView/CustomScrollView.test.tsx.snap @@ -0,0 +1,254 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CustomScrollView scrolls properly upon focus and stops when overflowing 1`] = ` + + + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + +`; diff --git a/packages/lib/src/spatial-navigation/components/ScrollView/CustomScrollView/CustomScrollView.tsx b/packages/lib/src/spatial-navigation/components/ScrollView/CustomScrollView/CustomScrollView.tsx index 71e41fdb..0f3caa82 100644 --- a/packages/lib/src/spatial-navigation/components/ScrollView/CustomScrollView/CustomScrollView.tsx +++ b/packages/lib/src/spatial-navigation/components/ScrollView/CustomScrollView/CustomScrollView.tsx @@ -11,11 +11,20 @@ type Props = { contentContainerStyle?: ViewStyle; scrollDuration?: number; onScroll?: (event: { nativeEvent: { contentOffset: { y: number; x: number } } }) => void; + testID?: string; }; export const CustomScrollView = forwardRef( ( - { style, contentContainerStyle, children, onScroll, horizontal = false, scrollDuration = 200 }, + { + style, + contentContainerStyle, + children, + onScroll, + horizontal = false, + scrollDuration = 200, + testID, + }, ref, ) => { const [scroll, setScroll] = useState(0); @@ -70,11 +79,13 @@ export const CustomScrollView = forwardRef( style, ]} onLayout={onParentLayout} + testID={testID} > {children} diff --git a/packages/lib/src/spatial-navigation/components/ScrollView/ScrollView.tsx b/packages/lib/src/spatial-navigation/components/ScrollView/ScrollView.tsx index d0888b0e..f2ed684c 100644 --- a/packages/lib/src/spatial-navigation/components/ScrollView/ScrollView.tsx +++ b/packages/lib/src/spatial-navigation/components/ScrollView/ScrollView.tsx @@ -35,6 +35,7 @@ type Props = { useCssScroll?: boolean; /** Configures the scroll duration in the case of CSS scroll */ scrollDuration?: number; + testID?: string; }; const getNodeRef = (node: CustomScrollViewRef | null | undefined) => { @@ -60,6 +61,7 @@ export const SpatialNavigationScrollView = forwardRef( contentContainerStyle, useCssScroll = false, scrollDuration = 200, + testID, }, ref, ) => { @@ -114,6 +116,7 @@ export const SpatialNavigationScrollView = forwardRef( style={style} contentContainerStyle={contentContainerStyle} onScroll={onScroll} + testID={testID} > {children} diff --git a/packages/lib/src/spatial-navigation/components/tests/TestButton.tsx b/packages/lib/src/spatial-navigation/components/tests/TestButton.tsx index 88568fd8..04c84007 100644 --- a/packages/lib/src/spatial-navigation/components/tests/TestButton.tsx +++ b/packages/lib/src/spatial-navigation/components/tests/TestButton.tsx @@ -3,7 +3,7 @@ import { Text } from 'react-native'; import { SpatialNavigationNode } from '../Node'; export type PropsTestButton = { - onSelect: () => void; + onSelect?: () => void; title: string; };