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 
13 
14 #import <OCMock/OCMock.h>
15 #import "flutter/testing/testing.h"
16 
17 #include <cstdint>
18 #include "flutter/common/constants.h"
19 
21 - (void)setPlatformNode:(flutter::FlutterTextPlatformNode*)node;
22 @end
23 
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 
41 // This is a private method.
42 - (BOOL)isActive;
43 @end
44 
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
58 @end
59 
61  id<FlutterBinaryMessenger> _binaryMessenger;
64 }
65 
66 @end
67 
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) {
100  } else {
101  return nil;
102  }
103 }
104 
105 @end
106 
107 @implementation FlutterInputPluginTestObjc
108 
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 
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 
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 
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 
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 
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];
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];
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];
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];
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];
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];
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];
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];
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];
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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 
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];
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 
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 
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 
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
void(^ FlutterResult)(id _Nullable result)
FlutterBinaryMessengerRelay * _binaryMessenger
static const FlutterViewIdentifier kViewId
int64_t FlutterViewIdentifier
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 ...
The ax platform node for a text field.
gfx::NativeViewAccessible GetNativeViewAccessible() override
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
NSTextInputContext * textInputContext
FlutterViewController * currentViewController
NSRect firstRectForCharacterRange:actualRange:(NSRange range,[actualRange] NSRangePointer actualRange)
void handleMethodCall:result:(FlutterMethodCall *call,[result] FlutterResult result)
NSDictionary * editingState()
id< FlutterBinaryMessenger > _binaryMessenger
TEST(FlutterTextInputPluginTest, InsertTextWithCollapsedSelectionInsideComposing)
id CreateMockFlutterEngine(NSString *pasteboardString)
id< FlutterBinaryMessenger > binaryMessenger