Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 8e4a718

Browse files
authored
Made FlutterTextField that outlive FlutterTextPlatformNode not crash (#37735)
* Made FlutterTextField that outlive FlutterTextPlatformNode not crash. * added test
1 parent 4311774 commit 8e4a718

File tree

3 files changed

+96
-59
lines changed

3 files changed

+96
-59
lines changed

shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
#import <OCMock/OCMock.h>
1313
#import "flutter/testing/testing.h"
1414

15+
@interface FlutterTextField (Testing)
16+
- (void)setPlatformNode:(flutter::FlutterTextPlatformNode*)node;
17+
@end
18+
1519
@interface FlutterTextFieldMock : FlutterTextField
1620

1721
@property(nonatomic) NSString* lastUpdatedString;
@@ -1434,37 +1438,47 @@ - (bool)testSelectorsAreForwardedToFramework {
14341438
node_data.SetValue("initial text");
14351439
ax_node.SetData(node_data);
14361440
delegate.Init(engine.accessibilityBridge, &ax_node);
1437-
FlutterTextPlatformNode text_platform_node(&delegate, viewController);
1441+
{
1442+
FlutterTextPlatformNode text_platform_node(&delegate, viewController);
1443+
1444+
FlutterTextFieldMock* mockTextField =
1445+
[[FlutterTextFieldMock alloc] initWithPlatformNode:&text_platform_node
1446+
fieldEditor:viewController.textInputPlugin];
1447+
[viewController.view addSubview:mockTextField];
1448+
[mockTextField startEditing];
1449+
1450+
NSDictionary* arguments = @{
1451+
@"inputAction" : @"action",
1452+
@"inputType" : @{@"name" : @"inputName"},
1453+
};
1454+
FlutterMethodCall* methodCall =
1455+
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
1456+
arguments:@[ @(1), arguments ]];
1457+
FlutterResult result = ^(id result) {
1458+
};
1459+
[viewController.textInputPlugin handleMethodCall:methodCall result:result];
1460+
1461+
arguments = @{
1462+
@"text" : @"new text",
1463+
@"selectionBase" : @(1),
1464+
@"selectionExtent" : @(2),
1465+
@"composingBase" : @(-1),
1466+
@"composingExtent" : @(-1),
1467+
};
14381468

1439-
FlutterTextFieldMock* mockTextField =
1440-
[[FlutterTextFieldMock alloc] initWithPlatformNode:&text_platform_node
1441-
fieldEditor:viewController.textInputPlugin];
1442-
[viewController.view addSubview:mockTextField];
1443-
[mockTextField startEditing];
1469+
methodCall = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
1470+
arguments:arguments];
1471+
[viewController.textInputPlugin handleMethodCall:methodCall result:result];
1472+
EXPECT_EQ([mockTextField.lastUpdatedString isEqualToString:@"new text"], YES);
1473+
EXPECT_EQ(NSEqualRanges(mockTextField.lastUpdatedSelection, NSMakeRange(1, 1)), YES);
14441474

1445-
NSDictionary* arguments = @{
1446-
@"inputAction" : @"action",
1447-
@"inputType" : @{@"name" : @"inputName"},
1448-
};
1449-
FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
1450-
arguments:@[ @(1), arguments ]];
1451-
FlutterResult result = ^(id result) {
1452-
};
1453-
[viewController.textInputPlugin handleMethodCall:methodCall result:result];
1454-
1455-
arguments = @{
1456-
@"text" : @"new text",
1457-
@"selectionBase" : @(1),
1458-
@"selectionExtent" : @(2),
1459-
@"composingBase" : @(-1),
1460-
@"composingExtent" : @(-1),
1461-
};
1475+
// This blocks the FlutterTextFieldMock, which is held onto by the main event
1476+
// loop, from crashing.
1477+
[mockTextField setPlatformNode:nil];
1478+
}
14621479

1463-
methodCall = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
1464-
arguments:arguments];
1465-
[viewController.textInputPlugin handleMethodCall:methodCall result:result];
1466-
EXPECT_EQ([mockTextField.lastUpdatedString isEqualToString:@"new text"], YES);
1467-
EXPECT_EQ(NSEqualRanges(mockTextField.lastUpdatedSelection, NSMakeRange(1, 1)), YES);
1480+
// This verifies that clearing the platform node works.
1481+
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
14681482
}
14691483

