| // 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.g.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 kKeyCodePeriod = (UIKeyboardHIDUsage)0x37; |
| 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 kKeyCodeCommandLeft = (UIKeyboardHIDUsage)0xe3; |
| API_AVAILABLE(ios(13.4)) |
| constexpr UIKeyboardHIDUsage kKeyCodeAltRight = (UIKeyboardHIDUsage)0xe6; |
| API_AVAILABLE(ios(13.4)) |
| constexpr UIKeyboardHIDUsage kKeyCodeEject = (UIKeyboardHIDUsage)0xb8; |
| |
| 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 { |
| // All of these tests were designed to run on iOS 13.4 or later. |
| if (@available(iOS 13.4, *)) { |
| } else { |
| XCTSkip(@"Required API not present for test."); |
| } |
| } |
| |
| - (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)testIosKeyPlane 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; |
| // Verify that the eject key (keycode 0xb8, which is not present in the keymap) |
| // should be translated to the right logical and physical keys. |
| [responder handlePress:keyDownEvent(kKeyCodeEject, kModifierFlagNone, 123.0f) |
| callback:^(BOOL handled) { |
| last_handled = handled; |
| }]; |
| |
| XCTAssertEqual([events count], 1u); |
| event = [events lastObject].data; |
| XCTAssertEqual(event->type, kFlutterKeyEventTypeDown); |
| XCTAssertEqual(event->physical, kKeyCodeEject | kIosPlane); |
| XCTAssertEqual(event->logical, kKeyCodeEject | kIosPlane); |
| XCTAssertEqual(event->character, nullptr); |
| 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(kKeyCodeEject, kModifierFlagNone, 123.0f) |
| callback:^(BOOL handled) { |
| last_handled = handled; |
| }]; |
| |
| XCTAssertEqual([events count], 1u); |
| event = [events lastObject].data; |
| XCTAssertEqual(event->type, kFlutterKeyEventTypeUp); |
| XCTAssertEqual(event->physical, kKeyCodeEject | kIosPlane); |
| XCTAssertEqual(event->logical, kKeyCodeEject | kIosPlane); |
| 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]; |
| } |
| |
| // Press Cmd-. should correctly result in an Escape event. |
| - (void)testCommandPeriodKey API_AVAILABLE(ios(13.4)) { |
| __block NSMutableArray<TestKeyEvent*>* events = [[NSMutableArray<TestKeyEvent*> alloc] init]; |
| id keyEventCallback = ^(BOOL 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:nil userData:nil]]; |
| callback(true, user_data); |
| }]; |
| |
| // MetaLeft down. |
| [responder handlePress:keyDownEvent(kKeyCodeCommandLeft, kModifierFlagMetaAny, 123.0f, "", "") |
| callback:keyEventCallback]; |
| XCTAssertEqual([events count], 1u); |
| event = events[0].data; |
| XCTAssertEqual(event->type, kFlutterKeyEventTypeDown); |
| XCTAssertEqual(event->physical, kPhysicalMetaLeft); |
| XCTAssertEqual(event->logical, kLogicalMetaLeft); |
| XCTAssertEqual(event->character, nullptr); |
| XCTAssertEqual(event->synthesized, false); |
| [events removeAllObjects]; |
| |
| // Period down, which is logically Escape. |
| [responder handlePress:keyDownEvent(kKeyCodePeriod, kModifierFlagMetaAny, 123.0f, |
| "UIKeyInputEscape", "UIKeyInputEscape") |
| callback:keyEventCallback]; |
| XCTAssertEqual([events count], 1u); |
| event = events[0].data; |
| XCTAssertEqual(event->type, kFlutterKeyEventTypeDown); |
| XCTAssertEqual(event->physical, kPhysicalPeriod); |
| XCTAssertEqual(event->logical, kLogicalEscape); |
| XCTAssertEqual(event->character, nullptr); |
| XCTAssertEqual(event->synthesized, false); |
| [events removeAllObjects]; |
| |
| // Period up, which unconventionally has characters. |
| [responder handlePress:keyUpEvent(kKeyCodePeriod, kModifierFlagMetaAny, 123.0f, |
| "UIKeyInputEscape", "UIKeyInputEscape") |
| callback:keyEventCallback]; |
| XCTAssertEqual([events count], 1u); |
| event = events[0].data; |
| XCTAssertEqual(event->type, kFlutterKeyEventTypeUp); |
| XCTAssertEqual(event->physical, kPhysicalPeriod); |
| XCTAssertEqual(event->logical, kLogicalEscape); |
| XCTAssertEqual(event->character, nullptr); |
| XCTAssertEqual(event->synthesized, false); |
| [events removeAllObjects]; |
| |
| // MetaLeft up. |
| [responder handlePress:keyUpEvent(kKeyCodeCommandLeft, kModifierFlagMetaAny, 123.0f, "", "") |
| callback:keyEventCallback]; |
| XCTAssertEqual([events count], 1u); |
| event = events[0].data; |
| XCTAssertEqual(event->type, kFlutterKeyEventTypeUp); |
| XCTAssertEqual(event->physical, kPhysicalMetaLeft); |
| XCTAssertEqual(event->logical, kLogicalMetaLeft); |
| XCTAssertEqual(event->character, nullptr); |
| XCTAssertEqual(event->synthesized, false); |
| [events removeAllObjects]; |
| } |
| |
| @end |