Native Animated - Add tests on iOS

Summary:
Adds unit tests to the Native Animated implementation on iOS. This pretty much mirrors the tests we currently have on Android.

It also fixes 2 bugs I've found when adding the tests and pass the current time in `stepAnimation` instead of using `CACurrentMediaTime` to make testing easier.

- `stopListeningToAnimatedNodeValue` did not actually work at all, it should set the listener to nil.
- The finished value in the animation end callback was always true, this simplifies the `RCTAnimationDriver` interface to get rid of `removeAnimation` and fixes the end callback value.

**Test plan**
- Run the tests
- Make sure the UIExplorer example still works
Closes https://github.com/facebook/react-native/pull/13068

Differential Revision: D4786701

Pulled By: javache

fbshipit-source-id: a4f07e6eec1f363ca47b6f27984041793c915bfc
This commit is contained in:
Janic Duplessis
2017-03-28 09:08:51 -07:00
committed by Facebook Github Bot
parent fb54a1eb3e
commit 1d37dd063c
13 changed files with 1069 additions and 73 deletions

View File

@@ -55,6 +55,10 @@
14D6D7281B2222EF001FB087 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDED91B0651EA00C62182 /* libRCTWebSocket.a */; };
14D6D7291B2222EF001FB087 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14AADF041AC3DB95002390C9 /* libReact.a */; };
14DC67F41AB71881001358AB /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14DC67F11AB71876001358AB /* libRCTPushNotification.a */; };
192F69B81E82409A008692C7 /* RCTAnimationUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 192F69B51E82409A008692C7 /* RCTAnimationUtilsTests.m */; };
192F69B91E82409A008692C7 /* RCTConvert_YGValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */; };
192F69BA1E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */; };
192F69DA1E8240E2008692C7 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13E501A31D07A502005F35D8 /* libRCTAnimation.a */; };
272E6B3F1BEA849E001FCF37 /* UpdatePropertiesExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.m */; };
27B885561BED29AF00008352 /* RCTRootViewIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B885551BED29AF00008352 /* RCTRootViewIntegrationTests.m */; };
27F441EC1BEBE5030039B79C /* FlexibleSizeExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F441E81BEBE5030039B79C /* FlexibleSizeExampleView.m */; };
@@ -422,6 +426,9 @@
14D6D7101B220EB3001FB087 /* libOCMock.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libOCMock.a; sourceTree = "<group>"; };
14DC67E71AB71876001358AB /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = ../../Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj; sourceTree = "<group>"; };
14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../../Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = "<group>"; };
192F69B51E82409A008692C7 /* RCTAnimationUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimationUtilsTests.m; sourceTree = "<group>"; };
192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_YGValueTests.m; sourceTree = "<group>"; };
192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNativeAnimatedNodesManagerTests.m; sourceTree = "<group>"; };
272E6B3B1BEA849E001FCF37 /* UpdatePropertiesExampleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = UpdatePropertiesExampleView.h; path = UIExplorer/NativeExampleViews/UpdatePropertiesExampleView.h; sourceTree = "<group>"; };
272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UpdatePropertiesExampleView.m; path = UIExplorer/NativeExampleViews/UpdatePropertiesExampleView.m; sourceTree = "<group>"; };
27B885551BED29AF00008352 /* RCTRootViewIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootViewIntegrationTests.m; sourceTree = "<group>"; };
@@ -456,6 +463,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
192F69DA1E8240E2008692C7 /* libRCTAnimation.a in Frameworks */,
14D6D71E1B2222EF001FB087 /* libRCTActionSheet.a in Frameworks */,
14D6D71F1B2222EF001FB087 /* libRCTAdSupport.a in Frameworks */,
14D6D7201B2222EF001FB087 /* libRCTGeolocation.a in Frameworks */,
@@ -660,6 +668,9 @@
143BC57C1B21E18100462512 /* UIExplorerUnitTests */ = {
isa = PBXGroup;
children = (
192F69B51E82409A008692C7 /* RCTAnimationUtilsTests.m */,
192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */,
192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */,
13B6C1A21C34225900D3FAF5 /* RCTURLUtilsTests.m */,
68FF44371CF6111500720EFD /* RCTBundleURLProviderTests.m */,
1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */,
@@ -1433,8 +1444,10 @@
1497CFAF1B21F5E400C1F8F2 /* RCTConvert_NSURLTests.m in Sources */,
1497CFAE1B21F5E400C1F8F2 /* RCTJSCExecutorTests.m in Sources */,
13129DD41C85F87C007D611C /* RCTModuleInitNotificationRaceTests.m in Sources */,
192F69B81E82409A008692C7 /* RCTAnimationUtilsTests.m in Sources */,
1497CFAD1B21F5E400C1F8F2 /* RCTBridgeTests.m in Sources */,
134CB92A1C85A38800265FA6 /* RCTModuleInitTests.m in Sources */,
192F69BA1E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m in Sources */,
1497CFB11B21F5E400C1F8F2 /* RCTEventDispatcherTests.m in Sources */,
1497CFB31B21F5E400C1F8F2 /* RCTUIManagerTests.m in Sources */,
13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */,
@@ -1445,6 +1458,7 @@
39AA31A41DC1DFDC000F7EBB /* RCTUnicodeDecodeTests.m in Sources */,
13B6C1A31C34225900D3FAF5 /* RCTURLUtilsTests.m in Sources */,
8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */,
192F69B91E82409A008692C7 /* RCTConvert_YGValueTests.m in Sources */,
BC9C03401DC9F1D600B1C635 /* RCTDevMenuTests.m in Sources */,
68FF44381CF6111500720EFD /* RCTBundleURLProviderTests.m in Sources */,
8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */,
@@ -1613,9 +1627,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)";
INFOPLIST_FILE = "$(SRCROOT)/UIExplorer/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited)";
LIBRARY_SEARCH_PATHS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = com.facebook.react.uiapp;
PRODUCT_NAME = UIExplorer;
@@ -1629,7 +1641,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = V9WTTPBFK9;
INFOPLIST_FILE = "$(SRCROOT)/UIExplorer/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited)";
LIBRARY_SEARCH_PATHS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = com.facebook.react.uiapp;
PRODUCT_NAME = UIExplorer;
@@ -1864,6 +1875,7 @@
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
@@ -1907,9 +1919,7 @@
WARNING_CFLAGS = (
"-Wextra",
"-Wall",
"-Wincompatible-pointer-types",
"-Wincompatible-pointer-types-discards-qualifiers",
"-Wshadow",
"-Wno-semicolon-before-method-body",
);
};
name = Debug;
@@ -1933,6 +1943,7 @@
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
@@ -1969,9 +1980,7 @@
WARNING_CFLAGS = (
"-Wextra",
"-Wall",
"-Wincompatible-pointer-types",
"-Wincompatible-pointer-types-discards-qualifiers",
"-Wshadow",
"-Wno-semicolon-before-method-body",
);
};
name = Release;

