Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
Classes | Namespaces | Macros | Functions | Variables
FlutterKeyboardManagerTest.mm File Reference
#include <Carbon/Carbon.h>
import <Foundation/Foundation.h>
import <OCMock/OCMock.h>
import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h"
import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h"
#include "flutter/shell/platform/embedder/test_utils/key_codes.g.h"
import "flutter/testing/testing.h"
#include "third_party/googletest/googletest/include/gtest/gtest.h"

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, ShouldNotHoldStrongReferenceToViewDelegate)
 

Variables

AsyncKeyCallbackHandler _channelHandler
 
TextInputCallback _textCallback
 
NSMutableArray< NSNumber * > * _typeStorage
 
uint32_t _typeStorageMask
 
flutter::KeyboardLayoutNotifier _keyboardLayoutNotifier
 
const MockLayoutData * _currentLayout
 
id _keyboardChannelResult
 
NSObject< FlutterBinaryMessenger > * _messengerMock
 
FlutterBinaryMessageHandler _keyboardHandler
 

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);
@ kFlutterKeyEventTypeDown
Definition embedder.h:1074

Definition at line 172 of file FlutterKeyboardManagerTest.mm.

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

Variable Documentation

◆ _channelHandler

AsyncKeyCallbackHandler _channelHandler
Initial value:
{
AsyncEmbedderCallbackHandler _embedderHandler

Definition at line 237 of file FlutterKeyboardManagerTest.mm.

◆ _currentLayout

const MockLayoutData* _currentLayout

Definition at line 244 of file FlutterKeyboardManagerTest.mm.

◆ _keyboardChannelResult

id _keyboardChannelResult

Definition at line 246 of file FlutterKeyboardManagerTest.mm.

◆ _keyboardHandler

FlutterBinaryMessageHandler _keyboardHandler

Definition at line 248 of file FlutterKeyboardManagerTest.mm.

◆ _keyboardLayoutNotifier

flutter::KeyboardLayoutNotifier _keyboardLayoutNotifier

Definition at line 243 of file FlutterKeyboardManagerTest.mm.

◆ _messengerMock

NSObject<FlutterBinaryMessenger>* _messengerMock

Definition at line 247 of file FlutterKeyboardManagerTest.mm.

◆ _textCallback

TextInputCallback _textCallback

Definition at line 238 of file FlutterKeyboardManagerTest.mm.

◆ _typeStorage

NSMutableArray<NSNumber*>* _typeStorage

Definition at line 240 of file FlutterKeyboardManagerTest.mm.

◆ _typeStorageMask

uint32_t _typeStorageMask

Definition at line 241 of file FlutterKeyboardManagerTest.mm.