Flutter Engine Uber Docs
Docs for the entire Flutter Engine repo.
 
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.