Files
libvirtualhid/tests/unit/test_macos_backend.cpp

385 lines
19 KiB
C++

/**
* @file tests/unit/test_macos_backend.cpp
* @brief Unit tests for macOS backend internals.
*/
// standard includes
#include <array>
// lib includes
#include <libvirtualhid/libvirtualhid.hpp>
// local includes
#include "fixtures/fixtures.hpp"
#include "fixtures/macos_backend_test_hooks.hpp"
// platform includes
#include <Carbon/Carbon.h>
/**
* @brief Test fixture for macOS backend internals.
*/
class MacosBackendTest: public MacOSTest {};
TEST_F(MacosBackendTest, TranslatesKeyboardKeys) {
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x08), kVK_Delete);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x09), kVK_Tab);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x0D), kVK_Return);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x1B), kVK_Escape);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x20), kVK_Space);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x21), kVK_PageUp);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x22), kVK_PageDown);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x25), kVK_LeftArrow);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x26), kVK_UpArrow);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x27), kVK_RightArrow);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x28), kVK_DownArrow);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x41), kVK_ANSI_A);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x5B), kVK_Command);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x5C), kVK_RightCommand);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x6B), kVK_ANSI_KeypadPlus);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0xA0), kVK_Shift);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0xA1), kVK_RightShift);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0xA2), kVK_Control);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0xA3), kVK_RightControl);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0xA4), kVK_Option);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0xA5), kVK_RightOption);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0xBB), kVK_ANSI_Equal);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0xBD), kVK_ANSI_Minus);
EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0xDE), kVK_ANSI_Quote);
EXPECT_FALSE(lvh::detail::test::macos_backend_key_code(0x13).has_value());
EXPECT_FALSE(lvh::detail::test::macos_backend_key_code(0xFFFF).has_value());
}
TEST_F(MacosBackendTest, IdentifiesModifierKeys) {
EXPECT_TRUE(lvh::detail::test::macos_backend_is_modifier_key(0x10));
EXPECT_TRUE(lvh::detail::test::macos_backend_is_modifier_key(0x11));
EXPECT_TRUE(lvh::detail::test::macos_backend_is_modifier_key(0x12));
EXPECT_TRUE(lvh::detail::test::macos_backend_is_modifier_key(0x5B));
EXPECT_TRUE(lvh::detail::test::macos_backend_is_modifier_key(0x5C));
EXPECT_TRUE(lvh::detail::test::macos_backend_is_modifier_key(0xA0));
EXPECT_TRUE(lvh::detail::test::macos_backend_is_modifier_key(0xA1));
EXPECT_TRUE(lvh::detail::test::macos_backend_is_modifier_key(0xA2));
EXPECT_TRUE(lvh::detail::test::macos_backend_is_modifier_key(0xA3));
EXPECT_TRUE(lvh::detail::test::macos_backend_is_modifier_key(0xA4));
EXPECT_TRUE(lvh::detail::test::macos_backend_is_modifier_key(0xA5));
EXPECT_FALSE(lvh::detail::test::macos_backend_is_modifier_key(0x41));
EXPECT_FALSE(lvh::detail::test::macos_backend_is_modifier_key(0x13));
}
TEST_F(MacosBackendTest, AddsImplicitFlagsForFunctionAndKeypadKeys) {
using lvh::detail::test::macos_backend_implicit_key_flags;
constexpr std::uint64_t fn = kCGEventFlagMaskSecondaryFn;
constexpr std::uint64_t numeric_pad = kCGEventFlagMaskNumericPad;
EXPECT_EQ(macos_backend_implicit_key_flags(0x25), fn | numeric_pad); // VKEY_LEFT
EXPECT_EQ(macos_backend_implicit_key_flags(0x26), fn | numeric_pad); // VKEY_UP
EXPECT_EQ(macos_backend_implicit_key_flags(0x27), fn | numeric_pad); // VKEY_RIGHT
EXPECT_EQ(macos_backend_implicit_key_flags(0x28), fn | numeric_pad); // VKEY_DOWN
EXPECT_EQ(macos_backend_implicit_key_flags(0x21), fn); // VKEY_PRIOR
EXPECT_EQ(macos_backend_implicit_key_flags(0x22), fn); // VKEY_NEXT
EXPECT_EQ(macos_backend_implicit_key_flags(0x23), fn); // VKEY_END
EXPECT_EQ(macos_backend_implicit_key_flags(0x24), fn); // VKEY_HOME
EXPECT_EQ(macos_backend_implicit_key_flags(0x2D), fn); // VKEY_INSERT
EXPECT_EQ(macos_backend_implicit_key_flags(0x2E), fn); // VKEY_DELETE
EXPECT_EQ(macos_backend_implicit_key_flags(0x70), fn); // VKEY_F1
EXPECT_EQ(macos_backend_implicit_key_flags(0x83), fn); // VKEY_F20
EXPECT_EQ(macos_backend_implicit_key_flags(0x60), numeric_pad); // VKEY_NUMPAD0
EXPECT_EQ(macos_backend_implicit_key_flags(0x6B), numeric_pad); // VKEY_ADD
EXPECT_EQ(macos_backend_implicit_key_flags(0x6E), numeric_pad); // VKEY_DECIMAL
EXPECT_EQ(macos_backend_implicit_key_flags(0x0D), 0U); // VKEY_RETURN
EXPECT_EQ(macos_backend_implicit_key_flags(0x41), 0U); // VKEY_A
EXPECT_EQ(macos_backend_implicit_key_flags(0xA2), 0U); // VKEY_LCONTROL
EXPECT_EQ(macos_backend_implicit_key_flags(0x13), 0U); // unmapped VKEY_PAUSE
EXPECT_EQ(macos_backend_implicit_key_flags(0xFFFF), 0U);
}
TEST_F(MacosBackendTest, BuildsControlArrowEventsWithFnFlags) {
using lvh::detail::test::macos_backend_key_events;
constexpr std::uint64_t control = kCGEventFlagMaskControl;
constexpr std::uint64_t fn = kCGEventFlagMaskSecondaryFn;
constexpr std::uint64_t numeric_pad = kCGEventFlagMaskNumericPad;
constexpr std::uint64_t checked = kCGEventFlagMaskShift | kCGEventFlagMaskControl | kCGEventFlagMaskAlternate |
kCGEventFlagMaskCommand | fn | numeric_pad;
// Control+Up as a client sends it, then Control+A for contrast.
const auto events = macos_backend_key_events({
{0xA2, true}, // VKEY_LCONTROL down
{0x26, true}, // VKEY_UP down
{0x26, false}, // VKEY_UP up
{0x41, true}, // VKEY_A down
{0x41, false}, // VKEY_A up
{0xA2, false}, // VKEY_LCONTROL up
});
ASSERT_EQ(events.size(), 6U);
EXPECT_EQ(events[0].event_type, kCGEventFlagsChanged);
EXPECT_EQ(events[0].flags & checked, control);
// The arrow event carries the flags a physical arrow key reports, on top of the held Control.
EXPECT_EQ(events[1].event_type, kCGEventKeyDown);
EXPECT_EQ(events[1].key_code, kVK_UpArrow);
EXPECT_EQ(events[1].flags & checked, control | fn | numeric_pad);
EXPECT_EQ(events[2].event_type, kCGEventKeyUp);
EXPECT_EQ(events[2].flags & checked, control | fn | numeric_pad);
// The per-key flags never enter the shared modifier state, so Control+A is unchanged.
EXPECT_EQ(events[1].tracked_flags & checked, control);
EXPECT_EQ(events[2].tracked_flags & checked, control);
EXPECT_EQ(events[3].event_type, kCGEventKeyDown);
EXPECT_EQ(events[3].key_code, kVK_ANSI_A);
EXPECT_EQ(events[3].flags & checked, control);
EXPECT_EQ(events[4].flags & checked, control);
EXPECT_EQ(events[5].event_type, kCGEventFlagsChanged);
EXPECT_EQ(events[5].flags & checked, 0U);
EXPECT_EQ(events[5].tracked_flags & checked, 0U);
}
TEST_F(MacosBackendTest, BuildsKeypadEventsWithNumericPadFlag) {
using lvh::detail::test::macos_backend_key_events;
constexpr std::uint64_t fn = kCGEventFlagMaskSecondaryFn;
constexpr std::uint64_t numeric_pad = kCGEventFlagMaskNumericPad;
const auto events = macos_backend_key_events({
{0x67, true}, // VKEY_NUMPAD7 down
{0x0D, true}, // VKEY_RETURN down
});
ASSERT_EQ(events.size(), 2U);
EXPECT_EQ(events[0].key_code, kVK_ANSI_Keypad7);
EXPECT_EQ(events[0].flags & (fn | numeric_pad), numeric_pad);
EXPECT_EQ(events[1].key_code, kVK_Return);
EXPECT_EQ(events[1].flags & (fn | numeric_pad), 0U);
}
TEST_F(MacosBackendTest, ConvertsScrollSettings) {
EXPECT_EQ(lvh::detail::test::macos_backend_scroll_lines_per_detent(0.0), 1);
EXPECT_EQ(lvh::detail::test::macos_backend_scroll_lines_per_detent(0.3125), 5);
EXPECT_EQ(lvh::detail::test::macos_backend_scroll_lines_per_detent(1.0), 14);
EXPECT_EQ(lvh::detail::test::macos_backend_scroll_pixels(120, 10, 5), 50);
EXPECT_EQ(lvh::detail::test::macos_backend_scroll_pixels(-240, 10, 5), -100);
EXPECT_EQ(lvh::detail::test::macos_backend_scroll_pixels(120, 0, 0), 1);
}
TEST_F(MacosBackendTest, ConvertsAbsoluteMouseCoordinates) {
lvh::MouseEvent event {
.kind = lvh::MouseEventKind::absolute_motion,
.x = 40,
.y = 50,
.width = 200,
.height = 100,
};
auto location = lvh::detail::test::macos_backend_absolute_mouse_location(event, 10.0, 20.0, 400.0, 200.0);
EXPECT_DOUBLE_EQ(location.x, 90.0);
EXPECT_DOUBLE_EQ(location.y, 120.0);
event.x = 250;
event.y = -10;
location = lvh::detail::test::macos_backend_absolute_mouse_location(event, 10.0, 20.0, 400.0, 200.0);
EXPECT_DOUBLE_EQ(location.x, 410.0);
EXPECT_DOUBLE_EQ(location.y, 20.0);
event = {
.kind = lvh::MouseEventKind::absolute_motion,
.absolute_x = 0.25F,
.absolute_y = 0.75F,
.has_fractional_absolute_coordinates = true,
.width = 1,
.height = 1,
};
location = lvh::detail::test::macos_backend_absolute_mouse_location(event, 10.0, 20.0, 400.0, 200.0);
EXPECT_DOUBLE_EQ(location.x, 110.0);
EXPECT_DOUBLE_EQ(location.y, 170.0);
}
TEST_F(MacosBackendTest, UsesConfiguredMouseViewport) {
const auto bounds = lvh::detail::test::macos_backend_mouse_viewport_bounds({
.offset_x = -1920,
.offset_y = 120,
.width = 1920,
.height = 1080,
});
EXPECT_DOUBLE_EQ(bounds.origin_x, -1920.0);
EXPECT_DOUBLE_EQ(bounds.origin_y, 120.0);
EXPECT_DOUBLE_EQ(bounds.width, 1920.0);
EXPECT_DOUBLE_EQ(bounds.height, 1080.0);
}
TEST_F(MacosBackendTest, RefreshesDefaultMouseBoundsBetweenSubmissions) {
const lvh::MouseEvent center {
.kind = lvh::MouseEventKind::absolute_motion,
.x = 960,
.y = 540,
.width = 1920,
.height = 1080,
};
const std::vector<lvh::detail::test::MacosMouseSubmission> submissions {
{.display_bounds = {0, 0, 1920, 1080}, .cursor_location = {}, .event = center},
{.display_bounds = {0, 0, 2560, 1440}, .cursor_location = {}, .event = center},
{.display_bounds = {0, 0, 2560, 1440}, .cursor_location = {2400, 1300}, .event = {.kind = lvh::MouseEventKind::relative_motion, .x = 400, .y = 300}},
{.display_bounds = {-1280, -720, 1280, 720}, .cursor_location = {100, 100}, .event = {.kind = lvh::MouseEventKind::button, .button = lvh::MouseButton::left, .pressed = true}},
};
const auto results = lvh::detail::test::macos_backend_mouse_submissions({}, {0, 0, 1920, 1080}, submissions);
ASSERT_EQ(results.size(), 4U);
const std::array<lvh::detail::test::MacosPoint, 4> expected {{{960, 540}, {1280, 720}, {2559, 1439}, {-1, -1}}};
for (std::size_t index = 0; index < results.size(); ++index) {
SCOPED_TRACE(index);
ASSERT_TRUE(results[index].status.ok()) << results[index].status.message();
ASSERT_TRUE(results[index].posted_location);
ASSERT_TRUE(results[index].warped_location);
EXPECT_DOUBLE_EQ(results[index].posted_location->x, expected[index].x);
EXPECT_DOUBLE_EQ(results[index].posted_location->y, expected[index].y);
EXPECT_DOUBLE_EQ(results[index].warped_location->x, expected[index].x);
EXPECT_DOUBLE_EQ(results[index].warped_location->y, expected[index].y);
}
}
TEST_F(MacosBackendTest, RecoversFromTemporarilyUnavailableDefaultMouseBounds) {
const lvh::MouseEvent center {
.kind = lvh::MouseEventKind::absolute_motion,
.x = 50,
.y = 50,
.width = 100,
.height = 100,
};
const std::vector<lvh::detail::test::MacosMouseSubmission> submissions {
{.display_bounds = {0, 0, 1920, 1080}, .cursor_location = {}, .event = center},
{.display_bounds = {0, 0, 0, 1080}, .cursor_location = {}, .event = center},
{.display_bounds = {0, 0, 1920, 0}, .cursor_location = {}, .event = center},
{.display_bounds = {0, 0, 2560, 1440}, .cursor_location = {}, .event = center},
};
const auto results = lvh::detail::test::macos_backend_mouse_submissions({}, {0, 0, 0, 0}, submissions);
ASSERT_EQ(results.size(), 4U);
ASSERT_TRUE(results[0].status.ok()) << results[0].status.message();
ASSERT_TRUE(results[0].posted_location);
EXPECT_DOUBLE_EQ(results[0].posted_location->x, 960);
EXPECT_DOUBLE_EQ(results[0].posted_location->y, 540);
for (const auto index : {1U, 2U}) {
EXPECT_EQ(results[index].status.code(), lvh::ErrorCode::backend_failure);
EXPECT_FALSE(results[index].posted_location);
EXPECT_FALSE(results[index].warped_location);
}
ASSERT_TRUE(results[3].status.ok()) << results[3].status.message();
ASSERT_TRUE(results[3].posted_location);
ASSERT_TRUE(results[3].warped_location);
EXPECT_DOUBLE_EQ(results[3].posted_location->x, 1280);
EXPECT_DOUBLE_EQ(results[3].posted_location->y, 720);
EXPECT_DOUBLE_EQ(results[3].warped_location->x, 1280);
EXPECT_DOUBLE_EQ(results[3].warped_location->y, 720);
}
TEST_F(MacosBackendTest, KeepsConfiguredMouseViewportWhenDefaultDisplayChanges) {
const lvh::PointerViewport viewport {.offset_x = -1920, .offset_y = 120, .width = 1920, .height = 1080};
const lvh::MouseEvent center {
.kind = lvh::MouseEventKind::absolute_motion,
.x = 50,
.y = 50,
.width = 100,
.height = 100,
};
const std::vector<lvh::detail::test::MacosMouseSubmission> submissions {
{.display_bounds = {0, 0, 2560, 1440}, .cursor_location = {}, .event = center},
{.display_bounds = {0, 0, 0, 0}, .cursor_location = {-100, 200}, .event = {.kind = lvh::MouseEventKind::relative_motion, .x = 400, .y = -300}},
};
const auto results = lvh::detail::test::macos_backend_mouse_submissions(viewport, {0, 0, 1920, 1080}, submissions);
ASSERT_EQ(results.size(), 2U);
const std::array<lvh::detail::test::MacosPoint, 2> expected {{{-960, 660}, {-1, 120}}};
for (std::size_t index = 0; index < results.size(); ++index) {
ASSERT_TRUE(results[index].status.ok()) << results[index].status.message();
ASSERT_TRUE(results[index].posted_location);
ASSERT_TRUE(results[index].warped_location);
EXPECT_DOUBLE_EQ(results[index].posted_location->x, expected[index].x);
EXPECT_DOUBLE_EQ(results[index].posted_location->y, expected[index].y);
EXPECT_DOUBLE_EQ(results[index].warped_location->x, expected[index].x);
EXPECT_DOUBLE_EQ(results[index].warped_location->y, expected[index].y);
}
}
TEST_F(MacosBackendTest, ConfiguredAndDefaultRetinaMouseMappingsAgreeInScreenPoints) {
const std::vector<lvh::detail::test::MacosMouseSubmission> submissions {
{.display_bounds = {0, 0, 1920, 1080}, .cursor_location = {}, .event = {.kind = lvh::MouseEventKind::absolute_motion, .x = 1920, .y = 1080, .width = 3840, .height = 2160}},
{.display_bounds = {0, 0, 1920, 1080}, .cursor_location = {}, .event = {.kind = lvh::MouseEventKind::absolute_motion, .x = 3840, .y = 2160, .width = 3840, .height = 2160}},
};
const auto defaults = lvh::detail::test::macos_backend_mouse_submissions({}, {0, 0, 1920, 1080}, submissions);
const auto configured = lvh::detail::test::macos_backend_mouse_submissions({.width = 1920, .height = 1080}, {0, 0, 1920, 1080}, submissions);
ASSERT_EQ(defaults.size(), 2U);
ASSERT_EQ(configured.size(), defaults.size());
const std::array<lvh::detail::test::MacosPoint, 2> expected {{{960, 540}, {1919, 1079}}};
for (std::size_t index = 0; index < defaults.size(); ++index) {
SCOPED_TRACE(index);
ASSERT_TRUE(defaults[index].status.ok()) << defaults[index].status.message();
ASSERT_TRUE(configured[index].status.ok()) << configured[index].status.message();
ASSERT_TRUE(defaults[index].posted_location);
ASSERT_TRUE(configured[index].posted_location);
ASSERT_TRUE(defaults[index].warped_location);
ASSERT_TRUE(configured[index].warped_location);
EXPECT_DOUBLE_EQ(defaults[index].posted_location->x, expected[index].x);
EXPECT_DOUBLE_EQ(defaults[index].posted_location->y, expected[index].y);
EXPECT_DOUBLE_EQ(configured[index].posted_location->x, expected[index].x);
EXPECT_DOUBLE_EQ(configured[index].posted_location->y, expected[index].y);
EXPECT_DOUBLE_EQ(defaults[index].warped_location->x, expected[index].x);
EXPECT_DOUBLE_EQ(defaults[index].warped_location->y, expected[index].y);
EXPECT_DOUBLE_EQ(configured[index].warped_location->x, expected[index].x);
EXPECT_DOUBLE_EQ(configured[index].warped_location->y, expected[index].y);
}
}
TEST_F(MacosBackendTest, SelectsMouseMotionMetadataForHeldButtons) {
using lvh::detail::test::macos_backend_mouse_motion;
auto motion = macos_backend_mouse_motion(false, false, false);
EXPECT_EQ(motion.button, kCGMouseButtonLeft);
EXPECT_EQ(motion.event_type, kCGEventMouseMoved);
motion = macos_backend_mouse_motion(true, false, false);
EXPECT_EQ(motion.button, kCGMouseButtonLeft);
EXPECT_EQ(motion.event_type, kCGEventLeftMouseDragged);
motion = macos_backend_mouse_motion(false, true, false);
EXPECT_EQ(motion.button, kCGMouseButtonRight);
EXPECT_EQ(motion.event_type, kCGEventRightMouseDragged);
motion = macos_backend_mouse_motion(false, false, true);
EXPECT_EQ(motion.button, kCGMouseButtonCenter);
EXPECT_EQ(motion.event_type, kCGEventOtherMouseDragged);
}
TEST_F(MacosBackendTest, ReportsCapabilitiesAndUnsupportedDevices) {
const auto result = lvh::detail::test::macos_backend_utilities();
EXPECT_EQ(result.capabilities.backend_name, "macos-virtual-hid-coregraphics");
EXPECT_TRUE(result.capabilities.supports_virtual_hid);
EXPECT_TRUE(result.capabilities.supports_gamepad);
EXPECT_TRUE(result.capabilities.supports_keyboard);
EXPECT_TRUE(result.capabilities.supports_mouse);
EXPECT_FALSE(result.capabilities.supports_touchscreen);
EXPECT_FALSE(result.capabilities.supports_trackpad);
EXPECT_FALSE(result.capabilities.supports_pen_tablet);
EXPECT_TRUE(result.capabilities.supports_output_reports);
EXPECT_FALSE(result.capabilities.requires_installed_driver);
ASSERT_TRUE(result.keyboard_create_status.ok()) << result.keyboard_create_status.message();
EXPECT_TRUE(result.keyboard_text_status.ok()) << result.keyboard_text_status.message();
EXPECT_TRUE(result.keyboard_empty_text_status.ok()) << result.keyboard_empty_text_status.message();
EXPECT_EQ(result.keyboard_invalid_text_status.code(), lvh::ErrorCode::invalid_argument);
ASSERT_TRUE(result.keyboard_close_status.ok()) << result.keyboard_close_status.message();
EXPECT_EQ(result.keyboard_submit_after_close_status.code(), lvh::ErrorCode::device_closed);
EXPECT_EQ(result.keyboard_text_after_close_status.code(), lvh::ErrorCode::device_closed);
EXPECT_EQ(result.keyboard_invalid_profile_status.code(), lvh::ErrorCode::unsupported_profile);
ASSERT_TRUE(result.mouse_create_status.ok()) << result.mouse_create_status.message();
ASSERT_TRUE(result.mouse_close_status.ok()) << result.mouse_close_status.message();
EXPECT_EQ(result.mouse_submit_after_close_status.code(), lvh::ErrorCode::device_closed);
EXPECT_EQ(result.mouse_invalid_profile_status.code(), lvh::ErrorCode::unsupported_profile);
EXPECT_EQ(result.gamepad_status.code(), lvh::ErrorCode::unsupported_profile);
EXPECT_EQ(result.touchscreen_status.code(), lvh::ErrorCode::unsupported_profile);
EXPECT_EQ(result.trackpad_status.code(), lvh::ErrorCode::unsupported_profile);
EXPECT_EQ(result.pen_tablet_status.code(), lvh::ErrorCode::unsupported_profile);
}