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