Open sourced CatalystSubviewsClippingTestCase test for RN Android

Reviewed By: bestander

Differential Revision: D3411402

fbshipit-source-id: c4f51f0366c30815a3a409ee13b61900d882fcc9
This commit is contained in:
Ovidiu Viorel Iepure 2016-06-09 11:27:48 -07:00 committed by Facebook Github Bot 7
parent c99fb9cb9b
commit a7e6eea919
4 changed files with 615 additions and 0 deletions

View File

@ -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'),

View File

@ -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<String> 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<String> mEvents;
public ClippableView(Context context, List<String> 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<String> mEvents;
public ClippableViewManager(List<String> 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);
}
}
}

View File

@ -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 (
<View>
<ClippableView clippableViewID="outer" style={styles.outer} removeClippedSubviews={true}>
<ClippableView clippableViewID="inner1" style={[styles.inner, styles.inner1]}/>
<ClippableView clippableViewID="inner2" style={[styles.inner, styles.inner2]}/>
<ClippableView clippableViewID="inner3" style={[styles.inner, styles.inner3]}/>
<ClippableView clippableViewID="inner4" style={[styles.inner, styles.inner4]}/>
<ClippableView clippableViewID="inner5" style={[styles.inner, styles.inner5]}/>
</ClippableView>
</View>
);
}
});
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 (
<View>
<ClippableView clippableViewID="outer" style={styles.outer} removeClippedSubviews={true}>
<ClippableView
clippableViewID="complexInner"
style={styles.complexInner}
removeClippedSubviews={true}>
<ClippableView clippableViewID="inner1" style={[styles.inner, styles.inner1]}/>
<ClippableView clippableViewID="inner2" style={[styles.inner, styles.inner2]}/>
<ClippableView clippableViewID="inner3" style={[styles.inner, styles.inner3]}/>
<ClippableView clippableViewID="inner4" style={[styles.inner, styles.inner4]}/>
</ClippableView>
</ClippableView>
</View>
);
}
});
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 (
<View>
<ClippableView clippableViewID="outer" style={styles.outer} removeClippedSubviews={true}>
<ClippableView clippableViewID="inner1" style={inner1Styles}/>
<ClippableView clippableViewID="inner2" style={inner2Styles}/>
</ClippableView>
</View>
);
}
});
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 (
<View>
<ClippableView clippableViewID="outer" style={outerStyles} removeClippedSubviews={true}>
<ClippableView clippableViewID="inner" style={styles.inner}/>
</ClippableView>
</View>
);
}
});
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] = (
<ClippableView key={i} style={styles.row} clippableViewID={'' + i}/>
);
}
for (var i = 4; i < 6; i++) {
var viewID = 'C' + (i - 4);
children[i] = (
<ClippableView
key={i}
style={styles.complex}
clippableViewID={viewID}
removeClippedSubviews={true}>
<ClippableView style={styles.inner} clippableViewID={viewID + '.1'}/>
<ClippableView style={styles.inner} clippableViewID={viewID + '.2'}/>
</ClippableView>
);
}
return (
<ScrollView removeClippedSubviews={true} style={styles.scrollView} testID="scroll_view">
{children}
</ScrollView>
);
}
});
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 (
<View>
{component}
</View>
);
},
});
var SubviewsClippingTestModule = {
App: SubviewsClippingTestApp,
renderClippingSample1: function() {
appInstance.setComponent(<ClippingSample1/>);
},
renderClippingSample2: function() {
appInstance.setComponent(<ClippingSample2/>);
},
renderUpdatingSample1: function(update1, update2) {
appInstance.setComponent(<UpdatingSample1 update1={update1} update2={update2}/>);
},
renderUpdatingSample2: function(update) {
appInstance.setComponent(<UpdatingSample2 update={update}/>);
},
renderScrollViewTest: function() {
appInstance.setComponent(<ScrollViewTest/>);
},
};
BatchedBridge.registerCallableModule(
'SubviewsClippingTestModule',
SubviewsClippingTestModule
);
module.exports = SubviewsClippingTestModule;

View File

@ -60,6 +60,10 @@ var apps = [
appKey: 'ScrollViewTestApp',
component: () => require('ScrollViewTestModule').ScrollViewTestApp,
},
{
appKey: 'SubviewsClippingTestApp',
component: () => require('SubviewsClippingTestModule').App,
},
{
appKey: 'SwipeRefreshLayoutTestApp',
component: () => require('SwipeRefreshLayoutTestModule').SwipeRefreshLayoutTestApp