| Flutter macOS Embedder |

FlutterTextInputPluginTest.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h"
6 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h"
7 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h"
8 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
9 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h"
10 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h"
11 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
12 #import "flutter/shell/platform/darwin/macos/framework/Source/NSView+ClipsToBounds.h"
13
14 #import <OCMock/OCMock.h>
15 #import "flutter/testing/testing.h"
16
17 @interface FlutterTextField (Testing)
18 - (void)setPlatformNode:(flutter::FlutterTextPlatformNode*)node;
19 @end
20
21 @interface FlutterTextFieldMock : FlutterTextField
22
23 @property(nonatomic, nullable, copy) NSString* lastUpdatedString;
24 @property(nonatomic) NSRange lastUpdatedSelection;
25
26 @end
27
28 @implementation FlutterTextFieldMock
29
30 - (void)updateString:(NSString*)string withSelection:(NSRange)selection {
31 _lastUpdatedString = string;
32 _lastUpdatedSelection = selection;
33 }
34
35 @end
36
37 @interface NSTextInputContext (Private)
38 // This is a private method.
39 - (BOOL)isActive;
40 @end
41
42 @interface TextInputTestViewController : FlutterViewController
43 @end
44
45 @implementation TextInputTestViewController
46 - (nonnull FlutterView*)createFlutterViewWithMTLDevice:(id<MTLDevice>)device
47 commandQueue:(id<MTLCommandQueue>)commandQueue {
48 return OCMClassMock([NSView class]);
49 }
50 @end
51
52 @interface FlutterInputPluginTestObjc : NSObject
53 - (bool)testEmptyCompositionRange;
54 - (bool)testClearClientDuringComposing;
55 @end
56
57 @implementation FlutterInputPluginTestObjc
58
59 - (bool)testEmptyCompositionRange {
60 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
61 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
62 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
63 [engineMock binaryMessenger])
64 .andReturn(binaryMessengerMock);
65
66 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
67 nibName:@""
68 bundle:nil];
69
70 FlutterTextInputPlugin* plugin =
71 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
72
73 NSDictionary* setClientConfig = @{
74 @"inputAction" : @"action",
75 @"inputType" : @{@"name" : @"inputName"},
76 };
77 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
78 arguments:@[ @(1), setClientConfig ]]
79 result:^(id){
80 }];
81
82 FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
83 arguments:@{
84 @"text" : @"Text",
85 @"selectionBase" : @(0),
86 @"selectionExtent" : @(0),
87 @"composingBase" : @(-1),
88 @"composingExtent" : @(-1),
89 }];
90
91 [plugin handleMethodCall:call
92 result:^(id){
93 }];
94
95 // Verify editing state was set.
96 NSDictionary* editingState = [plugin editingState];
97 EXPECT_STREQ([editingState[@"text"] UTF8String], "Text");
98 EXPECT_STREQ([editingState[@"selectionAffinity"] UTF8String], "TextAffinity.upstream");
99 EXPECT_FALSE([editingState[@"selectionIsDirectional"] boolValue]);
100 EXPECT_EQ([editingState[@"selectionBase"] intValue], 0);
101 EXPECT_EQ([editingState[@"selectionExtent"] intValue], 0);
102 EXPECT_EQ([editingState[@"composingBase"] intValue], -1);
103 EXPECT_EQ([editingState[@"composingExtent"] intValue], -1);
104 return true;
105 }
106
107 - (bool)testSetMarkedTextWithSelectionChange {
108 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
109 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
110 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
111 [engineMock binaryMessenger])
112 .andReturn(binaryMessengerMock);
113
114 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
115 nibName:@""
116 bundle:nil];
117
118 FlutterTextInputPlugin* plugin =
119 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
120
121 NSDictionary* setClientConfig = @{
122 @"inputAction" : @"action",
123 @"inputType" : @{@"name" : @"inputName"},
124 };
125 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
126 arguments:@[ @(1), setClientConfig ]]
127 result:^(id){
128 }];
129
130 FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
131 arguments:@{
132 @"text" : @"Text",
133 @"selectionBase" : @(4),
134 @"selectionExtent" : @(4),
135 @"composingBase" : @(-1),
136 @"composingExtent" : @(-1),
137 }];
138 [plugin handleMethodCall:call
139 result:^(id){
140 }];
141
142 [plugin setMarkedText:@"marked"
143 selectedRange:NSMakeRange(1, 0)
144 replacementRange:NSMakeRange(NSNotFound, 0)];
145
146 NSDictionary* expectedState = @{
147 @"selectionBase" : @(5),
148 @"selectionExtent" : @(5),
149 @"selectionAffinity" : @"TextAffinity.upstream",
150 @"selectionIsDirectional" : @(NO),
151 @"composingBase" : @(4),
152 @"composingExtent" : @(10),
153 @"text" : @"Textmarked",
154 };
155
156 NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance]
157 encodeMethodCall:[FlutterMethodCall
158 methodCallWithMethodName:@"TextInputClient.updateEditingState"
159 arguments:@[ @(1), expectedState ]]];
160
161 OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
162 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
163
164 @try {
165 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
166 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
167 } @catch (...) {
168 return false;
169 }
170 return true;
171 }
172
173 - (bool)testSetMarkedTextWithReplacementRange {
174 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
175 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
176 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
177 [engineMock binaryMessenger])
178 .andReturn(binaryMessengerMock);
179
180 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
181 nibName:@""
182 bundle:nil];
183
184 FlutterTextInputPlugin* plugin =
185 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
186
187 NSDictionary* setClientConfig = @{
188 @"inputAction" : @"action",
189 @"inputType" : @{@"name" : @"inputName"},
190 };
191 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
192 arguments:@[ @(1), setClientConfig ]]
193 result:^(id){
194 }];
195
196 FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
197 arguments:@{
198 @"text" : @"1234",
199 @"selectionBase" : @(3),
200 @"selectionExtent" : @(3),
201 @"composingBase" : @(-1),
202 @"composingExtent" : @(-1),
203 }];
204 [plugin handleMethodCall:call
205 result:^(id){
206 }];
207
208 [plugin setMarkedText:@"marked"
209 selectedRange:NSMakeRange(1, 0)
210 replacementRange:NSMakeRange(1, 2)];
211
212 NSDictionary* expectedState = @{
213 @"selectionBase" : @(2),
214 @"selectionExtent" : @(2),
215 @"selectionAffinity" : @"TextAffinity.upstream",
216 @"selectionIsDirectional" : @(NO),
217 @"composingBase" : @(1),
218 @"composingExtent" : @(7),
219 @"text" : @"1marked4",
220 };
221
222 NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance]
223 encodeMethodCall:[FlutterMethodCall
224 methodCallWithMethodName:@"TextInputClient.updateEditingState"
225 arguments:@[ @(1), expectedState ]]];
226
227 OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
228 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
229
230 @try {
231 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
232 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
233 } @catch (...) {
234 return false;
235 }
236 return true;
237 }
238
239 - (bool)testComposingRegionRemovedByFramework {
240 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
241 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
242 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
243 [engineMock binaryMessenger])
244 .andReturn(binaryMessengerMock);
245
246 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
247 nibName:@""
248 bundle:nil];
249
250 FlutterTextInputPlugin* plugin =
251 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
252
253 NSDictionary* setClientConfig = @{
254 @"inputAction" : @"action",
255 @"inputType" : @{@"name" : @"inputName"},
256 };
257 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
258 arguments:@[ @(1), setClientConfig ]]
259 result:^(id){
260 }];
261
262 FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
263 arguments:@{
264 @"text" : @"Text",
265 @"selectionBase" : @(4),
266 @"selectionExtent" : @(4),
267 @"composingBase" : @(2),
268 @"composingExtent" : @(4),
269 }];
270 [plugin handleMethodCall:call
271 result:^(id){
272 }];
273
274 // Update with the composing region removed.
275 call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
276 arguments:@{
277 @"text" : @"Te",
278 @"selectionBase" : @(2),
279 @"selectionExtent" : @(2),
280 @"composingBase" : @(-1),
281 @"composingExtent" : @(-1),
282 }];
283 [plugin handleMethodCall:call
284 result:^(id){
285 }];
286
287 // Verify editing state was set.
288 NSDictionary* editingState = [plugin editingState];
289 EXPECT_STREQ([editingState[@"text"] UTF8String], "Te");
290 EXPECT_STREQ([editingState[@"selectionAffinity"] UTF8String], "TextAffinity.upstream");
291 EXPECT_FALSE([editingState[@"selectionIsDirectional"] boolValue]);
292 EXPECT_EQ([editingState[@"selectionBase"] intValue], 2);
293 EXPECT_EQ([editingState[@"selectionExtent"] intValue], 2);
294 EXPECT_EQ([editingState[@"composingBase"] intValue], -1);
295 EXPECT_EQ([editingState[@"composingExtent"] intValue], -1);
296 return true;
297 }
298
299 - (bool)testClearClientDuringComposing {
300 // Set up FlutterTextInputPlugin.
301 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
302 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
303 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
304 [engineMock binaryMessenger])
305 .andReturn(binaryMessengerMock);
306 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
307 nibName:@""
308 bundle:nil];
309 FlutterTextInputPlugin* plugin =
310 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
311
312 // Set input client 1.
313 NSDictionary* setClientConfig = @{
314 @"inputAction" : @"action",
315 @"inputType" : @{@"name" : @"inputName"},
316 };
317 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
318 arguments:@[ @(1), setClientConfig ]]
319 result:^(id){
320 }];
321
322 // Set editing state with an active composing range.
323 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
324 arguments:@{
325 @"text" : @"Text",
326 @"selectionBase" : @(0),
327 @"selectionExtent" : @(0),
328 @"composingBase" : @(0),
329 @"composingExtent" : @(1),
330 }]
331 result:^(id){
332 }];
333
334 // Verify composing range is (0, 1).
335 NSDictionary* editingState = [plugin editingState];
336 EXPECT_EQ([editingState[@"composingBase"] intValue], 0);
337 EXPECT_EQ([editingState[@"composingExtent"] intValue], 1);
338
339 // Clear input client.
340 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.clearClient"
341 arguments:@[]]
342 result:^(id){
343 }];
344
345 // Verify composing range is collapsed.
346 editingState = [plugin editingState];
347 EXPECT_EQ([editingState[@"composingBase"] intValue], [editingState[@"composingExtent"] intValue]);
348 return true;
349 }
350
351 - (bool)testAutocompleteDisabledWhenAutofillNotSet {
352 // Set up FlutterTextInputPlugin.
353 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
354 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
355 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
356 [engineMock binaryMessenger])
357 .andReturn(binaryMessengerMock);
358 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
359 nibName:@""
360 bundle:nil];
361 FlutterTextInputPlugin* plugin =
362 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
363
364 // Set input client 1.
365 NSDictionary* setClientConfig = @{
366 @"inputAction" : @"action",
367 @"inputType" : @{@"name" : @"inputName"},
368 };
369 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
370 arguments:@[ @(1), setClientConfig ]]
371 result:^(id){
372 }];
373
374 // Verify autocomplete is disabled.
375 EXPECT_FALSE([plugin isAutomaticTextCompletionEnabled]);
376 return true;
377 }
378
379 - (bool)testAutocompleteEnabledWhenAutofillSet {
380 // Set up FlutterTextInputPlugin.
381 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
382 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
383 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
384 [engineMock binaryMessenger])
385 .andReturn(binaryMessengerMock);
386 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
387 nibName:@""
388 bundle:nil];
389 FlutterTextInputPlugin* plugin =
390 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
391
392 // Set input client 1.
393 NSDictionary* setClientConfig = @{
394 @"inputAction" : @"action",
395 @"inputType" : @{@"name" : @"inputName"},
396 @"autofill" : @{
397 @"uniqueIdentifier" : @"field1",
398 @"hints" : @[ @"name" ],
399 @"editingValue" : @{@"text" : @""},
400 }
401 };
402 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
403 arguments:@[ @(1), setClientConfig ]]
404 result:^(id){
405 }];
406
407 // Verify autocomplete is enabled.
408 EXPECT_TRUE([plugin isAutomaticTextCompletionEnabled]);
409
410 // Verify content type is nil for unsupported content types.
411 if (@available(macOS 11.0, *)) {
412 EXPECT_EQ([plugin contentType], nil);
413 }
414 return true;
415 }
416
417 - (bool)testAutocompleteEnabledWhenAutofillSetNoHint {
418 // Set up FlutterTextInputPlugin.
419 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
420 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
421 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
422 [engineMock binaryMessenger])
423 .andReturn(binaryMessengerMock);
424 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
425 nibName:@""
426 bundle:nil];
427 FlutterTextInputPlugin* plugin =
428 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
429
430 // Set input client 1.
431 NSDictionary* setClientConfig = @{
432 @"inputAction" : @"action",
433 @"inputType" : @{@"name" : @"inputName"},
434 @"autofill" : @{
435 @"uniqueIdentifier" : @"field1",
436 @"hints" : @[],
437 @"editingValue" : @{@"text" : @""},
438 }
439 };
440 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
441 arguments:@[ @(1), setClientConfig ]]
442 result:^(id){
443 }];
444
445 // Verify autocomplete is enabled.
446 EXPECT_TRUE([plugin isAutomaticTextCompletionEnabled]);
447 return true;
448 }
449
450 - (bool)testAutocompleteDisabledWhenObscureTextSet {
451 // Set up FlutterTextInputPlugin.
452 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
453 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
454 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
455 [engineMock binaryMessenger])
456 .andReturn(binaryMessengerMock);
457 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
458 nibName:@""
459 bundle:nil];
460 FlutterTextInputPlugin* plugin =
461 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
462
463 // Set input client 1.
464 NSDictionary* setClientConfig = @{
465 @"inputAction" : @"action",
466 @"inputType" : @{@"name" : @"inputName"},
467 @"obscureText" : @YES,
468 @"autofill" : @{
469 @"uniqueIdentifier" : @"field1",
470 @"editingValue" : @{@"text" : @""},
471 }
472 };
473 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
474 arguments:@[ @(1), setClientConfig ]]
475 result:^(id){
476 }];
477
478 // Verify autocomplete is disabled.
479 EXPECT_FALSE([plugin isAutomaticTextCompletionEnabled]);
480 return true;
481 }
482
483 - (bool)testAutocompleteDisabledWhenPasswordAutofillSet {
484 // Set up FlutterTextInputPlugin.
485 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
486 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
487 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
488 [engineMock binaryMessenger])
489 .andReturn(binaryMessengerMock);
490 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
491 nibName:@""
492 bundle:nil];
493 FlutterTextInputPlugin* plugin =
494 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
495
496 // Set input client 1.
497 NSDictionary* setClientConfig = @{
498 @"inputAction" : @"action",
499 @"inputType" : @{@"name" : @"inputName"},
500 @"autofill" : @{
501 @"uniqueIdentifier" : @"field1",
502 @"hints" : @[ @"password" ],
503 @"editingValue" : @{@"text" : @""},
504 }
505 };
506 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
507 arguments:@[ @(1), setClientConfig ]]
508 result:^(id){
509 }];
510
511 // Verify autocomplete is disabled.
512 EXPECT_FALSE([plugin isAutomaticTextCompletionEnabled]);
513
514 // Verify content type is password.
515 if (@available(macOS 11.0, *)) {
516 EXPECT_EQ([plugin contentType], NSTextContentTypePassword);
517 }
518 return true;
519 }
520
521 - (bool)testAutocompleteDisabledWhenAutofillGroupIncludesPassword {
522 // Set up FlutterTextInputPlugin.
523 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
524 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
525 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
526 [engineMock binaryMessenger])
527 .andReturn(binaryMessengerMock);
528 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
529 nibName:@""
530 bundle:nil];
531 FlutterTextInputPlugin* plugin =
532 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
533
534 // Set input client 1.
535 NSDictionary* setClientConfig = @{
536 @"inputAction" : @"action",
537 @"inputType" : @{@"name" : @"inputName"},
538 @"fields" : @[
539 @{
540 @"inputAction" : @"action",
541 @"inputType" : @{@"name" : @"inputName"},
542 @"autofill" : @{
543 @"uniqueIdentifier" : @"field1",
544 @"hints" : @[ @"password" ],
545 @"editingValue" : @{@"text" : @""},
546 }
547 },
548 @{
549 @"inputAction" : @"action",
550 @"inputType" : @{@"name" : @"inputName"},
551 @"autofill" : @{
552 @"uniqueIdentifier" : @"field2",
553 @"hints" : @[ @"name" ],
554 @"editingValue" : @{@"text" : @""},
555 }
556 }
557 ]
558 };
559 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
560 arguments:@[ @(1), setClientConfig ]]
561 result:^(id){
562 }];
563
564 // Verify autocomplete is disabled.
565 EXPECT_FALSE([plugin isAutomaticTextCompletionEnabled]);
566 return true;
567 }
568
569 - (bool)testContentTypeWhenAutofillTypeIsUsername {
570 // Set up FlutterTextInputPlugin.
571 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
572 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
573 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
574 [engineMock binaryMessenger])
575 .andReturn(binaryMessengerMock);
576 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
577 nibName:@""
578 bundle:nil];
579 FlutterTextInputPlugin* plugin =
580 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
581
582 // Set input client 1.
583 NSDictionary* setClientConfig = @{
584 @"inputAction" : @"action",
585 @"inputType" : @{@"name" : @"inputName"},
586 @"autofill" : @{
587 @"uniqueIdentifier" : @"field1",
588 @"hints" : @[ @"name" ],
589 @"editingValue" : @{@"text" : @""},
590 }
591 };
592 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
593 arguments:@[ @(1), setClientConfig ]]
594 result:^(id){
595 }];
596
597 // Verify autocomplete is disabled.
598 EXPECT_FALSE([plugin isAutomaticTextCompletionEnabled]);
599
600 // Verify content type is username.
601 if (@available(macOS 11.0, *)) {
602 EXPECT_EQ([plugin contentType], NSTextContentTypeUsername);
603 }
604 return true;
605 }
606
607 - (bool)testContentTypeWhenAutofillTypeIsOneTimeCode {
608 // Set up FlutterTextInputPlugin.
609 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
610 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
611 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
612 [engineMock binaryMessenger])
613 .andReturn(binaryMessengerMock);
614 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
615 nibName:@""
616 bundle:nil];
617 FlutterTextInputPlugin* plugin =
618 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
619
620 // Set input client 1.
621 NSDictionary* setClientConfig = @{
622 @"inputAction" : @"action",
623 @"inputType" : @{@"name" : @"inputName"},
624 @"autofill" : @{
625 @"uniqueIdentifier" : @"field1",
626 @"hints" : @[ @"oneTimeCode" ],
627 @"editingValue" : @{@"text" : @""},
628 }
629 };
630 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
631 arguments:@[ @(1), setClientConfig ]]
632 result:^(id){
633 }];
634
635 // Verify autocomplete is disabled.
636 EXPECT_FALSE([plugin isAutomaticTextCompletionEnabled]);
637
638 // Verify content type is username.
639 if (@available(macOS 11.0, *)) {
640 EXPECT_EQ([plugin contentType], NSTextContentTypeOneTimeCode);
641 }
642 return true;
643 }
644
645 - (bool)testFirstRectForCharacterRange {
646 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
647 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
648 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
649 [engineMock binaryMessenger])
650 .andReturn(binaryMessengerMock);
651 FlutterViewController* controllerMock =
652 [[TextInputTestViewController alloc] initWithEngine:engineMock nibName:nil bundle:nil];
653 [controllerMock loadView];
654 id viewMock = controllerMock.flutterView;
655 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
656 [viewMock bounds])
657 .andReturn(NSMakeRect(0, 0, 200, 200));
658
659 id windowMock = OCMClassMock([NSWindow class]);
660 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
661 [viewMock window])
662 .andReturn(windowMock);
663
664 OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
665 [viewMock convertRect:NSMakeRect(28, 10, 2, 19) toView:nil])
666 .andReturn(NSMakeRect(28, 10, 2, 19));
667
668 OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
669 [windowMock convertRectToScreen:NSMakeRect(28, 10, 2, 19)])
670 .andReturn(NSMakeRect(38, 20, 2, 19));
671
672 FlutterTextInputPlugin* plugin =
673 [[FlutterTextInputPlugin alloc] initWithViewController:controllerMock];
674
675 FlutterMethodCall* call = [FlutterMethodCall
676 methodCallWithMethodName:@"TextInput.setEditableSizeAndTransform"
677 arguments:@{
678 @"height" : @(20.0),
679 @"transform" : @[
680 @(1.0), @(0.0), @(0.0), @(0.0), @(0.0), @(1.0), @(0.0), @(0.0), @(0.0),
681 @(0.0), @(1.0), @(0.0), @(20.0), @(10.0), @(0.0), @(1.0)
682 ],
683 @"width" : @(400.0),
684 }];
685
686 [plugin handleMethodCall:call
687 result:^(id){
688 }];
689
690 call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setCaretRect"
691 arguments:@{
692 @"height" : @(19.0),
693 @"width" : @(2.0),
694 @"x" : @(8.0),
695 @"y" : @(0.0),
696 }];
697
698 [plugin handleMethodCall:call
699 result:^(id){
700 }];
701
702 NSRect rect = [plugin firstRectForCharacterRange:NSMakeRange(0, 0) actualRange:nullptr];
703 @try {
704 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
705 [windowMock convertRectToScreen:NSMakeRect(28, 10, 2, 19)]);
706 } @catch (...) {
707 return false;
708 }
709
710 return NSEqualRects(rect, NSMakeRect(38, 20, 2, 19));
711 }
712
713 - (bool)testFirstRectForCharacterRangeAtInfinity {
714 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
715 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
716 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
717 [engineMock binaryMessenger])
718 .andReturn(binaryMessengerMock);
719 FlutterViewController* controllerMock =
720 [[TextInputTestViewController alloc] initWithEngine:engineMock nibName:nil bundle:nil];
721 [controllerMock loadView];
722 id viewMock = controllerMock.flutterView;
723 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
724 [viewMock bounds])
725 .andReturn(NSMakeRect(0, 0, 200, 200));
726
727 id windowMock = OCMClassMock([NSWindow class]);
728 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
729 [viewMock window])
730 .andReturn(windowMock);
731
732 FlutterTextInputPlugin* plugin =
733 [[FlutterTextInputPlugin alloc] initWithViewController:controllerMock];
734
735 FlutterMethodCall* call = [FlutterMethodCall
736 methodCallWithMethodName:@"TextInput.setEditableSizeAndTransform"
737 arguments:@{
738 @"height" : @(20.0),
739 // Projects all points to infinity.
740 @"transform" : @[
741 @(1.0), @(0.0), @(0.0), @(0.0), @(0.0), @(1.0), @(0.0), @(0.0), @(0.0),
742 @(0.0), @(1.0), @(0.0), @(20.0), @(10.0), @(0.0), @(0.0)
743 ],
744 @"width" : @(400.0),
745 }];
746
747 [plugin handleMethodCall:call
748 result:^(id){
749 }];
750
751 call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setCaretRect"
752 arguments:@{
753 @"height" : @(19.0),
754 @"width" : @(2.0),
755 @"x" : @(8.0),
756 @"y" : @(0.0),
757 }];
758
759 [plugin handleMethodCall:call
760 result:^(id){
761 }];
762
763 NSRect rect = [plugin firstRectForCharacterRange:NSMakeRange(0, 0) actualRange:nullptr];
764 return NSEqualRects(rect, CGRectZero);
765 }
766
767 - (bool)testFirstRectForCharacterRangeWithEsotericAffineTransform {
768 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
769 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
770 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
771 [engineMock binaryMessenger])
772 .andReturn(binaryMessengerMock);
773 FlutterViewController* controllerMock =
774 [[TextInputTestViewController alloc] initWithEngine:engineMock nibName:nil bundle:nil];
775 [controllerMock loadView];
776 id viewMock = controllerMock.flutterView;
777 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
778 [viewMock bounds])
779 .andReturn(NSMakeRect(0, 0, 200, 200));
780
781 id windowMock = OCMClassMock([NSWindow class]);
782 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
783 [viewMock window])
784 .andReturn(windowMock);
785
786 OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
787 [viewMock convertRect:NSMakeRect(-18, 6, 3, 3) toView:nil])
788 .andReturn(NSMakeRect(-18, 6, 3, 3));
789
790 OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
791 [windowMock convertRectToScreen:NSMakeRect(-18, 6, 3, 3)])
792 .andReturn(NSMakeRect(-18, 6, 3, 3));
793
794 FlutterTextInputPlugin* plugin =
795 [[FlutterTextInputPlugin alloc] initWithViewController:controllerMock];
796
797 FlutterMethodCall* call = [FlutterMethodCall
798 methodCallWithMethodName:@"TextInput.setEditableSizeAndTransform"
799 arguments:@{
800 @"height" : @(20.0),
801 // This matrix can be generated by running this dart code snippet:
802 // Matrix4.identity()..scale(3.0)..rotateZ(math.pi/2)..translate(1.0, 2.0,
803 // 3.0);
804 @"transform" : @[
805 @(0.0), @(3.0), @(0.0), @(0.0), @(-3.0), @(0.0), @(0.0), @(0.0), @(0.0),
806 @(0.0), @(3.0), @(0.0), @(-6.0), @(3.0), @(9.0), @(1.0)
807 ],
808 @"width" : @(400.0),
809 }];
810
811 [plugin handleMethodCall:call
812 result:^(id){
813 }];
814
815 call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setCaretRect"
816 arguments:@{
817 @"height" : @(1.0),
818 @"width" : @(1.0),
819 @"x" : @(1.0),
820 @"y" : @(3.0),
821 }];
822
823 [plugin handleMethodCall:call
824 result:^(id){
825 }];
826
827 NSRect rect = [plugin firstRectForCharacterRange:NSMakeRange(0, 0) actualRange:nullptr];
828
829 @try {
830 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
831 [windowMock convertRectToScreen:NSMakeRect(-18, 6, 3, 3)]);
832 } @catch (...) {
833 return false;
834 }
835
836 return NSEqualRects(rect, NSMakeRect(-18, 6, 3, 3));
837 }
838
839 - (bool)testSetEditingStateWithTextEditingDelta {
840 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
841 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
842 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
843 [engineMock binaryMessenger])
844 .andReturn(binaryMessengerMock);
845
846 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
847 nibName:@""
848 bundle:nil];
849
850 FlutterTextInputPlugin* plugin =
851 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
852
853 NSDictionary* setClientConfig = @{
854 @"inputAction" : @"action",
855 @"enableDeltaModel" : @"true",
856 @"inputType" : @{@"name" : @"inputName"},
857 };
858 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
859 arguments:@[ @(1), setClientConfig ]]
860 result:^(id){
861 }];
862
863 FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
864 arguments:@{
865 @"text" : @"Text",
866 @"selectionBase" : @(0),
867 @"selectionExtent" : @(0),
868 @"composingBase" : @(-1),
869 @"composingExtent" : @(-1),
870 }];
871
872 [plugin handleMethodCall:call
873 result:^(id){
874 }];
875
876 // Verify editing state was set.
877 NSDictionary* editingState = [plugin editingState];
878 EXPECT_STREQ([editingState[@"text"] UTF8String], "Text");
879 EXPECT_STREQ([editingState[@"selectionAffinity"] UTF8String], "TextAffinity.upstream");
880 EXPECT_FALSE([editingState[@"selectionIsDirectional"] boolValue]);
881 EXPECT_EQ([editingState[@"selectionBase"] intValue], 0);
882 EXPECT_EQ([editingState[@"selectionExtent"] intValue], 0);
883 EXPECT_EQ([editingState[@"composingBase"] intValue], -1);
884 EXPECT_EQ([editingState[@"composingExtent"] intValue], -1);
885 return true;
886 }
887
888 - (bool)testOperationsThatTriggerDelta {
889 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
890 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
891 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
892 [engineMock binaryMessenger])
893 .andReturn(binaryMessengerMock);
894
895 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
896 nibName:@""
897 bundle:nil];
898
899 FlutterTextInputPlugin* plugin =
900 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
901
902 NSDictionary* setClientConfig = @{
903 @"inputAction" : @"action",
904 @"enableDeltaModel" : @"true",
905 @"inputType" : @{@"name" : @"inputName"},
906 };
907 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
908 arguments:@[ @(1), setClientConfig ]]
909 result:^(id){
910 }];
911 [plugin insertText:@"text to insert"];
912
913 NSDictionary* deltaToFramework = @{
914 @"oldText" : @"",
915 @"deltaText" : @"text to insert",
916 @"deltaStart" : @(0),
917 @"deltaEnd" : @(0),
918 @"selectionBase" : @(14),
919 @"selectionExtent" : @(14),
920 @"selectionAffinity" : @"TextAffinity.upstream",
921 @"selectionIsDirectional" : @(false),
922 @"composingBase" : @(-1),
923 @"composingExtent" : @(-1),
924 };
925 NSDictionary* expectedState = @{
926 @"deltas" : @[ deltaToFramework ],
927 };
928
929 NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance]
930 encodeMethodCall:[FlutterMethodCall
931 methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas"
932 arguments:@[ @(1), expectedState ]]];
933
934 @try {
935 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
936 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
937 } @catch (...) {
938 return false;
939 }
940
941 [plugin setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
942
943 deltaToFramework = @{
944 @"oldText" : @"text to insert",
945 @"deltaText" : @"marked text",
946 @"deltaStart" : @(14),
947 @"deltaEnd" : @(14),
948 @"selectionBase" : @(14),
949 @"selectionExtent" : @(15),
950 @"selectionAffinity" : @"TextAffinity.upstream",
951 @"selectionIsDirectional" : @(false),
952 @"composingBase" : @(14),
953 @"composingExtent" : @(25),
954 };
955 expectedState = @{
956 @"deltas" : @[ deltaToFramework ],
957 };
958
959 updateCall = [[FlutterJSONMethodCodec sharedInstance]
960 encodeMethodCall:[FlutterMethodCall
961 methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas"
962 arguments:@[ @(1), expectedState ]]];
963
964 @try {
965 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
966 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
967 } @catch (...) {
968 return false;
969 }
970
971 [plugin unmarkText];
972
973 deltaToFramework = @{
974 @"oldText" : @"text to insertmarked text",
975 @"deltaText" : @"",
976 @"deltaStart" : @(-1),
977 @"deltaEnd" : @(-1),
978 @"selectionBase" : @(25),
979 @"selectionExtent" : @(25),
980 @"selectionAffinity" : @"TextAffinity.upstream",
981 @"selectionIsDirectional" : @(false),
982 @"composingBase" : @(-1),
983 @"composingExtent" : @(-1),
984 };
985 expectedState = @{
986 @"deltas" : @[ deltaToFramework ],
987 };
988
989 updateCall = [[FlutterJSONMethodCodec sharedInstance]
990 encodeMethodCall:[FlutterMethodCall
991 methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas"
992 arguments:@[ @(1), expectedState ]]];
993
994 @try {
995 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
996 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
997 } @catch (...) {
998 return false;
999 }
1000 return true;
1001 }
1002
1003 - (bool)testComposingWithDelta {
1004 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
1005 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1006 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1007 [engineMock binaryMessenger])
1008 .andReturn(binaryMessengerMock);
1009
1010 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1011 nibName:@""
1012 bundle:nil];
1013
1014 FlutterTextInputPlugin* plugin =
1015 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
1016
1017 NSDictionary* setClientConfig = @{
1018 @"inputAction" : @"action",
1019 @"enableDeltaModel" : @"true",
1020 @"inputType" : @{@"name" : @"inputName"},
1021 };
1022 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
1023 arguments:@[ @(1), setClientConfig ]]
1024 result:^(id){
1025 }];
1026 [plugin setMarkedText:@"m" selectedRange:NSMakeRange(0, 1)];
1027
1028 NSDictionary* deltaToFramework = @{
1029 @"oldText" : @"",
1030 @"deltaText" : @"m",
1031 @"deltaStart" : @(0),
1032 @"deltaEnd" : @(0),
1033 @"selectionBase" : @(0),
1034 @"selectionExtent" : @(1),
1035 @"selectionAffinity" : @"TextAffinity.upstream",
1036 @"selectionIsDirectional" : @(false),
1037 @"composingBase" : @(0),
1038 @"composingExtent" : @(1),
1039 };
1040 NSDictionary* expectedState = @{
1041 @"deltas" : @[ deltaToFramework ],
1042 };
1043
1044 NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance]
1045 encodeMethodCall:[FlutterMethodCall
1046 methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas"
1047 arguments:@[ @(1), expectedState ]]];
1048
1049 @try {
1050 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1051 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
1052 } @catch (...) {
1053 return false;
1054 }
1055
1056 [plugin setMarkedText:@"ma" selectedRange:NSMakeRange(0, 1)];
1057
1058 deltaToFramework = @{
1059 @"oldText" : @"m",
1060 @"deltaText" : @"ma",
1061 @"deltaStart" : @(0),
1062 @"deltaEnd" : @(1),
1063 @"selectionBase" : @(0),
1064 @"selectionExtent" : @(1),
1065 @"selectionAffinity" : @"TextAffinity.upstream",
1066 @"selectionIsDirectional" : @(false),
1067 @"composingBase" : @(0),
1068 @"composingExtent" : @(2),
1069 };
1070 expectedState = @{
1071 @"deltas" : @[ deltaToFramework ],
1072 };
1073
1074 updateCall = [[FlutterJSONMethodCodec sharedInstance]
1075 encodeMethodCall:[FlutterMethodCall
1076 methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas"
1077 arguments:@[ @(1), expectedState ]]];
1078
1079 @try {
1080 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1081 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
1082 } @catch (...) {
1083 return false;
1084 }
1085
1086 [plugin setMarkedText:@"mar" selectedRange:NSMakeRange(0, 1)];
1087
1088 deltaToFramework = @{
1089 @"oldText" : @"ma",
1090 @"deltaText" : @"mar",
1091 @"deltaStart" : @(0),
1092 @"deltaEnd" : @(2),
1093 @"selectionBase" : @(0),
1094 @"selectionExtent" : @(1),
1095 @"selectionAffinity" : @"TextAffinity.upstream",
1096 @"selectionIsDirectional" : @(false),
1097 @"composingBase" : @(0),
1098 @"composingExtent" : @(3),
1099 };
1100 expectedState = @{
1101 @"deltas" : @[ deltaToFramework ],
1102 };
1103
1104 updateCall = [[FlutterJSONMethodCodec sharedInstance]
1105 encodeMethodCall:[FlutterMethodCall
1106 methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas"
1107 arguments:@[ @(1), expectedState ]]];
1108
1109 @try {
1110 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1111 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
1112 } @catch (...) {
1113 return false;
1114 }
1115
1116 [plugin setMarkedText:@"mark" selectedRange:NSMakeRange(0, 1)];
1117
1118 deltaToFramework = @{
1119 @"oldText" : @"mar",
1120 @"deltaText" : @"mark",
1121 @"deltaStart" : @(0),
1122 @"deltaEnd" : @(3),
1123 @"selectionBase" : @(0),
1124 @"selectionExtent" : @(1),
1125 @"selectionAffinity" : @"TextAffinity.upstream",
1126 @"selectionIsDirectional" : @(false),
1127 @"composingBase" : @(0),
1128 @"composingExtent" : @(4),
1129 };
1130 expectedState = @{
1131 @"deltas" : @[ deltaToFramework ],
1132 };
1133
1134 updateCall = [[FlutterJSONMethodCodec sharedInstance]
1135 encodeMethodCall:[FlutterMethodCall
1136 methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas"
1137 arguments:@[ @(1), expectedState ]]];
1138
1139 @try {
1140 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1141 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
1142 } @catch (...) {
1143 return false;
1144 }
1145
1146 [plugin setMarkedText:@"marke" selectedRange:NSMakeRange(0, 1)];
1147
1148 deltaToFramework = @{
1149 @"oldText" : @"mark",
1150 @"deltaText" : @"marke",
1151 @"deltaStart" : @(0),
1152 @"deltaEnd" : @(4),
1153 @"selectionBase" : @(0),
1154 @"selectionExtent" : @(1),
1155 @"selectionAffinity" : @"TextAffinity.upstream",
1156 @"selectionIsDirectional" : @(false),
1157 @"composingBase" : @(0),
1158 @"composingExtent" : @(5),
1159 };
1160 expectedState = @{
1161 @"deltas" : @[ deltaToFramework ],
1162 };
1163
1164 updateCall = [[FlutterJSONMethodCodec sharedInstance]
1165 encodeMethodCall:[FlutterMethodCall
1166 methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas"
1167 arguments:@[ @(1), expectedState ]]];
1168
1169 @try {
1170 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1171 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
1172 } @catch (...) {
1173 return false;
1174 }
1175
1176 [plugin setMarkedText:@"marked" selectedRange:NSMakeRange(0, 1)];
1177
1178 deltaToFramework = @{
1179 @"oldText" : @"marke",
1180 @"deltaText" : @"marked",
1181 @"deltaStart" : @(0),
1182 @"deltaEnd" : @(5),
1183 @"selectionBase" : @(0),
1184 @"selectionExtent" : @(1),
1185 @"selectionAffinity" : @"TextAffinity.upstream",
1186 @"selectionIsDirectional" : @(false),
1187 @"composingBase" : @(0),
1188 @"composingExtent" : @(6),
1189 };
1190 expectedState = @{
1191 @"deltas" : @[ deltaToFramework ],
1192 };
1193
1194 updateCall = [[FlutterJSONMethodCodec sharedInstance]
1195 encodeMethodCall:[FlutterMethodCall
1196 methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas"
1197 arguments:@[ @(1), expectedState ]]];
1198
1199 @try {
1200 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1201 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
1202 } @catch (...) {
1203 return false;
1204 }
1205
1206 [plugin unmarkText];
1207
1208 deltaToFramework = @{
1209 @"oldText" : @"marked",
1210 @"deltaText" : @"",
1211 @"deltaStart" : @(-1),
1212 @"deltaEnd" : @(-1),
1213 @"selectionBase" : @(6),
1214 @"selectionExtent" : @(6),
1215 @"selectionAffinity" : @"TextAffinity.upstream",
1216 @"selectionIsDirectional" : @(false),
1217 @"composingBase" : @(-1),
1218 @"composingExtent" : @(-1),
1219 };
1220 expectedState = @{
1221 @"deltas" : @[ deltaToFramework ],
1222 };
1223
1224 updateCall = [[FlutterJSONMethodCodec sharedInstance]
1225 encodeMethodCall:[FlutterMethodCall
1226 methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas"
1227 arguments:@[ @(1), expectedState ]]];
1228
1229 @try {
1230 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1231 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
1232 } @catch (...) {
1233 return false;
1234 }
1235 return true;
1236 }
1237
1238 - (bool)testComposingWithDeltasWhenSelectionIsActive {
1239 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
1240 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1241 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1242 [engineMock binaryMessenger])
1243 .andReturn(binaryMessengerMock);
1244
1245 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1246 nibName:@""
1247 bundle:nil];
1248
1249 FlutterTextInputPlugin* plugin =
1250 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
1251
1252 NSDictionary* setClientConfig = @{
1253 @"inputAction" : @"action",
1254 @"enableDeltaModel" : @"true",
1255 @"inputType" : @{@"name" : @"inputName"},
1256 };
1257 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
1258 arguments:@[ @(1), setClientConfig ]]
1259 result:^(id){
1260 }];
1261
1262 FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
1263 arguments:@{
1264 @"text" : @"Text",
1265 @"selectionBase" : @(0),
1266 @"selectionExtent" : @(4),
1267 @"composingBase" : @(-1),
1268 @"composingExtent" : @(-1),
1269 }];
1270 [plugin handleMethodCall:call
1271 result:^(id){
1272 }];
1273
1274 [plugin setMarkedText:@"~"
1275 selectedRange:NSMakeRange(1, 0)
1276 replacementRange:NSMakeRange(NSNotFound, 0)];
1277
1278 NSDictionary* deltaToFramework = @{
1279 @"oldText" : @"Text",
1280 @"deltaText" : @"~",
1281 @"deltaStart" : @(0),
1282 @"deltaEnd" : @(4),
1283 @"selectionBase" : @(1),
1284 @"selectionExtent" : @(1),
1285 @"selectionAffinity" : @"TextAffinity.upstream",
1286 @"selectionIsDirectional" : @(false),
1287 @"composingBase" : @(0),
1288 @"composingExtent" : @(1),
1289 };
1290 NSDictionary* expectedState = @{
1291 @"deltas" : @[ deltaToFramework ],
1292 };
1293
1294 NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance]
1295 encodeMethodCall:[FlutterMethodCall
1296 methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas"
1297 arguments:@[ @(1), expectedState ]]];
1298
1299 @try {
1300 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1301 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
1302 } @catch (...) {
1303 return false;
1304 }
1305 return true;
1306 }
1307
1308 - (bool)testPerformKeyEquivalent {
1309 __block NSEvent* eventBeingDispatchedByKeyboardManager = nil;
1310 FlutterViewController* viewControllerMock = OCMClassMock([FlutterViewController class]);
1311 OCMStub([viewControllerMock isDispatchingKeyEvent:[OCMArg any]])
1312 .andDo(^(NSInvocation* invocation) {
1313 NSEvent* event;
1314 [invocation getArgument:(void*)&event atIndex:2];
1315 BOOL result = event == eventBeingDispatchedByKeyboardManager;
1316 [invocation setReturnValue:&result];
1317 });
1318
1319 NSEvent* event = [NSEvent keyEventWithType:NSEventTypeKeyDown
1320 location:NSZeroPoint
1321 modifierFlags:0x100
1322 timestamp:0
1323 windowNumber:0
1324 context:nil
1325 characters:@""
1326 charactersIgnoringModifiers:@""
1327 isARepeat:NO
1328 keyCode:0x50];
1329
1330 FlutterTextInputPlugin* plugin =
1331 [[FlutterTextInputPlugin alloc] initWithViewController:viewControllerMock];
1332
1333 OCMExpect([viewControllerMock keyDown:event]);
1334
1335 // Require that event is handled (returns YES)
1336 if (![plugin performKeyEquivalent:event]) {
1337 return false;
1338 };
1339
1340 @try {
1341 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1342 [viewControllerMock keyDown:event]);
1343 } @catch (...) {
1344 return false;
1345 }
1346
1347 // performKeyEquivalent must not forward event if it is being
1348 // dispatched by keyboard manager
1349 eventBeingDispatchedByKeyboardManager = event;
1350
1351 OCMReject([viewControllerMock keyDown:event]);
1352 @try {
1353 // Require that event is not handled (returns NO) and not
1354 // forwarded to controller
1355 if ([plugin performKeyEquivalent:event]) {
1356 return false;
1357 };
1358 } @catch (...) {
1359 return false;
1360 }
1361
1362 return true;
1363 }
1364
1365 - (bool)handleArrowKeyWhenImePopoverIsActive {
1366 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
1367 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1368 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1369 [engineMock binaryMessenger])
1370 .andReturn(binaryMessengerMock);
1371 OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
1372 callback:nil
1373 userData:nil]);
1374
1375 NSTextInputContext* textInputContext = OCMClassMock([NSTextInputContext class]);
1376 OCMStub([textInputContext handleEvent:[OCMArg any]]).andReturn(YES);
1377
1378 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1379 nibName:@""
1380 bundle:nil];
1381
1382 FlutterTextInputPlugin* plugin =
1383 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
1384
1385 plugin.textInputContext = textInputContext;
1386
1387 NSDictionary* setClientConfig = @{
1388 @"inputAction" : @"action",
1389 @"enableDeltaModel" : @"true",
1390 @"inputType" : @{@"name" : @"inputName"},
1391 };
1392 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
1393 arguments:@[ @(1), setClientConfig ]]
1394 result:^(id){
1395 }];
1396
1397 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.show"
1398 arguments:@[]]
1399 result:^(id){
1400 }];
1401
1402 // Set marked text, simulate active IME popover.
1403 [plugin setMarkedText:@"m"
1404 selectedRange:NSMakeRange(0, 1)
1405 replacementRange:NSMakeRange(NSNotFound, 0)];
1406
1407 // Right arrow key. This, unlike the key below should be handled by the plugin.
1408 NSEvent* event = [NSEvent keyEventWithType:NSEventTypeKeyDown
1409 location:NSZeroPoint
1410 modifierFlags:0xa00100
1411 timestamp:0
1412 windowNumber:0
1413 context:nil
1414 characters:@"\uF702"
1415 charactersIgnoringModifiers:@"\uF702"
1416 isARepeat:NO
1417 keyCode:0x4];
1418
1419 // Plugin should mark the event as key equivalent.
1420 [plugin performKeyEquivalent:event];
1421
1422 if ([plugin handleKeyEvent:event] != true) {
1423 return false;
1424 }
1425
1426 // CTRL+H (delete backwards)
1427 event = [NSEvent keyEventWithType:NSEventTypeKeyDown
1428 location:NSZeroPoint
1429 modifierFlags:0x40101
1430 timestamp:0
1431 windowNumber:0
1432 context:nil
1433 characters:@"\uF702"
1434 charactersIgnoringModifiers:@"\uF702"
1435 isARepeat:NO
1436 keyCode:0x4];
1437
1438 // Plugin should mark the event as key equivalent.
1439 [plugin performKeyEquivalent:event];
1440
1441 if ([plugin handleKeyEvent:event] != false) {
1442 return false;
1443 }
1444
1445 return true;
1446 }
1447
1448 - (bool)unhandledKeyEquivalent {
1449 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
1450 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1451 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1452 [engineMock binaryMessenger])
1453 .andReturn(binaryMessengerMock);
1454
1455 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1456 nibName:@""
1457 bundle:nil];
1458
1459 FlutterTextInputPlugin* plugin =
1460 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
1461
1462 NSDictionary* setClientConfig = @{
1463 @"inputAction" : @"action",
1464 @"enableDeltaModel" : @"true",
1465 @"inputType" : @{@"name" : @"inputName"},
1466 };
1467 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
1468 arguments:@[ @(1), setClientConfig ]]
1469 result:^(id){
1470 }];
1471
1472 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.show"
1473 arguments:@[]]
1474 result:^(id){
1475 }];
1476
1477 // CTRL+H (delete backwards)
1478 NSEvent* event = [NSEvent keyEventWithType:NSEventTypeKeyDown
1479 location:NSZeroPoint
1480 modifierFlags:0x40101
1481 timestamp:0
1482 windowNumber:0
1483 context:nil
1484 characters:@""
1485 charactersIgnoringModifiers:@"h"
1486 isARepeat:NO
1487 keyCode:0x4];
1488
1489 // Plugin should mark the event as key equivalent.
1490 [plugin performKeyEquivalent:event];
1491
1492 // Simulate KeyboardManager sending unhandled event to plugin. This must return
1493 // true because it is a known editing command.
1494 if ([plugin handleKeyEvent:event] != true) {
1495 return false;
1496 }
1497
1498 // CMD+W
1499 event = [NSEvent keyEventWithType:NSEventTypeKeyDown
1500 location:NSZeroPoint
1501 modifierFlags:0x100108
1502 timestamp:0
1503 windowNumber:0
1504 context:nil
1505 characters:@"w"
1506 charactersIgnoringModifiers:@"w"
1507 isARepeat:NO
1508 keyCode:0x13];
1509
1510 // Plugin should mark the event as key equivalent.
1511 [plugin performKeyEquivalent:event];
1512
1513 // This is not a valid editing command, plugin must return false so that
1514 // KeyboardManager sends the event to next responder.
1515 if ([plugin handleKeyEvent:event] != false) {
1516 return false;
1517 }
1518
1519 return true;
1520 }
1521
1522 - (bool)testInsertNewLine {
1523 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
1524 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1525 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1526 [engineMock binaryMessenger])
1527 .andReturn(binaryMessengerMock);
1528 OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
1529 callback:nil
1530 userData:nil]);
1531
1532 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1533 nibName:@""
1534 bundle:nil];
1535
1536 FlutterTextInputPlugin* plugin =
1537 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
1538
1539 NSDictionary* setClientConfig = @{
1540 @"inputType" : @{@"name" : @"TextInputType.multiline"},
1541 @"inputAction" : @"TextInputAction.newline",
1542 };
1543 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
1544 arguments:@[ @(1), setClientConfig ]]
1545 result:^(id){
1546 }];
1547
1548 FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
1549 arguments:@{
1550 @"text" : @"Text",
1551 @"selectionBase" : @(4),
1552 @"selectionExtent" : @(4),
1553 @"composingBase" : @(-1),
1554 @"composingExtent" : @(-1),
1555 }];
1556
1557 [plugin handleMethodCall:call
1558 result:^(id){
1559 }];
1560
1561 // Verify editing state was set.
1562 NSDictionary* editingState = [plugin editingState];
1563 EXPECT_STREQ([editingState[@"text"] UTF8String], "Text");
1564 EXPECT_STREQ([editingState[@"selectionAffinity"] UTF8String], "TextAffinity.upstream");
1565 EXPECT_FALSE([editingState[@"selectionIsDirectional"] boolValue]);
1566 EXPECT_EQ([editingState[@"selectionBase"] intValue], 4);
1567 EXPECT_EQ([editingState[@"selectionExtent"] intValue], 4);
1568 EXPECT_EQ([editingState[@"composingBase"] intValue], -1);
1569 EXPECT_EQ([editingState[@"composingExtent"] intValue], -1);
1570
1571 [plugin doCommandBySelector:@selector(insertNewline:)];
1572
1573 // Verify editing state was set.
1574 editingState = [plugin editingState];
1575 EXPECT_STREQ([editingState[@"text"] UTF8String], "Text\n");
1576 EXPECT_STREQ([editingState[@"selectionAffinity"] UTF8String], "TextAffinity.upstream");
1577 EXPECT_FALSE([editingState[@"selectionIsDirectional"] boolValue]);
1578 EXPECT_EQ([editingState[@"selectionBase"] intValue], 5);
1579 EXPECT_EQ([editingState[@"selectionExtent"] intValue], 5);
1580 EXPECT_EQ([editingState[@"composingBase"] intValue], -1);
1581 EXPECT_EQ([editingState[@"composingExtent"] intValue], -1);
1582
1583 return true;
1584 }
1585
1586 - (bool)testSendActionDoNotInsertNewLine {
1587 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
1588 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1589 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1590 [engineMock binaryMessenger])
1591 .andReturn(binaryMessengerMock);
1592 OCMStub([[engineMock ignoringNonObjectArgs] sendKeyEvent:FlutterKeyEvent {}
1593 callback:nil
1594 userData:nil]);
1595
1596 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1597 nibName:@""
1598 bundle:nil];
1599
1600 FlutterTextInputPlugin* plugin =
1601 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
1602
1603 NSDictionary* setClientConfig = @{
1604 @"inputType" : @{@"name" : @"TextInputType.multiline"},
1605 @"inputAction" : @"TextInputAction.send",
1606 };
1607 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
1608 arguments:@[ @(1), setClientConfig ]]
1609 result:^(id){
1610 }];
1611
1612 FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
1613 arguments:@{
1614 @"text" : @"Text",
1615 @"selectionBase" : @(4),
1616 @"selectionExtent" : @(4),
1617 @"composingBase" : @(-1),
1618 @"composingExtent" : @(-1),
1619 }];
1620
1621 NSDictionary* expectedState = @{
1622 @"selectionBase" : @(4),
1623 @"selectionExtent" : @(4),
1624 @"selectionAffinity" : @"TextAffinity.upstream",
1625 @"selectionIsDirectional" : @(NO),
1626 @"composingBase" : @(-1),
1627 @"composingExtent" : @(-1),
1628 @"text" : @"Text",
1629 };
1630
1631 NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance]
1632 encodeMethodCall:[FlutterMethodCall
1633 methodCallWithMethodName:@"TextInputClient.updateEditingState"
1634 arguments:@[ @(1), expectedState ]]];
1635
1636 OCMExpect( // NOLINT(google-objc-avoid-throwing-exception)
1637 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
1638
1639 [plugin handleMethodCall:call
1640 result:^(id){
1641 }];
1642
1643 [plugin doCommandBySelector:@selector(insertNewline:)];
1644
1645 NSData* performActionCall = [[FlutterJSONMethodCodec sharedInstance]
1646 encodeMethodCall:[FlutterMethodCall
1647 methodCallWithMethodName:@"TextInputClient.performAction"
1648 arguments:@[ @(1), @"TextInputAction.send" ]]];
1649
1650 // Input action should be notified.
1651 @try {
1652 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1653 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:performActionCall]);
1654 } @catch (...) {
1655 return false;
1656 }
1657
1658 NSDictionary* updatedState = @{
1659 @"selectionBase" : @(5),
1660 @"selectionExtent" : @(5),
1661 @"selectionAffinity" : @"TextAffinity.upstream",
1662 @"selectionIsDirectional" : @(NO),
1663 @"composingBase" : @(-1),
1664 @"composingExtent" : @(-1),
1665 @"text" : @"Text\n",
1666 };
1667
1668 updateCall = [[FlutterJSONMethodCodec sharedInstance]
1669 encodeMethodCall:[FlutterMethodCall
1670 methodCallWithMethodName:@"TextInputClient.updateEditingState"
1671 arguments:@[ @(1), updatedState ]]];
1672
1673 // Verify that editing state was not be updated.
1674 @try {
1675 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1676 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
1677 return false;
1678 } @catch (...) {
1679 // Expected.
1680 }
1681
1682 return true;
1683 }
1684
1685 - (bool)testLocalTextAndSelectionUpdateAfterDelta {
1686 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
1687 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1688 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1689 [engineMock binaryMessenger])
1690 .andReturn(binaryMessengerMock);
1691
1692 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1693 nibName:@""
1694 bundle:nil];
1695
1696 FlutterTextInputPlugin* plugin =
1697 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
1698
1699 NSDictionary* setClientConfig = @{
1700 @"inputAction" : @"action",
1701 @"enableDeltaModel" : @"true",
1702 @"inputType" : @{@"name" : @"inputName"},
1703 };
1704 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
1705 arguments:@[ @(1), setClientConfig ]]
1706 result:^(id){
1707 }];
1708 [plugin insertText:@"text to insert"];
1709
1710 NSDictionary* deltaToFramework = @{
1711 @"oldText" : @"",
1712 @"deltaText" : @"text to insert",
1713 @"deltaStart" : @(0),
1714 @"deltaEnd" : @(0),
1715 @"selectionBase" : @(14),
1716 @"selectionExtent" : @(14),
1717 @"selectionAffinity" : @"TextAffinity.upstream",
1718 @"selectionIsDirectional" : @(false),
1719 @"composingBase" : @(-1),
1720 @"composingExtent" : @(-1),
1721 };
1722 NSDictionary* expectedState = @{
1723 @"deltas" : @[ deltaToFramework ],
1724 };
1725
1726 NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance]
1727 encodeMethodCall:[FlutterMethodCall
1728 methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas"
1729 arguments:@[ @(1), expectedState ]]];
1730
1731 @try {
1732 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1733 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]);
1734 } @catch (...) {
1735 return false;
1736 }
1737
1738 bool localTextAndSelectionUpdated = [plugin.string isEqualToString:@"text to insert"] &&
1739 NSEqualRanges(plugin.selectedRange, NSMakeRange(14, 0));
1740
1741 return localTextAndSelectionUpdated;
1742 }
1743
1744 - (bool)testSelectorsAreForwardedToFramework {
1745 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
1746 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1747 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1748 [engineMock binaryMessenger])
1749 .andReturn(binaryMessengerMock);
1750
1751 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1752 nibName:@""
1753 bundle:nil];
1754
1755 FlutterTextInputPlugin* plugin =
1756 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
1757
1758 NSDictionary* setClientConfig = @{
1759 @"inputAction" : @"action",
1760 @"enableDeltaModel" : @"true",
1761 @"inputType" : @{@"name" : @"inputName"},
1762 };
1763 [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
1764 arguments:@[ @(1), setClientConfig ]]
1765 result:^(id){
1766 }];
1767
1768 // Can't run CFRunLoop in default mode because it causes crashes from scheduled
1769 // sources from other tests.
1770 NSString* runLoopMode = @"FlutterTestRunLoopMode";
1771 plugin.customRunLoopMode = runLoopMode;
1772
1773 // Ensure both selectors are grouped in one platform channel call.
1774 [plugin doCommandBySelector:@selector(moveUp:)];
1775 [plugin doCommandBySelector:@selector(moveRightAndModifySelection:)];
1776
1777 __block bool done = false;
1778 CFRunLoopPerformBlock(CFRunLoopGetMain(), (__bridge CFStringRef)runLoopMode, ^{
1779 done = true;
1780 });
1781
1782 while (!done) {
1783 // Each invocation will handle one source.
1784 CFRunLoopRunInMode((__bridge CFStringRef)runLoopMode, 0, true);
1785 }
1786
1787 NSData* performSelectorCall = [[FlutterJSONMethodCodec sharedInstance]
1788 encodeMethodCall:[FlutterMethodCall
1789 methodCallWithMethodName:@"TextInputClient.performSelectors"
1790 arguments:@[
1791 @(1), @[ @"moveUp:", @"moveRightAndModifySelection:" ]
1792 ]]];
1793
1794 @try {
1795 OCMVerify( // NOLINT(google-objc-avoid-throwing-exception)
1796 [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:performSelectorCall]);
1797 } @catch (...) {
1798 return false;
1799 }
1800
1801 return true;
1802 }
1803
1804 - (bool)testSelectorsNotForwardedToFrameworkIfNoClient {
1805 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
1806 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1807 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
1808 [engineMock binaryMessenger])
1809 .andReturn(binaryMessengerMock);
1810 // Make sure the selectors are not forwarded to the framework.
1811 OCMReject([binaryMessengerMock sendOnChannel:@"flutter/textinput" message:[OCMArg any]]);
1812 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
1813 nibName:@""
1814 bundle:nil];
1815
1816 FlutterTextInputPlugin* plugin =
1817 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
1818
1819 // Can't run CFRunLoop in default mode because it causes crashes from scheduled
1820 // sources from other tests.
1821 NSString* runLoopMode = @"FlutterTestRunLoopMode";
1822 plugin.customRunLoopMode = runLoopMode;
1823
1824 // Call selectors without setting a client.
1825 [plugin doCommandBySelector:@selector(moveUp:)];
1826 [plugin doCommandBySelector:@selector(moveRightAndModifySelection:)];
1827
1828 __block bool done = false;
1829 CFRunLoopPerformBlock(CFRunLoopGetMain(), (__bridge CFStringRef)runLoopMode, ^{
1830 done = true;
1831 });
1832
1833 while (!done) {
1834 CFRunLoopRunInMode((__bridge CFStringRef)runLoopMode, 0, true);
1835 }
1836 // At this point the selectors should be dropped; otherwise, OCMReject will throw.
1837 return true;
1838 }
1839
1840 @end
1841
1842 namespace flutter::testing {
1843
1844 namespace {
1845 // Allocates and returns an engine configured for the text fixture resource configuration.
1846 FlutterEngine* CreateTestEngine() {
1847 NSString* fixtures = @(testing::GetFixturesPath());
1848 FlutterDartProject* project = [[FlutterDartProject alloc]
1849 initWithAssetsPath:fixtures
1850 ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
1851 return [[FlutterEngine alloc] initWithName:@"test" project:project allowHeadlessExecution:true];
1852 }
1853 } // namespace
1854
1855 TEST(FlutterTextInputPluginTest, TestEmptyCompositionRange) {
1856 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testEmptyCompositionRange]);
1857 }
1858
1859 TEST(FlutterTextInputPluginTest, TestSetMarkedTextWithSelectionChange) {
1860 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSetMarkedTextWithSelectionChange]);
1861 }
1862
1863 TEST(FlutterTextInputPluginTest, TestSetMarkedTextWithReplacementRange) {
1864 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSetMarkedTextWithReplacementRange]);
1865 }
1866
1867 TEST(FlutterTextInputPluginTest, TestComposingRegionRemovedByFramework) {
1868 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingRegionRemovedByFramework]);
1869 }
1870
1871 TEST(FlutterTextInputPluginTest, TestClearClientDuringComposing) {
1872 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testClearClientDuringComposing]);
1873 }
1874
1875 TEST(FlutterTextInputPluginTest, TestAutocompleteDisabledWhenAutofillNotSet) {
1876 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testAutocompleteDisabledWhenAutofillNotSet]);
1877 }
1878
1879 TEST(FlutterTextInputPluginTest, TestAutocompleteEnabledWhenAutofillSet) {
1880 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testAutocompleteEnabledWhenAutofillSet]);
1881 }
1882
1883 TEST(FlutterTextInputPluginTest, TestAutocompleteEnabledWhenAutofillSetNoHint) {
1884 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testAutocompleteEnabledWhenAutofillSetNoHint]);
1885 }
1886
1887 TEST(FlutterTextInputPluginTest, TestAutocompleteDisabledWhenObscureTextSet) {
1888 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testAutocompleteDisabledWhenObscureTextSet]);
1889 }
1890
1891 TEST(FlutterTextInputPluginTest, TestAutocompleteDisabledWhenPasswordAutofillSet) {
1892 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testAutocompleteDisabledWhenPasswordAutofillSet]);
1893 }
1894
1895 TEST(FlutterTextInputPluginTest, TestAutocompleteDisabledWhenAutofillGroupIncludesPassword) {
1896 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc]
1897 testAutocompleteDisabledWhenAutofillGroupIncludesPassword]);
1898 }
1899
1900 TEST(FlutterTextInputPluginTest, TestFirstRectForCharacterRange) {
1901 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testFirstRectForCharacterRange]);
1902 }
1903
1904 TEST(FlutterTextInputPluginTest, TestFirstRectForCharacterRangeAtInfinity) {
1905 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testFirstRectForCharacterRangeAtInfinity]);
1906 }
1907
1908 TEST(FlutterTextInputPluginTest, TestFirstRectForCharacterRangeWithEsotericAffineTransform) {
1909 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc]
1910 testFirstRectForCharacterRangeWithEsotericAffineTransform]);
1911 }
1912
1913 TEST(FlutterTextInputPluginTest, TestSetEditingStateWithTextEditingDelta) {
1914 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSetEditingStateWithTextEditingDelta]);
1915 }
1916
1917 TEST(FlutterTextInputPluginTest, TestOperationsThatTriggerDelta) {
1918 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testOperationsThatTriggerDelta]);
1919 }
1920
1921 TEST(FlutterTextInputPluginTest, TestComposingWithDelta) {
1922 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingWithDelta]);
1923 }
1924
1925 TEST(FlutterTextInputPluginTest, TestComposingWithDeltasWhenSelectionIsActive) {
1926 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingWithDeltasWhenSelectionIsActive]);
1927 }
1928
1929 TEST(FlutterTextInputPluginTest, TestLocalTextAndSelectionUpdateAfterDelta) {
1930 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testLocalTextAndSelectionUpdateAfterDelta]);
1931 }
1932
1933 TEST(FlutterTextInputPluginTest, TestPerformKeyEquivalent) {
1934 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testPerformKeyEquivalent]);
1935 }
1936
1937 TEST(FlutterTextInputPluginTest, HandleArrowKeyWhenImePopoverIsActive) {
1938 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] handleArrowKeyWhenImePopoverIsActive]);
1939 }
1940
1941 TEST(FlutterTextInputPluginTest, UnhandledKeyEquivalent) {
1942 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] unhandledKeyEquivalent]);
1943 }
1944
1945 TEST(FlutterTextInputPluginTest, TestSelectorsAreForwardedToFramework) {
1946 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSelectorsAreForwardedToFramework]);
1947 }
1948
1949 TEST(FlutterTextInputPluginTest, TestSelectorsNotForwardedToFrameworkIfNoClient) {
1950 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSelectorsNotForwardedToFrameworkIfNoClient]);
1951 }
1952
1953 TEST(FlutterTextInputPluginTest, TestInsertNewLine) {
1954 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testInsertNewLine]);
1955 }
1956
1957 TEST(FlutterTextInputPluginTest, TestSendActionDoNotInsertNewLine) {
1958 ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testSendActionDoNotInsertNewLine]);
1959 }
1960
1961 TEST(FlutterTextInputPluginTest, CanWorkWithFlutterTextField) {
1962 FlutterEngine* engine = CreateTestEngine();
1963 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
1964 nibName:nil
1965 bundle:nil];
1966 [viewController loadView];
1967 // Create a NSWindow so that the native text field can become first responder.
1968 NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
1969 styleMask:NSBorderlessWindowMask
1970 backing:NSBackingStoreBuffered
1971 defer:NO];
1972 window.contentView = viewController.view;
1973
1974 engine.semanticsEnabled = YES;
1975
1976 auto bridge = viewController.accessibilityBridge.lock();
1977 FlutterPlatformNodeDelegateMac delegate(bridge, viewController);
1978 ui::AXTree tree;
1979 ui::AXNode ax_node(&tree, nullptr, 0, 0);
1980 ui::AXNodeData node_data;
1981 node_data.SetValue("initial text");
1982 ax_node.SetData(node_data);
1983 delegate.Init(viewController.accessibilityBridge, &ax_node);
1984 {
1985 FlutterTextPlatformNode text_platform_node(&delegate, viewController);
1986
1987 FlutterTextFieldMock* mockTextField =
1988 [[FlutterTextFieldMock alloc] initWithPlatformNode:&text_platform_node
1989 fieldEditor:viewController.textInputPlugin];
1990 [viewController.view addSubview:mockTextField];
1991 [mockTextField startEditing];
1992
1993 NSDictionary* setClientConfig = @{
1994 @"inputAction" : @"action",
1995 @"inputType" : @{@"name" : @"inputName"},
1996 };
1997 FlutterMethodCall* methodCall =
1998 [FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
1999 arguments:@[ @(1), setClientConfig ]];
2000 FlutterResult result = ^(id result) {
2001 };
2002 [viewController.textInputPlugin handleMethodCall:methodCall result:result];
2003
2004 NSDictionary* arguments = @{
2005 @"text" : @"new text",
2006 @"selectionBase" : @(1),
2007 @"selectionExtent" : @(2),
2008 @"composingBase" : @(-1),
2009 @"composingExtent" : @(-1),
2010 };
2011 methodCall = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState"
2012 arguments:arguments];
2013 [viewController.textInputPlugin handleMethodCall:methodCall result:result];
2014 EXPECT_EQ([mockTextField.lastUpdatedString isEqualToString:@"new text"], YES);
2015 EXPECT_EQ(NSEqualRanges(mockTextField.lastUpdatedSelection, NSMakeRange(1, 1)), YES);
2016
2017 // This blocks the FlutterTextFieldMock, which is held onto by the main event
2018 // loop, from crashing.
2019 [mockTextField setPlatformNode:nil];
2020 }
2021
2022 // This verifies that clearing the platform node works.
2023 [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
2024 }
2025
2026 TEST(FlutterTextInputPluginTest, CanNotBecomeResponderIfNoViewController) {
2027 FlutterEngine* engine = CreateTestEngine();
2028 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2029 nibName:nil
2030 bundle:nil];
2031 [viewController loadView];
2032 // Creates a NSWindow so that the native text field can become first responder.
2033 NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
2034 styleMask:NSBorderlessWindowMask
2035 backing:NSBackingStoreBuffered
2036 defer:NO];
2037 window.contentView = viewController.view;
2038
2039 engine.semanticsEnabled = YES;
2040
2041 auto bridge = viewController.accessibilityBridge.lock();
2042 FlutterPlatformNodeDelegateMac delegate(bridge, viewController);
2043 ui::AXTree tree;
2044 ui::AXNode ax_node(&tree, nullptr, 0, 0);
2045 ui::AXNodeData node_data;
2046 node_data.SetValue("initial text");
2047 ax_node.SetData(node_data);
2048 delegate.Init(viewController.accessibilityBridge, &ax_node);
2049 FlutterTextPlatformNode text_platform_node(&delegate, viewController);
2050
2051 FlutterTextField* textField = text_platform_node.GetNativeViewAccessible();
2052 EXPECT_EQ([textField becomeFirstResponder], YES);
2053 // Removes view controller.
2054 [engine setViewController:nil];
2055 FlutterTextPlatformNode text_platform_node_no_controller(&delegate, nil);
2056 textField = text_platform_node_no_controller.GetNativeViewAccessible();
2057 EXPECT_EQ([textField becomeFirstResponder], NO);
2058 }
2059
2060 TEST(FlutterTextInputPluginTest, IsAddedAndRemovedFromViewHierarchy) {
2061 FlutterEngine* engine = CreateTestEngine();
2062 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2063 nibName:nil
2064 bundle:nil];
2065 [viewController loadView];
2066
2067 NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
2068 styleMask:NSBorderlessWindowMask
2069 backing:NSBackingStoreBuffered
2070 defer:NO];
2071 window.contentView = viewController.view;
2072
2073 ASSERT_EQ(viewController.textInputPlugin.superview, nil);
2074 ASSERT_FALSE(window.firstResponder == viewController.textInputPlugin);
2075
2076 [viewController.textInputPlugin
2077 handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.show" arguments:@[]]
2078 result:^(id){
2079 }];
2080
2081 ASSERT_EQ(viewController.textInputPlugin.superview, viewController.view);
2082 ASSERT_TRUE(window.firstResponder == viewController.textInputPlugin);
2083
2084 [viewController.textInputPlugin
2085 handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.hide" arguments:@[]]
2086 result:^(id){
2087 }];
2088
2089 ASSERT_EQ(viewController.textInputPlugin.superview, nil);
2090 ASSERT_FALSE(window.firstResponder == viewController.textInputPlugin);
2091 }
2092
2093 TEST(FlutterTextInputPluginTest, FirstResponderIsCorrect) {
2094 FlutterEngine* engine = CreateTestEngine();
2095 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2096 nibName:nil
2097 bundle:nil];
2098 [viewController loadView];
2099
2100 NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
2101 styleMask:NSBorderlessWindowMask
2102 backing:NSBackingStoreBuffered
2103 defer:NO];
2104 window.contentView = viewController.view;
2105
2106 ASSERT_TRUE(viewController.flutterView.acceptsFirstResponder);
2107
2108 [window makeFirstResponder:viewController.flutterView];
2109
2110 [viewController.textInputPlugin
2111 handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.show" arguments:@[]]
2112 result:^(id){
2113 }];
2114
2115 ASSERT_TRUE(window.firstResponder == viewController.textInputPlugin);
2116
2117 ASSERT_FALSE(viewController.flutterView.acceptsFirstResponder);
2118
2119 [viewController.textInputPlugin
2120 handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.hide" arguments:@[]]
2121 result:^(id){
2122 }];
2123
2124 ASSERT_TRUE(viewController.flutterView.acceptsFirstResponder);
2125 ASSERT_TRUE(window.firstResponder == viewController.flutterView);
2126 }
2127
2128 TEST(FlutterTextInputPluginTest, HasZeroSizeAndClipsToBounds) {
2129 id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
2130 id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
2131 OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
2132 [engineMock binaryMessenger])
2133 .andReturn(binaryMessengerMock);
2134
2135 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
2136 nibName:@""
2137 bundle:nil];
2138
2139 FlutterTextInputPlugin* plugin =
2140 [[FlutterTextInputPlugin alloc] initWithViewController:viewController];
2141
2142 ASSERT_TRUE(NSIsEmptyRect(plugin.frame));
2143 ASSERT_TRUE(plugin.clipsToBounds);
2144 }
2145
2146 } // namespace flutter::testing
flutter::FlutterPlatformNodeDelegateMac::Init
void Init(std::weak_ptr< OwnerBridge > bridge, ui::AXNode *node) override
Called only once, immediately after construction. The constructor doesn't take any arguments because ...
Definition: FlutterPlatformNodeDelegateMac.mm:28
FlutterEngine
Definition: FlutterEngine.h:30
FlutterTextFieldMock::lastUpdatedString
NSString * lastUpdatedString
Definition: FlutterTextInputPluginTest.mm:23
+[FlutterMethodCall methodCallWithMethodName:arguments:]
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
-[FlutterTextInputPlugin handleMethodCall:result:]
void handleMethodCall:result:(FlutterMethodCall *call,[result] FlutterResult result)
-[FlutterInputPluginTestObjc testClearClientDuringComposing]
bool testClearClientDuringComposing()
Definition: FlutterTextInputPluginTest.mm:299
FlutterViewController
Definition: FlutterViewController.h:73
flutter::testing::CreateMockFlutterEngine
id CreateMockFlutterEngine(NSString *pasteboardString)
Definition: FlutterEngineTestUtils.mm:76
flutter::FlutterTextPlatformNode
The ax platform node for a text field.
Definition: FlutterTextInputSemanticsObject.h:22
FlutterTextInputPlugin.h
FlutterTextInputPlugin::customRunLoopMode
NSString * customRunLoopMode
Definition: FlutterTextInputPlugin.h:71
FlutterEngine_Internal.h
FlutterTextFieldMock
Definition: FlutterTextInputPluginTest.mm:21
flutter::FlutterTextPlatformNode::GetNativeViewAccessible
gfx::NativeViewAccessible GetNativeViewAccessible() override
Definition: FlutterTextInputSemanticsObject.mm:179
FlutterTextField(Testing)
Definition: FlutterTextInputPluginTest.mm:17
flutter::testing
Definition: AccessibilityBridgeMacTest.mm:13
FlutterEngineTestUtils.h
-[FlutterTextInputPlugin editingState]
NSDictionary * editingState()
FlutterTextFieldMock::lastUpdatedSelection
NSRange lastUpdatedSelection
Definition: FlutterTextInputPluginTest.mm:24
FlutterMethodCall
Definition: FlutterCodecs.h:220
flutter
Definition: AccessibilityBridgeMac.h:16
-[NSTextInputContext(Private) isActive]
BOOL isActive()
-[FlutterTextField startEditing]
void startEditing()
Definition: FlutterTextInputSemanticsObject.mm:112
FlutterTextInputPlugin
Definition: FlutterTextInputPlugin.h:27
flutter::testing::TEST
TEST(FlutterTextInputPluginTest, HasZeroSizeAndClipsToBounds)
Definition: FlutterTextInputPluginTest.mm:2128
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:194
NSView+ClipsToBounds.h
FlutterDartProject_Internal.h
FlutterViewController_Internal.h
FlutterView
Definition: FlutterView.h:35
FlutterTextInputSemanticsObject.h
FlutterJSONMethodCodec
Definition: FlutterCodecs.h:455
NSTextInputContext(Private)
Definition: FlutterTextInputPluginTest.mm:37
-[FlutterTextInputPlugin firstRectForCharacterRange:actualRange:]
NSRect firstRectForCharacterRange:actualRange:(NSRange range,[actualRange] NSRangePointer actualRange)
FlutterDartProject
Definition: FlutterDartProject.mm:24
TextInputTestViewController
Definition: FlutterTextInputPluginTest.mm:42
FlutterBinaryMessenger-p
Definition: FlutterBinaryMessenger.h:49
FlutterTextField
Definition: FlutterTextInputSemanticsObject.h:81
flutter::FlutterPlatformNodeDelegateMac
Definition: FlutterPlatformNodeDelegateMac.h:22
FlutterTextInputPlugin::textInputContext
NSTextInputContext * textInputContext
Definition: FlutterTextInputPlugin.h:70
FlutterViewController.h
-[FlutterInputPluginTestObjc testEmptyCompositionRange]
bool testEmptyCompositionRange()
Definition: FlutterTextInputPluginTest.mm:59
FlutterInputPluginTestObjc
Definition: FlutterTextInputPluginTest.mm:52