blob: 6de6c840486bee66673cd4e8fe86623b8d44a407 [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 <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <_types/_uint64_t.h>
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/KeyCodeMap_Internal.h"
#include "flutter/shell/platform/embedder/embedder.h"
#include "flutter/shell/platform/embedder/test_utils/key_codes.h"
using namespace flutter::testing::keycodes;
FLUTTER_ASSERT_ARC;
#define XCTAssertStrEqual(value, expected) \
XCTAssertTrue(strcmp(value, expected) == 0, \
@"String \"%s\" not equal to the expected value of \"%s\"", value, expected)
// A wrap to convert FlutterKeyEvent to a ObjC class.
@interface TestKeyEvent : NSObject
@property(nonatomic) FlutterKeyEvent* data;
@property(nonatomic) FlutterKeyEventCallback callback;
@property(nonatomic) _VoidPtr userData;
- (nonnull instancetype)initWithEvent:(const FlutterKeyEvent*)event
callback:(nullable FlutterKeyEventCallback)callback
userData:(nullable _VoidPtr)userData;
- (BOOL)hasCallback;
- (void)respond:(BOOL)handled;
@end
@implementation TestKeyEvent
- (instancetype)initWithEvent:(const FlutterKeyEvent*)event
callback:(nullable FlutterKeyEventCallback)callback
userData:(nullable _VoidPtr)userData {
self = [super init];
_data = new FlutterKeyEvent(*event);
if (event->character != nullptr) {
size_t len = strlen(event->character);
char* character = new char[len + 1];
strcpy(character, event->character);
_data->character = character;
}
_callback = callback;
_userData = userData;
return self;
}
- (BOOL)hasCallback {
return _callback != nil;
}
- (void)respond:(BOOL)handled {
NSAssert(
_callback != nil,
@"Improper call to `respond` that does not have a callback."); // Caller's responsibility
_callback(handled, _userData);
}
- (void)dealloc {
if (_data->character != nullptr)
delete[] _data->character;
delete _data;
}
@end
namespace {
API_AVAILABLE(ios(13.4))
constexpr UIKeyboardHIDUsage kKeyCodeUndefined = (UIKeyboardHIDUsage)0x03;
API_AVAILABLE(ios(13.4))
constexpr UIKeyboardHIDUsage kKeyCodeKeyA = (UIKeyboardHIDUsage)0x04;
API_AVAILABLE(ios(13.4))
constexpr UIKeyboardHIDUsage kKeyCodeKeyW = (UIKeyboardHIDUsage)0x1a;
API_AVAILABLE(ios(13.4))
constexpr UIKeyboardHIDUsage kKeyCodeShiftLeft = (UIKeyboardHIDUsage)0xe1;
API_AVAILABLE(ios(13.4))
constexpr UIKeyboardHIDUsage kKeyCodeShiftRight = (UIKeyboardHIDUsage)0xe5;
API_AVAILABLE(ios(13.4))
constexpr UIKeyboardHIDUsage kKeyCodeNumpad1 = (UIKeyboardHIDUsage)0x59;
API_AVAILABLE(ios(13.4))
constexpr UIKeyboardHIDUsage kKeyCodeCapsLock = (UIKeyboardHIDUsage)0x39;
API_AVAILABLE(ios(13.4))
constexpr UIKeyboardHIDUsage kKeyCodeF1 = (UIKeyboardHIDUsage)0x3a;
API_AVAILABLE(ios(13.4))
constexpr UIKeyboardHIDUsage kKeyCodeAltRight = (UIKeyboardHIDUsage)0xe6;
constexpr uint64_t kPhysicalKeyUndefined = 0x00070003;
constexpr uint64_t kLogicalKeyUndefined = 0x1300000003;
constexpr uint64_t kModifierFlagNone = 0x0;
typedef void (^ResponseCallback)(bool handled);
} // namespace
@interface FlutterEmbedderKeyResponderTest : XCTestCase
@end
@implementation FlutterEmbedderKeyResponderTest
- (void)setUp {
}
- (void)tearDown {
}
// Test the most basic key events.
//
// Press, hold, and release key A on an US keyboard.
- (void)testBasicKeyEvent API_AVAILABLE(ios(13.4)) {
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
__block BOOL last_handled = TRUE;
FlutterKeyEvent* event;
FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc]
initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
_Nullable _VoidPtr user_data) {
[events addObject:[[TestKeyEvent alloc] initWithEvent:&event
callback:callback
userData:user_data]];
}];
last_handled = FALSE;
[responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "a", "a")
callback:^(BOOL handled) {
last_handled = handled;
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->timestamp, 123000000.0f);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertStrEqual(event->character, "a");
XCTAssertEqual(event->synthesized, false);
XCTAssertEqual(last_handled, FALSE);
XCTAssert([[events lastObject] hasCallback]);
[[events lastObject] respond:TRUE];
XCTAssertEqual(last_handled, TRUE);
[events removeAllObjects];
last_handled = TRUE;
[responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
callback:^(BOOL handled) {
last_handled = handled;
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->timestamp, 123000000.0f);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
XCTAssertEqual(last_handled, TRUE);
XCTAssert([[events lastObject] hasCallback]);
[[events lastObject] respond:FALSE]; // Check if responding FALSE works
XCTAssertEqual(last_handled, FALSE);
[events removeAllObjects];
}
- (void)testOutOfOrderModifiers API_AVAILABLE(ios(13.4)) {
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
FlutterKeyEvent* event;
FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc]
initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
_Nullable _VoidPtr user_data) {
[events addObject:[[TestKeyEvent alloc] initWithEvent:&event
callback:callback
userData:user_data]];
}];
// This tests that we synthesize the correct modifier keys when we release the
// modifier key that created the letter before we release the letter.
[responder handlePress:keyDownEvent(kKeyCodeAltRight, kModifierFlagAltAny, 123.0f)
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalAltRight);
XCTAssertEqual(event->logical, kLogicalAltRight);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[events removeAllObjects];
// Test non-ASCII characters being produced.
[responder handlePress:keyDownEvent(kKeyCodeKeyW, kModifierFlagAltAny, 123.0f, "∑", "w")
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalKeyW);
XCTAssertEqual(event->logical, kLogicalKeyW);
XCTAssertStrEqual(event->character, "∑");
XCTAssertEqual(event->synthesized, false);
[events removeAllObjects];
// Releasing the modifier key before the letter should send the key up to the
// framework.
[responder handlePress:keyUpEvent(kKeyCodeAltRight, kModifierFlagAltAny, 123.0f)
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalAltRight);
XCTAssertEqual(event->logical, kLogicalAltRight);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[events removeAllObjects];
// Yes, iOS sends a modifier flag for the Alt key being down on this event,
// even though the Alt (Option) key has already been released. This means that
// for the framework to be in the correct state, we must synthesize a key down
// event for the modifier key here, and another key up before the next key
// event.
[responder handlePress:keyUpEvent(kKeyCodeKeyW, kModifierFlagAltAny, 123.0f)
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalKeyW);
XCTAssertEqual(event->logical, kLogicalKeyW);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[events removeAllObjects];
// Here we should simulate a key up for the Alt key, since it is no longer
// shown as down in the modifier flags.
[responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "Ã¥", "a")
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertStrEqual(event->character, "Ã¥");
XCTAssertEqual(event->synthesized, false);
}
- (void)testIgnoreDuplicateDownEvent API_AVAILABLE(ios(13.4)) {
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
__block BOOL last_handled = TRUE;
FlutterKeyEvent* event;
FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc]
initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
_Nullable _VoidPtr user_data) {
[events addObject:[[TestKeyEvent alloc] initWithEvent:&event
callback:callback
userData:user_data]];
}];
last_handled = FALSE;
[responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "a", "a")
callback:^(BOOL handled) {
last_handled = handled;
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertStrEqual(event->character, "a");
XCTAssertEqual(event->synthesized, false);
XCTAssertEqual(last_handled, FALSE);
[[events lastObject] respond:TRUE];
XCTAssertEqual(last_handled, TRUE);
[events removeAllObjects];
last_handled = FALSE;
[responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "a", "a")
callback:^(BOOL handled) {
last_handled = handled;
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->physical, 0ull);
XCTAssertEqual(event->logical, 0ull);
XCTAssertEqual(event->synthesized, false);
XCTAssertFalse([[events lastObject] hasCallback]);
XCTAssertEqual(last_handled, TRUE);
[events removeAllObjects];
last_handled = FALSE;
[responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
callback:^(BOOL handled) {
last_handled = handled;
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
XCTAssertEqual(last_handled, FALSE);
[[events lastObject] respond:TRUE];
XCTAssertEqual(last_handled, TRUE);
[events removeAllObjects];
}
- (void)testIgnoreAbruptUpEvent API_AVAILABLE(ios(13.4)) {
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
__block BOOL last_handled = TRUE;
FlutterKeyEvent* event;
FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc]
initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
_Nullable _VoidPtr user_data) {
[events addObject:[[TestKeyEvent alloc] initWithEvent:&event
callback:callback
userData:user_data]];
}];
last_handled = FALSE;
[responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
callback:^(BOOL handled) {
last_handled = handled;
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->physical, 0ull);
XCTAssertEqual(event->logical, 0ull);
XCTAssertEqual(event->synthesized, false);
XCTAssertFalse([[events lastObject] hasCallback]);
XCTAssertEqual(last_handled, TRUE);
[events removeAllObjects];
}
// Press R-Shift, A, then release R-Shift then A, on a US keyboard.
//
// This is special because the characters for the A key will change in this
// process.
- (void)testToggleModifiersDuringKeyTap API_AVAILABLE(ios(13.4)) {
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
FlutterKeyEvent* event;
FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc]
initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
_Nullable _VoidPtr user_data) {
[events addObject:[[TestKeyEvent alloc] initWithEvent:&event
callback:callback
userData:user_data]];
}];
[responder handlePress:keyDownEvent(kKeyCodeShiftRight, kModifierFlagShiftAny, 123.0f)
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->timestamp, 123000000.0f);
XCTAssertEqual(event->physical, kPhysicalShiftRight);
XCTAssertEqual(event->logical, kLogicalShiftRight);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
[responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagShiftAny, 123.0f, "A", "A")
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertStrEqual(event->character, "A");
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
[responder handlePress:keyUpEvent(kKeyCodeShiftRight, kModifierFlagNone, 123.0f)
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalShiftRight);
XCTAssertEqual(event->logical, kLogicalShiftRight);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
[responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
}
// Special modifier flags.
//
// Some keys in modifierFlags are not to indicate modifier state, but to mark
// the key area that the key belongs to, such as numpad keys or function keys.
// Ensure these flags do not obstruct other keys.
- (void)testSpecialModiferFlags API_AVAILABLE(ios(13.4)) {
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
FlutterKeyEvent* event;
__block BOOL last_handled = TRUE;
id keyEventCallback = ^(BOOL handled) {
last_handled = handled;
};
FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc]
initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
_Nullable _VoidPtr user_data) {
[events addObject:[[TestKeyEvent alloc] initWithEvent:&event
callback:callback
userData:user_data]];
}];
// Keydown: Numpad1, Fn (undefined), F1, KeyA, ShiftLeft
// Then KeyUp: Numpad1, Fn (undefined), F1, KeyA, ShiftLeft
// Numpad 1
// OS provides: char: "1", code: 0x59, modifiers: 0x200000
[responder handlePress:keyDownEvent(kKeyCodeNumpad1, kModifierFlagNumPadKey, 123.0, "1", "1")
callback:keyEventCallback];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalNumpad1);
XCTAssertEqual(event->logical, kLogicalNumpad1);
XCTAssertStrEqual(event->character, "1");
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
// Fn Key (sends HID undefined)
// OS provides: char: nil, keycode: 0x3, modifiers: 0x0
[responder handlePress:keyDownEvent(kKeyCodeUndefined, kModifierFlagNone, 123.0)
callback:keyEventCallback];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalKeyUndefined);
XCTAssertEqual(event->logical, kLogicalKeyUndefined);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
// F1 Down
// OS provides: char: UIKeyInputF1, code: 0x3a, modifiers: 0x0
[responder handlePress:keyDownEvent(kKeyCodeF1, kModifierFlagNone, 123.0f, "\\^P", "\\^P")
callback:keyEventCallback];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalF1);
XCTAssertEqual(event->logical, kLogicalF1);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
// KeyA Down
// OS provides: char: "q", code: 0x4, modifiers: 0x0
[responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "a", "a")
callback:keyEventCallback];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertStrEqual(event->character, "a");
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
// ShiftLeft Down
// OS Provides: char: nil, code: 0xe1, modifiers: 0x20000
[responder handlePress:keyDownEvent(kKeyCodeShiftLeft, kModifierFlagShiftAny, 123.0f)
callback:keyEventCallback];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalShiftLeft);
XCTAssertEqual(event->logical, kLogicalShiftLeft);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[events removeAllObjects];
// Numpad 1 Up
// OS provides: char: "1", code: 0x59, modifiers: 0x200000
[responder handlePress:keyUpEvent(kKeyCodeNumpad1, kModifierFlagNumPadKey, 123.0f)
callback:keyEventCallback];
XCTAssertEqual([events count], 2u);
// Because the OS no longer provides the 0x20000 (kModifierFlagShiftAny), we
// have to simulate a keyup.
event = [events firstObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalShiftLeft);
XCTAssertEqual(event->logical, kLogicalShiftLeft);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, true);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalNumpad1);
XCTAssertEqual(event->logical, kLogicalNumpad1);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
// F1 Up
// OS provides: char: UIKeyInputF1, code: 0x3a, modifiers: 0x0
[responder handlePress:keyUpEvent(kKeyCodeF1, kModifierFlagNone, 123.0f)
callback:keyEventCallback];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalF1);
XCTAssertEqual(event->logical, kLogicalF1);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
// Fn Key (sends HID undefined)
// OS provides: char: nil, code: 0x3, modifiers: 0x0
[responder handlePress:keyUpEvent(kKeyCodeUndefined, kModifierFlagNone, 123.0)
callback:keyEventCallback];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalKeyUndefined);
XCTAssertEqual(event->logical, kLogicalKeyUndefined);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
// KeyA Up
// OS provides: char: "a", code: 0x4, modifiers: 0x0
[responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f)
callback:keyEventCallback];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
// ShiftLeft Up
// OS provides: char: nil, code: 0xe1, modifiers: 0x20000
[responder handlePress:keyUpEvent(kKeyCodeShiftLeft, kModifierFlagShiftAny, 123.0f)
callback:keyEventCallback];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->physical, 0ull);
XCTAssertEqual(event->logical, 0ull);
XCTAssertEqual(event->synthesized, false);
XCTAssertFalse([[events lastObject] hasCallback]);
XCTAssertEqual(last_handled, TRUE);
[events removeAllObjects];
}
- (void)testIdentifyLeftAndRightModifiers API_AVAILABLE(ios(13.4)) {
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
FlutterKeyEvent* event;
FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc]
initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
_Nullable _VoidPtr user_data) {
[events addObject:[[TestKeyEvent alloc] initWithEvent:&event
callback:callback
userData:user_data]];
}];
[responder handlePress:keyDownEvent(kKeyCodeShiftLeft, kModifierFlagShiftAny, 123.0f)
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalShiftLeft);
XCTAssertEqual(event->logical, kLogicalShiftLeft);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
[responder handlePress:keyDownEvent(kKeyCodeShiftRight, kModifierFlagShiftAny, 123.0f)
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalShiftRight);
XCTAssertEqual(event->logical, kLogicalShiftRight);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
[responder handlePress:keyUpEvent(kKeyCodeShiftLeft, kModifierFlagShiftAny, 123.0f)
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalShiftLeft);
XCTAssertEqual(event->logical, kLogicalShiftLeft);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
[responder handlePress:keyUpEvent(kKeyCodeShiftRight, kModifierFlagShiftAny, 123.0f)
callback:^(BOOL handled){
}];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalShiftRight);
XCTAssertEqual(event->logical, kLogicalShiftRight);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
[[events lastObject] respond:TRUE];
[events removeAllObjects];
}
// Press the CapsLock key when CapsLock state is desynchronized
- (void)testSynchronizeCapsLockStateOnCapsLock API_AVAILABLE(ios(13.4)) {
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
__block BOOL last_handled = TRUE;
id keyEventCallback = ^(BOOL handled) {
last_handled = handled;
};
FlutterKeyEvent* event;
FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc]
initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
_Nullable _VoidPtr user_data) {
[events addObject:[[TestKeyEvent alloc] initWithEvent:&event
callback:callback
userData:user_data]];
}];
last_handled = FALSE;
[responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagCapsLock, 123.0f, "A", "A")
callback:keyEventCallback];
XCTAssertEqual([events count], 3u);
event = events[0].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalCapsLock);
XCTAssertEqual(event->logical, kLogicalCapsLock);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, true);
XCTAssertFalse([events[0] hasCallback]);
event = events[1].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalCapsLock);
XCTAssertEqual(event->logical, kLogicalCapsLock);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, true);
XCTAssertFalse([events[1] hasCallback]);
event = events[2].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertStrEqual(event->character, "A");
XCTAssertEqual(event->synthesized, false);
XCTAssert([events[2] hasCallback]);
XCTAssertEqual(last_handled, FALSE);
[[events lastObject] respond:TRUE];
XCTAssertEqual(last_handled, TRUE);
[events removeAllObjects];
// Release the "A" key.
[responder handlePress:keyUpEvent(kKeyCodeKeyA, kModifierFlagCapsLock, 123.0f)
callback:keyEventCallback];
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertEqual(event->synthesized, false);
[events removeAllObjects];
// In: CapsLock down
// Out: CapsLock down
last_handled = FALSE;
[responder handlePress:keyDownEvent(kKeyCodeCapsLock, kModifierFlagCapsLock, 123.0f)
callback:keyEventCallback];
XCTAssertEqual([events count], 1u);
event = [events firstObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalCapsLock);
XCTAssertEqual(event->logical, kLogicalCapsLock);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
XCTAssert([[events firstObject] hasCallback]);
[events removeAllObjects];
// In: CapsLock up
// Out: CapsLock up
// This turns off the caps lock, triggering a synthesized up/down to tell the
// framework that.
last_handled = FALSE;
[responder handlePress:keyUpEvent(kKeyCodeCapsLock, kModifierFlagCapsLock, 123.0f)
callback:keyEventCallback];
XCTAssertEqual([events count], 1u);
event = [events firstObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalCapsLock);
XCTAssertEqual(event->logical, kLogicalCapsLock);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, false);
XCTAssert([[events firstObject] hasCallback]);
[events removeAllObjects];
last_handled = FALSE;
[responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagNone, 123.0f, "a", "a")
callback:keyEventCallback];
// Just to make sure that we aren't simulating events now, since the state is
// consistent, and should be off.
XCTAssertEqual([events count], 1u);
event = [events lastObject].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertStrEqual(event->character, "a");
XCTAssertEqual(event->synthesized, false);
XCTAssert([[events firstObject] hasCallback]);
}
// Press the CapsLock key when CapsLock state is desynchronized
- (void)testSynchronizeCapsLockStateOnNormalKey API_AVAILABLE(ios(13.4)) {
__block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init];
__block BOOL last_handled = TRUE;
id keyEventCallback = ^(BOOL handled) {
last_handled = handled;
};
FlutterKeyEvent* event;
FlutterEmbedderKeyResponder* responder = [[FlutterEmbedderKeyResponder alloc]
initWithSendEvent:^(const FlutterKeyEvent& event, _Nullable FlutterKeyEventCallback callback,
_Nullable _VoidPtr user_data) {
[events addObject:[[TestKeyEvent alloc] initWithEvent:&event
callback:callback
userData:user_data]];
}];
last_handled = FALSE;
[responder handlePress:keyDownEvent(kKeyCodeKeyA, kModifierFlagCapsLock, 123.0f, "A", "a")
callback:keyEventCallback];
XCTAssertEqual([events count], 3u);
event = events[0].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalCapsLock);
XCTAssertEqual(event->logical, kLogicalCapsLock);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, true);
XCTAssertFalse([events[0] hasCallback]);
event = events[1].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeUp);
XCTAssertEqual(event->physical, kPhysicalCapsLock);
XCTAssertEqual(event->logical, kLogicalCapsLock);
XCTAssertEqual(event->character, nullptr);
XCTAssertEqual(event->synthesized, true);
XCTAssertFalse([events[1] hasCallback]);
event = events[2].data;
XCTAssertEqual(event->type, kFlutterKeyEventTypeDown);
XCTAssertEqual(event->physical, kPhysicalKeyA);
XCTAssertEqual(event->logical, kLogicalKeyA);
XCTAssertStrEqual(event->character, "A");
XCTAssertEqual(event->synthesized, false);
XCTAssert([events[2] hasCallback]);
XCTAssertEqual(last_handled, FALSE);
[[events lastObject] respond:TRUE];
XCTAssertEqual(last_handled, TRUE);
[events removeAllObjects];
}
@end