blob: 004f00799d21d1c3a6818f87005ba3235fb05f87 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h"
#include <Carbon/Carbon.h>
#import <Foundation/Foundation.h>
#include "flutter/shell/platform/embedder/test_utils/key_codes.g.h"
#include "flutter/testing/autoreleasepool_test.h"
#include "flutter/testing/testing.h"
#include "third_party/googletest/googletest/include/gtest/gtest.h"
// FlutterBasicMessageChannel fake instance that records Flutter key event messages.
//
// When a sendMessage:reply: callback is specified, it is invoked with the value stored in the
// nextResponse property.
@interface FakeMessageChannel : FlutterBasicMessageChannel
@property(nonatomic, readonly) NSMutableArray<id>* messages;
@property(nonatomic) NSDictionary* nextResponse;
- (instancetype)init;
- (void)sendMessage:(id _Nullable)message;
- (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback;
@end
@implementation FakeMessageChannel
- (instancetype)init {
self = [super init];
if (self != nil) {
_messages = [[NSMutableArray<id> alloc] init];
}
return self;
}
- (void)sendMessage:(id _Nullable)message {
[self sendMessage:message reply:nil];
}
- (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback {
[_messages addObject:message];
if (callback) {
callback(_nextResponse);
}
}
@end
namespace flutter::testing {
namespace {
using flutter::testing::keycodes::kLogicalKeyQ;
NSEvent* CreateKeyEvent(NSEventType type,
NSEventModifierFlags modifierFlags,
NSString* characters,
NSString* charactersIgnoringModifiers,
BOOL isARepeat,
unsigned short keyCode) {
return [NSEvent keyEventWithType:type
location:NSZeroPoint
modifierFlags:modifierFlags
timestamp:0
windowNumber:0
context:nil
characters:characters
charactersIgnoringModifiers:charactersIgnoringModifiers
isARepeat:isARepeat
keyCode:keyCode];
}
} // namespace
using FlutterChannelKeyResponderTest = AutoreleasePoolTest;
TEST_F(FlutterChannelKeyResponderTest, BasicKeyEvent) {
__block NSMutableArray<NSNumber*>* responses = [[NSMutableArray<NSNumber*> alloc] init];
FakeMessageChannel* channel = [[FakeMessageChannel alloc] init];
FlutterChannelKeyResponder* responder =
[[FlutterChannelKeyResponder alloc] initWithChannel:channel];
// Initial empty modifiers.
//
// This can happen when user opens window while modifier key is pressed and then releases the
// modifier. No events should be sent, but the callback should still be called.
// Regression test for https://github.com/flutter/flutter/issues/87339.
channel.nextResponse = @{@"handled" : @YES};
[responder handleEvent:CreateKeyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", NO, 60)
callback:^(BOOL handled) {
[responses addObject:@(handled)];
}];
EXPECT_EQ([channel.messages count], 0u);
ASSERT_EQ([responses count], 1u);
EXPECT_EQ([responses[0] boolValue], YES);
[responses removeAllObjects];
// Key down
channel.nextResponse = @{@"handled" : @YES};
[responder handleEvent:CreateKeyEvent(NSEventTypeKeyDown, 0x100, @"a", @"a", NO, 0)
callback:^(BOOL handled) {
[responses addObject:@(handled)];
}];
ASSERT_EQ([channel.messages count], 1u);
EXPECT_STREQ([[channel.messages lastObject][@"keymap"] UTF8String], "macos");
EXPECT_STREQ([[channel.messages lastObject][@"type"] UTF8String], "keydown");
EXPECT_EQ([[channel.messages lastObject][@"keyCode"] intValue], 0);
EXPECT_EQ([[channel.messages lastObject][@"modifiers"] intValue], 0x0);
EXPECT_STREQ([[channel.messages lastObject][@"characters"] UTF8String], "a");
EXPECT_STREQ([[channel.messages lastObject][@"charactersIgnoringModifiers"] UTF8String], "a");
ASSERT_EQ([responses count], 1u);
EXPECT_EQ([[responses lastObject] boolValue], YES);
[channel.messages removeAllObjects];
[responses removeAllObjects];
// Key up
channel.nextResponse = @{@"handled" : @NO};
[responder handleEvent:CreateKeyEvent(NSEventTypeKeyUp, 0x100, @"a", @"a", NO, 0)
callback:^(BOOL handled) {
[responses addObject:@(handled)];
}];
ASSERT_EQ([channel.messages count], 1u);
EXPECT_STREQ([[channel.messages lastObject][@"keymap"] UTF8String], "macos");
EXPECT_STREQ([[channel.messages lastObject][@"type"] UTF8String], "keyup");
EXPECT_EQ([[channel.messages lastObject][@"keyCode"] intValue], 0);
EXPECT_EQ([[channel.messages lastObject][@"modifiers"] intValue], 0);
EXPECT_STREQ([[channel.messages lastObject][@"characters"] UTF8String], "a");
EXPECT_STREQ([[channel.messages lastObject][@"charactersIgnoringModifiers"] UTF8String], "a");
ASSERT_EQ([responses count], 1u);
EXPECT_EQ([[responses lastObject] boolValue], NO);
[channel.messages removeAllObjects];
[responses removeAllObjects];
// LShift down
channel.nextResponse = @{@"handled" : @YES};
[responder handleEvent:CreateKeyEvent(NSEventTypeFlagsChanged, 0x20102, @"", @"", NO, 56)
callback:^(BOOL handled) {
[responses addObject:@(handled)];
}];
ASSERT_EQ([channel.messages count], 1u);
EXPECT_STREQ([[channel.messages lastObject][@"keymap"] UTF8String], "macos");
EXPECT_STREQ([[channel.messages lastObject][@"type"] UTF8String], "keydown");
EXPECT_EQ([[channel.messages lastObject][@"keyCode"] intValue], 56);
EXPECT_EQ([[channel.messages lastObject][@"modifiers"] intValue], 0x20002);
ASSERT_EQ([responses count], 1u);
EXPECT_EQ([[responses lastObject] boolValue], YES);
[channel.messages removeAllObjects];
[responses removeAllObjects];
// RShift down
channel.nextResponse = @{@"handled" : @NO};
[responder handleEvent:CreateKeyEvent(NSEventTypeFlagsChanged, 0x20006, @"", @"", NO, 60)
callback:^(BOOL handled) {
[responses addObject:@(handled)];
}];
ASSERT_EQ([channel.messages count], 1u);
EXPECT_STREQ([[channel.messages lastObject][@"keymap"] UTF8String], "macos");
EXPECT_STREQ([[channel.messages lastObject][@"type"] UTF8String], "keydown");
EXPECT_EQ([[channel.messages lastObject][@"keyCode"] intValue], 60);
EXPECT_EQ([[channel.messages lastObject][@"modifiers"] intValue], 0x20006);
ASSERT_EQ([responses count], 1u);
EXPECT_EQ([[responses lastObject] boolValue], NO);
[channel.messages removeAllObjects];
[responses removeAllObjects];
// LShift up
channel.nextResponse = @{@"handled" : @NO};
[responder handleEvent:CreateKeyEvent(NSEventTypeFlagsChanged, 0x20104, @"", @"", NO, 56)
callback:^(BOOL handled) {
[responses addObject:@(handled)];
}];
ASSERT_EQ([channel.messages count], 1u);
EXPECT_STREQ([[channel.messages lastObject][@"keymap"] UTF8String], "macos");
EXPECT_STREQ([[channel.messages lastObject][@"type"] UTF8String], "keyup");
EXPECT_EQ([[channel.messages lastObject][@"keyCode"] intValue], 56);
EXPECT_EQ([[channel.messages lastObject][@"modifiers"] intValue], 0x20004);
ASSERT_EQ([responses count], 1u);
EXPECT_EQ([[responses lastObject] boolValue], NO);
[channel.messages removeAllObjects];
[responses removeAllObjects];
// RShift up
channel.nextResponse = @{@"handled" : @NO};
[responder handleEvent:CreateKeyEvent(NSEventTypeFlagsChanged, 0, @"", @"", NO, 60)
callback:^(BOOL handled) {
[responses addObject:@(handled)];
}];
ASSERT_EQ([channel.messages count], 1u);
EXPECT_STREQ([[channel.messages lastObject][@"keymap"] UTF8String], "macos");
EXPECT_STREQ([[channel.messages lastObject][@"type"] UTF8String], "keyup");
EXPECT_EQ([[channel.messages lastObject][@"keyCode"] intValue], 60);
EXPECT_EQ([[channel.messages lastObject][@"modifiers"] intValue], 0);
ASSERT_EQ([responses count], 1u);
EXPECT_EQ([[responses lastObject] boolValue], NO);
[channel.messages removeAllObjects];
[responses removeAllObjects];
// RShift up again, should be ignored and not produce a keydown event, but the
// callback should be called.
channel.nextResponse = @{@"handled" : @NO};
[responder handleEvent:CreateKeyEvent(NSEventTypeFlagsChanged, 0x100, @"", @"", NO, 60)
callback:^(BOOL handled) {
[responses addObject:@(handled)];
}];
EXPECT_EQ([channel.messages count], 0u);
ASSERT_EQ([responses count], 1u);
EXPECT_EQ([responses[0] boolValue], YES);
}
TEST_F(FlutterChannelKeyResponderTest, EmptyResponseIsTakenAsHandled) {
__block NSMutableArray<NSNumber*>* responses = [[NSMutableArray<NSNumber*> alloc] init];
FakeMessageChannel* channel = [[FakeMessageChannel alloc] init];
FlutterChannelKeyResponder* responder =
[[FlutterChannelKeyResponder alloc] initWithChannel:channel];
channel.nextResponse = nil;
[responder handleEvent:CreateKeyEvent(NSEventTypeKeyDown, 0x100, @"a", @"a", NO, 0)
callback:^(BOOL handled) {
[responses addObject:@(handled)];
}];
ASSERT_EQ([channel.messages count], 1u);
EXPECT_STREQ([[channel.messages lastObject][@"keymap"] UTF8String], "macos");
EXPECT_STREQ([[channel.messages lastObject][@"type"] UTF8String], "keydown");
EXPECT_EQ([[channel.messages lastObject][@"keyCode"] intValue], 0);
EXPECT_EQ([[channel.messages lastObject][@"modifiers"] intValue], 0);
EXPECT_STREQ([[channel.messages lastObject][@"characters"] UTF8String], "a");
EXPECT_STREQ([[channel.messages lastObject][@"charactersIgnoringModifiers"] UTF8String], "a");
ASSERT_EQ([responses count], 1u);
EXPECT_EQ([[responses lastObject] boolValue], YES);
}
TEST_F(FlutterChannelKeyResponderTest, FollowsLayoutMap) {
__block NSMutableArray<NSNumber*>* responses = [[NSMutableArray<NSNumber*> alloc] init];
FakeMessageChannel* channel = [[FakeMessageChannel alloc] init];
FlutterChannelKeyResponder* responder =
[[FlutterChannelKeyResponder alloc] initWithChannel:channel];
NSMutableDictionary<NSNumber*, NSNumber*>* layoutMap =
[NSMutableDictionary<NSNumber*, NSNumber*> dictionary];
responder.layoutMap = layoutMap;
// French layout
layoutMap[@(kVK_ANSI_A)] = @(kLogicalKeyQ);
channel.nextResponse = @{@"handled" : @YES};
[responder handleEvent:CreateKeyEvent(NSEventTypeKeyDown, kVK_ANSI_A, @"q", @"q", NO, 0)
callback:^(BOOL handled) {
[responses addObject:@(handled)];
}];
ASSERT_EQ([channel.messages count], 1u);
EXPECT_STREQ([[channel.messages lastObject][@"keymap"] UTF8String], "macos");
EXPECT_STREQ([[channel.messages lastObject][@"type"] UTF8String], "keydown");
EXPECT_EQ([[channel.messages lastObject][@"keyCode"] intValue], 0);
EXPECT_EQ([[channel.messages lastObject][@"modifiers"] intValue], 0x0);
EXPECT_STREQ([[channel.messages lastObject][@"characters"] UTF8String], "q");
EXPECT_STREQ([[channel.messages lastObject][@"charactersIgnoringModifiers"] UTF8String], "q");
EXPECT_EQ([channel.messages lastObject][@"specifiedLogicalKey"], @(kLogicalKeyQ));
ASSERT_EQ([responses count], 1u);
EXPECT_EQ([[responses lastObject] boolValue], YES);
[channel.messages removeAllObjects];
[responses removeAllObjects];
}
} // namespace flutter::testing