diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index df376b5534fdd..f1ee0a951d585 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -780,22 +780,16 @@ - (void)deleteBackward { [self replaceRange:_selectedTextRange withText:@""]; } -@end - -/** - * Hides `FlutterTextInputView` from iOS accessibility system so it - * does not show up twice, once where it is in the `UIView` hierarchy, - * and a second time as part of the `SemanticsObject` hierarchy. - */ -@interface FlutterTextInputViewAccessibilityHider : UIView { -} - -@end - -@implementation FlutterTextInputViewAccessibilityHider { -} - - (BOOL)accessibilityElementsHidden { + // We are hiding this accessibility element. + // There are 2 accessible elements involved in text entry in 2 different parts of the view + // hierarchy. This `FlutterTextInputView` is injected at the top of key window. We use this as a + // `UITextInput` protocol to bridge text edit events between Flutter and iOS. + // + // We also create ur own custom `UIAccessibilityElements` tree with our `SemanticsObject` to + // mimic the semantics tree from Flutter. We want the text field to be represented as a + // `TextInputSemanticsObject` in that `SemanticsObject` tree rather than in this + // `FlutterTextInputView` bridge which doesn't appear above a text field from the Flutter side. return YES; } @@ -806,7 +800,6 @@ @interface FlutterTextInputPlugin () @property(nonatomic, retain) FlutterTextInputView* nonAutofillSecureInputView; @property(nonatomic, retain) NSMutableArray* inputViews; @property(nonatomic, assign) FlutterTextInputView* activeView; -@property(nonatomic, retain) FlutterTextInputViewAccessibilityHider* inputHider; @end @implementation FlutterTextInputPlugin @@ -824,7 +817,6 @@ - (instancetype)init { _inputViews = [[NSMutableArray alloc] init]; _activeView = _nonAutofillInputView; - _inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init]; } return self; @@ -834,7 +826,6 @@ - (void)dealloc { [self hideTextInput]; [_nonAutofillInputView release]; [_nonAutofillSecureInputView release]; - [_inputHider release]; [_inputViews release]; [super dealloc]; @@ -873,19 +864,19 @@ - (void)showTextInput { @"The application must have a key window since the keyboard client " @"must be part of the responder chain to function"); _activeView.textInputDelegate = _textInputDelegate; - if (![_activeView isDescendantOfView:_inputHider]) { - [_inputHider addSubview:_activeView]; + + if (_activeView.window != keyWindow) { + [keyWindow addSubview:_activeView]; } - [keyWindow addSubview:_inputHider]; [_activeView becomeFirstResponder]; } - (void)hideTextInput { [_activeView resignFirstResponder]; - [_inputHider removeFromSuperview]; } - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration { + UIWindow* keyWindow = [UIApplication sharedApplication].keyWindow; NSArray* fields = configuration[@"fields"]; NSString* clientUniqueId = uniqueIdFromDictionary(configuration); bool isSecureTextEntry = [configuration[@"obscureText"] boolValue]; @@ -894,16 +885,19 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur _activeView = isSecureTextEntry ? _nonAutofillSecureInputView : _nonAutofillInputView; [FlutterTextInputPlugin setupInputView:_activeView withConfiguration:configuration]; - if (![_activeView isDescendantOfView:_inputHider]) { - [_inputHider addSubview:_activeView]; + if (_activeView.window != keyWindow) { + [keyWindow addSubview:_activeView]; } } else { NSAssert(clientUniqueId != nil, @"The client's unique id can't be null"); for (FlutterTextInputView* view in _inputViews) { [view removeFromSuperview]; } - for (UIView* subview in _inputHider.subviews) { - [subview removeFromSuperview]; + + for (UIView* view in keyWindow.subviews) { + if ([view isKindOfClass:[FlutterTextInputView class]]) { + [view removeFromSuperview]; + } } [_inputViews removeAllObjects]; @@ -921,7 +915,7 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur } [FlutterTextInputPlugin setupInputView:newInputView withConfiguration:field]; - [_inputHider addSubview:newInputView]; + [keyWindow addSubview:newInputView]; } } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m index a8eb3ce00604b..fb59ff03d37be 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m @@ -38,18 +38,30 @@ - (void)testSecureInput { result:^(id _Nullable result){ }]; - // Find all input views in the input hider view. - NSArray* inputFields = - [[[textInputPlugin textInputView] superview] subviews]; - - // Find the inactive autofillable input field. + // Find all the FlutterTextInputViews we created. + NSArray* inputFields = [[[[textInputPlugin textInputView] superview] + subviews] + filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"class == %@", + [FlutterTextInputView class]]]; + + // There are no autofill and the mock framework requested a secure entry. The first and only + // inserted FlutterTextInputView should be a secure text entry one. FlutterTextInputView* inputView = inputFields[0]; // Verify secureTextEntry is set to the correct value. XCTAssertTrue(inputView.secureTextEntry); - // Clean up mocks + // We should have only ever created one FlutterTextInputView. + XCTAssertEqual(inputFields.count, 1); + + // The one FlutterTextInputView we inserted into the view hierarchy should be the text input + // plugin's active text input view. + XCTAssertEqual(inputView, textInputPlugin.textInputView); + + // Clean up. [engine stopMocking]; + [[[[textInputPlugin textInputView] superview] subviews] + makeObjectsPerformSelector:@selector(removeFromSuperview)]; } - (void)testAutofillInputViews { // Setup test. @@ -94,9 +106,11 @@ - (void)testAutofillInputViews { result:^(id _Nullable result){ }]; - // Find all input views in the input hider view. - NSArray* inputFields = - [[[textInputPlugin textInputView] superview] subviews]; + // Find all the FlutterTextInputViews we created. + NSArray* inputFields = [[[[textInputPlugin textInputView] superview] + subviews] + filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"class == %@", + [FlutterTextInputView class]]]; XCTAssertEqual(inputFields.count, 2); @@ -108,8 +122,10 @@ - (void)testAutofillInputViews { // Verify behavior. OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil] withTag:@"field2"]); - // Clean up mocks + // Clean up. [engine stopMocking]; + [[[[textInputPlugin textInputView] superview] subviews] + makeObjectsPerformSelector:@selector(removeFromSuperview)]; } - (void)testAutocorrectionPromptRectAppears { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 37f3d6068cdc3..7c986c120f46a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -4,7 +4,7 @@ #import #import -#include "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" diff --git a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/xcshareddata/xcschemes/IosUnitTests.xcscheme b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/xcshareddata/xcschemes/IosUnitTests.xcscheme new file mode 100644 index 0000000000000..3c05147cb501b --- /dev/null +++ b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/xcshareddata/xcschemes/IosUnitTests.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenarios/EngineLaunchE2ETest.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenarios/EngineLaunchE2ETest.java index 24d3ed5e4ae4f..727c4a36b88de 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenarios/EngineLaunchE2ETest.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenarios/EngineLaunchE2ETest.java @@ -34,15 +34,16 @@ public void smokeTestEngineLaunch() throws Throwable { UiThreadStatement.runOnUiThread(() -> engine.set(new FlutterEngine(applicationContext))); CompletableFuture statusReceived = new CompletableFuture<>(); - // The default Dart main entrypoint sends back a platform message on the "scenario_status" + // The default Dart main entrypoint sends back a platform message on the "waiting_for_status" // channel. That will be our launch success assertion condition. engine .get() .getDartExecutor() .setMessageHandler( - "scenario_status", (byteBuffer, binaryReply) -> statusReceived.complete(Boolean.TRUE)); + "waiting_for_status", + (byteBuffer, binaryReply) -> statusReceived.complete(Boolean.TRUE)); - // Launching the entrypoint will run the Dart code that sends the "scenario_status" platform + // Launching the entrypoint will run the Dart code that sends the "waiting_for_status" platform // message. UiThreadStatement.runOnUiThread( () -> @@ -54,7 +55,7 @@ public void smokeTestEngineLaunch() throws Throwable { try { Boolean result = statusReceived.get(10, TimeUnit.SECONDS); if (!result) { - fail("expected message on scenario_status not received"); + fail("expected message on waiting_for_status not received"); } } catch (ExecutionException e) { fail(e.getMessage()); diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index 818d902b3e2e9..b0c4a579e85f4 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 0A42BFB42447E179007E212E /* TextSemanticsFocusTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A42BFB32447E179007E212E /* TextSemanticsFocusTest.m */; }; 0A57B3BD2323C4BD00DD9521 /* ScreenBeforeFlutter.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A57B3BC2323C4BD00DD9521 /* ScreenBeforeFlutter.m */; }; 0A57B3BF2323C74200DD9521 /* FlutterEngine+ScenariosTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A57B3BE2323C74200DD9521 /* FlutterEngine+ScenariosTest.m */; }; 0A57B3C22323D2D700DD9521 /* AppLifecycleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A57B3C12323D2D700DD9521 /* AppLifecycleTests.m */; }; @@ -110,6 +111,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0A42BFB32447E179007E212E /* TextSemanticsFocusTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TextSemanticsFocusTest.m; sourceTree = ""; }; + 0A42BFB52447E19F007E212E /* TextSemanticsFocusTest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TextSemanticsFocusTest.h; sourceTree = ""; }; 0A57B3BB2323C4BD00DD9521 /* ScreenBeforeFlutter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ScreenBeforeFlutter.h; sourceTree = ""; }; 0A57B3BC2323C4BD00DD9521 /* ScreenBeforeFlutter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScreenBeforeFlutter.m; sourceTree = ""; }; 0A57B3BE2323C74200DD9521 /* FlutterEngine+ScenariosTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FlutterEngine+ScenariosTest.m"; sourceTree = ""; }; @@ -280,6 +283,8 @@ 68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */, 0D8470A2240F0B1F0030B565 /* StatusBarTest.h */, 0D8470A3240F0B1F0030B565 /* StatusBarTest.m */, + 0A42BFB32447E179007E212E /* TextSemanticsFocusTest.m */, + 0A42BFB52447E19F007E212E /* TextSemanticsFocusTest.h */, ); path = ScenariosUITests; sourceTree = ""; @@ -498,6 +503,7 @@ 6816DBA42318358200A51400 /* PlatformViewGoldenTestManager.m in Sources */, 248D76EF22E388380012F0C1 /* PlatformViewUITests.m in Sources */, 0D8470A4240F0B1F0030B565 /* StatusBarTest.m in Sources */, + 0A42BFB42447E179007E212E /* TextSemanticsFocusTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/xcshareddata/xcschemes/Scenarios.xcscheme b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/xcshareddata/xcschemes/Scenarios.xcscheme index 87799ad5e6434..ebe5b1035abde 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/xcshareddata/xcschemes/Scenarios.xcscheme +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/xcshareddata/xcschemes/Scenarios.xcscheme @@ -27,6 +27,15 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + - - - - - - + + - - * launchArgsMap = @{ + // The Platform view golden test args should match `PlatformViewGoldenTestManager`. @"--platform-view" : @"platform_view", @"--platform-view-no-overlay-intersection" : @"platform_view_no_overlay_intersection", @"--platform-view-two-intersecting-overlays" : @"platform_view_two_intersecting_overlays", @@ -49,18 +47,19 @@ - (BOOL)application:(UIApplication*)application @"--gesture-reject-eager" : @"platform_view_gesture_reject_eager", @"--gesture-accept" : @"platform_view_gesture_accept", @"--tap-status-bar" : @"tap_status_bar", + @"--text-semantics-focus" : @"text_semantics_focus" }; - __block NSString* platformViewTestName = nil; + __block NSString* flutterViewControllerTestName = nil; [launchArgsMap enumerateKeysAndObjectsUsingBlock:^(NSString* argument, NSString* testName, BOOL* stop) { if ([[[NSProcessInfo processInfo] arguments] containsObject:argument]) { - platformViewTestName = testName; + flutterViewControllerTestName = testName; *stop = YES; } }]; - if (platformViewTestName) { - [self readyContextForPlatformViewTests:platformViewTestName]; + if (flutterViewControllerTestName) { + [self setupFlutterViewControllerTest:flutterViewControllerTestName]; } else if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--screen-before-flutter"]) { self.window.rootViewController = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:nil]; } else { @@ -71,22 +70,24 @@ - (BOOL)application:(UIApplication*)application return [super application:application didFinishLaunchingWithOptions:launchOptions]; } -- (void)readyContextForPlatformViewTests:(NSString*)scenarioIdentifier { - FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"PlatformViewTest" project:nil]; - [engine runWithEntrypoint:nil]; - - FlutterViewController* flutterViewController; +- (FlutterViewController*)flutterViewControllerForTest:(NSString*)scenarioIdentifier + withEngine:(FlutterEngine*)engine { if ([scenarioIdentifier isEqualToString:@"tap_status_bar"]) { - flutterViewController = [[FlutterViewController alloc] initWithEngine:engine - nibName:nil - bundle:nil]; + return [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; } else { - flutterViewController = [[NoStatusBarFlutterViewController alloc] initWithEngine:engine - nibName:nil - bundle:nil]; + return [[NoStatusBarFlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; } +} + +- (void)setupFlutterViewControllerTest:(NSString*)scenarioIdentifier { + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"FlutterControllerTest" project:nil]; + [engine run]; + + FlutterViewController* flutterViewController = + [self flutterViewControllerForTest:scenarioIdentifier withEngine:engine]; + [engine.binaryMessenger - setMessageHandlerOnChannel:@"scenario_status" + setMessageHandlerOnChannel:@"waiting_for_status" binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply) { [engine.binaryMessenger sendOnChannel:@"set_scenario" @@ -103,9 +104,9 @@ - (void)readyContextForPlatformViewTests:(NSString*)scenarioIdentifier { [flutterViewController.view addSubview:text]; }]; TextPlatformViewFactory* textPlatformViewFactory = - [[TextPlatformViewFactory alloc] initWithMessenger:flutterViewController.binaryMessenger]; + [[TextPlatformViewFactory alloc] initWithMessenger:engine.binaryMessenger]; NSObject* registrar = - [flutterViewController.engine registrarForPlugin:@"scenarios/TextPlatformViewPlugin"]; + [engine registrarForPlugin:@"scenarios/TextPlatformViewPlugin"]; [registrar registerViewFactory:textPlatformViewFactory withId:@"scenarios/textPlatformView" gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager]; diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m index 6291381c0fabe..c59191752b1c2 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m @@ -13,7 +13,7 @@ - (instancetype)initWithScenario:(NSString*)scenario project:nil]; [self runWithEntrypoint:nil]; [self.binaryMessenger - setMessageHandlerOnChannel:@"scenario_status" + setMessageHandlerOnChannel:@"waiting_for_status" binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) { [self.binaryMessenger sendOnChannel:@"set_scenario" diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m index 7f8f6902bde2a..b2bf497cf36c5 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m @@ -6,6 +6,8 @@ #import #import "ScreenBeforeFlutter.h" +FLUTTER_ASSERT_ARC + @interface XCAppLifecycleTestExpectation : XCTestExpectation - (instancetype)initForLifecycle:(NSString*)expectedLifecycle forStep:(NSString*)step; diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/TextSemanticsFocusTest.h b/testing/scenario_app/ios/Scenarios/ScenariosUITests/TextSemanticsFocusTest.h new file mode 100644 index 0000000000000..45e0a5dd9b7eb --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/TextSemanticsFocusTest.h @@ -0,0 +1,14 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TextSemanticsFocusTest : XCTestCase +@property(nonatomic, strong) XCUIApplication* application; +@end + +NS_ASSUME_NONNULL_END diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/TextSemanticsFocusTest.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/TextSemanticsFocusTest.m new file mode 100644 index 0000000000000..900723266219a --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/TextSemanticsFocusTest.m @@ -0,0 +1,58 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "TextSemanticsFocusTest.h" + +FLUTTER_ASSERT_ARC + +@implementation TextSemanticsFocusTest + +- (void)setUp { + [super setUp]; + self.continueAfterFailure = NO; + + self.application = [[XCUIApplication alloc] init]; + self.application.launchArguments = @[ @"--text-semantics-focus" ]; + [self.application launch]; +} + +- (void)testAccessibilityFocusOnTextSemanticsProducesCorrectIosViews { + // Find the initial TextInputSemanticsObject which was sent from the mock framework on first + // frame. + XCUIElement* textInputSemanticsObject = + [[[self.application textFields] matchingIdentifier:@"flutter textfield"] element]; + XCTAssertTrue([textInputSemanticsObject waitForExistenceWithTimeout:3]); + XCTAssertEqualObjects([textInputSemanticsObject valueForKey:@"hasKeyboardFocus"], @(NO)); + + // Since the first mock framework text field isn't focused on, it shouldn't produce a UITextInput + // in the view hierarchy. + XCUIElement* delegateTextInput = [[self.application textViews] element]; + XCTAssertFalse([delegateTextInput waitForExistenceWithTimeout:3]); + + // Nor should there be a keyboard for text entry. + XCUIElement* keyboard = [[self.application keyboards] element]; + XCTAssertFalse([keyboard waitForExistenceWithTimeout:3]); + + // The tap location doesn't matter. The mock framework just sends a focused text field on tap. + [textInputSemanticsObject tap]; + + // The new TextInputSemanticsObject now has keyboard focus (the only trait accessible through + // UI tests on a XCUIElement). + textInputSemanticsObject = + [[[self.application textFields] matchingIdentifier:@"focused flutter textfield"] element]; + XCTAssertTrue([textInputSemanticsObject waitForExistenceWithTimeout:3]); + XCTAssertEqualObjects([textInputSemanticsObject valueForKey:@"hasKeyboardFocus"], @(YES)); + + // The delegate UITextInput is also inserted on the window but we make only the + // TextInputSemanticsObject visible and not the FlutterTextInputView to avoid confusing + // accessibility, it shouldn't be visible to the UI test either. + delegateTextInput = [[self.application textViews] element]; + XCTAssertFalse([delegateTextInput waitForExistenceWithTimeout:3]); + + // But since there is focus, the soft keyboard is visible on the simulator. + keyboard = [[self.application keyboards] element]; + XCTAssertTrue([keyboard waitForExistenceWithTimeout:3]); +} + +@end diff --git a/testing/scenario_app/lib/main.dart b/testing/scenario_app/lib/main.dart index 17e56934cded8..4818bfeadc1a8 100644 --- a/testing/scenario_app/lib/main.dart +++ b/testing/scenario_app/lib/main.dart @@ -14,6 +14,7 @@ import 'src/animated_color_square.dart'; import 'src/platform_view.dart'; import 'src/poppable_screen.dart'; import 'src/scenario.dart'; +import 'src/send_text_focus_semantics.dart'; import 'src/touches_scenario.dart'; Map _scenarios = { @@ -37,7 +38,8 @@ Map _scenarios = { 'platform_view_gesture_reject_eager': PlatformViewForTouchIOSScenario(window, 'platform view touch', id: 11, accept: false), 'platform_view_gesture_accept': PlatformViewForTouchIOSScenario(window, 'platform view touch', id: 11, accept: true), 'platform_view_gesture_reject_after_touches_ended': PlatformViewForTouchIOSScenario(window, 'platform view touch', id: 11, accept: false, rejectUntilTouchesEnded: true), - 'tap_status_bar' : TouchesScenario(window), + 'tap_status_bar': TouchesScenario(window), + 'text_semantics_focus': SendTextFocusScemantics(window), }; Scenario _currentScenario = _scenarios['animated_color_square']; @@ -53,7 +55,7 @@ void main() { ..scheduleFrame(); final ByteData data = ByteData(1); data.setUint8(0, 1); - window.sendPlatformMessage('scenario_status', data, null); + window.sendPlatformMessage('waiting_for_status', data, null); } Future _handlePlatformMessage( diff --git a/testing/scenario_app/lib/src/channel_util.dart b/testing/scenario_app/lib/src/channel_util.dart new file mode 100644 index 0000000000000..5f8c4c0d1d296 --- /dev/null +++ b/testing/scenario_app/lib/src/channel_util.dart @@ -0,0 +1,31 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:ui'; + +import 'package:meta/meta.dart'; + +/// Util method to replicate the behavior of a `MethodChannel` in the Flutter +/// framework. +void sendJsonMethodCall({ + @required Window window, + @required String channel, + @required String method, + dynamic arguments, + PlatformMessageResponseCallback callback, +}) { + window.sendPlatformMessage( + channel, + // This recreates a combination of OptionalMethodChannel, JSONMethodCodec, + // and _DefaultBinaryMessenger in the framework. + utf8.encoder.convert( + const JsonCodec().encode({ + 'method': method, + 'args': arguments, + }) + ).buffer.asByteData(), + callback, + ); +} diff --git a/testing/scenario_app/lib/src/poppable_screen.dart b/testing/scenario_app/lib/src/poppable_screen.dart index a31002ab678bf..c2a1f91c71afe 100644 --- a/testing/scenario_app/lib/src/poppable_screen.dart +++ b/testing/scenario_app/lib/src/poppable_screen.dart @@ -3,9 +3,10 @@ // found in the LICENSE file. // @dart = 2.6 -import 'dart:convert'; import 'dart:ui'; +import 'package:scenario_app/src/channel_util.dart'; + import 'platform_echo_mixin.dart'; import 'scenario.dart'; @@ -72,22 +73,15 @@ class PoppableScreenScenario extends Scenario with PlatformEchoMixin { } void _pop() { - window.sendPlatformMessage( + sendJsonMethodCall( + window: window, // 'flutter/platform' is the hardcoded name of the 'platform' // `SystemChannel` from the `SystemNavigator` API. // https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/services/system_navigator.dart. - 'flutter/platform', - // This recreates a combination of OptionalMethodChannel, JSONMethodCodec, - // and _DefaultBinaryMessenger in the framework. - utf8.encoder.convert( - const JsonCodec().encode({ - 'method': 'SystemNavigator.pop', - 'args': null, - }) - ).buffer.asByteData(), + channel: 'flutter/platform', + method: 'SystemNavigator.pop', // Don't care about the response. If it doesn't go through, the test // will fail. - null, ); } } diff --git a/testing/scenario_app/lib/src/scenario.dart b/testing/scenario_app/lib/src/scenario.dart index 4355e7886b8cb..4d53abcd0b32c 100644 --- a/testing/scenario_app/lib/src/scenario.dart +++ b/testing/scenario_app/lib/src/scenario.dart @@ -17,7 +17,7 @@ abstract class Scenario { /// Called by the program when a frame is ready to be drawn. /// /// See [Window.onBeginFrame] for more details. - void onBeginFrame(Duration duration); + void onBeginFrame(Duration duration) {} /// Called by the program when the microtasks from [onBeginFrame] have been /// flushed. diff --git a/testing/scenario_app/lib/src/send_text_focus_semantics.dart b/testing/scenario_app/lib/src/send_text_focus_semantics.dart new file mode 100644 index 0000000000000..577331be1131c --- /dev/null +++ b/testing/scenario_app/lib/src/send_text_focus_semantics.dart @@ -0,0 +1,117 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:vector_math/vector_math_64.dart'; + +import 'channel_util.dart'; +import 'scenario.dart'; + +/// A scenario that sends back messages when touches are received. +class SendTextFocusScemantics extends Scenario { + /// Constructor for `SendTextFocusScemantics`. + SendTextFocusScemantics(Window window) : super(window); + + @override + void onBeginFrame(Duration duration) { + // Doesn't matter what we draw. Just paint white. + final SceneBuilder builder = SceneBuilder(); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + + canvas.drawRect( + Rect.fromLTWH(0, 0, window.physicalSize.width, window.physicalSize.height), + Paint()..color = const Color.fromARGB(255, 255, 255, 255), + ); + final Picture picture = recorder.endRecording(); + + builder.addPicture( + Offset.zero, + picture, + ); + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + + // On the first frame, also pretend that it drew a text field. Send the + // corresponding semantics tree comprised of 1 node for the text field. + window.updateSemantics((SemanticsUpdateBuilder() + ..updateNode( + id: 0, + // SemanticsFlag.isTextField. + flags: 16, + // SemanticsAction.tap. + actions: 1, + rect: const Rect.fromLTRB(0.0, 0.0, 414.0, 48.0), + label: 'flutter textfield', + textDirection: TextDirection.ltr, + textSelectionBase: -1, + textSelectionExtent: -1, + platformViewId: -1, + maxValueLength: -1, + currentValueLength: 0, + scrollChildren: 0, + scrollIndex: 0, + transform: Matrix4.identity().storage, + elevation: 0.0, + thickness: 0.0, + childrenInTraversalOrder: Int32List(0), + childrenInHitTestOrder: Int32List(0), + additionalActions: Int32List(0), + )).build() + ); + } + + // We don't really care about the touch itself. It's just a way for the + // XCUITest to communicate timing to the mock framework. + @override + void onPointerDataPacket(PointerDataPacket packet) { + // This mimics the framework which shows the FlutterTextInputView before + // updating the TextInputSemanticsObject. + sendJsonMethodCall( + window: window, + channel: 'flutter/textinput', + method: 'TextInput.setClient', + arguments: [ + 1, + // The arguments are text field configurations. It doesn't really matter + // since we're just testing text field accessibility here. + { 'obscureText': false }, + ] + ); + + sendJsonMethodCall( + window: window, + channel: 'flutter/textinput', + method: 'TextInput.show', + ); + + window.updateSemantics((SemanticsUpdateBuilder() + ..updateNode( + id: 0, + // SemanticsFlag.isTextField and SemanticsFlag.isFocused. + flags: 48, + actions: 18433, + rect: const Rect.fromLTRB(0.0, 0.0, 414.0, 48.0), + label: 'focused flutter textfield', + textDirection: TextDirection.ltr, + textSelectionBase: 0, + textSelectionExtent: 0, + platformViewId: -1, + maxValueLength: -1, + currentValueLength: 0, + scrollChildren: 0, + scrollIndex: 0, + transform: Matrix4.identity().storage, + elevation: 0.0, + thickness: 0.0, + childrenInTraversalOrder: Int32List(0), + childrenInHitTestOrder: Int32List(0), + additionalActions: Int32List(0), + )).build() + ); + } +} diff --git a/testing/scenario_app/lib/src/touches_scenario.dart b/testing/scenario_app/lib/src/touches_scenario.dart index d0148ef5a28d9..7afcf030fc8ab 100644 --- a/testing/scenario_app/lib/src/touches_scenario.dart +++ b/testing/scenario_app/lib/src/touches_scenario.dart @@ -12,9 +12,6 @@ class TouchesScenario extends Scenario { /// Constructor for `TouchesScenario`. TouchesScenario(Window window) : super(window); - @override - void onBeginFrame(Duration duration) {} - @override void onPointerDataPacket(PointerDataPacket packet) { window.sendPlatformMessage( diff --git a/testing/scenario_app/pubspec.yaml b/testing/scenario_app/pubspec.yaml index f17ddb080a7dc..e1b8e67272b17 100644 --- a/testing/scenario_app/pubspec.yaml +++ b/testing/scenario_app/pubspec.yaml @@ -4,8 +4,12 @@ publish_to: none # These are for convenience during local development. Changing them will not # impact the build. dependencies: - sky_engine: - path: ../../../out/host_debug_unopt/gen/dart-pkg/sky_engine - sky_services: - path: ../../../out/host_debug_unopt/gen/dart-pkg/sky_services - vector_math: ^2.0.8 + sky_engine: + path: ../../../out/host_debug_unopt/gen/dart-pkg/sky_engine + sky_services: + path: ../../../out/host_debug_unopt/gen/dart-pkg/sky_services + vector_math: ^2.0.8 + +dependency_overrides: + meta: + path: ../../../third_party/dart/pkg/meta