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