| Flutter macOS Embedder |

FlutterChannelKeyResponderTest.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/FlutterChannelKeyResponder.h"
10 #include "flutter/shell/platform/embedder/test_utils/key_codes.g.h"
11 #import "flutter/testing/testing.h"
12 #include "third_party/googletest/googletest/include/gtest/gtest.h"
13
14 namespace flutter::testing {
15
16 namespace {
17 using flutter::testing::keycodes::kLogicalKeyQ;
18
19 NSEvent* keyEvent(NSEventType type,
20 NSEventModifierFlags modifierFlags,
21 NSString* characters,
22 NSString* charactersIgnoringModifiers,
23 BOOL isARepeat,
24 unsigned short keyCode) {
25 return [NSEvent keyEventWithType:type
26 location:NSZeroPoint
27 modifierFlags:modifierFlags
28 timestamp:0
29 windowNumber:0
30 context:nil
31 characters:characters
32 charactersIgnoringModifiers:charactersIgnoringModifiers
33 isARepeat:isARepeat
34 keyCode:keyCode];
35 }
36 } // namespace
37
38 TEST(FlutterChannelKeyResponderUnittests, BasicKeyEvent) {
39 __block NSMutableArray<id>* messages = [[NSMutableArray<id> alloc] init];
40 __block BOOL next_response = TRUE;
41 __block NSMutableArray<NSNumber*>* responses = [[NSMutableArray<NSNumber*> alloc] init];
42
43 id mockKeyEventChannel = OCMStrictClassMock([FlutterBasicMessageChannel class]);
44 OCMStub([mockKeyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
45 .andDo((^(NSInvocation* invocation) {
46 [invocation retainArguments];
47 NSDictionary* message;
48 [invocation getArgument:&message atIndex:2];
49 [messages addObject:message];
50
51 FlutterReply callback;
52 [invocation getArgument:&callback atIndex:3];
53 NSDictionary* keyMessage = @{
54 @"handled" : @(next_response),
55 };
56 callback(keyMessage);
57 }));
58
59 FlutterChannelKeyResponder* responder =
60 [[FlutterChannelKeyResponder alloc] initWithChannel:mockKeyEventChannel];
61
62 // Initial empty modifiers. This can happen when user opens window while modifier key is pressed
63 // and then releases the modifier. No events should be sent, but the callback
64 // should still be called.
65 // Regression test for https://github.com/flutter/flutter/issues/87339.
66 [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, 60)
67 callback:^(BOOL handled) {
68 [responses addObject:@(handled)];
69 }];
70
71 EXPECT_EQ([messages count], 0u);
72 EXPECT_EQ([responses count], 1u);
73 EXPECT_EQ([responses[0] boolValue], TRUE);
74 [responses removeAllObjects];
75
76 // Key down
77 [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x100, @"a", @"a", FALSE, 0)
78 callback:^(BOOL handled) {
79 [responses addObject:@(handled)];
80 }];
81
82 EXPECT_EQ([messages count], 1u);
83 EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos");
84 EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keydown");
85 EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 0);
86 EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0x0);
87 EXPECT_STREQ([[messages lastObject][@"characters"] UTF8String], "a");
88 EXPECT_STREQ([[messages lastObject][@"charactersIgnoringModifiers"] UTF8String], "a");
89
90 EXPECT_EQ([responses count], 1u);
91 EXPECT_EQ([[responses lastObject] boolValue], TRUE);
92
93 [messages removeAllObjects];
94 [responses removeAllObjects];
95
96 // Key up
97 next_response = FALSE;
98 [responder handleEvent:keyEvent(NSEventTypeKeyUp, 0x100, @"a", @"a", FALSE, 0)
99 callback:^(BOOL handled) {
100 [responses addObject:@(handled)];
101 }];
102
103 EXPECT_EQ([messages count], 1u);
104 EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos");
105 EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keyup");
106 EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 0);
107 EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0);
108 EXPECT_STREQ([[messages lastObject][@"characters"] UTF8String], "a");
109 EXPECT_STREQ([[messages lastObject][@"charactersIgnoringModifiers"] UTF8String], "a");
110
111 EXPECT_EQ([responses count], 1u);
112 EXPECT_EQ([[responses lastObject] boolValue], FALSE);
113
114 [messages removeAllObjects];
115 [responses removeAllObjects];
116
117 // LShift down
118 next_response = TRUE;
119 [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20102, @"", @"", FALSE, 56)
120 callback:^(BOOL handled) {
121 [responses addObject:@(handled)];
122 }];
123
124 EXPECT_EQ([messages count], 1u);
125 EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos");
126 EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keydown");
127 EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 56);
128 EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0x20002);
129
130 EXPECT_EQ([responses count], 1u);
131 EXPECT_EQ([[responses lastObject] boolValue], TRUE);
132
133 [messages removeAllObjects];
134 [responses removeAllObjects];
135
136 // RShift down
137 next_response = false;
138 [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20006, @"", @"", FALSE, 60)
139 callback:^(BOOL handled) {
140 [responses addObject:@(handled)];
141 }];
142
143 EXPECT_EQ([messages count], 1u);
144 EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos");
145 EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keydown");
146 EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 60);
147 EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0x20006);
148
149 EXPECT_EQ([responses count], 1u);
150 EXPECT_EQ([[responses lastObject] boolValue], FALSE);
151
152 [messages removeAllObjects];
153 [responses removeAllObjects];
154
155 // LShift up
156 next_response = false;
157 [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x20104, @"", @"", FALSE, 56)
158 callback:^(BOOL handled) {
159 [responses addObject:@(handled)];
160 }];
161
162 EXPECT_EQ([messages count], 1u);
163 EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos");
164 EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keyup");
165 EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 56);
166 EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0x20004);
167
168 EXPECT_EQ([responses count], 1u);
169 EXPECT_EQ([[responses lastObject] boolValue], FALSE);
170
171 [messages removeAllObjects];
172 [responses removeAllObjects];
173
174 // RShift up
175 next_response = false;
176 [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0, @"", @"", FALSE, 60)
177 callback:^(BOOL handled) {
178 [responses addObject:@(handled)];
179 }];
180
181 EXPECT_EQ([messages count], 1u);
182 EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos");
183 EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keyup");
184 EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 60);
185 EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0);
186
187 EXPECT_EQ([responses count], 1u);
188 EXPECT_EQ([[responses lastObject] boolValue], FALSE);
189
190 [messages removeAllObjects];
191 [responses removeAllObjects];
192
193 // RShift up again, should be ignored and not produce a keydown event, but the
194 // callback should be called.
195 next_response = false;
196 [responder handleEvent:keyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", FALSE, 60)
197 callback:^(BOOL handled) {
198 [responses addObject:@(handled)];
199 }];
200
201 EXPECT_EQ([messages count], 0u);
202 EXPECT_EQ([responses count], 1u);
203 EXPECT_EQ([responses[0] boolValue], TRUE);
204 }
205
206 TEST(FlutterChannelKeyResponderUnittests, EmptyResponseIsTakenAsHandled) {
207 __block NSMutableArray<id>* messages = [[NSMutableArray<id> alloc] init];
208 __block NSMutableArray<NSNumber*>* responses = [[NSMutableArray<NSNumber*> alloc] init];
209
210 id mockKeyEventChannel = OCMStrictClassMock([FlutterBasicMessageChannel class]);
211 OCMStub([mockKeyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
212 .andDo((^(NSInvocation* invocation) {
213 [invocation retainArguments];
214 NSDictionary* message;
215 [invocation getArgument:&message atIndex:2];
216 [messages addObject:message];
217
218 FlutterReply callback;
219 [invocation getArgument:&callback atIndex:3];
220 callback(nullptr);
221 }));
222
223 FlutterChannelKeyResponder* responder =
224 [[FlutterChannelKeyResponder alloc] initWithChannel:mockKeyEventChannel];
225 [responder handleEvent:keyEvent(NSEventTypeKeyDown, 0x100, @"a", @"a", FALSE, 0)
226 callback:^(BOOL handled) {
227 [responses addObject:@(handled)];
228 }];
229
230 EXPECT_EQ([messages count], 1u);
231 EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos");
232 EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keydown");
233 EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 0);
234 EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0);
235 EXPECT_STREQ([[messages lastObject][@"characters"] UTF8String], "a");
236 EXPECT_STREQ([[messages lastObject][@"charactersIgnoringModifiers"] UTF8String], "a");
237
238 EXPECT_EQ([responses count], 1u);
239 EXPECT_EQ([[responses lastObject] boolValue], TRUE);
240 }
241
242 TEST(FlutterChannelKeyResponderUnittests, FollowsLayoutMap) {
243 __block NSMutableArray<id>* messages = [[NSMutableArray<id> alloc] init];
244 __block BOOL next_response = TRUE;
245 __block NSMutableArray<NSNumber*>* responses = [[NSMutableArray<NSNumber*> alloc] init];
246
247 id mockKeyEventChannel = OCMStrictClassMock([FlutterBasicMessageChannel class]);
248 OCMStub([mockKeyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
249 .andDo((^(NSInvocation* invocation) {
250 [invocation retainArguments];
251 NSDictionary* message;
252 [invocation getArgument:&message atIndex:2];
253 [messages addObject:message];
254
255 FlutterReply callback;
256 [invocation getArgument:&callback atIndex:3];
257 NSDictionary* keyMessage = @{
258 @"handled" : @(next_response),
259 };
260 callback(keyMessage);
261 }));
262
263 FlutterChannelKeyResponder* responder =
264 [[FlutterChannelKeyResponder alloc] initWithChannel:mockKeyEventChannel];
265
266 NSMutableDictionary<NSNumber*, NSNumber*>* layoutMap =
267 [NSMutableDictionary<NSNumber*, NSNumber*> dictionary];
268 responder.layoutMap = layoutMap;
269 // French layout
270 layoutMap[@(kVK_ANSI_A)] = @(kLogicalKeyQ);
271
272 [responder handleEvent:keyEvent(NSEventTypeKeyDown, kVK_ANSI_A, @"q", @"q", FALSE, 0)
273 callback:^(BOOL handled) {
274 [responses addObject:@(handled)];
275 }];
276
277 EXPECT_EQ([messages count], 1u);
278 EXPECT_STREQ([[messages lastObject][@"keymap"] UTF8String], "macos");
279 EXPECT_STREQ([[messages lastObject][@"type"] UTF8String], "keydown");
280 EXPECT_EQ([[messages lastObject][@"keyCode"] intValue], 0);
281 EXPECT_EQ([[messages lastObject][@"modifiers"] intValue], 0x0);
282 EXPECT_STREQ([[messages lastObject][@"characters"] UTF8String], "q");
283 EXPECT_STREQ([[messages lastObject][@"charactersIgnoringModifiers"] UTF8String], "q");
284 EXPECT_EQ([messages lastObject][@"specifiedLogicalKey"], @(kLogicalKeyQ));
285
286 EXPECT_EQ([responses count], 1u);
287 EXPECT_EQ([[responses lastObject] boolValue], TRUE);
288
289 [messages removeAllObjects];
290 [responses removeAllObjects];
291 }
292
293 } // namespace flutter::testing
FlutterBasicMessageChannel
Definition: FlutterChannels.h:39
flutter::testing
Definition: AccessibilityBridgeMacTest.mm:11
FlutterChannelKeyResponder.h
flutter::testing::TEST
TEST(AccessibilityBridgeMacTest, SendsAccessibilityCreateNotificationToWindowOfFlutterView)
Definition: AccessibilityBridgeMacTest.mm:61
FlutterKeyPrimaryResponder-p::layoutMap
NSMutableDictionary< NSNumber *, NSNumber * > * layoutMap
Definition: FlutterKeyPrimaryResponder.h:43
FlutterChannelKeyResponder
Definition: FlutterChannelKeyResponder.h:17
FlutterReply
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterReply)(id _Nullable reply)