Flutter macOS Embedder
FlutterKeyboardManagerTest.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include <Carbon/Carbon.h>
6 #import <Foundation/Foundation.h>
7 #import <OCMock/OCMock.h>
8 
12 #include "flutter/shell/platform/embedder/test_utils/key_codes.g.h"
13 #import "flutter/testing/testing.h"
14 #include "third_party/googletest/googletest/include/gtest/gtest.h"
15 
16 namespace {
17 
18 using flutter::testing::keycodes::kLogicalBracketLeft;
19 using flutter::testing::keycodes::kLogicalDigit1;
20 using flutter::testing::keycodes::kLogicalDigit2;
21 using flutter::testing::keycodes::kLogicalKeyA;
22 using flutter::testing::keycodes::kLogicalKeyM;
23 using flutter::testing::keycodes::kLogicalKeyQ;
24 using flutter::testing::keycodes::kLogicalKeyT;
25 using flutter::testing::keycodes::kPhysicalKeyA;
26 
28 
29 typedef BOOL (^BoolGetter)();
30 typedef void (^AsyncKeyCallbackHandler)(FlutterAsyncKeyCallback callback);
31 typedef void (^AsyncEmbedderCallbackHandler)(const FlutterKeyEvent* event,
32  FlutterAsyncKeyCallback callback);
33 typedef BOOL (^TextInputCallback)(NSEvent*);
34 
35 // When the Vietnamese IME converts messages into "pure text" messages, their
36 // key codes are set to "empty".
37 //
38 // The 0 also happens to be the key code for key A.
39 constexpr uint16_t kKeyCodeEmpty = 0x00;
40 
41 // Constants used for `recordCallTypesTo:forTypes:`.
42 constexpr uint32_t kEmbedderCall = 0x1;
43 constexpr uint32_t kChannelCall = 0x2;
44 constexpr uint32_t kTextCall = 0x4;
45 
46 // All key clues for a keyboard layout.
47 //
48 // The index is (keyCode * 2 + hasShift). The value is 0xMNNNN, where:
49 //
50 // - M is whether the key is a dead key (0x1 for true, 0x0 for false).
51 // - N is the character for this key. (It only supports UTF-16, but we don't
52 // need full UTF-32 support for unit tests. Moreover, Carbon's UCKeyTranslate
53 // only returns UniChar (UInt16) anyway.)
54 typedef const std::array<uint32_t, 256> MockLayoutData;
55 
56 // The following layout data is generated using DEBUG_PRINT_LAYOUT.
57 
58 MockLayoutData kUsLayout = {
59  // +0x0 Shift +0x1 Shift +0x2 Shift +0x3 Shift
60  /* 0x00 */ 0x00061, 0x00041, 0x00073, 0x00053, 0x00064, 0x00044, 0x00066, 0x00046,
61  /* 0x04 */ 0x00068, 0x00048, 0x00067, 0x00047, 0x0007a, 0x0005a, 0x00078, 0x00058,
62  /* 0x08 */ 0x00063, 0x00043, 0x00076, 0x00056, 0x000a7, 0x000b1, 0x00062, 0x00042,
63  /* 0x0c */ 0x00071, 0x00051, 0x00077, 0x00057, 0x00065, 0x00045, 0x00072, 0x00052,
64  /* 0x10 */ 0x00079, 0x00059, 0x00074, 0x00054, 0x00031, 0x00021, 0x00032, 0x00040,
65  /* 0x14 */ 0x00033, 0x00023, 0x00034, 0x00024, 0x00036, 0x0005e, 0x00035, 0x00025,
66  /* 0x18 */ 0x0003d, 0x0002b, 0x00039, 0x00028, 0x00037, 0x00026, 0x0002d, 0x0005f,
67  /* 0x1c */ 0x00038, 0x0002a, 0x00030, 0x00029, 0x0005d, 0x0007d, 0x0006f, 0x0004f,
68  /* 0x20 */ 0x00075, 0x00055, 0x0005b, 0x0007b, 0x00069, 0x00049, 0x00070, 0x00050,
69  /* 0x24 */ 0x00000, 0x00000, 0x0006c, 0x0004c, 0x0006a, 0x0004a, 0x00027, 0x00022,
70  /* 0x28 */ 0x0006b, 0x0004b, 0x0003b, 0x0003a, 0x0005c, 0x0007c, 0x0002c, 0x0003c,
71  /* 0x2c */ 0x0002f, 0x0003f, 0x0006e, 0x0004e, 0x0006d, 0x0004d, 0x0002e, 0x0003e,
72  /* 0x30 */ 0x00000, 0x00000, 0x00020, 0x00020, 0x00060, 0x0007e,
73 };
74 
75 MockLayoutData kFrenchLayout = {
76  // +0x0 Shift +0x1 Shift +0x2 Shift +0x3 Shift
77  /* 0x00 */ 0x00071, 0x00051, 0x00073, 0x00053, 0x00064, 0x00044, 0x00066, 0x00046,
78  /* 0x04 */ 0x00068, 0x00048, 0x00067, 0x00047, 0x00077, 0x00057, 0x00078, 0x00058,
79  /* 0x08 */ 0x00063, 0x00043, 0x00076, 0x00056, 0x00040, 0x00023, 0x00062, 0x00042,
80  /* 0x0c */ 0x00061, 0x00041, 0x0007a, 0x0005a, 0x00065, 0x00045, 0x00072, 0x00052,
81  /* 0x10 */ 0x00079, 0x00059, 0x00074, 0x00054, 0x00026, 0x00031, 0x000e9, 0x00032,
82  /* 0x14 */ 0x00022, 0x00033, 0x00027, 0x00034, 0x000a7, 0x00036, 0x00028, 0x00035,
83  /* 0x18 */ 0x0002d, 0x0005f, 0x000e7, 0x00039, 0x000e8, 0x00037, 0x00029, 0x000b0,
84  /* 0x1c */ 0x00021, 0x00038, 0x000e0, 0x00030, 0x00024, 0x0002a, 0x0006f, 0x0004f,
85  /* 0x20 */ 0x00075, 0x00055, 0x1005e, 0x100a8, 0x00069, 0x00049, 0x00070, 0x00050,
86  /* 0x24 */ 0x00000, 0x00000, 0x0006c, 0x0004c, 0x0006a, 0x0004a, 0x000f9, 0x00025,
87  /* 0x28 */ 0x0006b, 0x0004b, 0x0006d, 0x0004d, 0x10060, 0x000a3, 0x0003b, 0x0002e,
88  /* 0x2c */ 0x0003d, 0x0002b, 0x0006e, 0x0004e, 0x0002c, 0x0003f, 0x0003a, 0x0002f,
89  /* 0x30 */ 0x00000, 0x00000, 0x00020, 0x00020, 0x0003c, 0x0003e,
90 };
91 
92 MockLayoutData kRussianLayout = {
93  // +0x0 Shift +0x1 Shift +0x2 Shift +0x3 Shift
94  /* 0x00 */ 0x00444, 0x00424, 0x0044b, 0x0042b, 0x00432, 0x00412, 0x00430, 0x00410,
95  /* 0x04 */ 0x00440, 0x00420, 0x0043f, 0x0041f, 0x0044f, 0x0042f, 0x00447, 0x00427,
96  /* 0x08 */ 0x00441, 0x00421, 0x0043c, 0x0041c, 0x0003e, 0x0003c, 0x00438, 0x00418,
97  /* 0x0c */ 0x00439, 0x00419, 0x00446, 0x00426, 0x00443, 0x00423, 0x0043a, 0x0041a,
98  /* 0x10 */ 0x0043d, 0x0041d, 0x00435, 0x00415, 0x00031, 0x00021, 0x00032, 0x00022,
99  /* 0x14 */ 0x00033, 0x02116, 0x00034, 0x00025, 0x00036, 0x0002c, 0x00035, 0x0003a,
100  /* 0x18 */ 0x0003d, 0x0002b, 0x00039, 0x00028, 0x00037, 0x0002e, 0x0002d, 0x0005f,
101  /* 0x1c */ 0x00038, 0x0003b, 0x00030, 0x00029, 0x0044a, 0x0042a, 0x00449, 0x00429,
102  /* 0x20 */ 0x00433, 0x00413, 0x00445, 0x00425, 0x00448, 0x00428, 0x00437, 0x00417,
103  /* 0x24 */ 0x00000, 0x00000, 0x00434, 0x00414, 0x0043e, 0x0041e, 0x0044d, 0x0042d,
104  /* 0x28 */ 0x0043b, 0x0041b, 0x00436, 0x00416, 0x00451, 0x00401, 0x00431, 0x00411,
105  /* 0x2c */ 0x0002f, 0x0003f, 0x00442, 0x00422, 0x0044c, 0x0042c, 0x0044e, 0x0042e,
106  /* 0x30 */ 0x00000, 0x00000, 0x00020, 0x00020, 0x0005d, 0x0005b,
107 };
108 
109 MockLayoutData kKhmerLayout = {
110  // +0x0 Shift +0x1 Shift +0x2 Shift +0x3 Shift
111  /* 0x00 */ 0x017b6, 0x017ab, 0x0179f, 0x017c3, 0x0178a, 0x0178c, 0x01790, 0x01792,
112  /* 0x04 */ 0x017a0, 0x017c7, 0x01784, 0x017a2, 0x0178b, 0x0178d, 0x01781, 0x01783,
113  /* 0x08 */ 0x01785, 0x01787, 0x0179c, 0x017c8, 0x00000, 0x00000, 0x01794, 0x01796,
114  /* 0x0c */ 0x01786, 0x01788, 0x017b9, 0x017ba, 0x017c1, 0x017c2, 0x0179a, 0x017ac,
115  /* 0x10 */ 0x01799, 0x017bd, 0x0178f, 0x01791, 0x017e1, 0x00021, 0x017e2, 0x017d7,
116  /* 0x14 */ 0x017e3, 0x00022, 0x017e4, 0x017db, 0x017e6, 0x017cd, 0x017e5, 0x00025,
117  /* 0x18 */ 0x017b2, 0x017ce, 0x017e9, 0x017b0, 0x017e7, 0x017d0, 0x017a5, 0x017cc,
118  /* 0x1c */ 0x017e8, 0x017cf, 0x017e0, 0x017b3, 0x017aa, 0x017a7, 0x017c4, 0x017c5,
119  /* 0x20 */ 0x017bb, 0x017bc, 0x017c0, 0x017bf, 0x017b7, 0x017b8, 0x01795, 0x01797,
120  /* 0x24 */ 0x00000, 0x00000, 0x0179b, 0x017a1, 0x017d2, 0x01789, 0x017cb, 0x017c9,
121  /* 0x28 */ 0x01780, 0x01782, 0x017be, 0x017d6, 0x017ad, 0x017ae, 0x017a6, 0x017b1,
122  /* 0x2c */ 0x017ca, 0x017af, 0x01793, 0x0178e, 0x01798, 0x017c6, 0x017d4, 0x017d5,
123  /* 0x30 */ 0x00000, 0x00000, 0x00020, 0x0200b, 0x000ab, 0x000bb,
124 };
125 
126 NSEvent* keyDownEvent(unsigned short keyCode, NSString* chars = @"", NSString* charsUnmod = @"") {
127  return [NSEvent keyEventWithType:NSEventTypeKeyDown
128  location:NSZeroPoint
129  modifierFlags:0x100
130  timestamp:0
131  windowNumber:0
132  context:nil
133  characters:chars
134  charactersIgnoringModifiers:charsUnmod
135  isARepeat:NO
136  keyCode:keyCode];
137 }
138 
139 NSEvent* keyUpEvent(unsigned short keyCode) {
140  return [NSEvent keyEventWithType:NSEventTypeKeyUp
141  location:NSZeroPoint
142  modifierFlags:0x100
143  timestamp:0
144  windowNumber:0
145  context:nil
146  characters:@""
147  charactersIgnoringModifiers:@""
148  isARepeat:NO
149  keyCode:keyCode];
150 }
151 
152 id checkKeyDownEvent(unsigned short keyCode) {
153  return [OCMArg checkWithBlock:^BOOL(id value) {
154  if (![value isKindOfClass:[NSEvent class]]) {
155  return NO;
156  }
157  NSEvent* event = value;
158  return event.keyCode == keyCode;
159  }];
160 }
161 
162 // Clear a list of `FlutterKeyEvent` whose `character` is dynamically allocated.
163 void clearEvents(std::vector<FlutterKeyEvent>& events) {
164  for (FlutterKeyEvent& event : events) {
165  if (event.character != nullptr) {
166  delete[] event.character;
167  }
168  }
169  events.clear();
170 }
171 
172 #define VERIFY_DOWN(OUT_LOGICAL, OUT_CHAR) \
173  EXPECT_EQ(events[0].type, kFlutterKeyEventTypeDown); \
174  EXPECT_EQ(events[0].logical, static_cast<uint64_t>(OUT_LOGICAL)); \
175  EXPECT_STREQ(events[0].character, (OUT_CHAR)); \
176  clearEvents(events);
177 
178 } // namespace
179 
180 @interface KeyboardTester : NSObject
181 - (nonnull instancetype)init;
182 
183 // Set embedder calls to respond immediately with the given response.
184 - (void)respondEmbedderCallsWith:(BOOL)response;
185 
186 // Record embedder calls to the given storage.
187 //
188 // They are not responded to until the stored callbacks are manually called.
189 - (void)recordEmbedderCallsTo:(nonnull NSMutableArray<FlutterAsyncKeyCallback>*)storage;
190 
191 - (void)recordEmbedderEventsTo:(nonnull std::vector<FlutterKeyEvent>*)storage
192  returning:(bool)handled;
193 
194 // Set channel calls to respond immediately with the given response.
195 - (void)respondChannelCallsWith:(BOOL)response;
196 
197 // Record channel calls to the given storage.
198 //
199 // They are not responded to until the stored callbacks are manually called.
200 - (void)recordChannelCallsTo:(nonnull NSMutableArray<FlutterAsyncKeyCallback>*)storage;
201 
202 // Set text calls to respond with the given response.
203 - (void)respondTextInputWith:(BOOL)response;
204 
205 // At the start of any kind of call, record the call type to the given storage.
206 //
207 // Only calls that are included in `typeMask` will be added. Options are
208 // kEmbedderCall, kChannelCall, and kTextCall.
209 //
210 // This method does not conflict with other call settings, and the recording
211 // takes place before the callbacks are (or are not) invoked.
212 - (void)recordCallTypesTo:(nonnull NSMutableArray<NSNumber*>*)typeStorage
213  forTypes:(uint32_t)typeMask;
214 
216 
217 - (void)sendKeyboardChannelMessage:(NSData* _Nullable)message;
218 
219 @property(readonly, nonatomic, strong) FlutterKeyboardManager* manager;
220 @property(nonatomic, nullable, strong) NSResponder* nextResponder;
221 
222 #pragma mark - Private
223 
224 - (void)handleEmbedderEvent:(const FlutterKeyEvent&)event
225  callback:(nullable FlutterKeyEventCallback)callback
226  userData:(nullable void*)userData;
227 
228 - (void)handleChannelMessage:(NSString*)channel
229  message:(NSData* _Nullable)message
230  binaryReply:(FlutterBinaryReply _Nullable)callback;
231 
232 - (BOOL)handleTextInputKeyEvent:(NSEvent*)event;
233 @end
234 
235 @implementation KeyboardTester {
236  AsyncEmbedderCallbackHandler _embedderHandler;
237  AsyncKeyCallbackHandler _channelHandler;
238  TextInputCallback _textCallback;
239 
240  NSMutableArray<NSNumber*>* _typeStorage;
242 
244  const MockLayoutData* _currentLayout;
245 
247  NSObject<FlutterBinaryMessenger>* _messengerMock;
249 }
250 
251 - (nonnull instancetype)init {
252  self = [super init];
253  if (self == nil) {
254  return nil;
255  }
256 
257  _nextResponder = OCMClassMock([NSResponder class]);
258  [self respondChannelCallsWith:FALSE];
259  [self respondEmbedderCallsWith:FALSE];
260  [self respondTextInputWith:FALSE];
261 
262  _currentLayout = &kUsLayout;
263 
264  _messengerMock = OCMStrictProtocolMock(@protocol(FlutterBinaryMessenger));
265  OCMStub([_messengerMock sendOnChannel:@"flutter/keyevent"
266  message:[OCMArg any]
267  binaryReply:[OCMArg any]])
268  .andCall(self, @selector(handleChannelMessage:message:binaryReply:));
269  OCMStub([_messengerMock setMessageHandlerOnChannel:@"flutter/keyboard"
270  binaryMessageHandler:[OCMArg any]])
271  .andCall(self, @selector(setKeyboardChannelHandler:handler:));
272  OCMStub([_messengerMock sendOnChannel:@"flutter/keyboard" message:[OCMArg any]])
273  .andCall(self, @selector(handleKeyboardChannelMessage:message:));
274  id viewDelegateMock = OCMStrictProtocolMock(@protocol(FlutterKeyboardViewDelegate));
275  OCMStub([viewDelegateMock nextResponder]).andReturn(_nextResponder);
276  OCMStub([viewDelegateMock onTextInputKeyEvent:[OCMArg any]])
277  .andCall(self, @selector(handleTextInputKeyEvent:));
278  OCMStub([viewDelegateMock getBinaryMessenger]).andReturn(_messengerMock);
279  OCMStub([viewDelegateMock sendKeyEvent:*(const FlutterKeyEvent*)[OCMArg anyPointer]
280  callback:nil
281  userData:nil])
282  .ignoringNonObjectArgs()
283  .andCall(self, @selector(handleEmbedderEvent:callback:userData:));
284  OCMStub([viewDelegateMock subscribeToKeyboardLayoutChange:[OCMArg any]])
285  .andCall(self, @selector(onSetKeyboardLayoutNotifier:));
286  OCMStub([viewDelegateMock lookUpLayoutForKeyCode:0 shift:false])
287  .ignoringNonObjectArgs()
288  .andCall(self, @selector(lookUpLayoutForKeyCode:shift:));
289 
290  _manager = [[FlutterKeyboardManager alloc] initWithViewDelegate:viewDelegateMock];
291  return self;
292 }
293 
294 - (id)lastKeyboardChannelResult {
295  return _keyboardChannelResult;
296 }
297 
298 - (void)respondEmbedderCallsWith:(BOOL)response {
299  _embedderHandler = ^(const FlutterKeyEvent* event, FlutterAsyncKeyCallback callback) {
300  callback(response);
301  };
302 }
303 
304 - (void)recordEmbedderCallsTo:(nonnull NSMutableArray<FlutterAsyncKeyCallback>*)storage {
305  _embedderHandler = ^(const FlutterKeyEvent* event, FlutterAsyncKeyCallback callback) {
306  [storage addObject:callback];
307  };
308 }
309 
310 - (void)recordEmbedderEventsTo:(nonnull std::vector<FlutterKeyEvent>*)storage
311  returning:(bool)handled {
312  _embedderHandler = ^(const FlutterKeyEvent* event, FlutterAsyncKeyCallback callback) {
313  FlutterKeyEvent newEvent = *event;
314  if (event->character != nullptr) {
315  size_t charLen = strlen(event->character);
316  char* newCharacter = new char[charLen + 1];
317  strlcpy(newCharacter, event->character, charLen + 1);
318  newEvent.character = newCharacter;
319  }
320  storage->push_back(newEvent);
321  callback(handled);
322  };
323 }
324 
325 - (void)respondChannelCallsWith:(BOOL)response {
327  callback(response);
328  };
329 }
330 
331 - (void)recordChannelCallsTo:(nonnull NSMutableArray<FlutterAsyncKeyCallback>*)storage {
333  [storage addObject:callback];
334  };
335 }
336 
337 - (void)respondTextInputWith:(BOOL)response {
338  _textCallback = ^(NSEvent* event) {
339  return response;
340  };
341 }
342 
343 - (void)recordCallTypesTo:(nonnull NSMutableArray<NSNumber*>*)typeStorage
344  forTypes:(uint32_t)typeMask {
345  _typeStorage = typeStorage;
346  _typeStorageMask = typeMask;
347 }
348 
349 - (void)sendKeyboardChannelMessage:(NSData* _Nullable)message {
350  [_messengerMock sendOnChannel:@"flutter/keyboard" message:message];
351 }
352 
353 - (void)setLayout:(const MockLayoutData&)layout {
354  _currentLayout = &layout;
355  if (_keyboardLayoutNotifier != nil) {
357  }
358 }
359 
360 #pragma mark - Private
361 
362 - (void)handleEmbedderEvent:(const FlutterKeyEvent&)event
363  callback:(nullable FlutterKeyEventCallback)callback
364  userData:(nullable void*)userData {
365  if (_typeStorage != nil && (_typeStorageMask & kEmbedderCall) != 0) {
366  [_typeStorage addObject:@(kEmbedderCall)];
367  }
368  if (callback != nullptr) {
369  _embedderHandler(&event, ^(BOOL handled) {
370  callback(handled, userData);
371  });
372  }
373 }
374 
375 - (void)handleChannelMessage:(NSString*)channel
376  message:(NSData* _Nullable)message
377  binaryReply:(FlutterBinaryReply _Nullable)callback {
378  if (_typeStorage != nil && (_typeStorageMask & kChannelCall) != 0) {
379  [_typeStorage addObject:@(kChannelCall)];
380  }
381  _channelHandler(^(BOOL handled) {
382  NSDictionary* result = @{
383  @"handled" : @(handled),
384  };
385  NSData* encodedKeyEvent = [[FlutterJSONMessageCodec sharedInstance] encode:result];
386  callback(encodedKeyEvent);
387  });
388 }
389 
390 - (void)handleKeyboardChannelMessage:(NSString*)channel message:(NSData* _Nullable)message {
391  _keyboardHandler(message, ^(id result) {
392  _keyboardChannelResult = result;
393  });
394 }
395 
396 - (BOOL)handleTextInputKeyEvent:(NSEvent*)event {
397  if (_typeStorage != nil && (_typeStorageMask & kTextCall) != 0) {
398  [_typeStorage addObject:@(kTextCall)];
399  }
400  return _textCallback(event);
401 }
402 
403 - (void)onSetKeyboardLayoutNotifier:(nullable flutter::KeyboardLayoutNotifier)callback {
404  _keyboardLayoutNotifier = callback;
405 }
406 
407 - (LayoutClue)lookUpLayoutForKeyCode:(uint16_t)keyCode shift:(BOOL)shift {
408  uint32_t cluePair = (*_currentLayout)[(keyCode * 2) + (shift ? 1 : 0)];
409  const uint32_t kCharMask = 0xffff;
410  const uint32_t kDeadKeyMask = 0x10000;
411  return LayoutClue{cluePair & kCharMask, (cluePair & kDeadKeyMask) != 0};
412 }
413 
414 - (void)setKeyboardChannelHandler:(NSString*)channel handler:(FlutterBinaryMessageHandler)handler {
415  _keyboardHandler = handler;
416 }
417 
418 @end
419 
421 - (bool)singlePrimaryResponder;
422 - (bool)doublePrimaryResponder;
423 - (bool)textInputPlugin;
424 - (bool)emptyNextResponder;
425 - (bool)getPressedState;
430 @end
431 
432 namespace flutter::testing {
433 
434 TEST(FlutterKeyboardManagerUnittests, SinglePrimaryResponder) {
435  ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] singlePrimaryResponder]);
436 }
437 
438 TEST(FlutterKeyboardManagerUnittests, DoublePrimaryResponder) {
439  ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] doublePrimaryResponder]);
440 }
441 
442 TEST(FlutterKeyboardManagerUnittests, SingleFinalResponder) {
443  ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] textInputPlugin]);
444 }
445 
446 TEST(FlutterKeyboardManagerUnittests, EmptyNextResponder) {
447  ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] emptyNextResponder]);
448 }
449 
450 TEST(FlutterKeyboardManagerUnittests, GetPressedState) {
451  ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] getPressedState]);
452 }
453 
454 TEST(FlutterKeyboardManagerUnittests, KeyboardChannelGetPressedState) {
455  ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] keyboardChannelGetPressedState]);
456 }
457 
458 TEST(FlutterKeyboardManagerUnittests, RacingConditionBetweenKeyAndText) {
459  ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] racingConditionBetweenKeyAndText]);
460 }
461 
462 TEST(FlutterKeyboardManagerUnittests, CorrectLogicalKeyForLayouts) {
463  ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] correctLogicalKeyForLayouts]);
464 }
465 
466 TEST(FlutterKeyboardManagerUnittests, ShouldNotHoldStrongReferenceToViewDelegate) {
467  ASSERT_TRUE(
468  [[FlutterKeyboardManagerUnittestsObjC alloc] shouldNotHoldStrongReferenceToViewDelegate]);
469 }
470 
471 } // namespace flutter::testing
472 
474 
476  KeyboardTester* tester = [[KeyboardTester alloc] init];
477  NSMutableArray<FlutterAsyncKeyCallback>* embedderCallbacks =
478  [NSMutableArray<FlutterAsyncKeyCallback> array];
479  [tester recordEmbedderCallsTo:embedderCallbacks];
480 
481  // Case: The responder reports FALSE
482  [tester.manager handleEvent:keyDownEvent(0x50)];
483  EXPECT_EQ([embedderCallbacks count], 1u);
484  embedderCallbacks[0](FALSE);
485  OCMVerify([tester.nextResponder keyDown:checkKeyDownEvent(0x50)]);
486  [embedderCallbacks removeAllObjects];
487 
488  // Case: The responder reports TRUE
489  [tester.manager handleEvent:keyUpEvent(0x50)];
490  EXPECT_EQ([embedderCallbacks count], 1u);
491  embedderCallbacks[0](TRUE);
492  // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown.
493 
494  return true;
495 }
496 
498  KeyboardTester* tester = [[KeyboardTester alloc] init];
499 
500  // Send a down event first so we can send an up event later.
501  [tester respondEmbedderCallsWith:false];
502  [tester respondChannelCallsWith:false];
503  [tester.manager handleEvent:keyDownEvent(0x50)];
504 
505  NSMutableArray<FlutterAsyncKeyCallback>* embedderCallbacks =
506  [NSMutableArray<FlutterAsyncKeyCallback> array];
507  NSMutableArray<FlutterAsyncKeyCallback>* channelCallbacks =
508  [NSMutableArray<FlutterAsyncKeyCallback> array];
509  [tester recordEmbedderCallsTo:embedderCallbacks];
510  [tester recordChannelCallsTo:channelCallbacks];
511 
512  // Case: Both responders report TRUE.
513  [tester.manager handleEvent:keyUpEvent(0x50)];
514  EXPECT_EQ([embedderCallbacks count], 1u);
515  EXPECT_EQ([channelCallbacks count], 1u);
516  embedderCallbacks[0](TRUE);
517  channelCallbacks[0](TRUE);
518  EXPECT_EQ([embedderCallbacks count], 1u);
519  EXPECT_EQ([channelCallbacks count], 1u);
520  // [tester.nextResponder keyUp:] should not be called, otherwise an error will be thrown.
521  [embedderCallbacks removeAllObjects];
522  [channelCallbacks removeAllObjects];
523 
524  // Case: One responder reports TRUE.
525  [tester respondEmbedderCallsWith:false];
526  [tester respondChannelCallsWith:false];
527  [tester.manager handleEvent:keyDownEvent(0x50)];
528 
529  [tester recordEmbedderCallsTo:embedderCallbacks];
530  [tester recordChannelCallsTo:channelCallbacks];
531  [tester.manager handleEvent:keyUpEvent(0x50)];
532  EXPECT_EQ([embedderCallbacks count], 1u);
533  EXPECT_EQ([channelCallbacks count], 1u);
534  embedderCallbacks[0](FALSE);
535  channelCallbacks[0](TRUE);
536  EXPECT_EQ([embedderCallbacks count], 1u);
537  EXPECT_EQ([channelCallbacks count], 1u);
538  // [tester.nextResponder keyUp:] should not be called, otherwise an error will be thrown.
539  [embedderCallbacks removeAllObjects];
540  [channelCallbacks removeAllObjects];
541 
542  // Case: Both responders report FALSE.
543  [tester.manager handleEvent:keyDownEvent(0x53)];
544  EXPECT_EQ([embedderCallbacks count], 1u);
545  EXPECT_EQ([channelCallbacks count], 1u);
546  embedderCallbacks[0](FALSE);
547  channelCallbacks[0](FALSE);
548  EXPECT_EQ([embedderCallbacks count], 1u);
549  EXPECT_EQ([channelCallbacks count], 1u);
550  OCMVerify([tester.nextResponder keyDown:checkKeyDownEvent(0x53)]);
551  [embedderCallbacks removeAllObjects];
552  [channelCallbacks removeAllObjects];
553 
554  return true;
555 }
556 
558  KeyboardTester* tester = [[KeyboardTester alloc] init];
559 
560  // Send a down event first so we can send an up event later.
561  [tester respondEmbedderCallsWith:false];
562  [tester respondChannelCallsWith:false];
563  [tester.manager handleEvent:keyDownEvent(0x50)];
564 
565  NSMutableArray<FlutterAsyncKeyCallback>* callbacks =
566  [NSMutableArray<FlutterAsyncKeyCallback> array];
567  [tester recordEmbedderCallsTo:callbacks];
568 
569  // Case: Primary responder responds TRUE. The event shouldn't be handled by
570  // the secondary responder.
571  [tester respondTextInputWith:FALSE];
572  [tester.manager handleEvent:keyUpEvent(0x50)];
573  EXPECT_EQ([callbacks count], 1u);
574  callbacks[0](TRUE);
575  // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown.
576  [callbacks removeAllObjects];
577 
578  // Send a down event first so we can send an up event later.
579  [tester respondEmbedderCallsWith:false];
580  [tester.manager handleEvent:keyDownEvent(0x50)];
581 
582  // Case: Primary responder responds FALSE. The secondary responder returns
583  // TRUE.
584  [tester recordEmbedderCallsTo:callbacks];
585  [tester respondTextInputWith:TRUE];
586  [tester.manager handleEvent:keyUpEvent(0x50)];
587  EXPECT_EQ([callbacks count], 1u);
588  callbacks[0](FALSE);
589  // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown.
590  [callbacks removeAllObjects];
591 
592  // Case: Primary responder responds FALSE. The secondary responder returns FALSE.
593  [tester respondTextInputWith:FALSE];
594  [tester.manager handleEvent:keyDownEvent(0x50)];
595  EXPECT_EQ([callbacks count], 1u);
596  callbacks[0](FALSE);
597  OCMVerify([tester.nextResponder keyDown:checkKeyDownEvent(0x50)]);
598  [callbacks removeAllObjects];
599 
600  return true;
601 }
602 
604  KeyboardTester* tester = [[KeyboardTester alloc] init];
605  tester.nextResponder = nil;
606 
607  [tester respondEmbedderCallsWith:false];
608  [tester respondChannelCallsWith:false];
609  [tester respondTextInputWith:false];
610  [tester.manager handleEvent:keyDownEvent(0x50)];
611 
612  // Passes if no error is thrown.
613  return true;
614 }
615 
617  KeyboardTester* tester = [[KeyboardTester alloc] init];
618 
619  [tester respondEmbedderCallsWith:false];
620  [tester respondChannelCallsWith:false];
621  [tester respondTextInputWith:false];
622  [tester.manager handleEvent:keyDownEvent(kVK_ANSI_A)];
623 
624  NSDictionary* pressingRecords = [tester.manager getPressedState];
625  EXPECT_EQ([pressingRecords count], 1u);
626  EXPECT_EQ(pressingRecords[@(kPhysicalKeyA)], @(kLogicalKeyA));
627 
628  return true;
629 }
630 
632  KeyboardTester* tester = [[KeyboardTester alloc] init];
633 
634  [tester respondEmbedderCallsWith:false];
635  [tester respondChannelCallsWith:false];
636  [tester respondTextInputWith:false];
637  [tester.manager handleEvent:keyDownEvent(kVK_ANSI_A)];
638 
639  FlutterMethodCall* getKeyboardStateMethodCall =
640  [FlutterMethodCall methodCallWithMethodName:@"getKeyboardState" arguments:nil];
641  NSData* getKeyboardStateMessage =
642  [[FlutterStandardMethodCodec sharedInstance] encodeMethodCall:getKeyboardStateMethodCall];
643  [tester sendKeyboardChannelMessage:getKeyboardStateMessage];
644 
645  id encodedResult = [tester lastKeyboardChannelResult];
646  id decoded = [[FlutterStandardMethodCodec sharedInstance] decodeEnvelope:encodedResult];
647 
648  EXPECT_EQ([decoded count], 1u);
649  EXPECT_EQ(decoded[@(kPhysicalKeyA)], @(kLogicalKeyA));
650 
651  return true;
652 }
653 
654 // Regression test for https://github.com/flutter/flutter/issues/82673.
656  KeyboardTester* tester = [[KeyboardTester alloc] init];
657 
658  // Use Vietnamese IME (GoTiengViet, Telex mode) to type "uco".
659 
660  // The events received by the framework. The engine might receive
661  // a channel message "setEditingState" from the framework.
662  NSMutableArray<FlutterAsyncKeyCallback>* keyCallbacks =
663  [NSMutableArray<FlutterAsyncKeyCallback> array];
664  [tester recordEmbedderCallsTo:keyCallbacks];
665 
666  NSMutableArray<NSNumber*>* allCalls = [NSMutableArray<NSNumber*> array];
667  [tester recordCallTypesTo:allCalls forTypes:(kEmbedderCall | kTextCall)];
668 
669  // Tap key U, which is converted by IME into a pure text message "Æ°".
670 
671  [tester.manager handleEvent:keyDownEvent(kKeyCodeEmpty, @"Æ°", @"Æ°")];
672  EXPECT_EQ([keyCallbacks count], 1u);
673  EXPECT_EQ([allCalls count], 1u);
674  EXPECT_EQ(allCalls[0], @(kEmbedderCall));
675  keyCallbacks[0](false);
676  EXPECT_EQ([keyCallbacks count], 1u);
677  EXPECT_EQ([allCalls count], 2u);
678  EXPECT_EQ(allCalls[1], @(kTextCall));
679  [keyCallbacks removeAllObjects];
680  [allCalls removeAllObjects];
681 
682  [tester.manager handleEvent:keyUpEvent(kKeyCodeEmpty)];
683  EXPECT_EQ([keyCallbacks count], 1u);
684  keyCallbacks[0](false);
685  EXPECT_EQ([keyCallbacks count], 1u);
686  EXPECT_EQ([allCalls count], 2u);
687  [keyCallbacks removeAllObjects];
688  [allCalls removeAllObjects];
689 
690  // Tap key O, which is converted to normal KeyO events, but the responses are
691  // slow.
692 
693  [tester.manager handleEvent:keyDownEvent(kVK_ANSI_O, @"o", @"o")];
694  [tester.manager handleEvent:keyUpEvent(kVK_ANSI_O)];
695  EXPECT_EQ([keyCallbacks count], 1u);
696  EXPECT_EQ([allCalls count], 1u);
697  EXPECT_EQ(allCalls[0], @(kEmbedderCall));
698 
699  // Tap key C, which results in two Backspace messages first - and here they
700  // arrive before the key O messages are responded.
701 
702  [tester.manager handleEvent:keyDownEvent(kVK_Delete)];
703  [tester.manager handleEvent:keyUpEvent(kVK_Delete)];
704  EXPECT_EQ([keyCallbacks count], 1u);
705  EXPECT_EQ([allCalls count], 1u);
706 
707  // The key O down is responded, which releases a text call (for KeyO down) and
708  // an embedder call (for KeyO up) immediately.
709  keyCallbacks[0](false);
710  EXPECT_EQ([keyCallbacks count], 2u);
711  EXPECT_EQ([allCalls count], 3u);
712  EXPECT_EQ(allCalls[1], @(kTextCall)); // The order is important!
713  EXPECT_EQ(allCalls[2], @(kEmbedderCall));
714 
715  // The key O up is responded, which releases a text call (for KeyO up) and
716  // an embedder call (for Backspace down) immediately.
717  keyCallbacks[1](false);
718  EXPECT_EQ([keyCallbacks count], 3u);
719  EXPECT_EQ([allCalls count], 5u);
720  EXPECT_EQ(allCalls[3], @(kTextCall)); // The order is important!
721  EXPECT_EQ(allCalls[4], @(kEmbedderCall));
722 
723  // Finish up callbacks.
724  keyCallbacks[2](false);
725  keyCallbacks[3](false);
726 
727  return true;
728 }
729 
731  KeyboardTester* tester = [[KeyboardTester alloc] init];
732  tester.nextResponder = nil;
733 
734  std::vector<FlutterKeyEvent> events;
735  [tester recordEmbedderEventsTo:&events returning:true];
736  [tester respondChannelCallsWith:false];
737  [tester respondTextInputWith:false];
738 
739  auto sendTap = [&](uint16_t keyCode, NSString* chars, NSString* charsUnmod) {
740  [tester.manager handleEvent:keyDownEvent(keyCode, chars, charsUnmod)];
741  [tester.manager handleEvent:keyUpEvent(keyCode)];
742  };
743 
744  /* US keyboard layout */
745 
746  sendTap(kVK_ANSI_A, @"a", @"a"); // KeyA
747  VERIFY_DOWN(kLogicalKeyA, "a");
748 
749  sendTap(kVK_ANSI_A, @"A", @"A"); // Shift-KeyA
750  VERIFY_DOWN(kLogicalKeyA, "A");
751 
752  sendTap(kVK_ANSI_A, @"Ã¥", @"a"); // Option-KeyA
753  VERIFY_DOWN(kLogicalKeyA, "Ã¥");
754 
755  sendTap(kVK_ANSI_T, @"t", @"t"); // KeyT
756  VERIFY_DOWN(kLogicalKeyT, "t");
757 
758  sendTap(kVK_ANSI_1, @"1", @"1"); // Digit1
759  VERIFY_DOWN(kLogicalDigit1, "1");
760 
761  sendTap(kVK_ANSI_1, @"!", @"!"); // Shift-Digit1
762  VERIFY_DOWN(kLogicalDigit1, "!");
763 
764  sendTap(kVK_ANSI_Minus, @"-", @"-"); // Minus
765  VERIFY_DOWN('-', "-");
766 
767  sendTap(kVK_ANSI_Minus, @"=", @"="); // Shift-Minus
768  VERIFY_DOWN('=', "=");
769 
770  /* French keyboard layout */
771  [tester setLayout:kFrenchLayout];
772 
773  sendTap(kVK_ANSI_A, @"q", @"q"); // KeyA
774  VERIFY_DOWN(kLogicalKeyQ, "q");
775 
776  sendTap(kVK_ANSI_A, @"Q", @"Q"); // Shift-KeyA
777  VERIFY_DOWN(kLogicalKeyQ, "Q");
778 
779  sendTap(kVK_ANSI_Semicolon, @"m", @"m"); // ; but prints M
780  VERIFY_DOWN(kLogicalKeyM, "m");
781 
782  sendTap(kVK_ANSI_M, @",", @","); // M but prints ,
783  VERIFY_DOWN(',', ",");
784 
785  sendTap(kVK_ANSI_1, @"&", @"&"); // Digit1
786  VERIFY_DOWN(kLogicalDigit1, "&");
787 
788  sendTap(kVK_ANSI_1, @"1", @"1"); // Shift-Digit1
789  VERIFY_DOWN(kLogicalDigit1, "1");
790 
791  sendTap(kVK_ANSI_Minus, @")", @")"); // Minus
792  VERIFY_DOWN(')', ")");
793 
794  sendTap(kVK_ANSI_Minus, @"°", @"°"); // Shift-Minus
795  VERIFY_DOWN(L'°', "°");
796 
797  /* Russian keyboard layout */
798  [tester setLayout:kRussianLayout];
799 
800  sendTap(kVK_ANSI_A, @"Ñ„", @"Ñ„"); // KeyA
801  VERIFY_DOWN(kLogicalKeyA, "Ñ„");
802 
803  sendTap(kVK_ANSI_1, @"1", @"1"); // Digit1
804  VERIFY_DOWN(kLogicalDigit1, "1");
805 
806  sendTap(kVK_ANSI_LeftBracket, @"Ñ…", @"Ñ…");
807  VERIFY_DOWN(kLogicalBracketLeft, "Ñ…");
808 
809  /* Khmer keyboard layout */
810  // Regression test for https://github.com/flutter/flutter/issues/108729
811  [tester setLayout:kKhmerLayout];
812 
813  sendTap(kVK_ANSI_2, @"២", @"២"); // Digit2
814  VERIFY_DOWN(kLogicalDigit2, "២");
815 
816  return TRUE;
817 }
818 
820  __strong FlutterKeyboardManager* strongKeyboardManager;
821  __weak id weakViewDelegate;
822 
823  @autoreleasepool {
824  id binaryMessengerMock = OCMStrictProtocolMock(@protocol(FlutterBinaryMessenger));
825  OCMStub([binaryMessengerMock setMessageHandlerOnChannel:[OCMArg any]
826  binaryMessageHandler:[OCMArg any]]);
827 
828  id viewDelegateMock = OCMStrictProtocolMock(@protocol(FlutterKeyboardViewDelegate));
829  OCMStub([viewDelegateMock getBinaryMessenger]).andReturn(binaryMessengerMock);
830  OCMStub([viewDelegateMock subscribeToKeyboardLayoutChange:[OCMArg any]]);
831 
832  LayoutClue layoutClue;
833  OCMStub([viewDelegateMock lookUpLayoutForKeyCode:0 shift:NO])
834  .ignoringNonObjectArgs()
835  .andReturn(layoutClue);
836  FlutterKeyboardManager* keyboardManager =
837  [[FlutterKeyboardManager alloc] initWithViewDelegate:viewDelegateMock];
838  strongKeyboardManager = keyboardManager;
839  weakViewDelegate = viewDelegateMock;
840  }
841 
842  return weakViewDelegate == nil;
843 }
844 
845 @end
flutter::LayoutClue
Definition: FlutterKeyboardViewDelegate.h:20
-[KeyboardTester recordEmbedderCallsTo:]
void recordEmbedderCallsTo:(nonnull NSMutableArray< FlutterAsyncKeyCallback > *storage)
Definition: FlutterKeyboardManagerTest.mm:304
-[KeyboardTester recordChannelCallsTo:]
void recordChannelCallsTo:(nonnull NSMutableArray< FlutterAsyncKeyCallback > *storage)
Definition: FlutterKeyboardManagerTest.mm:331
-[FlutterKeyboardManagerUnittestsObjC correctLogicalKeyForLayouts]
bool correctLogicalKeyForLayouts()
Definition: FlutterKeyboardManagerTest.mm:730
+[FlutterMethodCall methodCallWithMethodName:arguments:]
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
FlutterKeyboardViewDelegate-p
Definition: FlutterKeyboardViewDelegate.h:42
FlutterEngine_Internal.h
-[KeyboardTester respondTextInputWith:]
void respondTextInputWith:(BOOL response)
Definition: FlutterKeyboardManagerTest.mm:337
_keyboardLayoutNotifier
flutter::KeyboardLayoutNotifier _keyboardLayoutNotifier
Definition: FlutterKeyboardManagerTest.mm:243
-[KeyboardTester recordEmbedderEventsTo:returning:]
void recordEmbedderEventsTo:returning:(nonnull std::vector< FlutterKeyEvent > *storage,[returning] bool handled)
Definition: FlutterKeyboardManagerTest.mm:310
flutter::testing
Definition: AccessibilityBridgeMacTest.mm:13
flutter::testing::TEST
TEST(FlutterAppDelegateTest, DoesNotCallDelegatesWithoutHandler)
Definition: FlutterAppDelegateTest.mm:32
flutter::LayoutClue::character
uint32_t character
Definition: FlutterKeyboardViewDelegate.h:25
-[FlutterKeyboardManagerUnittestsObjC getPressedState]
bool getPressedState()
Definition: FlutterKeyboardManagerTest.mm:616
-[KeyboardTester init]
nonnull instancetype init()
Definition: FlutterKeyboardManagerTest.mm:251
FlutterKeyPrimaryResponder.h
FlutterBinaryMessageHandler
void(^ FlutterBinaryMessageHandler)(NSData *_Nullable message, FlutterBinaryReply reply)
Definition: FlutterBinaryMessenger.h:30
-[KeyboardTester recordCallTypesTo:forTypes:]
void recordCallTypesTo:forTypes:(nonnull NSMutableArray< NSNumber * > *typeStorage,[forTypes] uint32_t typeMask)
Definition: FlutterKeyboardManagerTest.mm:343
-[FlutterKeyboardManagerUnittestsObjC racingConditionBetweenKeyAndText]
bool racingConditionBetweenKeyAndText()
Definition: FlutterKeyboardManagerTest.mm:655
-[KeyboardTester respondEmbedderCallsWith:]
void respondEmbedderCallsWith:(BOOL response)
Definition: FlutterKeyboardManagerTest.mm:298
_channelHandler
AsyncKeyCallbackHandler _channelHandler
Definition: FlutterKeyboardManagerTest.mm:235
KeyboardTester::nextResponder
NSResponder * nextResponder
Definition: FlutterKeyboardManagerTest.mm:220
FlutterMethodCall
Definition: FlutterCodecs.h:220
-[FlutterKeyboardManagerUnittestsObjC emptyNextResponder]
bool emptyNextResponder()
Definition: FlutterKeyboardManagerTest.mm:603
flutter::KeyboardLayoutNotifier
void(^ KeyboardLayoutNotifier)()
Definition: FlutterKeyboardViewDelegate.h:16
FlutterAsyncKeyCallback
void(^ FlutterAsyncKeyCallback)(BOOL handled)
Definition: FlutterKeyPrimaryResponder.h:10
flutter
Definition: AccessibilityBridgeMac.h:16
-[FlutterKeyboardManagerUnittestsObjC keyboardChannelGetPressedState]
bool keyboardChannelGetPressedState()
Definition: FlutterKeyboardManagerTest.mm:631
_typeStorageMask
uint32_t _typeStorageMask
Definition: FlutterKeyboardManagerTest.mm:241
-[FlutterKeyboardManagerUnittestsObjC singlePrimaryResponder]
bool singlePrimaryResponder()
Definition: FlutterKeyboardManagerTest.mm:475
KeyboardTester::manager
FlutterKeyboardManager * manager
Definition: FlutterKeyboardManagerTest.mm:219
_messengerMock
NSObject< FlutterBinaryMessenger > * _messengerMock
Definition: FlutterKeyboardManagerTest.mm:247
-[KeyboardTester lastKeyboardChannelResult]
id lastKeyboardChannelResult()
Definition: FlutterKeyboardManagerTest.mm:294
_keyboardChannelResult
id _keyboardChannelResult
Definition: FlutterKeyboardManagerTest.mm:246
FlutterKeyboardManager.h
VERIFY_DOWN
#define VERIFY_DOWN(OUT_LOGICAL, OUT_CHAR)
Definition: FlutterKeyboardManagerTest.mm:172
_keyboardHandler
FlutterBinaryMessageHandler _keyboardHandler
Definition: FlutterKeyboardManagerTest.mm:248
-[KeyboardTester respondChannelCallsWith:]
void respondChannelCallsWith:(BOOL response)
Definition: FlutterKeyboardManagerTest.mm:325
FlutterKeyboardManagerUnittestsObjC
Definition: FlutterKeyboardManagerTest.mm:420
FlutterKeyboardManager
Definition: FlutterKeyboardManager.h:27
_textCallback
TextInputCallback _textCallback
Definition: FlutterKeyboardManagerTest.mm:238
FlutterBinaryMessenger-p
Definition: FlutterBinaryMessenger.h:49
-[FlutterKeyboardManagerUnittestsObjC textInputPlugin]
bool textInputPlugin()
Definition: FlutterKeyboardManagerTest.mm:557
-[FlutterKeyboardManagerUnittestsObjC shouldNotHoldStrongReferenceToViewDelegate]
bool shouldNotHoldStrongReferenceToViewDelegate()
Definition: FlutterKeyboardManagerTest.mm:819
FlutterStandardMethodCodec
Definition: FlutterCodecs.h:469
_typeStorage
NSMutableArray< NSNumber * > * _typeStorage
Definition: FlutterKeyboardManagerTest.mm:240
KeyboardTester
Definition: FlutterKeyboardManagerTest.mm:180
FlutterBinaryReply
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
-[KeyboardTester sendKeyboardChannelMessage:]
void sendKeyboardChannelMessage:(NSData *_Nullable message)
Definition: FlutterKeyboardManagerTest.mm:349
+[FlutterMessageCodec-p sharedInstance]
instancetype sharedInstance()
-[FlutterKeyboardManagerUnittestsObjC doublePrimaryResponder]
bool doublePrimaryResponder()
Definition: FlutterKeyboardManagerTest.mm:497
FlutterJSONMessageCodec
Definition: FlutterCodecs.h:81
_currentLayout
const MockLayoutData * _currentLayout
Definition: FlutterKeyboardManagerTest.mm:244