diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index ebf5057d6e7ae..78a65b73e1cbf 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -61,6 +61,9 @@ static NSString* const kTextAffinityDownstream = @"TextAffinity.downstream"; static NSString* const kTextAffinityUpstream = @"TextAffinity.upstream"; +// TextInputAction types +static NSString* const kInputActionNewline = @"TextInputAction.newline"; + #pragma mark - Enums /** * The affinity of the current cursor position. If the cursor is at a position representing @@ -820,7 +823,8 @@ - (void)insertNewline:(id)sender { _activeModel->CommitComposing(); _activeModel->EndComposing(); } - if ([self.inputType isEqualToString:kMultilineInputType]) { + if ([self.inputType isEqualToString:kMultilineInputType] && + [self.inputAction isEqualToString:kInputActionNewline]) { [self insertText:@"\n" replacementRange:self.selectedRange]; } [_channel invokeMethod:kPerformAction arguments:@[ self.clientID, self.inputAction ]]; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index d91e88e4051fb..d5d31b5115fec 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -1470,6 +1470,196 @@ - (bool)unhandledKeyEquivalent { return true; } +- (bool)testInsertNewLine { + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [engineMock binaryMessenger]) + .andReturn(binaryMessengerMock); + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + callback:nil + userData:nil]); + + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock + nibName:@"" + bundle:nil]; + + FlutterTextInputPlugin* plugin = + [[FlutterTextInputPlugin alloc] initWithViewController:viewController]; + + NSDictionary* setClientConfig = @{ + @"inputType" : @{@"name" : @"TextInputType.multiline"}, + @"inputAction" : @"TextInputAction.newline", + }; + [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ @(1), setClientConfig ]] + result:^(id){ + }]; + + FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState" + arguments:@{ + @"text" : @"Text", + @"selectionBase" : @(4), + @"selectionExtent" : @(4), + @"composingBase" : @(-1), + @"composingExtent" : @(-1), + }]; + + NSDictionary* expectedState = @{ + @"selectionBase" : @(4), + @"selectionExtent" : @(4), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(NO), + @"composingBase" : @(-1), + @"composingExtent" : @(-1), + @"text" : @"Text", + }; + + NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingState" + arguments:@[ @(1), expectedState ]]]; + + OCMExpect( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + + [plugin handleMethodCall:call + result:^(id){ + }]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + + [plugin doCommandBySelector:@selector(insertNewline:)]; + + NSDictionary* updatedState = @{ + @"selectionBase" : @(5), + @"selectionExtent" : @(5), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(NO), + @"composingBase" : @(-1), + @"composingExtent" : @(-1), + @"text" : @"Text\n", + }; + + updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingState" + arguments:@[ @(1), updatedState ]]]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + + return true; +} + +- (bool)testSendActionDoNotInsertNewLine { + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [engineMock binaryMessenger]) + .andReturn(binaryMessengerMock); + OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {} + callback:nil + userData:nil]); + + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock + nibName:@"" + bundle:nil]; + + FlutterTextInputPlugin* plugin = + [[FlutterTextInputPlugin alloc] initWithViewController:viewController]; + + NSDictionary* setClientConfig = @{ + @"inputType" : @{@"name" : @"TextInputType.multiline"}, + @"inputAction" : @"TextInputAction.send", + }; + [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ @(1), setClientConfig ]] + result:^(id){ + }]; + + FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState" + arguments:@{ + @"text" : @"Text", + @"selectionBase" : @(4), + @"selectionExtent" : @(4), + @"composingBase" : @(-1), + @"composingExtent" : @(-1), + }]; + + NSDictionary* expectedState = @{ + @"selectionBase" : @(4), + @"selectionExtent" : @(4), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(NO), + @"composingBase" : @(-1), + @"composingExtent" : @(-1), + @"text" : @"Text", + }; + + NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingState" + arguments:@[ @(1), expectedState ]]]; + + OCMExpect( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + + [plugin handleMethodCall:call + result:^(id){ + }]; + + [plugin doCommandBySelector:@selector(insertNewline:)]; + + NSData* performActionCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.performAction" + arguments:@[ @(1), @"TextInputAction.send" ]]]; + + // Input action should be notified. + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:performActionCall]); + } @catch (...) { + return false; + } + + NSDictionary* updatedState = @{ + @"selectionBase" : @(5), + @"selectionExtent" : @(5), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(NO), + @"composingBase" : @(-1), + @"composingExtent" : @(-1), + @"text" : @"Text\n", + }; + + updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingState" + arguments:@[ @(1), updatedState ]]]; + + // Verify that editing state was not be updated. + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + return false; + } @catch (...) { + // Expected. + } + + return true; +} + - (bool)testLocalTextAndSelectionUpdateAfterDelta { id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); @@ -1694,6 +1884,14 @@ - (bool)testSelectorsAreForwardedToFramework { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSelectorsAreForwardedToFramework]); } +TEST(FlutterTextInputPluginTest, TestInsertNewLine) { + ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testInsertNewLine]); +} + +TEST(FlutterTextInputPluginTest, TestSendActionDoNotInsertNewLine) { + ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSendActionDoNotInsertNewLine]); +} + TEST(FlutterTextInputPluginTest, CanWorkWithFlutterTextField) { FlutterEngine* engine = CreateTestEngine(); FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine