Remove timeout from add2app test for iOS (#28746)
This commit is contained in:
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,6 +10,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface HybridViewController : UIViewController<NativeViewControllerDelegate>
|
||||
|
||||
@property (readonly, strong, nonatomic) FlutterViewController* flutterViewController;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -6,12 +6,39 @@
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#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<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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user