Unverified Commit ee3e621f authored by Dan Field's avatar Dan Field Committed by GitHub

Remove timeout from add2app test for iOS (#28746)

parent 5e27ebbe
...@@ -6,16 +6,23 @@ interaction. ...@@ -6,16 +6,23 @@ interaction.
The following functionality is currently implemented: The following functionality is currently implemented:
1. A regular iOS view controller (UIViewController), similar to the default `flutter create` template. 1. A regular iOS view controller (UIViewController), similar to the default
1. A FlutterViewController subclass that takes over full screen. Demos showing this both from a cold/fresh engine state and a warm engine state. `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 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 both the native and the Flutter views using a platform
1. A demo of showing two FlutterViewControllers simultaneously. 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 pre-warm the engine and attach/detatch a ViewController from
1. The ability to simultaneously run two instances of the engine. it.
1. The ability to use platform channels to communicate between views. 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. 1. That a FlutterEngine can be freed when no longer in use.
\ No newline at end of file
...@@ -500,6 +500,7 @@ ...@@ -500,6 +500,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
...@@ -559,6 +560,7 @@ ...@@ -559,6 +560,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_BITCODE = NO;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
......
...@@ -8,6 +8,9 @@ NS_ASSUME_NONNULL_BEGIN ...@@ -8,6 +8,9 @@ NS_ASSUME_NONNULL_BEGIN
@interface DualFlutterViewController : UIViewController @interface DualFlutterViewController : UIViewController
@property (readonly, strong, nonatomic) FlutterViewController* topFlutterViewController;
@property (readonly, strong, nonatomic) FlutterViewController* bottomFlutterViewController;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END
...@@ -23,19 +23,18 @@ ...@@ -23,19 +23,18 @@
stackView.layoutMarginsRelativeArrangement = YES; stackView.layoutMarginsRelativeArrangement = YES;
[self.view addSubview:stackView]; [self.view addSubview:stackView];
_topFlutterViewController = [[FlutterViewController alloc] init];
FlutterViewController* topFlutterViewController = [[FlutterViewController alloc] init]; _bottomFlutterViewController= [[FlutterViewController alloc] init];
FlutterViewController* bottomFlutterViewController= [[FlutterViewController alloc] init];
[_topFlutterViewController setInitialRoute:@"marquee_green"];
[topFlutterViewController setInitialRoute:@"marquee_green"]; [self addChildViewController:_topFlutterViewController];
[self addChildViewController:topFlutterViewController]; [stackView addArrangedSubview:_topFlutterViewController.view];
[stackView addArrangedSubview:topFlutterViewController.view]; [_topFlutterViewController didMoveToParentViewController:self];
[topFlutterViewController didMoveToParentViewController:self];
[_bottomFlutterViewController setInitialRoute:@"marquee_purple"];
[bottomFlutterViewController setInitialRoute:@"marquee_purple"]; [self addChildViewController:_bottomFlutterViewController];
[self addChildViewController:bottomFlutterViewController]; [stackView addArrangedSubview:_bottomFlutterViewController.view];
[stackView addArrangedSubview:bottomFlutterViewController.view]; [_bottomFlutterViewController didMoveToParentViewController:self];
[topFlutterViewController didMoveToParentViewController:self];
} }
@end @end
...@@ -10,6 +10,8 @@ NS_ASSUME_NONNULL_BEGIN ...@@ -10,6 +10,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface HybridViewController : UIViewController<NativeViewControllerDelegate> @interface HybridViewController : UIViewController<NativeViewControllerDelegate>
@property (readonly, strong, nonatomic) FlutterViewController* flutterViewController;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END
...@@ -43,7 +43,7 @@ static NSString *_kPing = @"ping"; ...@@ -43,7 +43,7 @@ static NSString *_kPing = @"ping";
[stackView addArrangedSubview:nativeViewController.view]; [stackView addArrangedSubview:nativeViewController.view];
[nativeViewController didMoveToParentViewController:self]; [nativeViewController didMoveToParentViewController:self];
FlutterViewController *flutterViewController = _flutterViewController =
[[FlutterViewController alloc] initWithEngine:[self engine] [[FlutterViewController alloc] initWithEngine:[self engine]
nibName:nil nibName:nil
bundle:nil]; bundle:nil];
...@@ -51,11 +51,11 @@ static NSString *_kPing = @"ping"; ...@@ -51,11 +51,11 @@ static NSString *_kPing = @"ping";
_messageChannel = [[FlutterBasicMessageChannel alloc] _messageChannel = [[FlutterBasicMessageChannel alloc]
initWithName:_kChannel initWithName:_kChannel
binaryMessenger:flutterViewController binaryMessenger:_flutterViewController
codec:[FlutterStringCodec sharedInstance]]; codec:[FlutterStringCodec sharedInstance]];
[self addChildViewController:flutterViewController]; [self addChildViewController:_flutterViewController];
[stackView addArrangedSubview:flutterViewController.view]; [stackView addArrangedSubview:_flutterViewController.view];
[flutterViewController didMoveToParentViewController:self]; [_flutterViewController didMoveToParentViewController:self];
__weak NativeViewController *weakNativeViewController = nativeViewController; __weak NativeViewController *weakNativeViewController = nativeViewController;
[_messageChannel setMessageHandler:^(id message, FlutterReply reply) { [_messageChannel setMessageHandler:^(id message, FlutterReply reply) {
......
...@@ -6,12 +6,39 @@ ...@@ -6,12 +6,39 @@
#import <XCTest/XCTest.h> #import <XCTest/XCTest.h>
#import "../ios_add2app/AppDelegate.h" #import "../ios_add2app/AppDelegate.h"
#import "../ios_add2app/MainViewController.h" #import "../ios_add2app/DualFlutterViewController.h"
#import "../ios_add2app/FullScreenViewController.h" #import "../ios_add2app/FullScreenViewController.h"
#import "../ios_add2app/MainViewController.h"
static void waitForInitialFlutterRender() { #import "../ios_add2app/HybridViewController.h"
// 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<NSObject> 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 @interface FlutterTests : XCTestCase
...@@ -38,43 +65,67 @@ static void waitForInitialFlutterRender() { ...@@ -38,43 +65,67 @@ static void waitForInitialFlutterRender() {
[[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Full Screen (Cold)")] [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Full Screen (Cold)")]
performAction:grey_tap()]; performAction:grey_tap()];
waitForInitialFlutterRender(); __weak FlutterViewController *weakViewController;
__weak FlutterViewController* weakViewController;
@autoreleasepool { @autoreleasepool {
UINavigationController* navController = UINavigationController *navController =
(UINavigationController*)((AppDelegate*)[ (UINavigationController *)((AppDelegate *)
[UIApplication sharedApplication] [[UIApplication sharedApplication]
delegate]) delegate])
.window.rootViewController; .window.rootViewController;
weakViewController = weakViewController =
(FullScreenViewController*)navController.visibleViewController; (FullScreenViewController *)navController.visibleViewController;
GREYAssertNotNil(weakViewController, @"Expected non-nil FullScreenViewController."); waitForFlutterSemanticsTree(weakViewController);
GREYAssertNotNil(weakViewController,
@"Expected non-nil FullScreenViewController.");
} }
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"POP")] performAction:grey_tap()]; [[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"POP")]
waitForInitialFlutterRender(); 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")] [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Native iOS View")]
assertWithMatcher:grey_sufficientlyVisible()]; assertWithMatcher:grey_sufficientlyVisible()];
GREYAssertNil(weakViewController, @"Expected FullScreenViewController to be deallocated."); GREYAssertNil(weakViewController,
@"Expected FullScreenViewController to be deallocated.");
} }
- (void)testDualFlutterView { - (void)testDualFlutterView {
[[EarlGrey selectElementWithMatcher:grey_keyWindow()] [[EarlGrey selectElementWithMatcher:grey_keyWindow()]
assertWithMatcher:grey_sufficientlyVisible()]; assertWithMatcher:grey_sufficientlyVisible()];
[[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Dual Flutter View (Cold)")] [[EarlGrey
selectElementWithMatcher:grey_buttonTitle(@"Dual Flutter View (Cold)")]
performAction:grey_tap()]; 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. // Verify that there are two Flutter views with the expected marquee text.
[[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"This is Marquee")] atIndex:0] [[[EarlGrey
assertWithMatcher:grey_notNil()]; selectElementWithMatcher:grey_accessibilityLabel(@"This is Marquee")]
[[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"This is Marquee")] atIndex:1] atIndex:0] assertWithMatcher:grey_notNil()];
assertWithMatcher:grey_notNil()]; [[[EarlGrey
selectElementWithMatcher:grey_accessibilityLabel(@"This is Marquee")]
[[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Back")] performAction:grey_tap()]; atIndex:1] assertWithMatcher:grey_notNil()];
[[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Back")]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Native iOS View")] [[EarlGrey selectElementWithMatcher:grey_buttonTitle(@"Native iOS View")]
assertWithMatcher:grey_sufficientlyVisible()]; assertWithMatcher:grey_sufficientlyVisible()];
...@@ -84,9 +135,21 @@ static void waitForInitialFlutterRender() { ...@@ -84,9 +135,21 @@ static void waitForInitialFlutterRender() {
[[EarlGrey selectElementWithMatcher:grey_keyWindow()] [[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 validateCountsFlutter:@"Platform" count:0];
[self validateCountsPlatform:@"Flutter" count:_flutterWarmEngineTaps]; [self validateCountsPlatform:@"Flutter" count:_flutterWarmEngineTaps];
...@@ -94,8 +157,10 @@ static void waitForInitialFlutterRender() { ...@@ -94,8 +157,10 @@ static void waitForInitialFlutterRender() {
static const int platformTapCount = 4; static const int platformTapCount = 4;
static const int flutterTapCount = 6; static const int flutterTapCount = 6;
for (int i = _flutterWarmEngineTaps; i < flutterTapCount; i++, _flutterWarmEngineTaps++) { for (int i = _flutterWarmEngineTaps; i < flutterTapCount;
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"Increment via Flutter")] i++, _flutterWarmEngineTaps++) {
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(
@"Increment via Flutter")]
performAction:grey_tap()]; performAction:grey_tap()];
} }
...@@ -103,36 +168,43 @@ static void waitForInitialFlutterRender() { ...@@ -103,36 +168,43 @@ static void waitForInitialFlutterRender() {
[self validateCountsPlatform:@"Flutter" count:_flutterWarmEngineTaps]; [self validateCountsPlatform:@"Flutter" count:_flutterWarmEngineTaps];
for (int i = 0; i < platformTapCount; i++) { for (int i = 0; i < platformTapCount; i++) {
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"Increment via iOS")] [[EarlGrey
selectElementWithMatcher:grey_accessibilityLabel(@"Increment via iOS")]
performAction:grey_tap()]; performAction:grey_tap()];
} }
[self validateCountsFlutter:@"Platform" count:platformTapCount]; [self validateCountsFlutter:@"Platform" count:platformTapCount];
[self validateCountsPlatform:@"Flutter" count:_flutterWarmEngineTaps]; [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")] [[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. */ /** Validates that the text labels showing the number of button taps match the
- (void)validateCountsFlutter:(NSString*)labelPrefix * expected counts. */
count:(int)flutterCount { - (void)validateCountsFlutter:(NSString *)labelPrefix count:(int)flutterCount {
NSString* flutterCountStr = NSString *flutterCountStr =
[NSString stringWithFormat:@"%@ button tapped %d times.", labelPrefix, flutterCount]; [NSString stringWithFormat:@"%@ button tapped %d times.", labelPrefix,
flutterCount];
// TODO(https://github.com/flutter/flutter/issues/17988): Flutter doesn't expose accessibility // TODO(https://github.com/flutter/flutter/issues/17988): Flutter doesn't
// IDs, so the best we can do is to search for an element with the text we expect. // 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)] [[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(flutterCountStr)]
assertWithMatcher:grey_sufficientlyVisible()]; assertWithMatcher:grey_sufficientlyVisible()];
} }
- (void)validateCountsPlatform:(NSString*)labelPrefix count:(int)platformCount { - (void)validateCountsPlatform:(NSString *)labelPrefix
NSString* platformCountStr = count:(int)platformCount {
[NSString stringWithFormat:@"%@ button tapped %d times.", labelPrefix, platformCount]; NSString *platformCountStr =
[NSString stringWithFormat:@"%@ button tapped %d times.", labelPrefix,
platformCount];
[[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"counter_on_iOS")] [[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"counter_on_iOS")]
assertWithMatcher:grey_text(platformCountStr)] assertWithMatcher:grey_sufficientlyVisible()]; assertWithMatcher:grey_text(platformCountStr)]
assertWithMatcher:grey_sufficientlyVisible()];
} }
@end @end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment