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