diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 969a4db69f9..4b8eb823684 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -781,7 +781,10 @@ static char markerKey; flutter::TextRange replacedRange(-1, -1); std::string textBeforeChange = _activeModel->GetText().c_str(); - std::string utf8String = [string UTF8String]; + // Input string may be NSString or NSAttributedString. + BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]]; + const NSString* rawString = isAttributedString ? [string string] : string; + std::string utf8String = rawString ? [rawString UTF8String] : ""; _activeModel->AddText(utf8String); if (_activeModel->composing()) { replacedRange = composingBeforeChange; diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index 326960d82cb..fb47168e404 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -2485,4 +2485,79 @@ TEST(FlutterTextInputPluginTest, InsertTextWithCollapsedSelectionInsideComposing [[FlutterInputPluginTestObjc alloc] testInsertTextWithCollapsedSelectionInsideComposing]); } +TEST(FlutterTextInputPluginTest, InsertTextHandlesNSAttributedString) { + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [engineMock binaryMessenger]) + .andReturn(binaryMessengerMock); + + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock + nibName:@"" + bundle:nil]; + + FlutterTextInputPluginTestDelegate* delegate = + [[FlutterTextInputPluginTestDelegate alloc] initWithBinaryMessenger:binaryMessengerMock + viewController:viewController]; + + FlutterTextInputPlugin* plugin = [[FlutterTextInputPlugin alloc] initWithDelegate:delegate]; + + NSDictionary* setClientConfig = @{ + @"viewId" : @(kViewId), + @"inputAction" : @"action", + @"inputType" : @{@"name" : @"inputName"}, + }; + [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ @(1), setClientConfig ]] + result:^(id){ + }]; + + // Test with NSAttributedString + NSAttributedString* attributedString = + [[NSAttributedString alloc] initWithString:@"attributed text"]; + [plugin insertText:attributedString replacementRange:NSMakeRange(NSNotFound, 0)]; + + NSDictionary* editingState = [plugin editingState]; + EXPECT_STREQ([editingState[@"text"] UTF8String], "attributed text"); + EXPECT_EQ([editingState[@"selectionBase"] intValue], 15); + EXPECT_EQ([editingState[@"selectionExtent"] intValue], 15); +} + +TEST(FlutterTextInputPluginTest, InsertTextHandlesEmptyAttributedString) { + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [engineMock binaryMessenger]) + .andReturn(binaryMessengerMock); + + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock + nibName:@"" + bundle:nil]; + + FlutterTextInputPluginTestDelegate* delegate = + [[FlutterTextInputPluginTestDelegate alloc] initWithBinaryMessenger:binaryMessengerMock + viewController:viewController]; + + FlutterTextInputPlugin* plugin = [[FlutterTextInputPlugin alloc] initWithDelegate:delegate]; + + NSDictionary* setClientConfig = @{ + @"viewId" : @(kViewId), + @"inputAction" : @"action", + @"inputType" : @{@"name" : @"inputName"}, + }; + [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ @(1), setClientConfig ]] + result:^(id){ + }]; + + // Test with empty NSAttributedString + NSAttributedString* emptyAttributedString = [[NSAttributedString alloc] initWithString:@""]; + [plugin insertText:emptyAttributedString replacementRange:NSMakeRange(NSNotFound, 0)]; + + NSDictionary* editingState = [plugin editingState]; + EXPECT_STREQ([editingState[@"text"] UTF8String], ""); + EXPECT_EQ([editingState[@"selectionBase"] intValue], 0); + EXPECT_EQ([editingState[@"selectionExtent"] intValue], 0); +} + } // namespace flutter::testing