ComponentKitTests/CKComponentAnimationsTests.mm (356 lines of code) (raw):
/*
* Copyright (c) 2014-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.
*
*/
#import <XCTest/XCTest.h>
#import <ComponentKit/CKCasting.h>
#import <ComponentKit/CKBuildComponent.h>
#import <ComponentKit/CKComponentEvents.h>
#import <ComponentKit/CKComponentLayout.h>
#import <ComponentKit/CKComponentScopeRootFactory.h>
#import <ComponentKit/CKComponentSubclass.h>
#import <ComponentKit/CKComponentAnimations.h>
#import <ComponentKit/CKCompositeComponent.h>
#import <ComponentKit/CKThreadLocalComponentScope.h>
#import <ComponentKitTestHelpers/CKComponentTestRootScope.h>
#import "CKComponentAnimationsEquality.h"
#import "CKComponentTestCase.h"
@interface CKComponentAnimationsTests_LayoutDiffing: CKComponentTestCase
@end
@interface ComponentWithInitialMountAnimations: CKComponent
+ (instancetype)newWithInitialMountAnimations:(std::vector<CKComponentAnimation>)animations;
@end
@interface ComponentWithAnimationsFromPreviousComponent: CKComponent
+ (instancetype)newWithAnimations:(std::vector<CKComponentAnimation>)animations
fromPreviousComponent:(CKComponent *const)component;
@end
@interface ComponentWithAnimationOnFinalUnmount: CKComponent
+ (instancetype)newWithAnimation:(CAAnimation *const)animation;
@end
@interface CompositeComponentWithScope: CKCompositeComponent
@end
const auto sizeRange = CKSizeRange {CGSizeZero, {INFINITY, INFINITY}};
@implementation CKComponentAnimationsTests_LayoutDiffing
- (void)test_WhenPreviousTreeIsEmpty_ReturnsAllComponentsWithInitialMountAnimationsAsAppeared
{
const auto r = CKComponentScopeRootWithDefaultPredicates(nil, nil);
const auto bcr = CKBuildComponent(r, {}, ^{
return CK::CompositeComponentBuilder().component([ComponentWithInitialMountAnimations new]).build();
});
const auto c = CK::objCForceCast<CKCompositeComponent>(bcr.component);
const auto l = CKComputeRootComponentLayout(c, sizeRange, nil);
const auto diff = CK::animatedComponentsBetweenLayouts(l, {});
XCTAssertFalse(diff.isEmpty());
const auto expectedDiff = CK::ComponentTreeDiff {
.appearedComponents = {CK::objCForceCast<CKComponent>(c.child)},
};
XCTAssert(diff == expectedDiff);
}
- (void)test_WhenPreviousTreeIsNotEmpty_ReturnsOnlyNewComponentsWithInitialMountAnimationsAsAppeared
{
const auto bcr = CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{
return CK::CompositeComponentBuilder().component([ComponentWithInitialMountAnimations new]).build();
});
const auto l = CKComputeRootComponentLayout(bcr.component, sizeRange, nil);
const auto bcr2 = CKBuildComponent(bcr.scopeRoot, {}, ^{
return CK::CompositeComponentBuilder().component([ComponentWithInitialMountAnimations new]).build();
});
const auto l2 = CKComputeRootComponentLayout(bcr2.component, sizeRange, nil);
const auto diff = CK::animatedComponentsBetweenLayouts(l2, l);
XCTAssert(diff.isEmpty());
}
- (void)test_WhenPreviousTreeIsNotEmpty_ReturnsComponentsWithChangeAnimationsAsUpdated
{
const auto bcr = CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{
return CK::CompositeComponentBuilder().component([ComponentWithAnimationsFromPreviousComponent new]).build();
});
const auto c = CK::objCForceCast<CKCompositeComponent>(bcr.component);
const auto l = CKComputeRootComponentLayout(c, sizeRange, nil);
const auto bcr2 = CKBuildComponent(bcr.scopeRoot, {}, ^{
return CK::CompositeComponentBuilder().component([ComponentWithAnimationsFromPreviousComponent new]).build();
});
const auto c2 = CK::objCForceCast<CKCompositeComponent>(bcr2.component);
const auto l2 = CKComputeRootComponentLayout(c2, sizeRange, nil);
const auto diff = CK::animatedComponentsBetweenLayouts(l2, l);
XCTAssertFalse(diff.isEmpty());
const auto expectedDiff = CK::ComponentTreeDiff {
.updatedComponents = {{CK::objCForceCast<CKComponent>(c.child), CK::objCForceCast<CKComponent>(c2.child)}},
};
XCTAssert(diff == expectedDiff);
}
- (void)test_WhenPreviousTreeHasTheSameComponents_DoesNotReturnThemAsUpdated
{
const auto bcr = CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{
return CK::CompositeComponentBuilder().component([ComponentWithAnimationsFromPreviousComponent new]).build();
});
const auto l = CKComputeRootComponentLayout(bcr.component, sizeRange, nil);
const auto diff = CK::animatedComponentsBetweenLayouts(l, l);
XCTAssert(diff.isEmpty());
}
- (void)test_WhenPreviousTreeIsNotEmpty_ReturnsOnlyDisappearedComponentsWithDisappearAnimation
{
__block ComponentWithAnimationOnFinalUnmount *c;
const auto bcr = CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{
c = [ComponentWithAnimationOnFinalUnmount new];
return CK::CompositeComponentBuilder().component([CompositeComponentWithScope newWithComponent:c]).build();
});
const auto l = CKComputeRootComponentLayout(bcr.component, sizeRange, nil);
const auto bcr2 = CKBuildComponent(bcr.scopeRoot, {}, ^{
// Compared to the previous tree there are two components that have disappeared: CompositeComponentWithScope and
// ComponentWithDisappearAnimation. However, we should get only the latter in ComponentTreeDiff::disappearedComponents
// since the former doesn't define any disappear animation.
return CK::CompositeComponentBuilder().component([CKComponent new]).build();
});
const auto l2 = CKComputeRootComponentLayout(bcr2.component, sizeRange, nil);
const auto diff = CK::animatedComponentsBetweenLayouts(l2, l);
XCTAssertFalse(diff.isEmpty());
const auto expectedDiff = CK::ComponentTreeDiff {
.disappearedComponents = {c},
};
XCTAssert(diff == expectedDiff);
}
@end
@interface CKComponentAnimationsTests: CKComponentTestCase
@end
@implementation CKComponentAnimationsTests
- (void)test_WhenThereAreNoComponentsToAnimate_ThereAreNoAnimations
{
const auto as = CK::animationsForComponents({}, [UIView new]);
const auto expected = CKComponentAnimations::AnimationsByComponentMap {};
XCTAssert(RC::animationsAreEqual(as.animationsOnInitialMount(), expected));
}
- (void)test_ForAllAppearedComponents_AnimationsOnInitialMountAreCollected
{
CKComponentTestRootScope testScope;
const auto a1 = CKComponentAnimation([CKComponent new], [CAAnimation new]);
const auto c1 = [ComponentWithInitialMountAnimations newWithInitialMountAnimations:{a1}];
const auto a2 = CKComponentAnimation([CKComponent new], [CAAnimation new]);
const auto c2 = [ComponentWithInitialMountAnimations newWithInitialMountAnimations:{a2}];
const auto diff = CK::ComponentTreeDiff {
.appearedComponents = {
c1,
c2,
},
};
const auto as = animationsForComponents(diff, [UIView new]);
const auto expected = CKComponentAnimations::AnimationsByComponentMap {
{c1, {a1}},
{c2, {a2}},
};
XCTAssert(RC::animationsAreEqual(as.animationsOnInitialMount(), expected));
}
- (void)test_ForAllUpdatedComponents_AnimationsFromPreviousComponentAreCollected
{
CKComponentTestRootScope testScope;
const auto a1 = CKComponentAnimation([CKComponent new], [CAAnimation new]);
const auto pc1 = [CKComponent new];
const auto c1 = [ComponentWithAnimationsFromPreviousComponent newWithAnimations:{a1} fromPreviousComponent:pc1];
const auto a2 = CKComponentAnimation([CKComponent new], [CAAnimation new]);
const auto pc2 = [CKComponent new];
const auto c2 = [ComponentWithAnimationsFromPreviousComponent newWithAnimations:{a2} fromPreviousComponent:pc2];
const auto componentPairs = std::vector<CK::ComponentTreeDiff::Pair> {
{pc1, c1},
{pc2, c2},
};
const auto diff = CK::ComponentTreeDiff {
.updatedComponents = componentPairs,
};
const auto as = animationsForComponents(diff, [UIView new]);
const auto expected = CKComponentAnimations::AnimationsByComponentMap {
{c1, {a1}},
{c2, {a2}},
};
XCTAssert(RC::animationsAreEqual(as.animationsFromPreviousComponent(), expected));
}
- (void)test_ForAllDisappearedComponents_AnimationsOnFinalUnmountAreCollected
{
const auto a1 = [CAAnimation new];
const auto c1 = [ComponentWithAnimationOnFinalUnmount newWithAnimation:a1];
const auto a2 = [CAAnimation new];
const auto c2 = [ComponentWithAnimationOnFinalUnmount newWithAnimation:a2];
const auto diff = CK::ComponentTreeDiff {
.disappearedComponents = {
c1,
c2,
},
};
const auto as = animationsForComponents(diff, [UIView new]);
XCTAssert(as.animationsOnFinalUnmount().size() == diff.disappearedComponents.size());
}
- (void)test_DefaultInitialised_IsEmpty
{
XCTAssert(CKComponentAnimations {}.isEmpty());
}
- (void)test_IfHasInitialAnimations_IsNotEmpty
{
CKComponentTestRootScope testScope;
const auto a1 = CKComponentAnimation([CKComponent new], [CAAnimation new]);
const auto c1 = [ComponentWithInitialMountAnimations newWithInitialMountAnimations:{a1}];
const auto as = CKComponentAnimations {
{
{c1, {a1}},
},
{},
{}
};
XCTAssertFalse(as.isEmpty());
}
- (void)test_IfHasAnimationsFromPreviousComponent_IsNotEmpty
{
CKComponentTestRootScope testScope;
const auto a1 = CKComponentAnimation([CKComponent new], [CAAnimation new]);
const auto c1 = [ComponentWithInitialMountAnimations newWithInitialMountAnimations:{a1}];
const auto as = CKComponentAnimations {
{},
{
{c1, {a1}},
},
{}
};
XCTAssertFalse(as.isEmpty());
}
- (void)test_IfComponentHasNoInitialAnimations_IsEmpty
{
CKComponentTestRootScope testScope;
const auto diff = CK::ComponentTreeDiff {
.appearedComponents = {
[ComponentWithInitialMountAnimations newWithInitialMountAnimations:{}],
},
};
const auto as = animationsForComponents(diff, [UIView new]);
XCTAssert(as.isEmpty());
}
- (void)test_IfComponentHasNoAnimationsFromPreviousComponent_IsEmpty
{
CKComponentTestRootScope testScope;
const auto pc1 = [CKComponent new];
const auto c1 = [ComponentWithAnimationsFromPreviousComponent newWithAnimations:{} fromPreviousComponent:pc1];
const auto diff = CK::ComponentTreeDiff {
.updatedComponents = {
{pc1, c1},
}
};
const auto as = animationsForComponents(diff, [UIView new]);
XCTAssert(as.isEmpty());
}
- (void)test_IfComponentHasNoFinalUnmountAnimations_IsEmpty
{
const auto diff = CK::ComponentTreeDiff {
.disappearedComponents = {
[ComponentWithAnimationOnFinalUnmount newWithAnimation:nil],
},
};
const auto as = animationsForComponents(diff, [UIView new]);
XCTAssert(as.isEmpty());
}
@end
@implementation ComponentWithInitialMountAnimations {
std::vector<CKComponentAnimation> _animations;
}
+ (instancetype)new
{
return [self newWithInitialMountAnimations:{}];
}
+ (instancetype)newWithInitialMountAnimations:(std::vector<CKComponentAnimation>)animations
{
CKComponentScope s(self);
const auto c = [super new];
c->_animations = std::move(animations);
return c;
}
- (std::vector<CKComponentAnimation>)animationsOnInitialMount { return _animations; }
@end
@implementation ComponentWithAnimationsFromPreviousComponent{
std::vector<CKComponentAnimation> _animations;
CKComponent *_previousComponent;
}
+ (instancetype)new
{
return [self newWithAnimations:{} fromPreviousComponent:nil];
}
+ (instancetype)newWithAnimations:(std::vector<CKComponentAnimation>)animations
fromPreviousComponent:(CKComponent *const)component
{
CKComponentScope s(self);
const auto c = [super new];
c->_animations = std::move(animations);
c->_previousComponent = component;
return c;
}
- (std::vector<CKComponentAnimation>)animationsFromPreviousComponent:(CKComponent *)previousComponent
{
if (previousComponent == _previousComponent) {
return _animations;
} else {
return {};
};
}
@end
@implementation ComponentWithAnimationOnFinalUnmount
{
CAAnimation *_animation;
}
+ (instancetype)new
{
CKComponentScope s(self);
return [self newWithAnimation:nil];
}
+ (instancetype)newWithAnimation:(CAAnimation *const)animation
{
const auto c = [super new];
if (c) {
c->_animation = animation;
}
return c;
}
- (std::vector<CKComponentFinalUnmountAnimation>)animationsOnFinalUnmount
{
if (!_animation) {
return {};
} else {
return {{self, _animation}};
}
}
@end
@implementation CompositeComponentWithScope
+ (instancetype)newWithComponent:(CKComponent *)component
{
CKComponentScope s(self);
return [super newWithComponent:component];
}
@end
@interface CKComponentAnimationTests : CKComponentTestCase
@end
@implementation CKComponentAnimationTests
- (void)test_WhenInitialisedWithComponentAndAnimation_CallsCompletionFromCleanup
{
__block auto numCompletionCalls = 0;
auto const a = CKComponentAnimation {
[CKComponent new],
[CAAnimation new],
nil,
^{ ++numCompletionCalls; }
};
a.cleanup(nil);
XCTAssertEqual(numCompletionCalls, 1);
}
- (void)test_WhenInitialisedWithFinalUnmountAnimation_CallsCompletionFromCleanup
{
__block auto numCompletionCalls = 0;
auto const a = CKComponentAnimation {
CKComponentFinalUnmountAnimation {
[CKComponent new], [CAAnimation new], ^{ ++numCompletionCalls; }
},
[UIView new]
};
a.cleanup(nil);
XCTAssertEqual(numCompletionCalls, 1);
}
- (void)test_WhenInitialisedWithHooks_CallsCompletionFromCleanup
{
__block auto numCompletionCalls = 0;
auto const a = CKComponentAnimation {
CKComponentAnimationHooks {
^id(){ return nil; },
^id(id){ return nil; },
^(id){}
},
^{ ++numCompletionCalls; }
};
a.cleanup(nil);
XCTAssertEqual(numCompletionCalls, 1);
}
@end