View File

@@ -0,0 +1,122 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import <XCTest/XCTest.h>
#import <RCTAnimation/RCTAnimationUtils.h>
@interface RCTAnimationUtilsTests : XCTestCase
@end
static CGFloat RCTSimpleInterpolation(CGFloat value, NSArray<NSNumber *> *inputRange, NSArray<NSNumber *> *outputRange) {
return RCTInterpolateValueInRange(value,
inputRange,
outputRange,
EXTRAPOLATE_TYPE_EXTEND,
EXTRAPOLATE_TYPE_EXTEND);
}
@implementation RCTAnimationUtilsTests
// RCTInterpolateValueInRange
- (void)testSimpleOneToOneMapping
{
NSArray<NSNumber *> *input = @[@0, @1];
NSArray<NSNumber *> *output = @[@0, @1];
XCTAssertEqual(RCTSimpleInterpolation(0, input, output), 0);
XCTAssertEqual(RCTSimpleInterpolation(0.5, input, output), 0.5);
XCTAssertEqual(RCTSimpleInterpolation(0.8, input, output), 0.8);
XCTAssertEqual(RCTSimpleInterpolation(1, input, output), 1);
}
- (void)testWiderOutputRange
{
NSArray<NSNumber *> *input = @[@0, @1];
NSArray<NSNumber *> *output = @[@100, @200];
XCTAssertEqual(RCTSimpleInterpolation(0, input, output), 100);
XCTAssertEqual(RCTSimpleInterpolation(0.5, input, output), 150);
XCTAssertEqual(RCTSimpleInterpolation(0.8, input, output), 180);
XCTAssertEqual(RCTSimpleInterpolation(1, input, output), 200);
}
- (void)testWiderInputRange
{
NSArray<NSNumber *> *input = @[@2000, @3000];
NSArray<NSNumber *> *output = @[@1, @2];
XCTAssertEqual(RCTSimpleInterpolation(2000, input, output), 1);
XCTAssertEqual(RCTSimpleInterpolation(2250, input, output), 1.25);
XCTAssertEqual(RCTSimpleInterpolation(2800, input, output), 1.8);
XCTAssertEqual(RCTSimpleInterpolation(3000, input, output), 2);
}
- (void)testManySegments
{
NSArray<NSNumber *> *input = @[@-1, @1, @5];
NSArray<NSNumber *> *output = @[@0, @10, @20];
XCTAssertEqual(RCTSimpleInterpolation(-1, input, output), 0);
XCTAssertEqual(RCTSimpleInterpolation(0, input, output), 5);
XCTAssertEqual(RCTSimpleInterpolation(1, input, output), 10);
XCTAssertEqual(RCTSimpleInterpolation(2, input, output), 12.5);
XCTAssertEqual(RCTSimpleInterpolation(5, input, output), 20);
}
- (void)testExtendExtrapolate
{
NSArray<NSNumber *> *input = @[@10, @20];
NSArray<NSNumber *> *output = @[@0, @1];
XCTAssertEqual(RCTSimpleInterpolation(30, input, output), 2);
XCTAssertEqual(RCTSimpleInterpolation(5, input, output), -0.5);
}
- (void)testClampExtrapolate
{
NSArray<NSNumber *> *input = @[@10, @20];
NSArray<NSNumber *> *output = @[@0, @1];
CGFloat value;
value = RCTInterpolateValueInRange(30,
input,
output,
EXTRAPOLATE_TYPE_CLAMP,
EXTRAPOLATE_TYPE_CLAMP);
XCTAssertEqual(value, 1);
value = RCTInterpolateValueInRange(5,
input,
output,
EXTRAPOLATE_TYPE_CLAMP,
EXTRAPOLATE_TYPE_CLAMP);
XCTAssertEqual(value, 0);
}
- (void)testIdentityExtrapolate
{
NSArray<NSNumber *> *input = @[@10, @20];
NSArray<NSNumber *> *output = @[@0, @1];
CGFloat value;
value = RCTInterpolateValueInRange(30,
input,
output,
EXTRAPOLATE_TYPE_IDENTITY,
EXTRAPOLATE_TYPE_IDENTITY);
XCTAssertEqual(value, 30);
value = RCTInterpolateValueInRange(5,
input,
output,
EXTRAPOLATE_TYPE_IDENTITY,
EXTRAPOLATE_TYPE_IDENTITY);
XCTAssertEqual(value, 5);
}
@end

