Flutter Engine
 
Loading...
Searching...
No Matches
FlutterKeyboardManagerTest.mm File Reference

Go to the source code of this file.

Classes

class  KeyboardTester
 
class  FlutterKeyboardManagerUnittestsObjC
 

Namespaces

namespace  flutter
 
namespace  flutter::testing
 

Macros

#define VERIFY_DOWN(OUT_LOGICAL, OUT_CHAR)
 

Functions

 flutter::testing::TEST (FlutterKeyboardManagerUnittests, SinglePrimaryResponder)
 
 flutter::testing::TEST (FlutterKeyboardManagerUnittests, DoublePrimaryResponder)
 
 flutter::testing::TEST (FlutterKeyboardManagerUnittests, SingleFinalResponder)
 
 flutter::testing::TEST (FlutterKeyboardManagerUnittests, EmptyNextResponder)
 
 flutter::testing::TEST (FlutterKeyboardManagerUnittests, GetPressedState)
 
 flutter::testing::TEST (FlutterKeyboardManagerUnittests, KeyboardChannelGetPressedState)
 
 flutter::testing::TEST (FlutterKeyboardManagerUnittests, RacingConditionBetweenKeyAndText)
 
 flutter::testing::TEST (FlutterKeyboardManagerUnittests, CorrectLogicalKeyForLayouts)
 
 flutter::testing::TEST (FlutterKeyboardManagerUnittests, ShouldNotHoldStrongReferenceToDelegate)
 

Variables

AsyncKeyCallbackHandler _channelHandler
 
TextInputCallback _textCallback
 
NSMutableArray< NSNumber * > * _typeStorage
 
uint32_t _typeStorageMask
 
__weak id< FlutterKeyboardLayoutDelegate_keyboardLayoutDelegate
 
const MockLayoutData * _currentLayout
 
id _keyboardChannelResult
 
NSObject< FlutterBinaryMessenger > * _messengerMock
 
FlutterBinaryMessageHandler _keyboardHandler
 
NSObject< FlutterKeyboardManagerEventContext > * _eventContextMock
 

Macro Definition Documentation

◆ VERIFY_DOWN

#define VERIFY_DOWN (   OUT_LOGICAL,
  OUT_CHAR 
)
Value:
EXPECT_EQ(events[0].type, kFlutterKeyEventTypeDown); \
EXPECT_EQ(events[0].logical, static_cast<uint64_t>(OUT_LOGICAL)); \
EXPECT_STREQ(events[0].character, (OUT_CHAR)); \
clearEvents(events);
GLenum type
@ kFlutterKeyEventTypeDown
Definition embedder.h:1348

Definition at line 173 of file FlutterKeyboardManagerTest.mm.

