// Copyright 2019-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 #import #import "MDCAvailability.h" #import "MDCButton.h" #import "MDCAlertController.h" #import "UIFont+MaterialScalable.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wprivate-header" #import "MDCDialogShadowedView.h" #import "UIColor+MaterialDynamic.h" #import "MDCSnapshotTestCase.h" #import "UIView+MDCSnapshot.h" #pragma clang diagnostic pop #import "MDCAlertController+ButtonForAction.h" #import "MDCFontScaler.h" NS_ASSUME_NONNULL_BEGIN static NSDictionary *CustomScalingCurve(void) { static NSDictionary *scalingCurve; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ scalingCurve = @{ UIContentSizeCategoryExtraSmall : @99, UIContentSizeCategorySmall : @98, UIContentSizeCategoryMedium : @97, UIContentSizeCategoryLarge : @96, UIContentSizeCategoryExtraLarge : @95, UIContentSizeCategoryExtraExtraLarge : @94, UIContentSizeCategoryExtraExtraExtraLarge : @93, UIContentSizeCategoryAccessibilityMedium : @92, UIContentSizeCategoryAccessibilityLarge : @91, UIContentSizeCategoryAccessibilityExtraLarge : @90, UIContentSizeCategoryAccessibilityExtraExtraLarge : @89, UIContentSizeCategoryAccessibilityExtraExtraExtraLarge : @88 }; }); return scalingCurve; } /** A test fake window that allows overriding its @c traitCollection. */ @interface MDCAlertControllerCustomTraitCollectionTestsWindowFake : UIWindow /** Set to override the value of @c traitCollection. */ @property(nonatomic, strong) UITraitCollection *traitCollectionOverride; @end @implementation MDCAlertControllerCustomTraitCollectionTestsWindowFake - (UITraitCollection *)traitCollection { return self.traitCollectionOverride ?: [super traitCollection]; } @end /** A @c MDCAlertController test fake to override the @c traitCollection to test for dynamic type. */ @interface AlertControllerCustomTraitCollectionSnapshotTestFake : MDCAlertController @property(nonatomic, strong) UITraitCollection *traitCollectionOverride; @end @implementation AlertControllerCustomTraitCollectionSnapshotTestFake - (UITraitCollection *)traitCollection { return self.traitCollectionOverride ?: [super traitCollection]; } @end /** An @c MDCDialogShadowedView test fake to override the @c traitCollection to test. */ @interface ShadowViewCustomTraitCollectionSnapshotTestFake : MDCDialogShadowedView @property(nonatomic, strong) UITraitCollection *traitCollectionOverride; @end @implementation ShadowViewCustomTraitCollectionSnapshotTestFake - (UITraitCollection *)traitCollection { return self.traitCollectionOverride ?: [super traitCollection]; } @end @interface MDCAlertControllerCustomTraitCollectionTests : MDCSnapshotTestCase @property(nonatomic, strong, nullable) AlertControllerCustomTraitCollectionSnapshotTestFake *alertController; @end @implementation MDCAlertControllerCustomTraitCollectionTests - (void)setUp { [super setUp]; // Uncomment below to recreate all the goldens (or add the following line to the specific // test you wish to recreate the golden for). // self.recordMode = YES; self.alertController = [[AlertControllerCustomTraitCollectionSnapshotTestFake alloc] init]; self.alertController.title = @"Material"; self.alertController.message = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " @"ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation " @"ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in " @"reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur " @"sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id " @"est laborum."; MDCAlertAction *fakeAction = [MDCAlertAction actionWithTitle:@"Foo" handler:^(MDCAlertAction *action){ }]; [self.alertController addAction:fakeAction]; MDCFontScaler *titleFontScaler = [MDCFontScaler scalerForMaterialTextStyle:MDCTextStyleSubtitle1]; UIFont *titleFont = [UIFont fontWithName:@"Zapfino" size:14]; titleFont = [titleFontScaler scaledFontWithFont:titleFont]; titleFont = [titleFont mdc_scaledFontAtDefaultSize]; self.alertController.titleFont = titleFont; MDCFontScaler *messageFontScaler = [MDCFontScaler scalerForMaterialTextStyle:MDCTextStyleBody2]; UIFont *messageFont = [UIFont fontWithName:@"Zapfino" size:14]; messageFont = [messageFontScaler scaledFontWithFont:messageFont]; messageFont = [messageFont mdc_scaledFontAtDefaultSize]; self.alertController.messageFont = messageFont; MDCFontScaler *buttonFontScaler = [MDCFontScaler scalerForMaterialTextStyle:MDCTextStyleButton]; UIFont *buttonFont = [UIFont fontWithName:@"Zapfino" size:14]; buttonFont = [buttonFontScaler scaledFontWithFont:buttonFont]; buttonFont = [buttonFont mdc_scaledFontAtDefaultSize]; for (MDCAlertAction *action in self.alertController.actions) { [[self.alertController buttonForAction:action] setTitleFont:buttonFont forState:UIControlStateNormal]; } self.alertController.view.bounds = CGRectMake(0, 0, 300, 300); } - (void)tearDown { self.alertController = nil; [super tearDown]; } - (void)generateSnapshotAndVerifyForView:(UIView *)view { [view layoutIfNeeded]; UIView *snapshotView = [view mdc_addToBackgroundView]; [self snapshotVerifyView:snapshotView]; } /** Used to set the @c UIContentSizeCategory on an @c MDCAlertController. */ - (void)setAlertControllerContentSizeCategory:(UIContentSizeCategory)sizeCategory { UITraitCollection *traitCollection = [[UITraitCollection alloc] init]; traitCollection = [UITraitCollection traitCollectionWithPreferredContentSizeCategory:sizeCategory]; self.alertController.traitCollectionOverride = traitCollection; } #pragma mark - Dynamic Type /** Tests the original MDCTypography behavior for Dynamic Type. @note The output depends on the host simulator and is equal to @c testSystemFontScaledWhenScaledFontUnavailableForContentSizeAXXXL as a result. */ - (void)testSystemFontScaledWhenScaledFontUnavailableForContentSizeExtraSmall { // Given UIFont *originalFont = [UIFont fontWithName:@"Zapfino" size:20]; UIFontMetrics *fontMetrics = [UIFontMetrics metricsForTextStyle:UIFontTextStyleHeadline]; self.alertController.messageFont = [fontMetrics scaledFontForFont:originalFont]; self.alertController.titleFont = [fontMetrics scaledFontForFont:originalFont]; for (MDCAlertAction *action in self.alertController.actions) { [[self.alertController buttonForAction:action] setTitleFont:originalFont forState:UIControlStateNormal]; } [self setAlertControllerContentSizeCategory:UIContentSizeCategoryExtraSmall]; // When [self.alertController loadViewIfNeeded]; self.alertController.adjustsFontForContentSizeCategory = YES; // Then [self generateSnapshotAndVerifyForView:self.alertController.view]; } /** Tests the original MDCTypography behavior for Dynamic Type. @note The output depends on the host simulator and is equal to @c testSystemFontScaledWhenScaledFontUnavailableForContentSizeExtraSmall as a result. */ - (void)testSystemFontScaledWhenScaledFontUnavailableForContentSizeAXXXL { // Given // Although the font is initialized with point size 1, the MDCTypography behavior will select // a fixed point size for the font at the current UIContentSizeCategory (of the host app), // which is not 1. UIFont *originalFont = [UIFont fontWithName:@"Zapfino" size:20]; UIFontMetrics *fontMetrics = [UIFontMetrics metricsForTextStyle:UIFontTextStyleHeadline]; self.alertController.messageFont = [fontMetrics scaledFontForFont:originalFont]; self.alertController.titleFont = [fontMetrics scaledFontForFont:originalFont]; for (MDCAlertAction *action in self.alertController.actions) { [[self.alertController buttonForAction:action] setTitleFont:originalFont forState:UIControlStateNormal]; } [self setAlertControllerContentSizeCategory:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]; // When [self.alertController loadViewIfNeeded]; self.alertController.adjustsFontForContentSizeCategory = YES; // Then [self generateSnapshotAndVerifyForView:self.alertController.view]; } /** Tests behavior when a font generated from a FontScaler is provided. */ - (void)testFontScalerFontScaledForContentSizeExtraSmall { // Given UIFont *originalFont = [UIFont fontWithName:@"Zapfino" size:20]; UIFontMetrics *fontMetrics = [UIFontMetrics metricsForTextStyle:UIFontTextStyleHeadline]; self.alertController.messageFont = [fontMetrics scaledFontForFont:originalFont]; self.alertController.titleFont = [fontMetrics scaledFontForFont:originalFont]; for (MDCAlertAction *action in self.alertController.actions) { [[self.alertController buttonForAction:action] setTitleFont:originalFont forState:UIControlStateNormal]; } [self setAlertControllerContentSizeCategory:UIContentSizeCategoryExtraSmall]; // When [self.alertController loadViewIfNeeded]; self.alertController.adjustsFontForContentSizeCategory = YES; // Then [self generateSnapshotAndVerifyForView:self.alertController.view]; } /** Tests behavior when a font generated from a FontScaler is provided. */ - (void)testFontScalerFontScaledForContentSizeAXXXL { // Given UIFont *originalFont = [UIFont fontWithName:@"Zapfino" size:1]; // Simulates a font scaler by providing scaling curve dictionary. originalFont.mdc_scalingCurve = CustomScalingCurve(); self.alertController.messageFont = originalFont; self.alertController.titleFont = originalFont; for (MDCAlertAction *action in self.alertController.actions) { [[self.alertController buttonForAction:action] setTitleFont:originalFont forState:UIControlStateNormal]; } [self setAlertControllerContentSizeCategory:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]; // When [self.alertController loadViewIfNeeded]; self.alertController.adjustsFontForContentSizeCategory = YES; // Then [self generateSnapshotAndVerifyForView:self.alertController.view]; } /** Test that @c adjustsFontForContentSizeCategory will scale an appropriate font to a larger size when the preferred content size category increases. */ - (void)testAdjustsFontForContentSizeUpscalesUIFontMetricsFontsForSizeCategoryAXXXL { // Given UIFontMetrics *bodyMetrics = [UIFontMetrics metricsForTextStyle:UIFontTextStyleBody]; UITraitCollection *extraSmallTraits = [UITraitCollection traitCollectionWithPreferredContentSizeCategory:UIContentSizeCategoryExtraSmall]; UIFont *titleFont = [UIFont fontWithName:@"Zapfino" size:20]; XCTAssertNotNil(titleFont); titleFont = [bodyMetrics scaledFontForFont:titleFont compatibleWithTraitCollection:extraSmallTraits]; self.alertController.titleFont = titleFont; UIFont *messageFont = [UIFont fontWithName:@"Zapfino" size:15]; messageFont = [bodyMetrics scaledFontForFont:messageFont compatibleWithTraitCollection:extraSmallTraits]; self.alertController.messageFont = messageFont; UIFont *buttonFont = [UIFont fontWithName:@"Zapfino" size:20]; buttonFont = [bodyMetrics scaledFontForFont:buttonFont compatibleWithTraitCollection:extraSmallTraits]; for (MDCAlertAction *action in self.alertController.actions) { MDCButton *button = [self.alertController buttonForAction:action]; button.titleLabel.font = buttonFont; } self.alertController.adjustsFontForContentSizeCategory = YES; [self.alertController loadViewIfNeeded]; // The initial size is calculated without constraints, so start the view bounds there. CGSize alertSize = self.alertController.preferredContentSize; self.alertController.view.bounds = CGRectMake(0, 0, alertSize.width, alertSize.height); // Create a window so the Alert's view can inherit the trait environment. MDCAlertControllerCustomTraitCollectionTestsWindowFake *window = [[MDCAlertControllerCustomTraitCollectionTestsWindowFake alloc] init]; [window makeKeyWindow]; window.hidden = NO; [window addSubview:self.alertController.view]; // When window.traitCollectionOverride = [UITraitCollection traitCollectionWithPreferredContentSizeCategory: UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]; [window traitCollectionDidChange:nil]; // Recalculates the preferredContentSize of the AlertController. [self.alertController.view layoutIfNeeded]; alertSize = self.alertController.preferredContentSize; window.bounds = CGRectMake(0, 0, alertSize.width, alertSize.height); self.alertController.view.frame = window.bounds; // Then // Can't add a UIWindow to a UIView, so just screenshot the window directly. [window layoutIfNeeded]; [self snapshotVerifyView:window]; } /** Test that @c adjustsFontForContentSizeCategory will scale an appropriate font to a smaller size when the preferred content size category decreases. */ - (void)testAdjustsFontForContentSizeDownscalesUIFontMetricsFontsForSizeCategoryXS { // Given UIFontMetrics *bodyMetrics = [UIFontMetrics metricsForTextStyle:UIFontTextStyleBody]; UITraitCollection *aXXXLTraits = [UITraitCollection traitCollectionWithPreferredContentSizeCategory: UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]; UIFont *titleFont = [UIFont fontWithName:@"Zapfino" size:20]; XCTAssertNotNil(titleFont); titleFont = [bodyMetrics scaledFontForFont:titleFont compatibleWithTraitCollection:aXXXLTraits]; self.alertController.titleFont = titleFont; UIFont *messageFont = [UIFont fontWithName:@"Zapfino" size:15]; messageFont = [bodyMetrics scaledFontForFont:messageFont compatibleWithTraitCollection:aXXXLTraits]; self.alertController.messageFont = messageFont; UIFont *buttonFont = [UIFont fontWithName:@"Zapfino" size:20]; buttonFont = [bodyMetrics scaledFontForFont:buttonFont compatibleWithTraitCollection:aXXXLTraits]; for (MDCAlertAction *action in self.alertController.actions) { MDCButton *button = [self.alertController buttonForAction:action]; button.titleLabel.font = buttonFont; } self.alertController.adjustsFontForContentSizeCategory = YES; [self.alertController loadViewIfNeeded]; // The initial size is calculated without constraints, so start the view bounds there. CGSize alertSize = self.alertController.preferredContentSize; self.alertController.view.bounds = CGRectMake(0, 0, alertSize.width, alertSize.height); // Create a window so the Alert's view can inherit the trait environment. MDCAlertControllerCustomTraitCollectionTestsWindowFake *window = [[MDCAlertControllerCustomTraitCollectionTestsWindowFake alloc] init]; [window makeKeyWindow]; window.hidden = NO; [window addSubview:self.alertController.view]; // When window.traitCollectionOverride = [UITraitCollection traitCollectionWithPreferredContentSizeCategory:UIContentSizeCategoryExtraSmall]; [window traitCollectionDidChange:nil]; // Recalculates the preferredContentSize of the AlertController. [self.alertController.view layoutIfNeeded]; alertSize = self.alertController.preferredContentSize; window.bounds = CGRectMake(0, 0, alertSize.width, alertSize.height); self.alertController.view.frame = window.bounds; // Then // Can't add a UIWindow to a UIView, so just screenshot the window directly. [window layoutIfNeeded]; [self snapshotVerifyView:window]; } #pragma mark - Dynamic Color - (void)testDynamicColorSupport { #if MDC_AVAILABLE_SDK_IOS(13_0) if (@available(iOS 13.0, *)) { // Given UIColor *titleColor = [UIColor colorWithUserInterfaceStyleDarkColor:UIColor.greenColor defaultColor:UIColor.blackColor]; UIColor *messageColor = [UIColor colorWithUserInterfaceStyleDarkColor:UIColor.purpleColor defaultColor:UIColor.blackColor]; UIColor *backgroundColor = [UIColor colorWithUserInterfaceStyleDarkColor:UIColor.blueColor defaultColor:UIColor.blackColor]; self.alertController.titleColor = titleColor; self.alertController.messageColor = messageColor; self.alertController.backgroundColor = backgroundColor; // When self.alertController.traitCollectionOverride = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]; // Then UIView *snapshotView = [self.alertController.view mdc_addToBackgroundViewWithInsets:UIEdgeInsetsMake(50, 50, 50, 50)]; [self snapshotVerifyViewForIOS13:snapshotView]; } #endif // MDC_AVAILABLE_SDK_IOS(13_0) } - (void)testDynamicColorSupportForTrackingView { #if MDC_AVAILABLE_SDK_IOS(13_0) if (@available(iOS 13.0, *)) { // Given UIColor *shadowColor = [UIColor colorWithUserInterfaceStyleDarkColor:UIColor.greenColor defaultColor:UIColor.blackColor]; ShadowViewCustomTraitCollectionSnapshotTestFake *trackingView = [[ShadowViewCustomTraitCollectionSnapshotTestFake alloc] init]; trackingView.frame = CGRectMake(0, 0, 100, 200); trackingView.shadowColor = shadowColor; trackingView.backgroundColor = UIColor.whiteColor; // When trackingView.traitCollectionOverride = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]; [trackingView layoutIfNeeded]; // Then UIView *snapshotView = [trackingView mdc_addToBackgroundViewWithInsets:UIEdgeInsetsMake(50, 50, 50, 50)]; [self snapshotVerifyViewForIOS13:snapshotView]; } #endif // MDC_AVAILABLE_SDK_IOS(13_0) } @end NS_ASSUME_NONNULL_END