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