From a7e6eea9192ee9be2ef9fae7d3456bc4ed485e7f Mon Sep 17 00:00:00 2001 From: Ovidiu Viorel Iepure Date: Thu, 9 Jun 2016 11:27:48 -0700 Subject: [PATCH] Open sourced CatalystSubviewsClippingTestCase test for RN Android Reviewed By: bestander Differential Revision: D3411402 fbshipit-source-id: c4f51f0366c30815a3a409ee13b61900d882fcc9 --- .../java/com/facebook/react/tests/BUCK | 1 + .../CatalystSubviewsClippingTestCase.java | 300 +++++++++++++++++ .../js/SubviewsClippingTestModule.js | 310 ++++++++++++++++++ ReactAndroid/src/androidTest/js/TestBundle.js | 4 + 4 files changed, 615 insertions(+) create mode 100644 ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystSubviewsClippingTestCase.java create mode 100644 ReactAndroid/src/androidTest/js/SubviewsClippingTestModule.js diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK index c14e4d035..8f16de6cb 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/BUCK @@ -9,6 +9,7 @@ SANDCASTLE_FLAKY = [ deps = [ react_native_dep('third-party/android/support/v4:lib-support-v4'), + react_native_dep('third-party/java/jsr-305:jsr-305'), react_native_dep('third-party/java/junit:junit'), react_native_integration_tests_target('java/com/facebook/react/testing:testing'), react_native_target('java/com/facebook/react/bridge:bridge'), diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystSubviewsClippingTestCase.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystSubviewsClippingTestCase.java new file mode 100644 index 000000000..04f1f8a2e --- /dev/null +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/CatalystSubviewsClippingTestCase.java @@ -0,0 +1,300 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.tests; + +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import javax.annotation.Nullable; +import android.widget.ScrollView; + +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.testing.ReactAppInstrumentationTestCase; +import com.facebook.react.testing.ReactInstanceSpecForTest; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.views.view.ReactViewGroup; +import com.facebook.react.views.view.ReactViewManager; + +import org.junit.Assert; +import org.junit.Ignore; + +/** + * Integration test for {@code removeClippedSubviews} property that verify correct scrollview + * behavior + */ +public class CatalystSubviewsClippingTestCase extends ReactAppInstrumentationTestCase { + + private interface SubviewsClippingTestModule extends JavaScriptModule { + void renderClippingSample1(); + void renderClippingSample2(); + void renderScrollViewTest(); + void renderUpdatingSample1(boolean update1, boolean update2); + void renderUpdatingSample2(boolean update); + } + + private final List mEvents = new ArrayList<>(); + + @Override + protected String getReactApplicationKeyUnderTest() { + return "SubviewsClippingTestApp"; + } + + @Override + protected ReactInstanceSpecForTest createReactInstanceSpecForTest() { + ReactInstanceSpecForTest instanceSpec = new ReactInstanceSpecForTest(); + instanceSpec.addJSModule(SubviewsClippingTestModule.class); + instanceSpec.addViewManager(new ClippableViewManager(mEvents)); + return instanceSpec; + } + + /** + * In this test view are layout in a following way: + * +-----------------------------+ + * | | + * | +---------------------+ | + * | | inner1 | | + * | +---------------------+ | + * | +-------------------------+ | + * | | outer (clip=true) | | + * | | +---------------------+ | | + * | | | inner2 | | | + * | | +---------------------+ | | + * | | | | + * | +-------------------------+ | + * | +---------------------+ | + * | | inner3 | | + * | +---------------------+ | + * | | + * +-----------------------------+ + * + * We expect only outer and inner2 to be attached + */ + public void XtestOneLevelClippingInView() throws Throwable { + mEvents.clear(); + getReactContext().getJSModule(SubviewsClippingTestModule.class).renderClippingSample1(); + waitForBridgeAndUIIdle(); + Assert.assertArrayEquals(new String[]{"Attach_outer", "Attach_inner2"}, mEvents.toArray()); + } + + /** + * In this test view are layout in a following way: + * +-----------------------------+ + * | outer (clip=true) | + * | | + * | | + * | | + * | +-----------------------------+ + * | | complexInner (clip=true) | + * | | +----------+ | +---------+ | + * | | | inner1 | | | inner2 | | + * | | | | | | | | + * | | +----------+ | +---------+ | + * +--------------+--------------+ | + * | +----------+ +---------+ | + * | | inner3 | | inner4 | | + * | | | | | | + * | +----------+ +---------+ | + * | | + * +-----------------------------+ + * + * We expect outer, complexInner & inner1 to be attached + */ + public void XtestTwoLevelClippingInView() throws Throwable { + mEvents.clear(); + getReactContext().getJSModule(SubviewsClippingTestModule.class).renderClippingSample2(); + waitForBridgeAndUIIdle(); + Assert.assertArrayEquals( + new String[]{"Attach_outer", "Attach_complexInner", "Attach_inner1"}, + mEvents.toArray()); + } + + /** + * This test verifies that we update clipped subviews appropriately when some of them gets + * re-layouted. + * + * In this test scenario we render clipping view ("outer") with two subviews, one is outside and + * clipped and one is inside (absolutely positioned). By updating view props we first change the + * height of the first element so that it should intersect with clipping "outer" view. Then we + * update top position of the second view so that is should go off screen. + */ + public void testClippingAfterLayoutInner() { + SubviewsClippingTestModule subviewsClippingTestModule = + getReactContext().getJSModule(SubviewsClippingTestModule.class); + + mEvents.clear(); + subviewsClippingTestModule.renderUpdatingSample1(false, false); + waitForBridgeAndUIIdle(); + Assert.assertArrayEquals(new String[]{"Attach_outer", "Attach_inner2"}, mEvents.toArray()); + + mEvents.clear(); + subviewsClippingTestModule.renderUpdatingSample1(true, false); + waitForBridgeAndUIIdle(); + Assert.assertArrayEquals(new String[]{"Attach_inner1"}, mEvents.toArray()); + + mEvents.clear(); + subviewsClippingTestModule.renderUpdatingSample1(true, true); + waitForBridgeAndUIIdle(); + Assert.assertArrayEquals(new String[]{"Detach_inner2"}, mEvents.toArray()); + } + + /** + * This test verifies that we update clipping views appropriately when parent view layout changes + * in a way that affects clipping. + * + * In this test we render clipping view ("outer") set to be 100x100dp with inner view that is + * absolutely positioned out of the clipping area of the parent view. Then we resize parent view + * so that inner view should be visible. + */ + public void testClippingAfterLayoutParent() { + SubviewsClippingTestModule subviewsClippingTestModule = + getReactContext().getJSModule(SubviewsClippingTestModule.class); + + mEvents.clear(); + subviewsClippingTestModule.renderUpdatingSample2(false); + waitForBridgeAndUIIdle(); + Assert.assertArrayEquals(new String[]{"Attach_outer"}, mEvents.toArray()); + + mEvents.clear(); + subviewsClippingTestModule.renderUpdatingSample2(true); + waitForBridgeAndUIIdle(); + Assert.assertArrayEquals(new String[]{"Attach_inner"}, mEvents.toArray()); + } + + public void testOneLevelClippingInScrollView() throws Throwable { + getReactContext().getJSModule(SubviewsClippingTestModule.class).renderScrollViewTest(); + waitForBridgeAndUIIdle(); + + // Only 3 first views should be attached at the beginning + Assert.assertArrayEquals(new String[]{"Attach_0", "Attach_1", "Attach_2"}, mEvents.toArray()); + mEvents.clear(); + + // We scroll down such that first view get out of the bounds, we expect the first view to be + // detached and 4th view to get attached + scrollToDpInUIThread(120); + Assert.assertArrayEquals(new String[]{"Detach_0", "Attach_3"}, mEvents.toArray()); + } + + public void testTwoLevelClippingInScrollView() throws Throwable { + getReactContext().getJSModule(SubviewsClippingTestModule.class).renderScrollViewTest(); + waitForBridgeAndUIIdle(); + + final int complexViewOffset = 4 * 120 - 300; + + // Step 1 + // We scroll down such that first "complex" view is clipped & just below the bottom of the + // scroll view + scrollToDpInUIThread(complexViewOffset); + + mEvents.clear(); + + // Step 2 + // Scroll a little bit so that "complex" view is visible, but it's inner views are not + scrollToDpInUIThread(complexViewOffset + 5); + + Assert.assertArrayEquals(new String[]{"Attach_C0"}, mEvents.toArray()); + mEvents.clear(); + + // Step 3 + // Scroll even more so that first subview of "complex" view is visible, view 1 will get off + // screen + scrollToDpInUIThread(complexViewOffset + 100); + Assert.assertArrayEquals(new String[]{"Detach_1", "Attach_C0.1"}, mEvents.toArray()); + mEvents.clear(); + + // Step 4 + // Scroll even more to reveal second subview of "complex" view + scrollToDpInUIThread(complexViewOffset + 150); + Assert.assertArrayEquals(new String[]{"Attach_C0.2"}, mEvents.toArray()); + mEvents.clear(); + + // Step 5 + // Scroll back to previous position (Step 3), second view should get detached + scrollToDpInUIThread(complexViewOffset + 100); + Assert.assertArrayEquals(new String[]{"Detach_C0.2"}, mEvents.toArray()); + mEvents.clear(); + + // Step 6 + // Scroll back to Step 2, complex view should be visible but all subviews should be detached + scrollToDpInUIThread(complexViewOffset + 5); + Assert.assertArrayEquals(new String[]{"Attach_1", "Detach_C0.1"}, mEvents.toArray()); + mEvents.clear(); + + // Step 7 + // Scroll back to Step 1, complex view should be gone + scrollToDpInUIThread(complexViewOffset); + Assert.assertArrayEquals(new String[]{"Detach_C0"}, mEvents.toArray()); + } + + private void scrollToDpInUIThread(final int yPositionInDP) throws Throwable { + final ScrollView mainScrollView = getViewByTestId("scroll_view"); + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + mainScrollView.scrollTo(0, (int) PixelUtil.toPixelFromDIP(yPositionInDP)); + } + }); + waitForBridgeAndUIIdle(); + } + + private static class ClippableView extends ReactViewGroup { + + private String mClippableViewID; + private final List mEvents; + + public ClippableView(Context context, List events) { + super(context); + mEvents = events; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mEvents.add("Attach_" + mClippableViewID); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mEvents.add("Detach_" + mClippableViewID); + } + + public void setClippableViewID(String clippableViewID) { + mClippableViewID = clippableViewID; + } + } + + private static class ClippableViewManager extends ReactViewManager { + + private final List mEvents; + + public ClippableViewManager(List events) { + mEvents = events; + } + + @Override + public String getName() { + return "ClippableView"; + } + + @Override + public ReactViewGroup createViewInstance(ThemedReactContext context) { + return new ClippableView(context, mEvents); + } + + @ReactProp(name = "clippableViewID") + public void setClippableViewId(ReactViewGroup view, @Nullable String clippableViewId) { + ((ClippableView) view).setClippableViewID(clippableViewId); + } + } +} diff --git a/ReactAndroid/src/androidTest/js/SubviewsClippingTestModule.js b/ReactAndroid/src/androidTest/js/SubviewsClippingTestModule.js new file mode 100644 index 000000000..3d18827d6 --- /dev/null +++ b/ReactAndroid/src/androidTest/js/SubviewsClippingTestModule.js @@ -0,0 +1,310 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule SubviewsClippingTestModule + */ + +'use strict'; + +var BatchedBridge = require('BatchedBridge'); +var React = require('React'); +var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); +var ScrollView = require('ScrollView'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var requireNativeComponent = require('requireNativeComponent'); + +var ClippableView = requireNativeComponent('ClippableView', null); + +var ClippingSample1 = React.createClass({ + render: function() { + var styles = sample1Styles; + return ( + + + + + + + + + + ); + } +}); + +var sample1Styles = StyleSheet.create({ + outer: { + width: 200, + height: 200, + backgroundColor: 'red', + }, + inner: { + position: 'absolute', + width: 100, + height: 100, + backgroundColor: 'green', + }, + inner1: { + top: -150, + left: 50, + }, + inner2: { + top: 50, + left: 50, + }, + inner3: { + top: 250, + left: 50, + }, + inner4: { + left: -150, + top: 50, + }, + inner5: { + left: 250, + top: 50, + }, +}); + +var ClippingSample2 = React.createClass({ + render: function() { + var styles = sample2Styles; + return ( + + + + + + + + + + + ); + } +}); + +var sample2Styles = StyleSheet.create({ + outer: { + width: 200, + height: 200, + backgroundColor: 'red', + }, + complexInner: { + position: 'absolute', + width: 200, + height: 200, + left: 100, + top: 100, + backgroundColor: 'green', + }, + inner: { + position: 'absolute', + width: 80, + height: 80, + backgroundColor: 'blue', + }, + inner1: { + left: 10, + top: 10, + }, + inner2: { + right: 10, + top: 10, + }, + inner3: { + left: 10, + bottom: 10, + }, + inner4: { + right: 10, + bottom: 10, + }, +}); + +var UpdatingSample1 = React.createClass({ + render: function() { + var styles = updating1Styles; + var inner1Styles = [styles.inner1, {height: this.props.update1 ? 200 : 100}]; + var inner2Styles = [styles.inner2, {top: this.props.update2 ? 200 : 50}]; + return ( + + + + + + + ); + } +}); + +var updating1Styles = StyleSheet.create({ + outer: { + width: 200, + height: 200, + backgroundColor: 'red', + }, + inner1: { + position: 'absolute', + width: 100, + height: 100, + left: 50, + top: -100, + backgroundColor: 'green', + }, + inner2: { + position: 'absolute', + width: 100, + height: 100, + left: 50, + top: 50, + backgroundColor: 'green', + } +}); + +var UpdatingSample2 = React.createClass({ + render: function() { + var styles = updating2Styles; + var outerStyles = [styles.outer, {height: this.props.update ? 200 : 100}]; + return ( + + + + + + ); + } +}); + +var updating2Styles = StyleSheet.create({ + outer: { + width: 100, + height: 100, + backgroundColor: 'red', + }, + inner: { + position: 'absolute', + width: 100, + height: 100, + top: 100, + backgroundColor: 'green', + }, +}); + +var ScrollViewTest = React.createClass({ + render: function() { + var styles = scrollTestStyles; + var children = []; + for (var i = 0; i < 4; i++) { + children[i] = ( + + ); + } + for (var i = 4; i < 6; i++) { + var viewID = 'C' + (i - 4); + children[i] = ( + + + + + ); + } + + return ( + + {children} + + ); + } +}); + +var scrollTestStyles = StyleSheet.create({ + scrollView: { + width: 200, + height: 300, + }, + row: { + flex: 1, + height: 120, + backgroundColor: 'red', + borderColor: 'blue', + borderBottomWidth: 1, + }, + complex: { + flex: 1, + height: 240, + backgroundColor: 'yellow', + borderColor: 'blue', + borderBottomWidth: 1, + }, + inner: { + flex: 1, + margin: 10, + height: 100, + backgroundColor: 'gray', + borderColor: 'green', + borderWidth: 1, + }, +}); + + +var appInstance = null; +var SubviewsClippingTestApp = React.createClass({ + componentWillMount: function() { + appInstance = this; + }, + setComponent: function(component) { + this.setState({component: component}); + }, + getInitialState: function() { + return {}; + }, + render: function() { + var component = this.state.component; + return ( + + {component} + + ); + }, +}); + +var SubviewsClippingTestModule = { + App: SubviewsClippingTestApp, + renderClippingSample1: function() { + appInstance.setComponent(); + }, + renderClippingSample2: function() { + appInstance.setComponent(); + }, + renderUpdatingSample1: function(update1, update2) { + appInstance.setComponent(); + }, + renderUpdatingSample2: function(update) { + appInstance.setComponent(); + }, + renderScrollViewTest: function() { + appInstance.setComponent(); + }, +}; + +BatchedBridge.registerCallableModule( + 'SubviewsClippingTestModule', + SubviewsClippingTestModule +); + +module.exports = SubviewsClippingTestModule; diff --git a/ReactAndroid/src/androidTest/js/TestBundle.js b/ReactAndroid/src/androidTest/js/TestBundle.js index 8dcae7d0b..f925d81f8 100644 --- a/ReactAndroid/src/androidTest/js/TestBundle.js +++ b/ReactAndroid/src/androidTest/js/TestBundle.js @@ -60,6 +60,10 @@ var apps = [ appKey: 'ScrollViewTestApp', component: () => require('ScrollViewTestModule').ScrollViewTestApp, }, +{ + appKey: 'SubviewsClippingTestApp', + component: () => require('SubviewsClippingTestModule').App, +}, { appKey: 'SwipeRefreshLayoutTestApp', component: () => require('SwipeRefreshLayoutTestModule').SwipeRefreshLayoutTestApp