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