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