180 : 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
215- (id)lastKeyboardChannelResult;
216
217- (void)sendKeyboardChannelMessage:(NSData* _Nullable)message;
218
219@property(readonly, nonatomic, strong) FlutterKeyboardManager* manager;
220@property(readonly, nonatomic, strong) id<FlutterKeyboardManagerEventContext> eventContextMock;
221@property(nonatomic, nullable, strong) NSResponder* nextResponder;
222
223#pragma mark - Private
224
225- (void)handleEmbedderEvent:(const FlutterKeyEvent&)event
227 userData:(nullable void*)userData;
228
229- (void)handleChannelMessage:(NSString*)channel
230 message:(NSData* _Nullable)message
231 binaryReply:(FlutterBinaryReply _Nullable)callback;
232
233- (BOOL)handleTextInputKeyEvent:(NSEvent*)event;
234@end
235
236@implementation KeyboardTester {
237 AsyncEmbedderCallbackHandler _embedderHandler;
238 AsyncKeyCallbackHandler _channelHandler;
239 TextInputCallback _textCallback;
240
241 NSMutableArray<NSNumber*>* _typeStorage;
242 uint32_t _typeStorageMask;
243
244 __weak id<FlutterKeyboardLayoutDelegate> _keyboardLayoutDelegate;
245 const MockLayoutData* _currentLayout;
246
248 NSObject<FlutterBinaryMessenger>* _messengerMock;
250
251 NSObject<FlutterKeyboardManagerEventContext>* _eventContextMock;
252}
253
254- (nonnull instancetype)init {
255 self = [super init];
256 if (self == nil) {
257 return nil;
258 }
259
260 _nextResponder = OCMClassMock([NSResponder class]);
261 [self respondChannelCallsWith:FALSE];
262 [self respondEmbedderCallsWith:FALSE];
263 [self respondTextInputWith:FALSE];
264
265 _currentLayout = &kUsLayout;
266
267 _messengerMock = OCMStrictProtocolMock(@protocol(FlutterBinaryMessenger));
268 OCMStub([_messengerMock sendOnChannel:@"flutter/keyevent"
269 message:[OCMArg any]
270 binaryReply:[OCMArg any]])
271 .andCall(self, @selector(handleChannelMessage:message:binaryReply:));
272 OCMStub([_messengerMock setMessageHandlerOnChannel:@"flutter/keyboard"
273 binaryMessageHandler:[OCMArg any]])
274 .andCall(self, @selector(setKeyboardChannelHandler:handler:));
275 OCMStub([_messengerMock sendOnChannel:@"flutter/keyboard" message:[OCMArg any]])
276 .andCall(self, @selector(handleKeyboardChannelMessage:message:));
277 id managerDelegateMock = OCMStrictProtocolMock(@protocol(FlutterKeyboardManagerDelegate));
278 OCMStub([managerDelegateMock binaryMessenger]).andReturn(_messengerMock);
279 OCMStub([managerDelegateMock sendKeyEvent:*(const FlutterKeyEvent*)[OCMArg anyPointer]
280 callback:nil
281 userData:nil])
282 .ignoringNonObjectArgs()
283 .andCall(self, @selector(handleEmbedderEvent:callback:userData:));
284
285 _eventContextMock = OCMStrictProtocolMock(@protocol(FlutterKeyboardManagerEventContext));
286 OCMStub([_eventContextMock nextResponder]).andReturn(_nextResponder);
287 OCMStub([_eventContextMock onTextInputKeyEvent:[OCMArg any]])
288 .andCall(self, @selector(handleTextInputKeyEvent:));
289
290 id keyboardLayoutMock = OCMStrictClassMock([FlutterKeyboardLayout class]);
291 OCMStub([keyboardLayoutMock lookUpLayoutForKeyCode:0 shift:false])
292 .ignoringNonObjectArgs()
293 .andCall(self, @selector(lookUpLayoutForKeyCode:shift:));
294 OCMStub([keyboardLayoutMock setDelegate:[OCMArg any]])
295 .andCall(self, @selector(onSetKeyboardLayoutDelegate:));
296
297 _manager = [[FlutterKeyboardManager alloc] initWithDelegate:managerDelegateMock
298 keyboardLayout:keyboardLayoutMock];
299 return self;
300}
301
302- (id)lastKeyboardChannelResult {
304}
305
306- (void)respondEmbedderCallsWith:(BOOL)response {
307 _embedderHandler = ^(const FlutterKeyEvent* event, FlutterAsyncKeyCallback callback) {
308 callback(response);
309 };
310}
311
312- (void)recordEmbedderCallsTo:(nonnull NSMutableArray<FlutterAsyncKeyCallback>*)storage {
313 _embedderHandler = ^(const FlutterKeyEvent* event, FlutterAsyncKeyCallback callback) {
314 [storage addObject:callback];
315 };
316}
317
318- (void)recordEmbedderEventsTo:(nonnull std::vector<FlutterKeyEvent>*)storage
319 returning:(bool)handled {
320 _embedderHandler = ^(const FlutterKeyEvent* event, FlutterAsyncKeyCallback callback) {
321 FlutterKeyEvent newEvent = *event;
322 if (event->character != nullptr) {
323 size_t charLen = strlen(event->character);
324 char* newCharacter = new char[charLen + 1];
325 strlcpy(newCharacter, event->character, charLen + 1);
326 newEvent.character = newCharacter;
327 }
328 storage->push_back(newEvent);
329 callback(handled);
330 };
331}
332
333- (void)respondChannelCallsWith:(BOOL)response {
335 callback(response);
336 };
337}
338
339- (void)recordChannelCallsTo:(nonnull NSMutableArray<FlutterAsyncKeyCallback>*)storage {
341 [storage addObject:callback];
342 };
343}
344
345- (void)respondTextInputWith:(BOOL)response {
346 _textCallback = ^(NSEvent* event) {
347 return response;
348 };
349}
350
351- (void)recordCallTypesTo:(nonnull NSMutableArray<NSNumber*>*)typeStorage
352 forTypes:(uint32_t)typeMask {
353 _typeStorage = typeStorage;
354 _typeStorageMask = typeMask;
355}
356
357- (void)sendKeyboardChannelMessage:(NSData* _Nullable)message {
358 [_messengerMock sendOnChannel:@"flutter/keyboard" message:message];
359}
360
361- (void)setLayout:(const MockLayoutData&)layout {
362 _currentLayout = &layout;
363 [_keyboardLayoutDelegate keyboardLayoutDidChange];
364}
365
366#pragma mark - Private
367
368- (void)handleEmbedderEvent:(const FlutterKeyEvent&)event
369 callback:(nullable FlutterKeyEventCallback)callback
370 userData:(nullable void*)userData {
371 if (_typeStorage != nil && (_typeStorageMask & kEmbedderCall) != 0) {
372 [_typeStorage addObject:@(kEmbedderCall)];
373 }
374 if (callback != nullptr) {
375 _embedderHandler(&event, ^(BOOL handled) {
376 callback(handled, userData);
377 });
378 }
379}
380
381- (void)handleChannelMessage:(NSString*)channel
382 message:(NSData* _Nullable)message
383 binaryReply:(FlutterBinaryReply _Nullable)callback {
384 if (_typeStorage != nil && (_typeStorageMask & kChannelCall) != 0) {
385 [_typeStorage addObject:@(kChannelCall)];
386 }
387 _channelHandler(^(BOOL handled) {
388 NSDictionary* result = @{
389 @"handled" : @(handled),
390 };
391 NSData* encodedKeyEvent = [[FlutterJSONMessageCodec sharedInstance] encode:result];
392 callback(encodedKeyEvent);
393 });
394}
395
396- (void)handleKeyboardChannelMessage:(NSString*)channel message:(NSData* _Nullable)message {
397 _keyboardHandler(message, ^(id result) {
398 _keyboardChannelResult = result;
399 });
400}
401
402- (BOOL)handleTextInputKeyEvent:(NSEvent*)event {
403 if (_typeStorage != nil && (_typeStorageMask & kTextCall) != 0) {
404 [_typeStorage addObject:@(kTextCall)];
405 }
406 return _textCallback(event);
407}
408
409- (void)onSetKeyboardLayoutDelegate:(id<FlutterKeyboardLayoutDelegate>)delegate {
410 _keyboardLayoutDelegate = delegate;
411}
412
413- (LayoutClue)lookUpLayoutForKeyCode:(uint16_t)keyCode shift:(BOOL)shift {
414 uint32_t cluePair = (*_currentLayout)[(keyCode * 2) + (shift ? 1 : 0)];
415 const uint32_t kCharMask = 0xffff;
416 const uint32_t kDeadKeyMask = 0x10000;
417 return LayoutClue{cluePair & kCharMask, (cluePair & kDeadKeyMask) != 0};
418}
419
420- (void)setKeyboardChannelHandler:(NSString*)channel handler:(FlutterBinaryMessageHandler)handler {
422}
423
424@end
425
426@interface FlutterKeyboardManagerUnittestsObjC : NSObject
429- (bool)textInputPlugin;
430- (bool)emptyNextResponder;
431- (bool)getPressedState;
436@end
437
438namespace flutter::testing {
439
440TEST(FlutterKeyboardManagerUnittests, SinglePrimaryResponder) {
441 ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] singlePrimaryResponder]);
442}
443
444TEST(FlutterKeyboardManagerUnittests, DoublePrimaryResponder) {
445 ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] doublePrimaryResponder]);
446}
447
448TEST(FlutterKeyboardManagerUnittests, SingleFinalResponder) {
450}
451
452TEST(FlutterKeyboardManagerUnittests, EmptyNextResponder) {
453 ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] emptyNextResponder]);
454}
455
456TEST(FlutterKeyboardManagerUnittests, GetPressedState) {
457 ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] getPressedState]);
458}
459
460TEST(FlutterKeyboardManagerUnittests, KeyboardChannelGetPressedState) {
461 ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] keyboardChannelGetPressedState]);
462}
463
464TEST(FlutterKeyboardManagerUnittests, RacingConditionBetweenKeyAndText) {
465 ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] racingConditionBetweenKeyAndText]);
466}
467
468TEST(FlutterKeyboardManagerUnittests, CorrectLogicalKeyForLayouts) {
469 ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] correctLogicalKeyForLayouts]);
470}
471
472TEST(FlutterKeyboardManagerUnittests, ShouldNotHoldStrongReferenceToDelegate) {
473 ASSERT_TRUE([[FlutterKeyboardManagerUnittestsObjC alloc] shouldNotHoldStrongReferenceToDelegate]);
474}
475
476} // namespace flutter::testing
477
479
481 KeyboardTester* tester = [[KeyboardTester alloc] init];
482 NSMutableArray<FlutterAsyncKeyCallback>* embedderCallbacks =
483 [NSMutableArray<FlutterAsyncKeyCallback> array];
484 [tester recordEmbedderCallsTo:embedderCallbacks];
485
486 // Case: The responder reports FALSE
487 [tester.manager handleEvent:keyDownEvent(0x50) withContext:tester.eventContextMock];
488 EXPECT_EQ([embedderCallbacks count], 1u);
489 embedderCallbacks[0](FALSE);
490 OCMVerify([tester.nextResponder keyDown:checkKeyDownEvent(0x50)]);
491 [embedderCallbacks removeAllObjects];
492
493 // Case: The responder reports TRUE
494 [tester.manager handleEvent:keyUpEvent(0x50) withContext:tester.eventContextMock];
495 EXPECT_EQ([embedderCallbacks count], 1u);
496 embedderCallbacks[0](TRUE);
497 // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown.
498
499 return true;
500}
501
503 KeyboardTester* tester = [[KeyboardTester alloc] init];
504
505 // Send a down event first so we can send an up event later.
506 [tester respondEmbedderCallsWith:false];
507 [tester respondChannelCallsWith:false];
508 [tester.manager handleEvent:keyDownEvent(0x50) withContext:tester.eventContextMock];
509
510 NSMutableArray<FlutterAsyncKeyCallback>* embedderCallbacks =
511 [NSMutableArray<FlutterAsyncKeyCallback> array];
512 NSMutableArray<FlutterAsyncKeyCallback>* channelCallbacks =
513 [NSMutableArray<FlutterAsyncKeyCallback> array];
514 [tester recordEmbedderCallsTo:embedderCallbacks];
515 [tester recordChannelCallsTo:channelCallbacks];
516
517 // Case: Both responders report TRUE.
518 [tester.manager handleEvent:keyUpEvent(0x50) withContext:tester.eventContextMock];
519 EXPECT_EQ([embedderCallbacks count], 1u);
520 EXPECT_EQ([channelCallbacks count], 1u);
521 embedderCallbacks[0](TRUE);
522 channelCallbacks[0](TRUE);
523 EXPECT_EQ([embedderCallbacks count], 1u);
524 EXPECT_EQ([channelCallbacks count], 1u);
525 // [tester.nextResponder keyUp:] should not be called, otherwise an error will be thrown.
526 [embedderCallbacks removeAllObjects];
527 [channelCallbacks removeAllObjects];
528
529 // Case: One responder reports TRUE.
530 [tester respondEmbedderCallsWith:false];
531 [tester respondChannelCallsWith:false];
532 [tester.manager handleEvent:keyDownEvent(0x50) withContext:tester.eventContextMock];
533
534 [tester recordEmbedderCallsTo:embedderCallbacks];
535 [tester recordChannelCallsTo:channelCallbacks];
536 [tester.manager handleEvent:keyUpEvent(0x50) withContext:tester.eventContextMock];
537 EXPECT_EQ([embedderCallbacks count], 1u);
538 EXPECT_EQ([channelCallbacks count], 1u);
539 embedderCallbacks[0](FALSE);
540 channelCallbacks[0](TRUE);
541 EXPECT_EQ([embedderCallbacks count], 1u);
542 EXPECT_EQ([channelCallbacks count], 1u);
543 // [tester.nextResponder keyUp:] should not be called, otherwise an error will be thrown.
544 [embedderCallbacks removeAllObjects];
545 [channelCallbacks removeAllObjects];
546
547 // Case: Both responders report FALSE.
548 [tester.manager handleEvent:keyDownEvent(0x53) withContext:tester.eventContextMock];
549 EXPECT_EQ([embedderCallbacks count], 1u);
550 EXPECT_EQ([channelCallbacks count], 1u);
551 embedderCallbacks[0](FALSE);
552 channelCallbacks[0](FALSE);
553 EXPECT_EQ([embedderCallbacks count], 1u);
554 EXPECT_EQ([channelCallbacks count], 1u);
555 OCMVerify([tester.nextResponder keyDown:checkKeyDownEvent(0x53)]);
556 [embedderCallbacks removeAllObjects];
557 [channelCallbacks removeAllObjects];
558
559 return true;
560}
561
562- (bool)textInputPlugin {
563 KeyboardTester* tester = [[KeyboardTester alloc] init];
564
565 // Send a down event first so we can send an up event later.
566 [tester respondEmbedderCallsWith:false];
567 [tester respondChannelCallsWith:false];
568 [tester.manager handleEvent:keyDownEvent(0x50) withContext:tester.eventContextMock];
569
570 NSMutableArray<FlutterAsyncKeyCallback>* callbacks =
571 [NSMutableArray<FlutterAsyncKeyCallback> array];
572 [tester recordEmbedderCallsTo:callbacks];
573
574 // Case: Primary responder responds TRUE. The event shouldn't be handled by
575 // the secondary responder.
576 [tester respondTextInputWith:FALSE];
577 [tester.manager handleEvent:keyUpEvent(0x50) withContext:tester.eventContextMock];
578 EXPECT_EQ([callbacks count], 1u);
579 callbacks[0](TRUE);
580 // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown.
581 [callbacks removeAllObjects];
582
583 // Send a down event first so we can send an up event later.
584 [tester respondEmbedderCallsWith:false];
585 [tester.manager handleEvent:keyDownEvent(0x50) withContext:tester.eventContextMock];
586
587 // Case: Primary responder responds FALSE. The secondary responder returns
588 // TRUE.
589 [tester recordEmbedderCallsTo:callbacks];
590 [tester respondTextInputWith:TRUE];
591 [tester.manager handleEvent:keyUpEvent(0x50) withContext:tester.eventContextMock];
592 EXPECT_EQ([callbacks count], 1u);
593 callbacks[0](FALSE);
594 // [owner.nextResponder keyUp:] should not be called, otherwise an error will be thrown.
595 [callbacks removeAllObjects];
596
597 // Case: Primary responder responds FALSE. The secondary responder returns FALSE.
598 [tester respondTextInputWith:FALSE];
599 [tester.manager handleEvent:keyDownEvent(0x50) withContext:tester.eventContextMock];
600 EXPECT_EQ([callbacks count], 1u);
601 callbacks[0](FALSE);
602 OCMVerify([tester.nextResponder keyDown:checkKeyDownEvent(0x50)]);
603 [callbacks removeAllObjects];
604
605 return true;
606}
607
608- (bool)emptyNextResponder {
609 KeyboardTester* tester = [[KeyboardTester alloc] init];
610 tester.nextResponder = nil;
611
612 [tester respondEmbedderCallsWith:false];
613 [tester respondChannelCallsWith:false];
614 [tester respondTextInputWith:false];
615 [tester.manager handleEvent:keyDownEvent(0x50) withContext:tester.eventContextMock];
616
617 // Passes if no error is thrown.
618 return true;
619}
620
621- (bool)getPressedState {
622 KeyboardTester* tester = [[KeyboardTester alloc] init];
623
624 [tester respondEmbedderCallsWith:false];
625 [tester respondChannelCallsWith:false];
626 [tester respondTextInputWith:false];
627 [tester.manager handleEvent:keyDownEvent(kVK_ANSI_A) withContext:tester.eventContextMock];
628
629 NSDictionary* pressingRecords = [tester.manager getPressedState];
630 EXPECT_EQ([pressingRecords count], 1u);
631 EXPECT_EQ(pressingRecords[@(kPhysicalKeyA)], @(kLogicalKeyA));
632
633 return true;
634}
635
637 KeyboardTester* tester = [[KeyboardTester alloc] init];
638
639 [tester respondEmbedderCallsWith:false];
640 [tester respondChannelCallsWith:false];
641 [tester respondTextInputWith:false];
642 [tester.manager handleEvent:keyDownEvent(kVK_ANSI_A) withContext:tester.eventContextMock];
643
644 FlutterMethodCall* getKeyboardStateMethodCall =
645 [FlutterMethodCall methodCallWithMethodName:@"getKeyboardState" arguments:nil];
646 NSData* getKeyboardStateMessage =
647 [[FlutterStandardMethodCodec sharedInstance] encodeMethodCall:getKeyboardStateMethodCall];
648 [tester sendKeyboardChannelMessage:getKeyboardStateMessage];
649
650 id encodedResult = [tester lastKeyboardChannelResult];
651 id decoded = [[FlutterStandardMethodCodec sharedInstance] decodeEnvelope:encodedResult];
652
653 EXPECT_EQ([decoded count], 1u);
654 EXPECT_EQ(decoded[@(kPhysicalKeyA)], @(kLogicalKeyA));
655
656 return true;
657}
658
659// Regression test for https://github.com/flutter/flutter/issues/82673.
661 KeyboardTester* tester = [[KeyboardTester alloc] init];
662
663 // Use Vietnamese IME (GoTiengViet, Telex mode) to type "uco".
664
665 // The events received by the framework. The engine might receive
666 // a channel message "setEditingState" from the framework.
667 NSMutableArray<FlutterAsyncKeyCallback>* keyCallbacks =
668 [NSMutableArray<FlutterAsyncKeyCallback> array];
669 [tester recordEmbedderCallsTo:keyCallbacks];
670
671 NSMutableArray<NSNumber*>* allCalls = [NSMutableArray<NSNumber*> array];
672 [tester recordCallTypesTo:allCalls forTypes:(kEmbedderCall | kTextCall)];
673
674 // Tap key U, which is converted by IME into a pure text message "ư".
675
676 [tester.manager handleEvent:keyDownEvent(kKeyCodeEmpty, @"ư", @"ư")
677 withContext:tester.eventContextMock];
678 EXPECT_EQ([keyCallbacks count], 1u);
679 EXPECT_EQ([allCalls count], 1u);
680 EXPECT_EQ(allCalls[0], @(kEmbedderCall));
681 keyCallbacks[0](false);
682 EXPECT_EQ([keyCallbacks count], 1u);
683 EXPECT_EQ([allCalls count], 2u);
684 EXPECT_EQ(allCalls[1], @(kTextCall));
685 [keyCallbacks removeAllObjects];
686 [allCalls removeAllObjects];
687
688 [tester.manager handleEvent:keyUpEvent(kKeyCodeEmpty) withContext:tester.eventContextMock];
689 EXPECT_EQ([keyCallbacks count], 1u);
690 keyCallbacks[0](false);
691 EXPECT_EQ([keyCallbacks count], 1u);
692 EXPECT_EQ([allCalls count], 2u);
693 [keyCallbacks removeAllObjects];
694 [allCalls removeAllObjects];
695
696 // Tap key O, which is converted to normal KeyO events, but the responses are
697 // slow.
698
699 [tester.manager handleEvent:keyDownEvent(kVK_ANSI_O, @"o", @"o")
700 withContext:tester.eventContextMock];
701 [tester.manager handleEvent:keyUpEvent(kVK_ANSI_O) withContext:tester.eventContextMock];
702 EXPECT_EQ([keyCallbacks count], 1u);
703 EXPECT_EQ([allCalls count], 1u);
704 EXPECT_EQ(allCalls[0], @(kEmbedderCall));
705
706 // Tap key C, which results in two Backspace messages first - and here they
707 // arrive before the key O messages are responded.
708
709 [tester.manager handleEvent:keyDownEvent(kVK_Delete) withContext:tester.eventContextMock];
710 [tester.manager handleEvent:keyUpEvent(kVK_Delete) withContext:tester.eventContextMock];
711 EXPECT_EQ([keyCallbacks count], 1u);
712 EXPECT_EQ([allCalls count], 1u);
713
714 // The key O down is responded, which releases a text call (for KeyO down) and
715 // an embedder call (for KeyO up) immediately.
716 keyCallbacks[0](false);
717 EXPECT_EQ([keyCallbacks count], 2u);
718 EXPECT_EQ([allCalls count], 3u);
719 EXPECT_EQ(allCalls[1], @(kTextCall)); // The order is important!
720 EXPECT_EQ(allCalls[2], @(kEmbedderCall));
721
722 // The key O up is responded, which releases a text call (for KeyO up) and
723 // an embedder call (for Backspace down) immediately.
724 keyCallbacks[1](false);
725 EXPECT_EQ([keyCallbacks count], 3u);
726 EXPECT_EQ([allCalls count], 5u);
727 EXPECT_EQ(allCalls[3], @(kTextCall)); // The order is important!
728 EXPECT_EQ(allCalls[4], @(kEmbedderCall));
729
730 // Finish up callbacks.
731 keyCallbacks[2](false);
732 keyCallbacks[3](false);
733
734 return true;
735}
736
738 KeyboardTester* tester = [[KeyboardTester alloc] init];
739 tester.nextResponder = nil;
740
741 std::vector<FlutterKeyEvent> events;
742 [tester recordEmbedderEventsTo:&events returning:true];
743 [tester respondChannelCallsWith:false];
744 [tester respondTextInputWith:false];
745
746 auto sendTap = [&](uint16_t keyCode, NSString* chars, NSString* charsUnmod) {
747 [tester.manager handleEvent:keyDownEvent(keyCode, chars, charsUnmod)
748 withContext:tester.eventContextMock];
749 [tester.manager handleEvent:keyUpEvent(keyCode) withContext:tester.eventContextMock];
750 };
751
752 /* US keyboard layout */
753
754 sendTap(kVK_ANSI_A, @"a", @"a"); // KeyA
755 VERIFY_DOWN(kLogicalKeyA, "a");
756
757 sendTap(kVK_ANSI_A, @"A", @"A"); // Shift-KeyA
758 VERIFY_DOWN(kLogicalKeyA, "A");
759
760 sendTap(kVK_ANSI_A, @"å", @"a"); // Option-KeyA
761 VERIFY_DOWN(kLogicalKeyA, "å");
762
763 sendTap(kVK_ANSI_T, @"t", @"t"); // KeyT
764 VERIFY_DOWN(kLogicalKeyT, "t");
765
766 sendTap(kVK_ANSI_1, @"1", @"1"); // Digit1
767 VERIFY_DOWN(kLogicalDigit1, "1");
768
769 sendTap(kVK_ANSI_1, @"!", @"!"); // Shift-Digit1
770 VERIFY_DOWN(kLogicalDigit1, "!");
771
772 sendTap(kVK_ANSI_Minus, @"-", @"-"); // Minus
773 VERIFY_DOWN('-', "-");
774
775 sendTap(kVK_ANSI_Minus, @"=", @"="); // Shift-Minus
776 VERIFY_DOWN('=', "=");
777
778 /* French keyboard layout */
779 [tester setLayout:kFrenchLayout];
780
781 sendTap(kVK_ANSI_A, @"q", @"q"); // KeyA
782 VERIFY_DOWN(kLogicalKeyQ, "q");
783
784 sendTap(kVK_ANSI_A, @"Q", @"Q"); // Shift-KeyA
785 VERIFY_DOWN(kLogicalKeyQ, "Q");
786
787 sendTap(kVK_ANSI_Semicolon, @"m", @"m"); // ; but prints M
788 VERIFY_DOWN(kLogicalKeyM, "m");
789
790 sendTap(kVK_ANSI_M, @",", @","); // M but prints ,
791 VERIFY_DOWN(',', ",");
792
793 sendTap(kVK_ANSI_1, @"&", @"&"); // Digit1
794 VERIFY_DOWN(kLogicalDigit1, "&");
795
796 sendTap(kVK_ANSI_1, @"1", @"1"); // Shift-Digit1
797 VERIFY_DOWN(kLogicalDigit1, "1");
798
799 sendTap(kVK_ANSI_Minus, @")", @")"); // Minus
800 VERIFY_DOWN(')', ")");
801
802 sendTap(kVK_ANSI_Minus, @"°", @"°"); // Shift-Minus
803 VERIFY_DOWN(L'°', "°");
804
805 /* Russian keyboard layout */
806 [tester setLayout:kRussianLayout];
807
808 sendTap(kVK_ANSI_A, @"ф", @"ф"); // KeyA
809 VERIFY_DOWN(kLogicalKeyA, "ф");
810
811 sendTap(kVK_ANSI_1, @"1", @"1"); // Digit1
812 VERIFY_DOWN(kLogicalDigit1, "1");
813
814 sendTap(kVK_ANSI_LeftBracket, @"х", @"х");
815 VERIFY_DOWN(kLogicalBracketLeft, "х");
816
817 /* Khmer keyboard layout */
818 // Regression test for https://github.com/flutter/flutter/issues/108729
819 [tester setLayout:kKhmerLayout];
820
821 sendTap(kVK_ANSI_2, @"២", @"២"); // Digit2
822 VERIFY_DOWN(kLogicalDigit2, "២");
823
824 return TRUE;
825}
826
828 __strong FlutterKeyboardManager* strongKeyboardManager;
829 __weak id weakDelegate;
830
831 @autoreleasepool {
832 id binaryMessengerMock = OCMStrictProtocolMock(@protocol(FlutterBinaryMessenger));
833 OCMStub([binaryMessengerMock setMessageHandlerOnChannel:[OCMArg any]
834 binaryMessageHandler:[OCMArg any]]);
835
836 id delegateMock = OCMStrictProtocolMock(@protocol(FlutterKeyboardManagerDelegate));
837 OCMStub([delegateMock binaryMessenger]).andReturn(binaryMessengerMock);
838
839 FlutterKeyboardManager* keyboardManager =
840 [[FlutterKeyboardManager alloc] initWithDelegate:delegateMock];
841 strongKeyboardManager = keyboardManager;
842 weakDelegate = delegateMock;
843 }
844
845 return weakDelegate == nil;
846}
847
848@end
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
void(^ FlutterBinaryMessageHandler)(NSData *_Nullable message, FlutterBinaryReply reply)
void(* FlutterKeyEventCallback)(bool, void *)
Definition embedder.h:1427
return TRUE
const gchar * channel
const gchar FlBinaryMessengerMessageHandler handler
G_BEGIN_DECLS GBytes * message
FlutterDesktopBinaryReply callback
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
void respondChannelCallsWith:(BOOL response)
void recordChannelCallsTo:(nonnull NSMutableArray< FlutterAsyncKeyCallback > *storage)
void recordEmbedderEventsTo:returning:(nonnull std::vector< FlutterKeyEvent > *storage,[returning] bool handled)
void sendKeyboardChannelMessage:(NSData *_Nullable message)
void respondEmbedderCallsWith:(BOOL response)
void respondTextInputWith:(BOOL response)
void recordCallTypesTo:forTypes:(nonnull NSMutableArray< NSNumber * > *typeStorage,[forTypes] uint32_t typeMask)
void recordEmbedderCallsTo:(nonnull NSMutableArray< FlutterAsyncKeyCallback > *storage)
void(^ FlutterAsyncKeyCallback)(BOOL handled)
FlutterTextInputPlugin * textInputPlugin
NSObject< FlutterKeyboardManagerEventContext > * _eventContextMock
FlutterBinaryMessageHandler _keyboardHandler
uint32_t _typeStorageMask
AsyncKeyCallbackHandler _channelHandler
TextInputCallback _textCallback
#define VERIFY_DOWN(OUT_LOGICAL, OUT_CHAR)
NSMutableArray< NSNumber * > * _typeStorage
const MockLayoutData * _currentLayout
NSObject< FlutterBinaryMessenger > * _messengerMock
__weak id< FlutterKeyboardLayoutDelegate > _keyboardLayoutDelegate
TEST(NativeAssetsManagerTest, NoAvailableAssets)
Definition ref_ptr.h:261
instancetype sharedInstance()
const char * character
Definition embedder.h:1409
const uintptr_t id
int BOOL

