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