// Copyright 2020-present the Material Components for iOS authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #import "MaterialButtons.h" #import "MDCFloatingButtonModeAnimator.h" #import "MDCFloatingButtonModeAnimatorDelegate.h" #import @interface MDCFloatingButtonModeAnimatorTests : XCTestCase @property(nonatomic, strong) MDCFloatingButtonModeAnimator *animator; @property(nonatomic, strong) UILabel *titleLabel; @property(nonatomic, strong) UIView *containerView; @property(nonatomic, strong) UIView *buttonView; @property(nonatomic) BOOL delegateDidAskToCommitLayoutChanges; @end static const CGRect kNormalFrame = (CGRect){{0, 0}, {100, 50}}; static const CGRect kExpandedFrame = (CGRect){{0, 0}, {200, 40}}; @implementation MDCFloatingButtonModeAnimatorTests - (void)setUp { [super setUp]; self.containerView = [[UIView alloc] init]; self.buttonView = [[UIView alloc] init]; self.titleLabel = [[UILabel alloc] init]; self.titleLabel.text = @"Some text"; self.buttonView.frame = kNormalFrame; self.containerView.frame = self.buttonView.bounds; self.containerView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); [self.buttonView addSubview:self.containerView]; [self.titleLabel sizeToFit]; self.titleLabel.center = CGPointMake(CGRectGetMidX(self.containerView.bounds), CGRectGetMidY(self.containerView.bounds)); self.titleLabel.autoresizingMask = (UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin); [self.containerView addSubview:self.titleLabel]; self.animator = [[MDCFloatingButtonModeAnimator alloc] initWithTitleLabel:self.titleLabel titleLabelContainerView:self.containerView]; self.delegateDidAskToCommitLayoutChanges = NO; } - (void)tearDown { self.titleLabel = nil; self.containerView = nil; self.buttonView = nil; self.animator = nil; self.delegateDidAskToCommitLayoutChanges = NO; [super tearDown]; } #pragma mark - MDCFloatingButtonModeAnimatorDelegate - (void)floatingButtonModeAnimatorCommitLayoutChanges:(MDCFloatingButtonModeAnimator *)modeAnimator mode:(MDCFloatingButtonMode)mode { self.delegateDidAskToCommitLayoutChanges = YES; [self commitMode:mode]; } #pragma mark - Defaults - (void)testDefaults { XCTAssertNil(self.animator.delegate); XCTAssertFalse(self.containerView.clipsToBounds); XCTAssertEqual([[self.titleLabel.layer animationKeys] count], 0); XCTAssertEqual([self.containerView.subviews count], 1); } #pragma mark - Container // The only effect the animator has on the container is to toggle clipsToBounds while animating. #pragma mark Non-animated - (void)testImmediateToNormalContainerDoesNotClip { // When [self.animator modeDidChange:MDCFloatingButtonModeNormal animated:NO animateAlongside:nil completion:nil]; // Then XCTAssertFalse(self.containerView.clipsToBounds); } - (void)testImmediateToExpandedContainerDoesNotClip { // When [self.animator modeDidChange:MDCFloatingButtonModeExpanded animated:NO animateAlongside:nil completion:nil]; // Then XCTAssertFalse(self.containerView.clipsToBounds); } #pragma mark Animated - (void)testAnimateToNormalContainerDoesClip { // When [self.animator modeDidChange:MDCFloatingButtonModeNormal animated:YES animateAlongside:nil completion:nil]; // Then XCTAssertTrue(self.containerView.clipsToBounds); } - (void)testAnimateToExpandedContainerDoesClip { // When [self.animator modeDidChange:MDCFloatingButtonModeExpanded animated:YES animateAlongside:nil completion:nil]; // Then XCTAssertTrue(self.containerView.clipsToBounds); } #pragma mark - Title label #pragma mark Non-animated - (void)testImmediateToExpandedDoesNotAskToCommitLayoutChanges { // Given self.animator.delegate = self; // When [self.animator modeDidChange:MDCFloatingButtonModeExpanded animated:NO animateAlongside:nil completion:nil]; // Then XCTAssertFalse(self.delegateDidAskToCommitLayoutChanges); } - (void)testImmediateToNormalDoesNotAskToCommitLayoutChanges { // Given self.animator.delegate = self; // When [self.animator modeDidChange:MDCFloatingButtonModeNormal animated:NO animateAlongside:nil completion:nil]; // Then XCTAssertFalse(self.delegateDidAskToCommitLayoutChanges); } #pragma mark Animated - (void)testAnimateToExpandedAsksToCommitLayoutChanges { // Given self.animator.delegate = self; // When [self.animator modeDidChange:MDCFloatingButtonModeExpanded animated:YES animateAlongside:nil completion:nil]; // Then XCTAssertTrue(self.delegateDidAskToCommitLayoutChanges); } - (void)testAnimateToNormalAsksToCommitLayoutChanges { // Given self.animator.delegate = self; // When [self.animator modeDidChange:MDCFloatingButtonModeExpanded animated:YES animateAlongside:nil completion:nil]; // Then XCTAssertTrue(self.delegateDidAskToCommitLayoutChanges); } - (void)testAnimateFromNormalToExpandedGeneratesLabelAnimations { // Given self.animator.delegate = self; // When [self.animator modeDidChange:MDCFloatingButtonModeExpanded animated:YES animateAlongside:nil completion:nil]; // Then NSSet *animatedKeyPaths = [self animatedKeyPathsForLayer:self.titleLabel.layer]; NSSet *expectedKeyPaths = [NSSet setWithObjects:@"position.y", @"opacity", nil]; XCTAssertEqualObjects(animatedKeyPaths, expectedKeyPaths); } - (void)testAnimateFromNormalToNormalDoesNotGenerateLabelAnimations { // Given self.animator.delegate = self; // When [self.animator modeDidChange:MDCFloatingButtonModeNormal animated:YES animateAlongside:nil completion:nil]; // Then XCTAssertEqual([[self.titleLabel.layer animationKeys] count], 0); } // Does not generate animations because it animates a replica view instead. - (void)testAnimateFromExpandedToNormalDoesNotGenerateLabelAnimations { // Given [self commitMode:MDCFloatingButtonModeExpanded]; self.animator.delegate = self; // When [self.animator modeDidChange:MDCFloatingButtonModeNormal animated:YES animateAlongside:nil completion:nil]; // Then XCTAssertEqual([[self.titleLabel.layer animationKeys] count], 0); } #pragma mark - Blocks #pragma mark Non-animated - (void)testImmediateToExpandedInvokesAnimateAlongsideAndCompletion { // When __block BOOL didInvokeAnimateAlongside = NO; __block BOOL didInvokeCompletion = NO; [self.animator modeDidChange:MDCFloatingButtonModeExpanded animated:NO animateAlongside:^{ didInvokeAnimateAlongside = YES; } completion:^(BOOL finished) { didInvokeCompletion = YES; }]; // Then XCTAssertTrue(didInvokeAnimateAlongside); XCTAssertTrue(didInvokeCompletion); } - (void)testImmediateToNormalInvokesAnimateAlongsideAndCompletion { // When __block BOOL didInvokeAnimateAlongside = NO; __block BOOL didInvokeCompletion = NO; [self.animator modeDidChange:MDCFloatingButtonModeNormal animated:NO animateAlongside:^{ didInvokeAnimateAlongside = YES; } completion:^(BOOL finished) { didInvokeCompletion = YES; }]; // Then XCTAssertTrue(didInvokeAnimateAlongside); XCTAssertTrue(didInvokeCompletion); } #pragma mark Animated - (void)testAnimateToExpandedInvokesAnimateAlongsideButNotCompletion { // When __block BOOL didInvokeAnimateAlongside = NO; __block BOOL didInvokeCompletion = NO; [self.animator modeDidChange:MDCFloatingButtonModeExpanded animated:YES animateAlongside:^{ didInvokeAnimateAlongside = YES; } completion:^(BOOL finished) { didInvokeCompletion = YES; }]; // Then XCTAssertTrue(didInvokeAnimateAlongside); XCTAssertFalse(didInvokeCompletion); } - (void)testAnimateToNormalInvokesAnimateAlongsideButNotCompletion { // When __block BOOL didInvokeAnimateAlongside = NO; __block BOOL didInvokeCompletion = NO; [self.animator modeDidChange:MDCFloatingButtonModeNormal animated:YES animateAlongside:^{ didInvokeAnimateAlongside = YES; } completion:^(BOOL finished) { didInvokeCompletion = YES; }]; // Then XCTAssertTrue(didInvokeAnimateAlongside); XCTAssertFalse(didInvokeCompletion); } #pragma mark - Helper methods - (NSSet *)animatedKeyPathsForLayer:(CALayer *)layer { NSMutableSet *animatedKeyPaths = [NSMutableSet set]; NSArray *animationKeys = [layer animationKeys]; for (NSString *key in animationKeys) { CAAnimation *animation = [layer animationForKey:key]; if ([animation isKindOfClass:[CABasicAnimation class]]) { CABasicAnimation *basicAnimation = (CABasicAnimation *)animation; [animatedKeyPaths addObject:basicAnimation.keyPath]; } } return animatedKeyPaths; } - (void)commitMode:(MDCFloatingButtonMode)mode { if (mode == MDCFloatingButtonModeNormal) { self.buttonView.frame = kNormalFrame; } else if (mode == MDCFloatingButtonModeExpanded) { self.buttonView.frame = kExpandedFrame; } [self.buttonView layoutIfNeeded]; } @end