Variable Documentation

◆ _channelHandler

AsyncKeyCallbackHandler _channelHandler
Initial value:
{
AsyncEmbedderCallbackHandler _embedderHandler

Definition at line 239 of file FlutterKeyboardManagerTest.mm.

◆ _currentLayout

const MockLayoutData* _currentLayout

Definition at line 246 of file FlutterKeyboardManagerTest.mm.

◆ _eventContextMock

NSObject<FlutterKeyboardManagerEventContext>* _eventContextMock

Definition at line 252 of file FlutterKeyboardManagerTest.mm.

◆ _keyboardChannelResult

id _keyboardChannelResult

Definition at line 248 of file FlutterKeyboardManagerTest.mm.

◆ _keyboardHandler

FlutterBinaryMessageHandler _keyboardHandler

Definition at line 250 of file FlutterKeyboardManagerTest.mm.

◆ _keyboardLayoutDelegate

__weak id<FlutterKeyboardLayoutDelegate> _keyboardLayoutDelegate

Definition at line 245 of file FlutterKeyboardManagerTest.mm.

◆ _messengerMock

NSObject<FlutterBinaryMessenger>* _messengerMock

Definition at line 249 of file FlutterKeyboardManagerTest.mm.

◆ _textCallback

TextInputCallback _textCallback

Definition at line 240 of file FlutterKeyboardManagerTest.mm.

◆ _typeStorage

NSMutableArray<NSNumber*>* _typeStorage

Definition at line 242 of file FlutterKeyboardManagerTest.mm.

◆ _typeStorageMask

uint32_t _typeStorageMask

Definition at line 243 of file FlutterKeyboardManagerTest.mm.