Skip to content

Commit

Permalink
Java unit tests for native animated module.
Browse files Browse the repository at this point in the history
Summary:This change adds some basic unit tests for native animated traversal algorithm. The following tests are added:
1) Build simple animated nodes graph, verify that frame-based animation execute updates and when it runs out of the frames we no longer schedule updates for the native view
2) Build simple animated nodes graph and start short timing animation, verify that animation JS callback gets called.

As a part of this change I'm fixing an issue that tests allowed me to discover, where I forgot to clear updates queue at the end of `runUpdates` method. It was causing the view to be updated even if there was no active animation for it (actually it was mitigated by another bug in `hasActiveAnimations` I'm fixing here too).

I'm also adding Nullable annotation in a bunch of places. To lazy to send it as a separate change - sorry.

Going forward I'm planning on adding more tests. Currently the number of nodes is pretty limited so it's difficult to construct more complex graphs, but once I land Add/Multiply
Closes facebook#6858

Differential Revision: D3168549

Pulled By: astreet

fb-gh-sync-id: 5295c75f3c7817775b5154bb808888650ff74e12
fbshipit-source-id: 5295c75f3c7817775b5154bb808888650ff74e12
  • Loading branch information
kmagiera authored and Facebook Github Bot 4 committed Apr 12, 2016
1 parent cbd72ad commit 21b3180
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 7 deletions.
24 changes: 24 additions & 0 deletions ReactAndroid/src/main/java/com/facebook/react/animated/BUCK
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
include_defs('//ReactAndroid/DEFS')

android_library(
name = 'animated',
srcs = glob([
'*.java',
]),
deps = [
react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/uimanager:uimanager'),

react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
react_native_dep('third-party/java/infer-annotations:infer-annotations'),
react_native_dep('third-party/java/jsr-305:jsr-305'),
react_native_dep('third-party/android/support/v4:lib-support-v4'),
],
visibility = [
'PUBLIC',
],
)

project_config(
src_target = ':animated',
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

package com.facebook.react.animated;

import android.support.annotation.Nullable;
import javax.annotation.Nullable;

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Callback;
Expand All @@ -20,7 +20,6 @@
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.GuardedChoreographerFrameCallback;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.ReactChoreographer;
import com.facebook.react.uimanager.UIImplementation;
import com.facebook.react.uimanager.UIManagerModule;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIImplementation;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Queue;

import javax.annotation.Nullable;

/**
* This is the main class that coordinates how native animated JS implementation drives UI changes.
*
Expand All @@ -49,12 +50,12 @@ public NativeAnimatedNodesManager(UIImplementation uiImplementation) {
mUIImplementation = uiImplementation;
}

/*package*/ AnimatedNode getNodeById(int id) {
/*package*/ @Nullable AnimatedNode getNodeById(int id) {
return mAnimatedNodes.get(id);
}

public boolean hasActiveAnimations() {
return !mActiveAnimations.isEmpty();
return !mActiveAnimations.isEmpty() || !mUpdatedNodes.isEmpty();
}

public void createAnimatedNode(int tag, ReadableMap config) {
Expand Down Expand Up @@ -315,6 +316,8 @@ public void runUpdates(long frameTimeNanos) {
+ activeNodesCount + " but toposort visited only " + updatedNodesCount);
}

// Clean mUpdatedNodes queue
mUpdatedNodes.clear();

// Cleanup finished animations. Iterate over the array of animations and override ones that has
// finished, then resize `mActiveAnimations`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import java.util.HashMap;
import java.util.Map;

import javax.annotation.Nullable;

/**
* Animated node that represents view properties. There is a special handling logic implemented for
* the nodes of this type in {@link NativeAnimatedNodesManager} that is responsible for extracting
Expand Down Expand Up @@ -50,7 +52,7 @@ public final void updateView(UIImplementation uiImplementation) {
}
JavaOnlyMap propsMap = new JavaOnlyMap();
for (Map.Entry<String, Integer> entry : mPropMapping.entrySet()) {
AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue());
@Nullable AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue());
if (node == null) {
throw new IllegalArgumentException("Mapped property node does not exists");
} else if (node instanceof StyleAnimatedNode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import java.util.HashMap;
import java.util.Map;

import javax.annotation.Nullable;

/**
* Native counterpart of style animated node (see AnimatedStyle class in AnimatedImplementation.js)
*/
Expand All @@ -38,7 +40,7 @@

public void collectViewUpdates(JavaOnlyMap propsMap) {
for (Map.Entry<String, Integer> entry : mPropMapping.entrySet()) {
AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue());
@Nullable AnimatedNode node = mNativeAnimatedNodesManager.getNodeById(entry.getValue());
if (node == null) {
throw new IllegalArgumentException("Mapped style node does not exists");
} else if (node instanceof ValueAnimatedNode) {
Expand Down
29 changes: 29 additions & 0 deletions ReactAndroid/src/test/java/com/facebook/react/animated/BUCK
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
include_defs('//ReactAndroid/DEFS')

robolectric3_test(
name = 'animated',
# Please change the contact to the oncall of your team
contacts = ['[email protected]'],
srcs = glob(['**/*.java']),
deps = [
react_native_target('java/com/facebook/react/animated:animated'),
react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/uimanager:uimanager'),
react_native_target('java/com/facebook/react:react'),

react_native_tests_target('java/com/facebook/react/bridge:testhelpers'),
react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'),
react_native_dep('third-party/java/fest:fest'),
react_native_dep('third-party/java/jsr-305:jsr-305'),
react_native_dep('third-party/java/junit:junit'),
react_native_dep('third-party/java/mockito:mockito'),
react_native_dep('third-party/java/robolectric3/robolectric:robolectric'),
],
visibility = [
'PUBLIC'
],
)

project_config(
test_target = ':animated',
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* Copyright (c) 2015-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.animated;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.JavaOnlyArray;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.UIImplementation;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricTestRunner;

import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

/**
* Tests the animated nodes graph traversal algorithm from {@link NativeAnimatedNodesManager}.
*/
@PrepareForTest({Arguments.class})
@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
public class NativeAnimatedNodeTraversalTest {

private static long FRAME_LEN_NANOS = 1000000000L / 60L;
private static long INITIAL_FRAME_TIME_NANOS = 14599233201256L; /* random */

@Rule
public PowerMockRule rule = new PowerMockRule();

private long mFrameTimeNanos;
private UIImplementation mUIImplementationMock;
private NativeAnimatedNodesManager mNativeAnimatedNodesManager;

private long nextFrameTime() {
return mFrameTimeNanos += FRAME_LEN_NANOS;
}

@Before
public void setUp() {
PowerMockito.mockStatic(Arguments.class);
PowerMockito.when(Arguments.createArray()).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return new JavaOnlyArray();
}
});
PowerMockito.when(Arguments.createMap()).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return new JavaOnlyMap();
}
});

mFrameTimeNanos = INITIAL_FRAME_TIME_NANOS;
mUIImplementationMock = mock(UIImplementation.class);
mNativeAnimatedNodesManager = new NativeAnimatedNodesManager(mUIImplementationMock);
}

