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