diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 33adfa909dc65..7f884943601eb 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -777,6 +777,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..b3b4a8a096824 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -1801,6 +1801,42 @@ - (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; + + // Call selectors without setting a client. + [plugin doCommandBySelector:@selector(moveUp:)]; + [plugin doCommandBySelector:@selector(moveRightAndModifySelection:)]; + + __block bool done = false; + CFRunLoopPerformBlock(CFRunLoopGetMain(), (__bridge CFStringRef)runLoopMode, ^{ + done = true; + }); + + while (!done) { + 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 +1922,7 @@ - (bool)testSelectorsAreForwardedToFramework { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingWithDelta]); } -TEST(FlutterTextInputPluginTest, testComposingWithDeltasWhenSelectionIsActive) { +TEST(FlutterTextInputPluginTest, TestComposingWithDeltasWhenSelectionIsActive) { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingWithDeltasWhenSelectionIsActive]); } @@ -1910,6 +1946,10 @@ - (bool)testSelectorsAreForwardedToFramework { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSelectorsAreForwardedToFramework]); } +TEST(FlutterTextInputPluginTest, TestSelectorsNotForwardedToFrameworkIfNoClient) { + ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSelectorsNotForwardedToFrameworkIfNoClient]); +} + TEST(FlutterTextInputPluginTest, TestInsertNewLine) { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testInsertNewLine]); }