Flutter iOS Embedder
FlutterEmbedderKeyResponderTest.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 #import <Foundation/Foundation.h>
6 #import <OCMock/OCMock.h>
7 #import <XCTest/XCTest.h>
8 #include <_types/_uint64_t.h>
9 
14 #include "flutter/shell/platform/embedder/embedder.h"
15 #include "flutter/shell/platform/embedder/test_utils/key_codes.g.h"
16 
17 using namespace flutter::testing::keycodes;
18 using namespace flutter::testing;
19 
21 
22 #define XCTAssertStrEqual(value, expected) \
23  XCTAssertTrue(strcmp(value, expected) == 0, \
24  @"String \"%s\" not equal to the expected value of \"%s\"", value, expected)
25 
26 // A wrap to convert FlutterKeyEvent to a ObjC class.
27 @interface TestKeyEvent : NSObject
28 @property(nonatomic) FlutterKeyEvent* data;
29 @property(nonatomic) FlutterKeyEventCallback callback;
30 @property(nonatomic) _VoidPtr userData;
31 - (nonnull instancetype)initWithEvent:(const FlutterKeyEvent*)event
32  callback:(nullable FlutterKeyEventCallback)callback
33  userData:(nullable _VoidPtr)userData;
34 - (BOOL)hasCallback;
35 - (void)respond:(BOOL)handled;
36 @end
37 
38 @implementation TestKeyEvent
39 - (instancetype)initWithEvent:(const FlutterKeyEvent*)event
40  callback:(nullable FlutterKeyEventCallback)callback
41  userData:(nullable _VoidPtr)userData {
42  self = [super init];
43  _data = new FlutterKeyEvent(*event);
44  if (event->character != nullptr) {
45  size_t len = strlen(event->character);
46  char* character = new char[len + 1];
47  strlcpy(character, event->character, len + 1);
48  _data->character = character;
49  }
50  _callback = callback;
51  _userData = userData;
52  return self;
53 }
54 
55 - (BOOL)hasCallback {
56  return _callback != nil;
57 }
58 
59 - (void)respond:(BOOL)handled {
60  NSAssert(
61  _callback != nil,
62  @"Improper call to `respond` that does not have a callback."); // Caller's responsibility
63  _callback(handled, _userData);
64 }
65 
66 - (void)dealloc {
67  if (_data->character != nullptr) {
68  delete[] _data->character;
69  }
70  delete _data;
71 }
72 @end
73 
74 namespace {
75 API_AVAILABLE(ios(13.4))
76 constexpr UIKeyboardHIDUsage kKeyCodeUndefined = (UIKeyboardHIDUsage)0x03;
77 API_AVAILABLE(ios(13.4))
78 constexpr UIKeyboardHIDUsage kKeyCodeKeyA = (UIKeyboardHIDUsage)0x04;
79 API_AVAILABLE(ios(13.4))
80 constexpr UIKeyboardHIDUsage kKeyCodePeriod = (UIKeyboardHIDUsage)0x37;
81 API_AVAILABLE(ios(13.4))
82 constexpr UIKeyboardHIDUsage kKeyCodeKeyW = (UIKeyboardHIDUsage)0x1a;
83 API_AVAILABLE(ios(13.4))
84 constexpr UIKeyboardHIDUsage kKeyCodeShiftLeft = (UIKeyboardHIDUsage)0xe1;
85 API_AVAILABLE(ios(13.4))
86 constexpr UIKeyboardHIDUsage kKeyCodeShiftRight = (UIKeyboardHIDUsage)0xe5;
87 API_AVAILABLE(ios(13.4))
88 constexpr UIKeyboardHIDUsage kKeyCodeNumpad1 = (UIKeyboardHIDUsage)0x59;
89 API_AVAILABLE(ios(13.4))
90 constexpr UIKeyboardHIDUsage kKeyCodeCapsLock = (UIKeyboardHIDUsage)0x39;
91 API_AVAILABLE(ios(13.4))
92 constexpr UIKeyboardHIDUsage kKeyCodeF1 = (UIKeyboardHIDUsage)0x3a;
93 API_AVAILABLE(ios(13.4))
94 constexpr UIKeyboardHIDUsage kKeyCodeCommandLeft = (UIKeyboardHIDUsage)0xe3;
95 API_AVAILABLE(ios(13.4))
96 constexpr UIKeyboardHIDUsage kKeyCodeAltRight = (UIKeyboardHIDUsage)0xe6;
97 API_AVAILABLE(ios(13.4))
98 constexpr UIKeyboardHIDUsage kKeyCodeEject = (UIKeyboardHIDUsage)0xb8;
99 
100 constexpr uint64_t kPhysicalKeyUndefined = 0x00070003;
101 
102 constexpr uint64_t kLogicalKeyUndefined = 0x1300000003;
103 
104 constexpr uint64_t kModifierFlagNone = 0x0;
105 
106 typedef void (^ResponseCallback)(bool handled);
107 } // namespace
108 
109 @interface FlutterEmbedderKeyResponderTest : XCTestCase
110 @end
111 
112 @implementation FlutterEmbedderKeyResponderTest
113 
114 - (void)setUp {
115  // All of these tests were designed to run on iOS 13.4 or later.
116  if (@available(iOS 13.4, *)) {
117  } else {
118  XCTSkip(@"Required API not present for test.");
119  }
120 }
121 
122 - (void)tearDown {
123 }
124 
125 // Test the most basic key events.
126 //
127 // Press, hold, and release key A on an US keyboard.
128 - (void)testBasicKeyEvent API_AVAILABLE(ios(13.4)) {
129  __block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
130  __block BOOL last_handled = TRUE;
131  FlutterKeyEvent* event;
132 
134  initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
135  _Nullable _VoidPtr user_data) {
136  [events addObject:[[TestKeyEvent alloc] initWithEvent:&event
137  callback:callback
138  userData:user_data]];
139  }];
140 
141  last_handled = FALSE;
142  [responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "a", "a")
143  callback:^(BOOL handled) {
144  last_handled = handled;
145  }];
146 
147  XCTAssertEqual([events count], 1u);
148  event = [events lastObject].data;
149  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
150  XCTAssertEqual(event->timestamp, 123000000.0f);
151  XCTAssertEqual(event->physical, kPhysicalKeyA);
152  XCTAssertEqual(event->logical, kLogicalKeyA);
153  XCTAssertStrEqual(event->character, "a");
154  XCTAssertEqual(event->synthesized, false);
155 
156  XCTAssertEqual(last_handled, FALSE);
157  XCTAssert([[events lastObject] hasCallback]);
158  [[events lastObject] respond:TRUE];
159  XCTAssertEqual(last_handled, TRUE);
160 
161  [events removeAllObjects];
162 
163  last_handled = TRUE;
164  [responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
165  callback:^(BOOL handled) {
166  last_handled = handled;
167  }];
168 
169  XCTAssertEqual([events count], 1u);
170  event = [events lastObject].data;
171  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
172  XCTAssertEqual(event->timestamp, 123000000.0f);
173  XCTAssertEqual(event->physical, kPhysicalKeyA);
174  XCTAssertEqual(event->logical, kLogicalKeyA);
175  XCTAssertEqual(event->character, nullptr);
176  XCTAssertEqual(event->synthesized, false);
177 
178  XCTAssertEqual(last_handled, TRUE);
179  XCTAssert([[events lastObject] hasCallback]);
180  [[events lastObject] respond:FALSE]; // Check if responding FALSE works
181  XCTAssertEqual(last_handled, FALSE);
182 
183  [events removeAllObjects];
184 }
185 
186 - (void)testIosKeyPlane API_AVAILABLE(ios(13.4)) {
187  __block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
188  __block BOOL last_handled = TRUE;
189  FlutterKeyEvent* event;
190 
192  initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
193  _Nullable _VoidPtr user_data) {
194  [events addObject:[[TestKeyEvent alloc] initWithEvent:&event
195  callback:callback
196  userData:user_data]];
197  }];
198 
199  last_handled = FALSE;
200  // Verify that the eject key (keycode 0xb8, which is not present in the keymap)
201  // should be translated to the right logical and physical keys.
202  [responder handlePress:keyDownEvent(kKeyCodeEject, kModifierFlagNone, 123.0f)
203  callback:^(BOOL handled) {
204  last_handled = handled;
205  }];
206 
207  XCTAssertEqual([events count], 1u);
208  event = [events lastObject].data;
209  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
210  XCTAssertEqual(event->physical, kKeyCodeEject | kIosPlane);
211  XCTAssertEqual(event->logical, kKeyCodeEject | kIosPlane);
212  XCTAssertEqual(event->character, nullptr);
213  XCTAssertEqual(event->synthesized, false);
214 
215  XCTAssertEqual(last_handled, FALSE);
216  XCTAssert([[events lastObject] hasCallback]);
217  [[events lastObject] respond:TRUE];
218  XCTAssertEqual(last_handled, TRUE);
219 
220  [events removeAllObjects];
221 
222  last_handled = TRUE;
223  [responder handlePress:keyUpEvent(kKeyCodeEject, kModifierFlagNone, 123.0f)
224  callback:^(BOOL handled) {
225  last_handled = handled;
226  }];
227 
228  XCTAssertEqual([events count], 1u);
229  event = [events lastObject].data;
230  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
231  XCTAssertEqual(event->physical, kKeyCodeEject | kIosPlane);
232  XCTAssertEqual(event->logical, kKeyCodeEject | kIosPlane);
233  XCTAssertEqual(event->character, nullptr);
234  XCTAssertEqual(event->synthesized, false);
235 
236  XCTAssertEqual(last_handled, TRUE);
237  XCTAssert([[events lastObject] hasCallback]);
238  [[events lastObject] respond:FALSE]; // Check if responding FALSE works
239  XCTAssertEqual(last_handled, FALSE);
240 
241  [events removeAllObjects];
242 }
243 
244 - (void)testOutOfOrderModifiers API_AVAILABLE(ios(13.4)) {
245  __block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
246  FlutterKeyEvent* event;
247 
249  initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
250  _Nullable _VoidPtr user_data) {
251  [events addObject:[[TestKeyEvent alloc] initWithEvent:&event
252  callback:callback
253  userData:user_data]];
254  }];
255 
256  // This tests that we synthesize the correct modifier keys when we release the
257  // modifier key that created the letter before we release the letter.
258  [responder handlePress:keyDownEvent(kKeyCodeAltRight, kModifierFlagAltAny, 123.0f)
259  callback:^(BOOL handled){
260  }];
261 
262  XCTAssertEqual([events count], 1u);
263  event = [events lastObject].data;
264  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
265  XCTAssertEqual(event->physical, kPhysicalAltRight);
266  XCTAssertEqual(event->logical, kLogicalAltRight);
267  XCTAssertEqual(event->character, nullptr);
268  XCTAssertEqual(event->synthesized, false);
269 
270  [events removeAllObjects];
271 
272  // Test non-ASCII characters being produced.
273  [responder handlePress:keyDownEvent(kKeyCodeKeyW, kModifierFlagAltAny, 123.0f, "∑", "w")
274  callback:^(BOOL handled){
275  }];
276 
277  XCTAssertEqual([events count], 1u);
278  event = [events lastObject].data;
279  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
280  XCTAssertEqual(event->physical, kPhysicalKeyW);
281  XCTAssertEqual(event->logical, kLogicalKeyW);
282  XCTAssertStrEqual(event->character, "∑");
283  XCTAssertEqual(event->synthesized, false);
284 
285  [events removeAllObjects];
286 
287  // Releasing the modifier key before the letter should send the key up to the
288  // framework.
289  [responder handlePress:keyUpEvent(kKeyCodeAltRight, kModifierFlagAltAny, 123.0f)
290  callback:^(BOOL handled){
291  }];
292 
293  XCTAssertEqual([events count], 1u);
294  event = [events lastObject].data;
295  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
296  XCTAssertEqual(event->physical, kPhysicalAltRight);
297  XCTAssertEqual(event->logical, kLogicalAltRight);
298  XCTAssertEqual(event->character, nullptr);
299  XCTAssertEqual(event->synthesized, false);
300 
301  [events removeAllObjects];
302 
303  // Yes, iOS sends a modifier flag for the Alt key being down on this event,
304  // even though the Alt (Option) key has already been released. This means that
305  // for the framework to be in the correct state, we must synthesize a key down
306  // event for the modifier key here, and another key up before the next key
307  // event.
308  [responder handlePress:keyUpEvent(kKeyCodeKeyW, kModifierFlagAltAny, 123.0f)
309  callback:^(BOOL handled){
310  }];
311 
312  XCTAssertEqual([events count], 1u);
313  event = [events lastObject].data;
314  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
315  XCTAssertEqual(event->physical, kPhysicalKeyW);
316  XCTAssertEqual(event->logical, kLogicalKeyW);
317  XCTAssertEqual(event->character, nullptr);
318  XCTAssertEqual(event->synthesized, false);
319 
320  [events removeAllObjects];
321 
322  // Here we should simulate a key up for the Alt key, since it is no longer
323  // shown as down in the modifier flags.
324  [responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "Ã¥", "a")
325  callback:^(BOOL handled){
326  }];
327 
328  XCTAssertEqual([events count], 1u);
329  event = [events lastObject].data;
330  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
331  XCTAssertEqual(event->physical, kPhysicalKeyA);
332  XCTAssertEqual(event->logical, kLogicalKeyA);
333  XCTAssertStrEqual(event->character, "Ã¥");
334  XCTAssertEqual(event->synthesized, false);
335 }
336 
337 - (void)testIgnoreDuplicateDownEvent API_AVAILABLE(ios(13.4)) {
338  __block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
339  __block BOOL last_handled = TRUE;
340  FlutterKeyEvent* event;
341 
343  initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
344  _Nullable _VoidPtr user_data) {
345  [events addObject:[[TestKeyEvent alloc] initWithEvent:&event
346  callback:callback
347  userData:user_data]];
348  }];
349 
350  last_handled = FALSE;
351  [responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "a", "a")
352  callback:^(BOOL handled) {
353  last_handled = handled;
354  }];
355 
356  XCTAssertEqual([events count], 1u);
357  event = [events lastObject].data;
358  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
359  XCTAssertEqual(event->physical, kPhysicalKeyA);
360  XCTAssertEqual(event->logical, kLogicalKeyA);
361  XCTAssertStrEqual(event->character, "a");
362  XCTAssertEqual(event->synthesized, false);
363  XCTAssertEqual(last_handled, FALSE);
364  [[events lastObject] respond:TRUE];
365  XCTAssertEqual(last_handled, TRUE);
366 
367  [events removeAllObjects];
368 
369  last_handled = FALSE;
370  [responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "a", "a")
371  callback:^(BOOL handled) {
372  last_handled = handled;
373  }];
374 
375  XCTAssertEqual([events count], 1u);
376  event = [events lastObject].data;
377  XCTAssertEqual(event->physical, 0ull);
378  XCTAssertEqual(event->logical, 0ull);
379  XCTAssertEqual(event->synthesized, false);
380  XCTAssertFalse([[events lastObject] hasCallback]);
381  XCTAssertEqual(last_handled, TRUE);
382 
383  [events removeAllObjects];
384 
385  last_handled = FALSE;
386  [responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
387  callback:^(BOOL handled) {
388  last_handled = handled;
389  }];
390 
391  XCTAssertEqual([events count], 1u);
392  event = [events lastObject].data;
393  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
394  XCTAssertEqual(event->physical, kPhysicalKeyA);
395  XCTAssertEqual(event->logical, kLogicalKeyA);
396  XCTAssertEqual(event->character, nullptr);
397  XCTAssertEqual(event->synthesized, false);
398  XCTAssertEqual(last_handled, FALSE);
399  [[events lastObject] respond:TRUE];
400  XCTAssertEqual(last_handled, TRUE);
401 
402  [events removeAllObjects];
403 }
404 
405 - (void)testIgnoreAbruptUpEvent API_AVAILABLE(ios(13.4)) {
406  __block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
407  __block BOOL last_handled = TRUE;
408  FlutterKeyEvent* event;
409 
411  initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
412  _Nullable _VoidPtr user_data) {
413  [events addObject:[[TestKeyEvent alloc] initWithEvent:&event
414  callback:callback
415  userData:user_data]];
416  }];
417 
418  last_handled = FALSE;
419  [responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
420  callback:^(BOOL handled) {
421  last_handled = handled;
422  }];
423 
424  XCTAssertEqual([events count], 1u);
425  event = [events lastObject].data;
426  XCTAssertEqual(event->physical, 0ull);
427  XCTAssertEqual(event->logical, 0ull);
428  XCTAssertEqual(event->synthesized, false);
429  XCTAssertFalse([[events lastObject] hasCallback]);
430  XCTAssertEqual(last_handled, TRUE);
431 
432  [events removeAllObjects];
433 }
434 
435 // Press R-Shift, A, then release R-Shift then A, on a US keyboard.
436 //
437 // This is special because the characters for the A key will change in this
438 // process.
439 - (void)testToggleModifiersDuringKeyTap API_AVAILABLE(ios(13.4)) {
440  __block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
441  FlutterKeyEvent* event;
442 
444  initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
445  _Nullable _VoidPtr user_data) {
446  [events addObject:[[TestKeyEvent alloc] initWithEvent:&event
447  callback:callback
448  userData:user_data]];
449  }];
450 
451  [responder handlePress:keyDownEvent(kKeyCodeShiftRight, kModifierFlagShiftAny, 123.0f)
452  callback:^(BOOL handled){
453  }];
454 
455  XCTAssertEqual([events count], 1u);
456 
457  event = [events lastObject].data;
458  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
459  XCTAssertEqual(event->timestamp, 123000000.0f);
460  XCTAssertEqual(event->physical, kPhysicalShiftRight);
461  XCTAssertEqual(event->logical, kLogicalShiftRight);
462  XCTAssertEqual(event->character, nullptr);
463  XCTAssertEqual(event->synthesized, false);
464  [[events lastObject] respond:TRUE];
465 
466  [events removeAllObjects];
467 
468  [responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagShiftAny, 123.0f, "A", "A")
469  callback:^(BOOL handled){
470  }];
471 
472  XCTAssertEqual([events count], 1u);
473  event = [events lastObject].data;
474  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
475  XCTAssertEqual(event->physical, kPhysicalKeyA);
476  XCTAssertEqual(event->logical, kLogicalKeyA);
477  XCTAssertStrEqual(event->character, "A");
478  XCTAssertEqual(event->synthesized, false);
479  [[events lastObject] respond:TRUE];
480 
481  [events removeAllObjects];
482 
483  [responder handlePress:keyUpEvent(kKeyCodeShiftRight, kModifierFlagNone, 123.0f)
484  callback:^(BOOL handled){
485  }];
486 
487  XCTAssertEqual([events count], 1u);
488  event = [events lastObject].data;
489  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
490  XCTAssertEqual(event->physical, kPhysicalShiftRight);
491  XCTAssertEqual(event->logical, kLogicalShiftRight);
492  XCTAssertEqual(event->character, nullptr);
493  XCTAssertEqual(event->synthesized, false);
494  [[events lastObject] respond:TRUE];
495 
496  [events removeAllObjects];
497 
498  [responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
499  callback:^(BOOL handled){
500  }];
501 
502  XCTAssertEqual([events count], 1u);
503  event = [events lastObject].data;
504  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
505  XCTAssertEqual(event->physical, kPhysicalKeyA);
506  XCTAssertEqual(event->logical, kLogicalKeyA);
507  XCTAssertEqual(event->character, nullptr);
508  XCTAssertEqual(event->synthesized, false);
509  [[events lastObject] respond:TRUE];
510 
511  [events removeAllObjects];
512 }
513 
514 // Special modifier flags.
515 //
516 // Some keys in modifierFlags are not to indicate modifier state, but to mark
517 // the key area that the key belongs to, such as numpad keys or function keys.
518 // Ensure these flags do not obstruct other keys.
519 - (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
520  __block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
521  FlutterKeyEvent* event;
522  __block BOOL last_handled = TRUE;
523  id keyEventCallback = ^(BOOL handled) {
524  last_handled = handled;
525  };
526 
528  initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
529  _Nullable _VoidPtr user_data) {
530  [events addObject:[[TestKeyEvent alloc] initWithEvent:&event
531  callback:callback
532  userData:user_data]];
533  }];
534 
535  // Keydown: Numpad1, Fn (undefined), F1, KeyA, ShiftLeft
536  // Then KeyUp: Numpad1, Fn (undefined), F1, KeyA, ShiftLeft
537 
538  // Numpad 1
539  // OS provides: char: "1", code: 0x59, modifiers: 0x200000
540  [responder handlePress:keyDownEvent(kKeyCodeNumpad1, kModifierFlagNumPadKey, 123.0, "1", "1")
541  callback:keyEventCallback];
542 
543  XCTAssertEqual([events count], 1u);
544  event = [events lastObject].data;
545  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
546  XCTAssertEqual(event->physical, kPhysicalNumpad1);
547  XCTAssertEqual(event->logical, kLogicalNumpad1);
548  XCTAssertStrEqual(event->character, "1");
549  XCTAssertEqual(event->synthesized, false);
550  [[events lastObject] respond:TRUE];
551 
552  [events removeAllObjects];
553 
554  // Fn Key (sends HID undefined)
555  // OS provides: char: nil, keycode: 0x3, modifiers: 0x0
556  [responder handlePress:keyDownEvent(kKeyCodeUndefined, kModifierFlagNone, 123.0)
557  callback:keyEventCallback];
558 
559  XCTAssertEqual([events count], 1u);
560  event = [events lastObject].data;
561  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
562  XCTAssertEqual(event->physical, kPhysicalKeyUndefined);
563  XCTAssertEqual(event->logical, kLogicalKeyUndefined);
564  XCTAssertEqual(event->character, nullptr);
565  XCTAssertEqual(event->synthesized, false);
566  [[events lastObject] respond:TRUE];
567 
568  [events removeAllObjects];
569 
570  // F1 Down
571  // OS provides: char: UIKeyInputF1, code: 0x3a, modifiers: 0x0
572  [responder handlePress:keyDownEvent(kKeyCodeF1, kModifierFlagNone, 123.0f, "\\^P", "\\^P")
573  callback:keyEventCallback];
574 
575  XCTAssertEqual([events count], 1u);
576  event = [events lastObject].data;
577  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
578  XCTAssertEqual(event->physical, kPhysicalF1);
579  XCTAssertEqual(event->logical, kLogicalF1);
580  XCTAssertEqual(event->character, nullptr);
581  XCTAssertEqual(event->synthesized, false);
582  [[events lastObject] respond:TRUE];
583 
584  [events removeAllObjects];
585 
586  // KeyA Down
587  // OS provides: char: "q", code: 0x4, modifiers: 0x0
588  [responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "a", "a")
589  callback:keyEventCallback];
590 
591  XCTAssertEqual([events count], 1u);
592  event = [events lastObject].data;
593  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
594  XCTAssertEqual(event->physical, kPhysicalKeyA);
595  XCTAssertEqual(event->logical, kLogicalKeyA);
596  XCTAssertStrEqual(event->character, "a");
597  XCTAssertEqual(event->synthesized, false);
598  [[events lastObject] respond:TRUE];
599 
600  [events removeAllObjects];
601 
602  // ShiftLeft Down
603  // OS Provides: char: nil, code: 0xe1, modifiers: 0x20000
604  [responder handlePress:keyDownEvent(kKeyCodeShiftLeft, kModifierFlagShiftAny, 123.0f)
605  callback:keyEventCallback];
606 
607  XCTAssertEqual([events count], 1u);
608  event = [events lastObject].data;
609  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
610  XCTAssertEqual(event->physical, kPhysicalShiftLeft);
611  XCTAssertEqual(event->logical, kLogicalShiftLeft);
612  XCTAssertEqual(event->character, nullptr);
613  XCTAssertEqual(event->synthesized, false);
614 
615  [events removeAllObjects];
616 
617  // Numpad 1 Up
618  // OS provides: char: "1", code: 0x59, modifiers: 0x200000
619  [responder handlePress:keyUpEvent(kKeyCodeNumpad1, kModifierFlagNumPadKey, 123.0f)
620  callback:keyEventCallback];
621 
622  XCTAssertEqual([events count], 2u);
623 
624  // Because the OS no longer provides the 0x20000 (kModifierFlagShiftAny), we
625  // have to simulate a keyup.
626  event = [events firstObject].data;
627  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
628  XCTAssertEqual(event->physical, kPhysicalShiftLeft);
629  XCTAssertEqual(event->logical, kLogicalShiftLeft);
630  XCTAssertEqual(event->character, nullptr);
631  XCTAssertEqual(event->synthesized, true);
632 
633  event = [events lastObject].data;
634  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
635  XCTAssertEqual(event->physical, kPhysicalNumpad1);
636  XCTAssertEqual(event->logical, kLogicalNumpad1);
637  XCTAssertEqual(event->character, nullptr);
638  XCTAssertEqual(event->synthesized, false);
639  [[events lastObject] respond:TRUE];
640 
641  [events removeAllObjects];
642 
643  // F1 Up
644  // OS provides: char: UIKeyInputF1, code: 0x3a, modifiers: 0x0
645  [responder handlePress:keyUpEvent(kKeyCodeF1, kModifierFlagNone, 123.0f)
646  callback:keyEventCallback];
647 
648  XCTAssertEqual([events count], 1u);
649  event = [events lastObject].data;
650  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
651  XCTAssertEqual(event->physical, kPhysicalF1);
652  XCTAssertEqual(event->logical, kLogicalF1);
653  XCTAssertEqual(event->character, nullptr);
654  XCTAssertEqual(event->synthesized, false);
655  [[events lastObject] respond:TRUE];
656 
657  [events removeAllObjects];
658 
659  // Fn Key (sends HID undefined)
660  // OS provides: char: nil, code: 0x3, modifiers: 0x0
661  [responder handlePress:keyUpEvent(kKeyCodeUndefined, kModifierFlagNone, 123.0)
662  callback:keyEventCallback];
663 
664  XCTAssertEqual([events count], 1u);
665  event = [events lastObject].data;
666  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
667  XCTAssertEqual(event->physical, kPhysicalKeyUndefined);
668  XCTAssertEqual(event->logical, kLogicalKeyUndefined);
669  XCTAssertEqual(event->synthesized, false);
670  [[events lastObject] respond:TRUE];
671 
672  [events removeAllObjects];
673 
674  // KeyA Up
675  // OS provides: char: "a", code: 0x4, modifiers: 0x0
676  [responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
677  callback:keyEventCallback];
678 
679  XCTAssertEqual([events count], 1u);
680  event = [events lastObject].data;
681  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
682  XCTAssertEqual(event->physical, kPhysicalKeyA);
683  XCTAssertEqual(event->logical, kLogicalKeyA);
684  XCTAssertEqual(event->character, nullptr);
685  XCTAssertEqual(event->synthesized, false);
686  [[events lastObject] respond:TRUE];
687 
688  [events removeAllObjects];
689 
690  // ShiftLeft Up
691  // OS provides: char: nil, code: 0xe1, modifiers: 0x20000
692  [responder handlePress:keyUpEvent(kKeyCodeShiftLeft, kModifierFlagShiftAny, 123.0f)
693  callback:keyEventCallback];
694 
695  XCTAssertEqual([events count], 1u);
696  event = [events lastObject].data;
697  XCTAssertEqual(event->physical, 0ull);
698  XCTAssertEqual(event->logical, 0ull);
699  XCTAssertEqual(event->synthesized, false);
700  XCTAssertFalse([[events lastObject] hasCallback]);
701  XCTAssertEqual(last_handled, TRUE);
702 
703  [events removeAllObjects];
704 }
705 
706 - (void)testIdentifyLeftAndRightModifiers API_AVAILABLE(ios(13.4)) {
707  __block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
708  FlutterKeyEvent* event;
709 
711  initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
712  _Nullable _VoidPtr user_data) {
713  [events addObject:[[TestKeyEvent alloc] initWithEvent:&event
714  callback:callback
715  userData:user_data]];
716  }];
717 
718  [responder handlePress:keyDownEvent(kKeyCodeShiftLeft, kModifierFlagShiftAny, 123.0f)
719  callback:^(BOOL handled){
720  }];
721 
722  XCTAssertEqual([events count], 1u);
723  event = [events lastObject].data;
724  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
725  XCTAssertEqual(event->physical, kPhysicalShiftLeft);
726  XCTAssertEqual(event->logical, kLogicalShiftLeft);
727  XCTAssertEqual(event->character, nullptr);
728  XCTAssertEqual(event->synthesized, false);
729  [[events lastObject] respond:TRUE];
730 
731  [events removeAllObjects];
732 
733  [responder handlePress:keyDownEvent(kKeyCodeShiftRight, kModifierFlagShiftAny, 123.0f)
734  callback:^(BOOL handled){
735  }];
736 
737  XCTAssertEqual([events count], 1u);
738  event = [events lastObject].data;
739  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
740  XCTAssertEqual(event->physical, kPhysicalShiftRight);
741  XCTAssertEqual(event->logical, kLogicalShiftRight);
742  XCTAssertEqual(event->character, nullptr);
743  XCTAssertEqual(event->synthesized, false);
744  [[events lastObject] respond:TRUE];
745 
746  [events removeAllObjects];
747 
748  [responder handlePress:keyUpEvent(kKeyCodeShiftLeft, kModifierFlagShiftAny, 123.0f)
749  callback:^(BOOL handled){
750  }];
751 
752  XCTAssertEqual([events count], 1u);
753  event = [events lastObject].data;
754  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
755  XCTAssertEqual(event->physical, kPhysicalShiftLeft);
756  XCTAssertEqual(event->logical, kLogicalShiftLeft);
757  XCTAssertEqual(event->character, nullptr);
758  XCTAssertEqual(event->synthesized, false);
759  [[events lastObject] respond:TRUE];
760 
761  [events removeAllObjects];
762 
763  [responder handlePress:keyUpEvent(kKeyCodeShiftRight, kModifierFlagShiftAny, 123.0f)
764  callback:^(BOOL handled){
765  }];
766 
767  XCTAssertEqual([events count], 1u);
768  event = [events lastObject].data;
769  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
770  XCTAssertEqual(event->physical, kPhysicalShiftRight);
771  XCTAssertEqual(event->logical, kLogicalShiftRight);
772  XCTAssertEqual(event->character, nullptr);
773  XCTAssertEqual(event->synthesized, false);
774  [[events lastObject] respond:TRUE];
775 
776  [events removeAllObjects];
777 }
778 
779 // Press the CapsLock key when CapsLock state is desynchronized
780 - (void)testSynchronizeCapsLockStateOnCapsLock API_AVAILABLE(ios(13.4)) {
781  __block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
782  __block BOOL last_handled = TRUE;
783  id keyEventCallback = ^(BOOL handled) {
784  last_handled = handled;
785  };
786  FlutterKeyEvent* event;
787 
789  initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
790  _Nullable _VoidPtr user_data) {
791  [events addObject:[[TestKeyEvent alloc] initWithEvent:&event
792  callback:callback
793  userData:user_data]];
794  }];
795 
796  last_handled = FALSE;
797  [responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagCapsLock, 123.0f, "A", "A")
798  callback:keyEventCallback];
799 
800  XCTAssertEqual([events count], 3u);
801 
802  event = events[0].data;
803  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
804  XCTAssertEqual(event->physical, kPhysicalCapsLock);
805  XCTAssertEqual(event->logical, kLogicalCapsLock);
806  XCTAssertEqual(event->character, nullptr);
807  XCTAssertEqual(event->synthesized, true);
808  XCTAssertFalse([events[0] hasCallback]);
809 
810  event = events[1].data;
811  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
812  XCTAssertEqual(event->physical, kPhysicalCapsLock);
813  XCTAssertEqual(event->logical, kLogicalCapsLock);
814  XCTAssertEqual(event->character, nullptr);
815  XCTAssertEqual(event->synthesized, true);
816  XCTAssertFalse([events[1] hasCallback]);
817 
818  event = events[2].data;
819  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
820  XCTAssertEqual(event->physical, kPhysicalKeyA);
821  XCTAssertEqual(event->logical, kLogicalKeyA);
822  XCTAssertStrEqual(event->character, "A");
823  XCTAssertEqual(event->synthesized, false);
824  XCTAssert([events[2] hasCallback]);
825 
826  XCTAssertEqual(last_handled, FALSE);
827  [[events lastObject] respond:TRUE];
828  XCTAssertEqual(last_handled, TRUE);
829 
830  [events removeAllObjects];
831 
832  // Release the "A" key.
833  [responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagCapsLock, 123.0f)
834  callback:keyEventCallback];
835  XCTAssertEqual([events count], 1u);
836  event = [events lastObject].data;
837  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
838  XCTAssertEqual(event->physical, kPhysicalKeyA);
839  XCTAssertEqual(event->logical, kLogicalKeyA);
840  XCTAssertEqual(event->synthesized, false);
841 
842  [events removeAllObjects];
843 
844  // In: CapsLock down
845  // Out: CapsLock down
846  last_handled = FALSE;
847  [responder handlePress:keyDownEvent(kKeyCodeCapsLock, kModifierFlagCapsLock, 123.0f)
848  callback:keyEventCallback];
849 
850  XCTAssertEqual([events count], 1u);
851  event = [events firstObject].data;
852  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
853  XCTAssertEqual(event->physical, kPhysicalCapsLock);
854  XCTAssertEqual(event->logical, kLogicalCapsLock);
855  XCTAssertEqual(event->character, nullptr);
856  XCTAssertEqual(event->synthesized, false);
857  XCTAssert([[events firstObject] hasCallback]);
858 
859  [events removeAllObjects];
860 
861  // In: CapsLock up
862  // Out: CapsLock up
863  // This turns off the caps lock, triggering a synthesized up/down to tell the
864  // framework that.
865  last_handled = FALSE;
866  [responder handlePress:keyUpEvent(kKeyCodeCapsLock, kModifierFlagCapsLock, 123.0f)
867  callback:keyEventCallback];
868 
869  XCTAssertEqual([events count], 1u);
870  event = [events firstObject].data;
871  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
872  XCTAssertEqual(event->physical, kPhysicalCapsLock);
873  XCTAssertEqual(event->logical, kLogicalCapsLock);
874  XCTAssertEqual(event->character, nullptr);
875  XCTAssertEqual(event->synthesized, false);
876  XCTAssert([[events firstObject] hasCallback]);
877 
878  [events removeAllObjects];
879 
880  last_handled = FALSE;
881  [responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "a", "a")
882  callback:keyEventCallback];
883 
884  // Just to make sure that we aren't simulating events now, since the state is
885  // consistent, and should be off.
886  XCTAssertEqual([events count], 1u);
887  event = [events lastObject].data;
888  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
889  XCTAssertEqual(event->physical, kPhysicalKeyA);
890  XCTAssertEqual(event->logical, kLogicalKeyA);
891  XCTAssertStrEqual(event->character, "a");
892  XCTAssertEqual(event->synthesized, false);
893  XCTAssert([[events firstObject] hasCallback]);
894 }
895 
896 // Press the CapsLock key when CapsLock state is desynchronized
897 - (void)testSynchronizeCapsLockStateOnNormalKey API_AVAILABLE(ios(13.4)) {
898  __block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
899  __block BOOL last_handled = TRUE;
900  id keyEventCallback = ^(BOOL handled) {
901  last_handled = handled;
902  };
903  FlutterKeyEvent* event;
904 
906  initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
907  _Nullable _VoidPtr user_data) {
908  [events addObject:[[TestKeyEvent alloc] initWithEvent:&event
909  callback:callback
910  userData:user_data]];
911  }];
912 
913  last_handled = FALSE;
914  [responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagCapsLock, 123.0f, "A", "a")
915  callback:keyEventCallback];
916 
917  XCTAssertEqual([events count], 3u);
918 
919  event = events[0].data;
920  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
921  XCTAssertEqual(event->physical, kPhysicalCapsLock);
922  XCTAssertEqual(event->logical, kLogicalCapsLock);
923  XCTAssertEqual(event->character, nullptr);
924  XCTAssertEqual(event->synthesized, true);
925  XCTAssertFalse([events[0] hasCallback]);
926 
927  event = events[1].data;
928  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
929  XCTAssertEqual(event->physical, kPhysicalCapsLock);
930  XCTAssertEqual(event->logical, kLogicalCapsLock);
931  XCTAssertEqual(event->character, nullptr);
932  XCTAssertEqual(event->synthesized, true);
933  XCTAssertFalse([events[1] hasCallback]);
934 
935  event = events[2].data;
936  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
937  XCTAssertEqual(event->physical, kPhysicalKeyA);
938  XCTAssertEqual(event->logical, kLogicalKeyA);
939  XCTAssertStrEqual(event->character, "A");
940  XCTAssertEqual(event->synthesized, false);
941  XCTAssert([events[2] hasCallback]);
942 
943  XCTAssertEqual(last_handled, FALSE);
944  [[events lastObject] respond:TRUE];
945  XCTAssertEqual(last_handled, TRUE);
946 
947  [events removeAllObjects];
948 }
949 
950 // Press Cmd-. should correctly result in an Escape event.
951 - (void)testCommandPeriodKey API_AVAILABLE(ios(13.4)) {
952  __block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
953  id keyEventCallback = ^(BOOL handled) {
954  };
955  FlutterKeyEvent* event;
956 
958  initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
959  _Nullable _VoidPtr user_data) {
960  [events addObject:[[TestKeyEvent alloc] initWithEvent:&event callback:nil userData:nil]];
961  callback(true, user_data);
962  }];
963 
964  // MetaLeft down.
965  [responder handlePress:keyDownEvent(kKeyCodeCommandLeft, kModifierFlagMetaAny, 123.0f, "", "")
966  callback:keyEventCallback];
967  XCTAssertEqual([events count], 1u);
968  event = events[0].data;
969  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
970  XCTAssertEqual(event->physical, kPhysicalMetaLeft);
971  XCTAssertEqual(event->logical, kLogicalMetaLeft);
972  XCTAssertEqual(event->character, nullptr);
973  XCTAssertEqual(event->synthesized, false);
974  [events removeAllObjects];
975 
976  // Period down, which is logically Escape.
977  [responder handlePress:keyDownEvent(kKeyCodePeriod, kModifierFlagMetaAny, 123.0f,
978  "UIKeyInputEscape", "UIKeyInputEscape")
979  callback:keyEventCallback];
980  XCTAssertEqual([events count], 1u);
981  event = events[0].data;
982  XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
983  XCTAssertEqual(event->physical, kPhysicalPeriod);
984  XCTAssertEqual(event->logical, kLogicalEscape);
985  XCTAssertEqual(event->character, nullptr);
986  XCTAssertEqual(event->synthesized, false);
987  [events removeAllObjects];
988 
989  // Period up, which unconventionally has characters.
990  [responder handlePress:keyUpEvent(kKeyCodePeriod, kModifierFlagMetaAny, 123.0f,
991  "UIKeyInputEscape", "UIKeyInputEscape")
992  callback:keyEventCallback];
993  XCTAssertEqual([events count], 1u);
994  event = events[0].data;
995  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
996  XCTAssertEqual(event->physical, kPhysicalPeriod);
997  XCTAssertEqual(event->logical, kLogicalEscape);
998  XCTAssertEqual(event->character, nullptr);
999  XCTAssertEqual(event->synthesized, false);
1000  [events removeAllObjects];
1001 
1002  // MetaLeft up.
1003  [responder handlePress:keyUpEvent(kKeyCodeCommandLeft, kModifierFlagMetaAny, 123.0f, "", "")
1004  callback:keyEventCallback];
1005  XCTAssertEqual([events count], 1u);
1006  event = events[0].data;
1007  XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
1008  XCTAssertEqual(event->physical, kPhysicalMetaLeft);
1009  XCTAssertEqual(event->logical, kLogicalMetaLeft);
1010  XCTAssertEqual(event->character, nullptr);
1011  XCTAssertEqual(event->synthesized, false);
1012  [events removeAllObjects];
1013 }
1014 
1015 @end
-[TestKeyEvent hasCallback]
BOOL hasCallback()
Definition: FlutterEmbedderKeyResponderTest.mm:55
TestKeyEvent
Definition: FlutterEmbedderKeyResponderTest.mm:27
API_AVAILABLE
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
flutter::testing
Definition: FlutterFakeKeyEvents.h:51
-[FlutterKeyPrimaryResponder-p handlePress:callback:]
void handlePress:callback:(nonnull FlutterUIPressProxy *press,[callback] ios(13.4) API_AVAILABLE)
FlutterMacros.h
FlutterEmbedderKeyResponder.h
TestKeyEvent::callback
FlutterKeyEventCallback callback
Definition: FlutterEmbedderKeyResponderTest.mm:29
FLUTTER_ASSERT_ARC
FLUTTER_ASSERT_ARC
Definition: FlutterEmbedderKeyResponderTest.mm:20
XCTAssertStrEqual
#define XCTAssertStrEqual(value, expected)
Definition: FlutterEmbedderKeyResponderTest.mm:22
FlutterFakeKeyEvents.h
TestKeyEvent::userData
_VoidPtr userData
Definition: FlutterEmbedderKeyResponderTest.mm:30
KeyCodeMap_Internal.h
FlutterEmbedderKeyResponderTest
Definition: FlutterEmbedderKeyResponderTest.mm:109
FlutterEmbedderKeyResponder
Definition: FlutterEmbedderKeyResponder.h:30
kIosPlane
const uint64_t kIosPlane
Definition: KeyCodeMap.g.mm:32