View File

@@ -0,0 +1,666 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import <RCTAnimation/RCTNativeAnimatedNodesManager.h>
#import <RCTAnimation/RCTValueAnimatedNode.h>
#import <React/RCTUIManager.h>
static const NSTimeInterval FRAME_LENGTH = 1.0 / 60.0;
@interface RCTFakeDisplayLink : CADisplayLink
@end
@implementation RCTFakeDisplayLink
{
NSTimeInterval _timestamp;
}
- (instancetype)init
{
self = [super init];
if (self) {
_timestamp = 1124.1234143251; // Random
}
return self;
}
- (NSTimeInterval)timestamp
{
_timestamp += FRAME_LENGTH;
return _timestamp;
}
@end
@interface RCTFakeValueObserver : NSObject<RCTValueAnimatedNodeObserver>
@property (nonatomic, strong) NSMutableArray<NSNumber *> *calls;
@end
@implementation RCTFakeValueObserver
- (instancetype)init
{
self = [super init];
if (self) {
_calls = [NSMutableArray new];
}
return self;
}
- (void)animatedNode:(__unused RCTValueAnimatedNode *)node didUpdateValue:(CGFloat)value
{
[_calls addObject:@(value)];
}
@end
@interface RCTFakeEvent : NSObject<RCTEvent>
@end
@implementation RCTFakeEvent
{
NSArray *_arguments;
}
@synthesize eventName = _eventName;
@synthesize viewTag = _viewTag;
@synthesize coalescingKey = _coalescingKey;
- (instancetype)initWithName:(NSString *)name viewTag:(NSNumber *)viewTag arguments:(NSArray *)arguments
{
self = [super init];
if (self) {
_eventName = name;
_viewTag = viewTag;
_arguments = arguments;
}
return self;
}
- (NSArray *)arguments
{
return _arguments;
}
RCT_NOT_IMPLEMENTED(+ (NSString *)moduleDotMethod);
RCT_NOT_IMPLEMENTED(- (BOOL)canCoalesce);
RCT_NOT_IMPLEMENTED(- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent);
@end
static id RCTPropChecker(NSString *prop, NSNumber *value)
{
return [OCMArg checkWithBlock:^BOOL(NSDictionary<NSString *, NSNumber *> *props) {
BOOL match = fabs(props[prop].doubleValue - value.doubleValue) < FLT_EPSILON;
if (!match) {
NSLog(@"Props `%@` with value `%@` is not close to `%@`", prop, props[prop], value);
}
return match;
}];
}
@interface RCTNativeAnimatedNodesManagerTests : XCTestCase
@end
@implementation RCTNativeAnimatedNodesManagerTests
{
id _uiManager;
RCTNativeAnimatedNodesManager *_nodesManager;
RCTFakeDisplayLink *_displayLink;
}
- (void)setUp
{
[super setUp];
_uiManager = [OCMockObject niceMockForClass:[RCTUIManager class]];
_nodesManager = [[RCTNativeAnimatedNodesManager alloc] initWithUIManager:_uiManager];
_displayLink = [RCTFakeDisplayLink new];
}
/**
* Generates a simple animated nodes graph and attaches the props node to a given viewTag
* Parameter 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)
*/
- (void)createSimpleAnimatedView:(NSNumber *)viewTag withOpacity:(CGFloat)opacity
{
[_nodesManager createAnimatedNode:@1
config:@{@"type": @"value", @"value": @(opacity), @"offset": @0}];
[_nodesManager createAnimatedNode:@2
config:@{@"type": @"style", @"style": @{@"opacity": @1}}];
[_nodesManager createAnimatedNode:@3
config:@{@"type": @"props", @"props": @{@"style": @2}}];
[_nodesManager connectAnimatedNodes:@1 childTag:@2];
[_nodesManager connectAnimatedNodes:@2 childTag:@3];
[_nodesManager connectAnimatedNodeToView:@3 viewTag:viewTag viewName:@"UIView"];
}
- (void)testFramesAnimation
{
[self createSimpleAnimatedView:@1000 withOpacity:0];
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
endCallback:nil];
for (NSNumber *frame in frames) {
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
viewName:@"UIView"
props:RCTPropChecker(@"opacity", frame)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:@1000
viewName:@"UIView"
props:RCTPropChecker(@"opacity", @1)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testNodeValueListenerIfNotListening
{
NSNumber *nodeId = @1;
[self createSimpleAnimatedView:@1000 withOpacity:0];
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
RCTFakeValueObserver *observer = [RCTFakeValueObserver new];
[_nodesManager startListeningToAnimatedNodeValue:nodeId valueObserver:observer];
[_nodesManager startAnimatingNode:@1
nodeTag:nodeId
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
endCallback:nil];
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(observer.calls.count, 1UL);
XCTAssertEqualObjects(observer.calls[0], @0);
[_nodesManager stopListeningToAnimatedNodeValue:nodeId];
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(observer.calls.count, 1UL);
}
- (void)testNodeValueListenerIfListening
{
NSNumber *nodeId = @1;
[self createSimpleAnimatedView:@1000 withOpacity:0];
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
RCTFakeValueObserver *observer = [RCTFakeValueObserver new];
[_nodesManager startListeningToAnimatedNodeValue:nodeId valueObserver:observer];
[_nodesManager startAnimatingNode:@1
nodeTag:nodeId
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
endCallback:nil];
for (NSUInteger i = 0; i < frames.count; i++) {
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(observer.calls.count, i + 1);
XCTAssertEqualWithAccuracy(observer.calls[i].doubleValue, frames[i].doubleValue, FLT_EPSILON);
}
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(observer.calls.count, 7UL);
XCTAssertEqualObjects(observer.calls[6], @1);
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(observer.calls.count, 7UL);
}
- (void)testSpringAnimation
{
[self createSimpleAnimatedView:@1000 withOpacity:0];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"spring",
@"friction": @7,
@"tension": @40,
@"initialVelocity": @0,
@"toValue": @1,
@"restSpeedThreshold": @0.001,
@"restDisplacementThreshold": @0.001,
@"overshootClamping": @NO}
endCallback:nil];
BOOL wasGreaterThanOne = NO;
CGFloat previousValue = 0;
__block CGFloat currentValue;
[[[_uiManager stub] andDo:^(NSInvocation *invocation) {
__unsafe_unretained NSDictionary<NSString *, NSNumber *> *props;
[invocation getArgument:&props atIndex:4];
currentValue = props[@"opacity"].doubleValue;
}] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
// Run for 3 seconds.
for (NSUInteger i = 0; i < 3 * 60; i++) {
[_nodesManager stepAnimations:_displayLink];
if (currentValue > 1) {
wasGreaterThanOne = YES;
}
// Verify that animation step is relatively small.
XCTAssertLessThan(fabs(currentValue - previousValue), 0.1);
previousValue = currentValue;
}
// Verify that we've reach the final value at the end of animation.
XCTAssertEqual(previousValue, 1.0);
// Verify that value has reached some maximum value that is greater than the final value (bounce).
XCTAssertTrue(wasGreaterThanOne);
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testAnimationCallbackFinish
{
[self createSimpleAnimatedView:@1000 withOpacity:0];
NSArray<NSNumber *> *frames = @[@0, @1];
__block NSInteger endCallbackCalls = 0;
RCTResponseSenderBlock endCallback = ^(NSArray *response) {
endCallbackCalls++;
XCTAssertEqualObjects(response, @[@{@"finished": @YES}]);
};
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
endCallback:endCallback];
[_nodesManager stepAnimations:_displayLink];
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(endCallbackCalls, 0);
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(endCallbackCalls, 1);
[_nodesManager stepAnimations:_displayLink];
XCTAssertEqual(endCallbackCalls, 1);
}
/**
* Creates a following graph of nodes:
* Value(1, firstValue) ----> Add(3) ---> Style(4) ---> Props(5) ---> View(viewTag)
* |
* Value(2, secondValue) --+
*
* Add(3) node maps to a "translateX" attribute of the Style(4) node.
*/
- (void)createAnimatedGraphWithAdditionNode:(NSNumber *)viewTag
firstValue:(CGFloat)firstValue
secondValue:(CGFloat)secondValue
{
[_nodesManager createAnimatedNode:@1
config:@{@"type": @"value", @"value": @(firstValue), @"offset": @0}];
[_nodesManager createAnimatedNode:@2
config:@{@"type": @"value", @"value": @(secondValue), @"offset": @0}];
[_nodesManager createAnimatedNode:@3
config:@{@"type": @"addition", @"input": @[@1, @2]}];
[_nodesManager createAnimatedNode:@4
config:@{@"type": @"style", @"style": @{@"translateX": @3}}];
[_nodesManager createAnimatedNode:@5
config:@{@"type": @"props", @"props": @{@"style": @4}}];
[_nodesManager connectAnimatedNodes:@1 childTag:@3];
[_nodesManager connectAnimatedNodes:@2 childTag:@3];
[_nodesManager connectAnimatedNodes:@3 childTag:@4];
[_nodesManager connectAnimatedNodes:@4 childTag:@5];
[_nodesManager connectAnimatedNodeToView:@5 viewTag:viewTag viewName:@"UIView"];
}
- (void)testAdditionNode
{
NSNumber *viewTag = @50;
[self createAnimatedGraphWithAdditionNode:viewTag firstValue:100 secondValue:1000];
NSArray<NSNumber *> *frames = @[@0, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @101}
endCallback:nil];
[_nodesManager startAnimatingNode:@2
nodeTag:@2
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1010}
endCallback:nil];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1100)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1111)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1111)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
/**
* Verifies that views are updated properly when one of the addition input nodes has started animating
* while the other one has not.
*
* We expect that the output of the addition node will take the starting value of the second input
* node even though the node hasn't been connected to an active animation driver.
*/
- (void)testViewReceiveUpdatesIfOneOfAnimationHasntStarted
{
NSNumber *viewTag = @50;
[self createAnimatedGraphWithAdditionNode:viewTag firstValue:100 secondValue:1000];
NSArray<NSNumber *> *frames = @[@0, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @101}
endCallback:nil];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1100)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1101)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1101)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
/**
* Verifies that views are updated properly when one of the addition input nodes animation finishes
* before the other.
*
* We expect that the output of the addition node after one of the animation has finished will
* take the last value of the animated node and the view will receive updates up until the second
* animation is over.
*/
- (void)testViewReceiveUpdatesWhenOneOfAnimationHasFinished
{
NSNumber *viewTag = @50;
[self createAnimatedGraphWithAdditionNode:viewTag firstValue:100 secondValue:1000];
NSArray<NSNumber *> *firstFrames = @[@0, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": firstFrames, @"toValue": @200}
endCallback:nil];
NSArray<NSNumber *> *secondFrames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
[_nodesManager startAnimatingNode:@2
nodeTag:@2
config:@{@"type": @"frames", @"frames": secondFrames, @"toValue": @1010}
endCallback:nil];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1100)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
for (NSUInteger i = 1; i < secondFrames.count; i++) {
CGFloat expected = 1200.0 + secondFrames[i].doubleValue * 10.0;
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @(expected))];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @1210)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testMultiplicationNode
{
NSNumber *viewTag = @50;
[_nodesManager createAnimatedNode:@1
config:@{@"type": @"value", @"value": @1, @"offset": @0}];
[_nodesManager createAnimatedNode:@2
config:@{@"type": @"value", @"value": @5, @"offset": @0}];
[_nodesManager createAnimatedNode:@3
config:@{@"type": @"multiplication", @"input": @[@1, @2]}];
[_nodesManager createAnimatedNode:@4
config:@{@"type": @"style", @"style": @{@"translateX": @3}}];
[_nodesManager createAnimatedNode:@5
config:@{@"type": @"props", @"props": @{@"style": @4}}];
[_nodesManager connectAnimatedNodes:@1 childTag:@3];
[_nodesManager connectAnimatedNodes:@2 childTag:@3];
[_nodesManager connectAnimatedNodes:@3 childTag:@4];
[_nodesManager connectAnimatedNodes:@4 childTag:@5];
[_nodesManager connectAnimatedNodeToView:@5 viewTag:viewTag viewName:@"UIView"];
NSArray<NSNumber *> *frames = @[@0, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @2}
endCallback:nil];
[_nodesManager startAnimatingNode:@2
nodeTag:@2
config:@{@"type": @"frames", @"frames": frames, @"toValue": @10}
endCallback:nil];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @5)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @20)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"translateX", @20)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testHandleStoppingAnimation
{
[self createSimpleAnimatedView:@1000 withOpacity:0];
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
__block BOOL endCallbackCalled = NO;
RCTResponseSenderBlock endCallback = ^(NSArray *response) {
endCallbackCalled = YES;
XCTAssertEqualObjects(response, @[@{@"finished": @NO}]);
};
[_nodesManager startAnimatingNode:@404
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @1}
endCallback:endCallback];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[_nodesManager stopAnimation:@404];
XCTAssertEqual(endCallbackCalled, YES);
// Run "update" loop a few more times -> we expect no further updates nor callback calls to be
// triggered
for (NSUInteger i = 0; i < 5; i++) {
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
}
- (void)testInterpolationNode
{
NSNumber *viewTag = @50;
[_nodesManager createAnimatedNode:@1
config:@{@"type": @"value", @"value": @10, @"offset": @0}];
[_nodesManager createAnimatedNode:@2
config:@{@"type": @"interpolation",
@"inputRange": @[@10, @20],
@"outputRange": @[@0, @1],
@"extrapolateLeft": @"extend",
@"extrapolateRight": @"extend"}];
[_nodesManager createAnimatedNode:@3
config:@{@"type": @"style", @"style": @{@"opacity": @2}}];
[_nodesManager createAnimatedNode:@4
config:@{@"type": @"props", @"props": @{@"style": @3}}];
[_nodesManager connectAnimatedNodes:@1 childTag:@2];
[_nodesManager connectAnimatedNodes:@2 childTag:@3];
[_nodesManager connectAnimatedNodes:@3 childTag:@4];
[_nodesManager connectAnimatedNodeToView:@4 viewTag:viewTag viewName:@"UIView"];
NSArray<NSNumber *> *frames = @[@0, @0.2, @0.4, @0.6, @0.8, @1];
[_nodesManager startAnimatingNode:@1
nodeTag:@1
config:@{@"type": @"frames", @"frames": frames, @"toValue": @20}
endCallback:nil];
for (NSNumber *frame in frames) {
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"opacity", frame)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"opacity", @1)];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (id<RCTEvent>)createScrollEventWithTag:(NSNumber *)viewTag value:(CGFloat)value
{
// The event value is the 3rd argument.
NSArray *arguments = @[@1, @1, @{@"contentOffset": @{@"y": @(value)}}];
return [[RCTFakeEvent alloc] initWithName:@"topScroll"
viewTag:viewTag
arguments:arguments];
}
- (void)testNativeAnimatedEventDoUpdate
{
NSNumber *viewTag = @1000;
[self createSimpleAnimatedView:viewTag withOpacity:0];
[_nodesManager addAnimatedEventToView:viewTag
eventName:@"topScroll"
eventMapping:@{@"animatedValueTag": @1,
@"nativeEventPath": @[@"contentOffset", @"y"]}];
// Make sure that the update actually happened synchronously in `handleAnimatedEvent` and does
// not wait for the next animation loop.
[[_uiManager expect] synchronouslyUpdateViewOnUIThread:viewTag
viewName:@"UIView"
props:RCTPropChecker(@"opacity", @10)];
[_nodesManager handleAnimatedEvent:[self createScrollEventWithTag:viewTag value:10]];
[_uiManager verify];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager stepAnimations:_displayLink];
[_uiManager verify];
}
- (void)testNativeAnimatedEventDoNotUpdate
{
NSNumber *viewTag = @1000;
[self createSimpleAnimatedView:viewTag withOpacity:0];
[_nodesManager addAnimatedEventToView:viewTag
eventName:@"otherEvent"
eventMapping:@{@"animatedValueTag": @1,
@"nativeEventPath": @[@"contentOffset", @"y"]}];
[_nodesManager addAnimatedEventToView:@999
eventName:@"topScroll"
eventMapping:@{@"animatedValueTag": @1,
@"nativeEventPath": @[@"contentOffset", @"y"]}];
[[_uiManager reject] synchronouslyUpdateViewOnUIThread:OCMOCK_ANY viewName:OCMOCK_ANY props:OCMOCK_ANY];
[_nodesManager handleAnimatedEvent:[self createScrollEventWithTag:viewTag value:10]];
[_uiManager verify];
}
@end