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