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