/**
* Generates a simple animated nodes graph and attaches the props node to a given {@param viewTag}
* Parameter {@param opacity} is used as a initial value for the "opacity" attribute.
*
* Nodes are connected as follows (nodes IDs in parens):
* ValueNode(1) -> StyleNode(2) -> PropNode(3)
*/
private void createSimpleAnimatedViewWithOpacity(int viewTag, double opacity) {
mNativeAnimatedNodesManager.createAnimatedNode(
1,
JavaOnlyMap.of("type", "value", "value", opacity));
mNativeAnimatedNodesManager.createAnimatedNode(
2,
JavaOnlyMap.of("type", "style", "style", JavaOnlyMap.of("opacity", 1)));
mNativeAnimatedNodesManager.createAnimatedNode(
3,
JavaOnlyMap.of("type", "props", "props", JavaOnlyMap.of("style", 2)));
mNativeAnimatedNodesManager.connectAnimatedNodes(1, 2);
mNativeAnimatedNodesManager.connectAnimatedNodes(2, 3);
mNativeAnimatedNodesManager.connectAnimatedNodeToView(3, viewTag);
}

@Test
public void testFramesAnimation() {
createSimpleAnimatedViewWithOpacity(1000, 0d);

JavaOnlyArray frames = JavaOnlyArray.of(0d, 0.2d, 0.4d, 0.6d, 0.8d, 1d);
Callback animationCallback = mock(Callback.class);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
animationCallback);

ArgumentCaptor<ReactStylesDiffMap> stylesCaptor =
ArgumentCaptor.forClass(ReactStylesDiffMap.class);

reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock).synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN)).isEqualTo(0);

for (int i = 0; i < frames.size(); i++) {
reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(mUIImplementationMock)
.synchronouslyUpdateViewOnUIThread(eq(1000), stylesCaptor.capture());
assertThat(stylesCaptor.getValue().getDouble("opacity", Double.NaN))
.isEqualTo(frames.getDouble(i));
}

reset(mUIImplementationMock);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(mUIImplementationMock);
}

@Test
public void testAnimationCallbackFinish() {
createSimpleAnimatedViewWithOpacity(1000, 0d);

JavaOnlyArray frames = JavaOnlyArray.of(0d, 1d);
Callback animationCallback = mock(Callback.class);
mNativeAnimatedNodesManager.startAnimatingNode(
1,
JavaOnlyMap.of("type", "frames", "frames", frames, "toValue", 1d),
animationCallback);

ArgumentCaptor<ReadableMap> callbackResponseCaptor = ArgumentCaptor.forClass(ReadableMap.class);

reset(animationCallback);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(animationCallback);

reset(animationCallback);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verify(animationCallback).invoke(callbackResponseCaptor.capture());

assertThat(callbackResponseCaptor.getValue().hasKey("finished")).isTrue();
assertThat(callbackResponseCaptor.getValue().getBoolean("finished")).isTrue();

reset(animationCallback);
mNativeAnimatedNodesManager.runUpdates(nextFrameTime());
verifyNoMoreInteractions(animationCallback);
}
}

0 comments on commit 21b3180

Please sign in to comment.