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