From fe5ba6700e3b725fc7b9907c00f17318a31bb4bc Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Wed, 1 May 2024 10:50:04 -0700 Subject: [PATCH 1/2] macOS fluttertextinputplugin drops selector called if no client --- .../Source/FlutterTextInputPlugin.mm | 5 +- .../Source/FlutterTextInputPluginTest.mm | 49 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 33adfa909dc65..2424327403b36 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -438,7 +438,6 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { _activeModel->EndComposing(); } [_textInputContext discardMarkedText]; - _clientID = nil; _inputAction = nil; _enableDeltaModel = NO; @@ -777,6 +776,10 @@ - (void)doCommandBySelector:(SEL)selector { void (*func)(id, SEL, id) = reinterpret_cast(imp); func(self, selector, nil); } + if (self.clientID == nil) { + // The macOS may still call selector even if it is no longer a first responder. + return; + } if (selector == @selector(insertNewline:)) { // Already handled through text insertion (multiline) or action. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index e3a203803d489..a281f1fef09f2 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -1801,6 +1801,49 @@ - (bool)testSelectorsAreForwardedToFramework { return true; } +- (bool)testSelectorsNotForwardedToFrameworkIfNoClient { + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [engineMock binaryMessenger]) + .andReturn(binaryMessengerMock); + // Make sure the selectors are not forwarded to the framework. + OCMReject([binaryMessengerMock sendOnChannel:@"flutter/textinput" message:[OCMArg any]]); + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock + nibName:@"" + bundle:nil]; + + FlutterTextInputPlugin* plugin = + [[FlutterTextInputPlugin alloc] initWithViewController:viewController]; + + // Can't run CFRunLoop in default mode because it causes crashes from scheduled + // sources from other tests. + NSString* runLoopMode = @"FlutterTestRunLoopMode"; + plugin.customRunLoopMode = runLoopMode; + + // Ensure both selectors are grouped in one platform channel call. + [plugin doCommandBySelector:@selector(moveUp:)]; + [plugin doCommandBySelector:@selector(moveRightAndModifySelection:)]; + + // Clear the client before the CFRunLoop is run. + [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.clearClient" + arguments:@[]] + result:^(id){ + }]; + + __block bool done = false; + CFRunLoopPerformBlock(CFRunLoopGetMain(), (__bridge CFStringRef)runLoopMode, ^{ + done = true; + }); + + while (!done) { + // Each invocation will handle one source. + CFRunLoopRunInMode((__bridge CFStringRef)runLoopMode, 0, true); + } + // At this point the selectors should be dropped; otherwise, OCMReject will throw. + return true; +} + @end namespace flutter::testing { @@ -1886,7 +1929,7 @@ - (bool)testSelectorsAreForwardedToFramework { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingWithDelta]); } -TEST(FlutterTextInputPluginTest, testComposingWithDeltasWhenSelectionIsActive) { +TEST(FlutterTextInputPluginTest, TestComposingWithDeltasWhenSelectionIsActive) { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingWithDeltasWhenSelectionIsActive]); } @@ -1910,6 +1953,10 @@ - (bool)testSelectorsAreForwardedToFramework { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSelectorsAreForwardedToFramework]); } +TEST(FlutterTextInputPluginTest, TestSelectorsNotForwardedToFrameworkIfNoClient) { + ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSelectorsNotForwardedToFrameworkIfNoClient]); +} + TEST(FlutterTextInputPluginTest, TestInsertNewLine) { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testInsertNewLine]); } From a24246776e1b395d0919f61a788731f47a6785de Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Wed, 1 May 2024 10:56:49 -0700 Subject: [PATCH 2/2] update --- .../macos/framework/Source/FlutterTextInputPlugin.mm | 1 + .../macos/framework/Source/FlutterTextInputPluginTest.mm | 9 +-------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 2424327403b36..7f884943601eb 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -438,6 +438,7 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { _activeModel->EndComposing(); } [_textInputContext discardMarkedText]; + _clientID = nil; _inputAction = nil; _enableDeltaModel = NO; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index a281f1fef09f2..b3b4a8a096824 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -1821,23 +1821,16 @@ - (bool)testSelectorsNotForwardedToFrameworkIfNoClient { NSString* runLoopMode = @"FlutterTestRunLoopMode"; plugin.customRunLoopMode = runLoopMode; - // Ensure both selectors are grouped in one platform channel call. + // Call selectors without setting a client. [plugin doCommandBySelector:@selector(moveUp:)]; [plugin doCommandBySelector:@selector(moveRightAndModifySelection:)]; - // Clear the client before the CFRunLoop is run. - [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.clearClient" - arguments:@[]] - result:^(id){ - }]; - __block bool done = false; CFRunLoopPerformBlock(CFRunLoopGetMain(), (__bridge CFStringRef)runLoopMode, ^{ done = true; }); while (!done) { - // Each invocation will handle one source. CFRunLoopRunInMode((__bridge CFStringRef)runLoopMode, 0, true); } // At this point the selectors should be dropped; otherwise, OCMReject will throw.