From ee3e621f026e8a3228c618ef8f4fb2595cf58b71 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Mon, 18 Mar 2019 21:40:21 -0700 Subject: [PATCH] Remove timeout from add2app test for iOS (#28746) --- dev/integration_tests/ios_add2app/README.md | 23 ++- .../ios_add2app.xcodeproj/project.pbxproj | 2 + .../ios_add2app/DualFlutterViewController.h | 3 + .../ios_add2app/DualFlutterViewController.m | 21 ++- .../ios_add2app/HybridViewController.h | 2 + .../ios_add2app/HybridViewController.m | 10 +- .../ios_add2appTests/IntegrationTests.m | 168 +++++++++++++----- 7 files changed, 157 insertions(+), 72 deletions(-) diff --git a/dev/integration_tests/ios_add2app/README.md b/dev/integration_tests/ios_add2app/README.md index ee2b1511c1..78037466b6 100644 --- a/dev/integration_tests/ios_add2app/README.md +++ b/dev/integration_tests/ios_add2app/README.md @@ -6,16 +6,23 @@ interaction. The following functionality is currently implemented: -1. A regular iOS view controller (UIViewController), similar to the default `flutter create` template. -1. A FlutterViewController subclass that takes over full screen. Demos showing this both from a cold/fresh engine state and a warm engine state. +1. A regular iOS view controller (UIViewController), similar to the default + `flutter create` template (NativeViewController.m). +1. A FlutterViewController subclass that takes over full screen. Demos showing + this both from a cold/fresh engine state and a warm engine state + (FullScreenViewController.m). 1. A demo of pushing a FlutterViewController on as a child view. -1. A demo of showing both the native and the Flutter views using a platform channel to to interact with each other. -1. A demo of showing two FlutterViewControllers simultaneously. +1. A demo of showing both the native and the Flutter views using a platform + channel to to interact with each other (HybridViewController.m). +1. A demo of showing two FlutterViewControllers simultaneously + (DualViewController.m). -A few key things are tested here: +A few key things are tested here (IntegrationTests.m): -1. The ability to pre-warm the engine and attach/detatch a ViewController from it. -1. The ability to simultaneously run two instances of the engine. +1. The ability to pre-warm the engine and attach/detatch a ViewController from + it. 1. The ability to use platform channels to communicate between views. -1. That a FlutterViewController can be freed when no longer in use. +1. The ability to simultaneously run two instances of the engine. +1. That a FlutterViewController can be freed when no longer in use (also tested + from FlutterViewControllerTests.m). 1. That a FlutterEngine can be freed when no longer in use. \ No newline at end of file diff --git a/dev/integration_tests/ios_add2app/ios_add2app.xcodeproj/project.pbxproj b/dev/integration_tests/ios_add2app/ios_add2app.xcodeproj/project.pbxproj index fdd45880b8..8ac6d94d7d 100644 --- a/dev/integration_tests/ios_add2app/ios_add2app.xcodeproj/project.pbxproj +++ b/dev/integration_tests/ios_add2app/ios_add2app.xcodeproj/project.pbxproj @@ -500,6 +500,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -559,6 +560,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; diff --git a/dev/integration_tests/ios_add2app/ios_add2app/DualFlutterViewController.h b/dev/integration_tests/ios_add2app/ios_add2app/DualFlutterViewController.h index a56d4e2d41..db75303f40 100644 --- a/dev/integration_tests/ios_add2app/ios_add2app/DualFlutterViewController.h +++ b/dev/integration_tests/ios_add2app/ios_add2app/DualFlutterViewController.h @@ -8,6 +8,9 @@ NS_ASSUME_NONNULL_BEGIN @interface DualFlutterViewController : UIViewController +@property (readonly, strong, nonatomic) FlutterViewController* topFlutterViewController; +@property (readonly, strong, nonatomic) FlutterViewController* bottomFlutterViewController; + @end NS_ASSUME_NONNULL_END diff --git a/dev/integration_tests/ios_add2app/ios_add2app/DualFlutterViewController.m b/dev/integration_tests/ios_add2app/ios_add2app/DualFlutterViewController.m index 56e0838c80..0d2d559a5d 100644 --- a/dev/integration_tests/ios_add2app/ios_add2app/DualFlutterViewController.m +++ b/dev/integration_tests/ios_add2app/ios_add2app/DualFlutterViewController.m @@ -23,19 +23,18 @@ stackView.layoutMarginsRelativeArrangement = YES; [self.view addSubview:stackView]; + _topFlutterViewController = [[FlutterViewController alloc] init]; + _bottomFlutterViewController= [[FlutterViewController alloc] init]; - FlutterViewController* topFlutterViewController = [[FlutterViewController alloc] init]; - FlutterViewController* bottomFlutterViewController= [[FlutterViewController alloc] init]; + [_topFlutterViewController setInitialRoute:@"marquee_green"]; + [self addChildViewController:_topFlutterViewController]; + [stackView addArrangedSubview:_topFlutterViewController.view]; + [_topFlutterViewController didMoveToParentViewController:self]; - [topFlutterViewController setInitialRoute:@"marquee_green"]; - [self addChildViewController:topFlutterViewController]; - [stackView addArrangedSubview:topFlutterViewController.view]; - [topFlutterViewController didMoveToParentViewController:self]; - - [bottomFlutterViewController setInitialRoute:@"marquee_purple"]; - [self addChildViewController:bottomFlutterViewController]; - [stackView addArrangedSubview:bottomFlutterViewController.view]; - [topFlutterViewController didMoveToParentViewController:self]; + [_bottomFlutterViewController setInitialRoute:@"marquee_purple"]; + [self addChildViewController:_bottomFlutterViewController]; + [stackView addArrangedSubview:_bottomFlutterViewController.view]; + [_bottomFlutterViewController didMoveToParentViewController:self]; } @end diff --git a/dev/integration_tests/ios_add2app/ios_add2app/HybridViewController.h b/dev/integration_tests/ios_add2app/ios_add2app/HybridViewController.h index ac852e6df8..71bf48db16 100644 --- a/dev/integration_tests/ios_add2app/ios_add2app/HybridViewController.h +++ b/dev/integration_tests/ios_add2app/ios_add2app/HybridViewController.h @@ -10,6 +10,8 @@ NS_ASSUME_NONNULL_BEGIN @interface HybridViewController : UIViewController +@property (readonly, strong, nonatomic) FlutterViewController* flutterViewController; + @end NS_ASSUME_NONNULL_END diff --git a/dev/integration_tests/ios_add2app/ios_add2app/HybridViewController.m b/dev/integration_tests/ios_add2app/ios_add2app/HybridViewController.m index 5023bb260f..7dc85f726a 100644 --- a/dev/integration_tests/ios_add2app/ios_add2app/HybridViewController.m +++ b/dev/integration_tests/ios_add2app/ios_add2app/HybridViewController.m @@ -43,7 +43,7 @@ static NSString *_kPing = @"ping"; [stackView addArrangedSubview:nativeViewController.view]; [nativeViewController didMoveToParentViewController:self]; - FlutterViewController *flutterViewController = + _flutterViewController = [[FlutterViewController alloc] initWithEngine:[self engine] nibName:nil bundle:nil]; @@ -51,11 +51,11 @@ static NSString *_kPing = @"ping"; _messageChannel = [[FlutterBasicMessageChannel alloc] initWithName:_kChannel - binaryMessenger:flutterViewController + binaryMessenger:_flutterViewController codec:[FlutterStringCodec sharedInstance]]; - [self addChildViewController:flutterViewController]; - [stackView addArrangedSubview:flutterViewController.view]; - [flutterViewController didMoveToParentViewController:self]; + [self addChildViewController:_flutterViewController]; + [stackView addArrangedSubview:_flutterViewController.view]; + [_flutterViewController didMoveToParentViewController:self]; __weak NativeViewController *weakNativeViewController = nativeViewController; [_messageChannel setMessageHandler:^(id message, FlutterReply reply) { diff --git a/dev/integration_tests/ios_add2app/ios_add2appTests/IntegrationTests.m b/dev/integration_tests/ios_add2app/ios_add2appTests/IntegrationTests.m index 677f5e955c..6c6b36ec5e 100644 --- a/dev/integration_tests/ios_add2app/ios_add2appTests/IntegrationTests.m +++ b/dev/integration_tests/ios_add2app/ios_add2appTests/IntegrationTests.m @@ -6,12 +6,39 @@ #import #import "../ios_add2app/AppDelegate.h" -#import "../ios_add2app/MainViewController.h" +#import "../ios_add2app/DualFlutterViewController.h" #import "../ios_add2app/FullScreenViewController.h" +#import "../ios_add2app/MainViewController.h" +#import "../ios_add2app/HybridViewController.h" -static void waitForInitialFlutterRender() { - // TODO(dnfield,jamesderlin): actually sync with Flutter rendering. - CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false); +static void waitForFlutterSemanticsTree(FlutterViewController *viewController) { + int tries = 10; + double delay = 1.0; + + // ensureSemanticsEnabled is a synchronous call, but only ensures that the + // semantics tree will be built on a subsequent frame (as opposed to being + // available at time it returns). + // To actually get the tree, we have to wait for the FlutterSemanticsUpdate + // notification, which lets us know that a semantics tree has been built; + // but we cannot block the main thread while waiting (so we use + // CFRunLoopRunInMode). + + __block BOOL semanticsAvailable = NO; + __block id observer = [[NSNotificationCenter defaultCenter] + addObserverForName:@"FlutterSemanticsUpdate" + object:viewController + queue:nil + usingBlock:^(NSNotification *notification) { + semanticsAvailable = YES; + [[NSNotificationCenter defaultCenter] removeObserver:observer]; + }]; + [viewController.engine ensureSemanticsEnabled]; + while (semanticsAvailable == NO && tries != 0) { + CFRunLoopRunInMode(kCFRunLoopDefaultMode, delay, false); + tries--; + [viewController.engine ensureSemanticsEnabled]; + } + GREYAssertTrue(semanticsAvailable, @"Semantics Tree did not build!"); } @interface FlutterTests : XCTestCase @@ -33,48 +60,72 @@ static void waitForInitialFlutterRender() { - (void)testFullScreenCanPop { [[EarlGrey selectElementWithMatcher:grey_keyWindow()] - assertWithMatcher:grey_sufficientlyVisible()]; + assertWithMatcher:grey_sufficientlyVisible()]; [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Full Screen (Cold)")] - performAction:grey_tap()]; + performAction:grey_tap()]; - waitForInitialFlutterRender(); - - __weak FlutterViewController* weakViewController; + __weak FlutterViewController *weakViewController; @autoreleasepool { - UINavigationController* navController = - (UINavigationController*)((AppDelegate*)[ - [UIApplication sharedApplication] - delegate]) + UINavigationController *navController = + (UINavigationController *)((AppDelegate *) + [[UIApplication sharedApplication] + delegate]) .window.rootViewController; weakViewController = - (FullScreenViewController*)navController.visibleViewController; - GREYAssertNotNil(weakViewController, @"Expected non-nil FullScreenViewController."); + (FullScreenViewController *)navController.visibleViewController; + waitForFlutterSemanticsTree(weakViewController); + GREYAssertNotNil(weakViewController, + @"Expected non-nil FullScreenViewController."); } - [[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"POP")] performAction:grey_tap()]; - waitForInitialFlutterRender(); + [[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"POP")] + performAction:grey_tap()]; + // EarlGrey v1 isn't good at detecting this yet - 2.0 will be able to do it + int tries = 10; + double delay = 1.0; + while (weakViewController != nil && tries != 0) { + CFRunLoopRunInMode(kCFRunLoopDefaultMode, delay, false); + tries--; + } [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Native iOS View")] assertWithMatcher:grey_sufficientlyVisible()]; - GREYAssertNil(weakViewController, @"Expected FullScreenViewController to be deallocated."); + GREYAssertNil(weakViewController, + @"Expected FullScreenViewController to be deallocated."); } - (void)testDualFlutterView { [[EarlGrey selectElementWithMatcher:grey_keyWindow()] - assertWithMatcher:grey_sufficientlyVisible()]; + assertWithMatcher:grey_sufficientlyVisible()]; - [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Dual Flutter View (Cold)")] - performAction:grey_tap()]; + [[EarlGrey + selectElementWithMatcher:grey_buttonTitle(@"Dual Flutter View (Cold)")] + performAction:grey_tap()]; - waitForInitialFlutterRender(); + @autoreleasepool { + UINavigationController *navController = + (UINavigationController *)((AppDelegate *) + [[UIApplication sharedApplication] + delegate]) + .window.rootViewController; + DualFlutterViewController *viewController = + (DualFlutterViewController *)navController.visibleViewController; + GREYAssertNotNil(viewController, + @"Expected non-nil DualFlutterViewController."); + waitForFlutterSemanticsTree(viewController.topFlutterViewController); + waitForFlutterSemanticsTree(viewController.bottomFlutterViewController); + } // Verify that there are two Flutter views with the expected marquee text. - [[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"This is Marquee")] atIndex:0] - assertWithMatcher:grey_notNil()]; - [[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"This is Marquee")] atIndex:1] - assertWithMatcher:grey_notNil()]; + [[[EarlGrey + selectElementWithMatcher:grey_accessibilityLabel(@"This is Marquee")] + atIndex:0] assertWithMatcher:grey_notNil()]; + [[[EarlGrey + selectElementWithMatcher:grey_accessibilityLabel(@"This is Marquee")] + atIndex:1] assertWithMatcher:grey_notNil()]; - [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Back")] performAction:grey_tap()]; + [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Back")] + performAction:grey_tap()]; [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Native iOS View")] assertWithMatcher:grey_sufficientlyVisible()]; @@ -82,11 +133,23 @@ static void waitForInitialFlutterRender() { - (void)testHybridView { [[EarlGrey selectElementWithMatcher:grey_keyWindow()] - assertWithMatcher:grey_sufficientlyVisible()]; + assertWithMatcher:grey_sufficientlyVisible()]; - [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Hybrid View (Warm)")] performAction:grey_tap()]; + [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Hybrid View (Warm)")] + performAction:grey_tap()]; - waitForInitialFlutterRender(); + @autoreleasepool { + UINavigationController *navController = + (UINavigationController *)((AppDelegate *) + [[UIApplication sharedApplication] + delegate]) + .window.rootViewController; + HybridViewController *viewController = + (HybridViewController *)navController.visibleViewController; + GREYAssertNotNil(viewController.flutterViewController, + @"Expected non-nil FlutterViewController."); + waitForFlutterSemanticsTree(viewController.flutterViewController); + } [self validateCountsFlutter:@"Platform" count:0]; [self validateCountsPlatform:@"Flutter" count:_flutterWarmEngineTaps]; @@ -94,45 +157,54 @@ static void waitForInitialFlutterRender() { static const int platformTapCount = 4; static const int flutterTapCount = 6; - for (int i = _flutterWarmEngineTaps; i < flutterTapCount; i++, _flutterWarmEngineTaps++) { - [[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"Increment via Flutter")] - performAction:grey_tap()]; + for (int i = _flutterWarmEngineTaps; i < flutterTapCount; + i++, _flutterWarmEngineTaps++) { + [[EarlGrey selectElementWithMatcher:grey_accessibilityLabel( + @"Increment via Flutter")] + performAction:grey_tap()]; } [self validateCountsFlutter:@"Platform" count:0]; [self validateCountsPlatform:@"Flutter" count:_flutterWarmEngineTaps]; for (int i = 0; i < platformTapCount; i++) { - [[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"Increment via iOS")] - performAction:grey_tap()]; + [[EarlGrey + selectElementWithMatcher:grey_accessibilityLabel(@"Increment via iOS")] + performAction:grey_tap()]; } [self validateCountsFlutter:@"Platform" count:platformTapCount]; [self validateCountsPlatform:@"Flutter" count:_flutterWarmEngineTaps]; - [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Back")] performAction:grey_tap()]; + [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Back")] + performAction:grey_tap()]; [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Native iOS View")] - assertWithMatcher:grey_sufficientlyVisible()]; + assertWithMatcher:grey_sufficientlyVisible()]; } -/** Validates that the text labels showing the number of button taps match the expected counts. */ -- (void)validateCountsFlutter:(NSString*)labelPrefix - count:(int)flutterCount { - NSString* flutterCountStr = - [NSString stringWithFormat:@"%@ button tapped %d times.", labelPrefix, flutterCount]; +/** Validates that the text labels showing the number of button taps match the + * expected counts. */ +- (void)validateCountsFlutter:(NSString *)labelPrefix count:(int)flutterCount { + NSString *flutterCountStr = + [NSString stringWithFormat:@"%@ button tapped %d times.", labelPrefix, + flutterCount]; - // TODO(https://github.com/flutter/flutter/issues/17988): Flutter doesn't expose accessibility - // IDs, so the best we can do is to search for an element with the text we expect. + // TODO(https://github.com/flutter/flutter/issues/17988): Flutter doesn't + // expose accessibility IDs, so the best we can do is to search for an element + // with the text we expect. [[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(flutterCountStr)] assertWithMatcher:grey_sufficientlyVisible()]; } -- (void)validateCountsPlatform:(NSString*)labelPrefix count:(int)platformCount { - NSString* platformCountStr = - [NSString stringWithFormat:@"%@ button tapped %d times.", labelPrefix, platformCount]; +- (void)validateCountsPlatform:(NSString *)labelPrefix + count:(int)platformCount { + NSString *platformCountStr = + [NSString stringWithFormat:@"%@ button tapped %d times.", labelPrefix, + platformCount]; [[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"counter_on_iOS")] - assertWithMatcher:grey_text(platformCountStr)] assertWithMatcher:grey_sufficientlyVisible()]; + assertWithMatcher:grey_text(platformCountStr)] + assertWithMatcher:grey_sufficientlyVisible()]; } @end