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