14701484
TEST(FlutterTextInputPluginTest, CanNotBecomeResponderIfNoViewController) {

shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.mm

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,18 @@ - (void)updateString:(NSString*)string withSelection:(NSRange)selection {
9191
#pragma mark - NSView
9292

9393
- (NSRect)frame {
94+
if (!_node) {
95+
return NSZeroRect;
96+
}
9497
return _node->GetFrame();
9598
}
9699

97100
#pragma mark - NSAccessibilityProtocol
98101

99102
- (void)setAccessibilityFocused:(BOOL)isFocused {
103+
if (!_node) {
104+
return;
105+
}
100106
[super setAccessibilityFocused:isFocused];
101107
ui::AXActionData data;
102108
data.action = isFocused ? ax::mojom::Action::kFocus : ax::mojom::Action::kBlur;
@@ -110,6 +116,9 @@ - (void)startEditing {
110116
if (self.currentEditor == _plugin) {
111117
return;
112118
}
119+
if (!_node) {
120+
return;
121+
}
113122
// Selecting text seems to be the only way to make the field editor
114123
// current editor.
115124
[self selectText:self];
@@ -133,6 +142,10 @@ - (void)startEditing {
133142
[self updateString:textValue withSelection:selection];
134143
}
135144

145+
- (void)setPlatformNode:(flutter::FlutterTextPlatformNode*)node {
146+
_node = node;
147+
}
148+
136149
#pragma mark - NSObject
137150

138151
- (void)dealloc {
@@ -159,6 +172,7 @@ - (void)dealloc {
159172
}
160173

161174
FlutterTextPlatformNode::~FlutterTextPlatformNode() {
175+
[appkit_text_field_ setPlatformNode:nil];
162176
EnsureDetachedFromView();
163177
}
164178

shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,39 +27,48 @@
2727

2828
TEST(FlutterTextInputSemanticsObjectTest, DoesInitialize) {
2929
FlutterEngine* engine = CreateTestEngine();
30-
NSString* fixtures = @(testing::GetFixturesPath());
31-
FlutterDartProject* project = [[FlutterDartProject alloc]
32-
initWithAssetsPath:fixtures
33-
ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
34-
FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project];
35-
[viewController loadView];
36-
[engine setViewController:viewController];
37-
// Create a NSWindow so that the native text field can become first responder.
38-
NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
39-
styleMask:NSBorderlessWindowMask
40-
backing:NSBackingStoreBuffered
41-
defer:NO];
42-
window.contentView = viewController.view;
30+
{
31+
NSString* fixtures = @(testing::GetFixturesPath());
32+
FlutterDartProject* project = [[FlutterDartProject alloc]
33+
initWithAssetsPath:fixtures
34+
ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
35+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project];
36+
[viewController loadView];
37+
[engine setViewController:viewController];
38+
// Create a NSWindow so that the native text field can become first responder.
39+
NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
40+
styleMask:NSBorderlessWindowMask
41+
backing:NSBackingStoreBuffered
42+
defer:NO];
43+
window.contentView = viewController.view;
44+
45+
engine.semanticsEnabled = YES;
4346

44-
engine.semanticsEnabled = YES;
47+
auto bridge = engine.accessibilityBridge.lock();
48+
FlutterPlatformNodeDelegateMac delegate(bridge, viewController);
49+
ui::AXTree tree;
50+
ui::AXNode ax_node(&tree, nullptr, 0, 0);
51+
ui::AXNodeData node_data;
52+
node_data.SetValue("initial text");
53+
ax_node.SetData(node_data);
54+
delegate.Init(engine.accessibilityBridge, &ax_node);
55+
// Verify that a FlutterTextField is attached to the view.
56+
FlutterTextPlatformNode text_platform_node(&delegate, viewController);
57+
id native_accessibility = text_platform_node.GetNativeViewAccessible();
58+
EXPECT_TRUE([native_accessibility isKindOfClass:[FlutterTextField class]]);
59+
auto subviews = [viewController.view subviews];
60+
EXPECT_EQ([subviews count], 2u);
61+
EXPECT_TRUE([subviews[0] isKindOfClass:[FlutterTextField class]]);
62+
FlutterTextField* nativeTextField = subviews[0];
63+
EXPECT_EQ(text_platform_node.GetNativeViewAccessible(), nativeTextField);
64+
}
4565

46-
auto bridge = engine.accessibilityBridge.lock();
47-
FlutterPlatformNodeDelegateMac delegate(bridge, viewController);
48-
ui::AXTree tree;
49-
ui::AXNode ax_node(&tree, nullptr, 0, 0);
50-
ui::AXNodeData node_data;
51-
node_data.SetValue("initial text");
52-
ax_node.SetData(node_data);
53-
delegate.Init(engine.accessibilityBridge, &ax_node);
54-
// Verify that a FlutterTextField is attached to the view.
55-
FlutterTextPlatformNode text_platform_node(&delegate, viewController);
56-
id native_accessibility = text_platform_node.GetNativeViewAccessible();
57-
EXPECT_TRUE([native_accessibility isKindOfClass:[FlutterTextField class]]);
58-
auto subviews = [viewController.view subviews];
59-
EXPECT_EQ([subviews count], 2u);
60-
EXPECT_TRUE([subviews[0] isKindOfClass:[FlutterTextField class]]);
61-
FlutterTextField* nativeTextField = subviews[0];
62-
EXPECT_EQ(text_platform_node.GetNativeViewAccessible(), nativeTextField);
66+
[engine shutDownEngine];
67+
engine = nil;
68+
// Pump the event loop to make sure no stray nodes cause crashes after the
69+
// engine has been destroyed.
70+
// From issue: https://github.com/flutter/flutter/issues/115599
71+
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
6372
}
6473

6574
} // namespace flutter::testing

0 commit comments

Comments
 (0)