// test includes #include "tests/conftest.cpp" // standard includes #include #include #include #include #include #if defined(_WIN32) || defined(_WIN64) #include // clang-format off // build fails if shellapi.h is included before Windows.h #include // clang-format on #define TRAY_WINAPI 1 #elif defined(__linux__) || defined(linux) || defined(__linux) #define TRAY_APPINDICATOR 1 #elif defined(__APPLE__) || defined(__MACH__) #include #define TRAY_APPKIT 1 #endif // local includes #include "src/tray.h" #include "tests/screenshot_utils.h" #if TRAY_APPINDICATOR constexpr const char *TRAY_ICON1 = "mail-message-new"; constexpr const char *TRAY_ICON2 = "mail-message-new"; #elif TRAY_APPKIT constexpr const char *TRAY_ICON1 = "icon.png"; constexpr const char *TRAY_ICON2 = "icon.png"; #elif TRAY_WINAPI constexpr const char *TRAY_ICON1 = "icon.ico"; constexpr const char *TRAY_ICON2 = "icon.ico"; #endif // File-scope tray data shared across all TrayTest instances namespace { struct tray_menu g_submenu7_8[] = { // NOSONAR(cpp:S5945, cpp:S5421) - C-style array with null sentinel required by tray C API; mutable for runtime callback assignment {.text = "7", .cb = nullptr}, {.text = "-"}, {.text = "8", .cb = nullptr}, {.text = nullptr} }; struct tray_menu g_submenu5_6[] = { // NOSONAR(cpp:S5945, cpp:S5421) - C-style array with null sentinel required by tray C API; mutable for runtime callback assignment {.text = "5", .cb = nullptr}, {.text = "6", .cb = nullptr}, {.text = nullptr} }; struct tray_menu g_submenu_second[] = { // NOSONAR(cpp:S5945, cpp:S5421) - C-style array with null sentinel required by tray C API; mutable for runtime callback assignment {.text = "THIRD", .submenu = g_submenu7_8}, {.text = "FOUR", .submenu = g_submenu5_6}, {.text = nullptr} }; struct tray_menu g_submenu[] = { // NOSONAR(cpp:S5945, cpp:S5421) - C-style array with null sentinel required by tray C API; mutable for runtime callback assignment {.text = "Hello", .cb = nullptr}, {.text = "Checked", .checked = 1, .checkbox = 1, .cb = nullptr}, {.text = "Disabled", .disabled = 1}, {.text = "-"}, {.text = "SubMenu", .submenu = g_submenu_second}, {.text = "-"}, {.text = "Quit", .cb = nullptr}, {.text = nullptr} }; struct tray g_testTray = { // NOSONAR(cpp:S5421) - mutable global required for shared tray state across TEST_F instances .icon = TRAY_ICON1, .tooltip = "TestTray", .menu = g_submenu }; } // namespace class TrayTest: public BaseTest { // NOSONAR(cpp:S3656) - fixture members must be protected for TEST_F-generated subclasses protected: // NOSONAR(cpp:S3656) - TEST_F generates subclasses that need access to fixture state/methods void ShutdownTray() { if (!trayRunning) { return; } tray_exit(); tray_loop(0); trayRunning = false; } // Dismisses the open menu and exits the tray event loop from a background thread. void closeMenuAndExit() { #if defined(TRAY_WINAPI) PostMessage(tray_get_hwnd(), WM_CANCELMODE, 0, 0); std::this_thread::sleep_for(std::chrono::milliseconds(100)); #elif defined(TRAY_APPKIT) CGEventRef event = CGEventCreateKeyboardEvent(NULL, kVK_Escape, true); CGEventPost(kCGHIDEventTap, event); CFRelease(event); CGEventRef event2 = CGEventCreateKeyboardEvent(NULL, kVK_Escape, false); CGEventPost(kCGHIDEventTap, event2); CFRelease(event2); std::this_thread::sleep_for(std::chrono::milliseconds(100)); #endif tray_exit(); } // Capture a screenshot while the tray menu is open, then dismiss and exit. void captureMenuStateAndExit(const char *screenshotName) { std::thread capture_thread([this, screenshotName]() { // NOSONAR(cpp:S6168) - std::jthread is unavailable on AppleClang 17/libc++ used in CI EXPECT_TRUE(captureScreenshot(screenshotName)); closeMenuAndExit(); }); tray_show_menu(); while (tray_loop(0) == 0) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } capture_thread.join(); } bool trayRunning {false}; // NOSONAR(cpp:S3656) - protected access required by gtest TEST_F subclass pattern struct tray &testTray = g_testTray; // NOSONAR(cpp:S3656) - protected access required by gtest TEST_F subclass pattern struct tray_menu *submenu = g_submenu; // NOSONAR(cpp:S3656) - protected access required by gtest TEST_F subclass pattern struct tray_menu *submenu7_8 = g_submenu7_8; // NOSONAR(cpp:S3656) - protected access required by gtest TEST_F subclass pattern struct tray_menu *submenu5_6 = g_submenu5_6; // NOSONAR(cpp:S3656) - protected access required by gtest TEST_F subclass pattern struct tray_menu *submenu_second = g_submenu_second; // NOSONAR(cpp:S3656) - protected access required by gtest TEST_F subclass pattern static void hello_cb([[maybe_unused]] struct tray_menu *item) { // Mock implementation } static void toggle_cb([[maybe_unused]] struct tray_menu *item) { // NOSONAR(cpp:S1172) - unused param required by tray_menu.cb function pointer type g_testTray.menu[1].checked = !g_testTray.menu[1].checked; tray_update(&g_testTray); } static void quit_cb([[maybe_unused]] struct tray_menu *item) { // NOSONAR(cpp:S1172) - unused param required by tray_menu.cb function pointer type tray_exit(); } static void submenu_cb([[maybe_unused]] struct tray_menu *item) { // NOSONAR(cpp:S1172) - unused param required by tray_menu.cb function pointer type // Mock implementation tray_update(&g_testTray); } void SetUp() override { BaseTest::SetUp(); // Wire up callbacks (file-scope arrays can't use addresses of class statics at init time) g_submenu[0].cb = hello_cb; g_submenu[1].cb = toggle_cb; g_submenu[6].cb = quit_cb; g_submenu7_8[0].cb = submenu_cb; g_submenu7_8[2].cb = submenu_cb; g_submenu5_6[0].cb = submenu_cb; g_submenu5_6[1].cb = submenu_cb; // Skip tests if screenshot tooling is not available if (!ensureScreenshotReady()) { GTEST_SKIP() << "Screenshot tooling missing: " << screenshotUnavailableReason; } if (screenshot::output_root().empty()) { GTEST_SKIP() << "Screenshot output path not initialized"; } #if defined(TRAY_WINAPI) || defined(TRAY_APPKIT) // Ensure icon files exist in test binary directory std::filesystem::path projectRoot = testBinaryDir.parent_path(); std::filesystem::path iconSource; if (std::filesystem::exists(projectRoot / "icons" / TRAY_ICON1)) { iconSource = projectRoot / "icons" / TRAY_ICON1; } else if (std::filesystem::exists(projectRoot / TRAY_ICON1)) { iconSource = projectRoot / TRAY_ICON1; } else if (std::filesystem::exists(std::filesystem::path(TRAY_ICON1))) { iconSource = std::filesystem::path(TRAY_ICON1); } if (!iconSource.empty()) { std::filesystem::path iconDest = testBinaryDir / TRAY_ICON1; if (!std::filesystem::exists(iconDest)) { std::error_code ec; std::filesystem::copy_file(iconSource, iconDest, ec); if (ec) { std::cout << "Warning: Failed to copy icon file: " << ec.message() << std::endl; } } } #endif trayRunning = false; testTray.icon = TRAY_ICON1; testTray.tooltip = "TestTray"; testTray.menu = g_submenu; g_submenu[1].checked = 1; } void TearDown() override { ShutdownTray(); BaseTest::TearDown(); } // Process pending events to allow tray icon to appear. // Call this ONLY before screenshots to ensure the icon is visible. void WaitForTrayReady() { #if defined(TRAY_APPINDICATOR) for (int i = 0; i < 100; i++) { tray_loop(0); std::this_thread::sleep_for(std::chrono::milliseconds(5)); } #elif defined(TRAY_APPKIT) static std::thread::id main_thread_id = std::this_thread::get_id(); if (std::this_thread::get_id() == main_thread_id) { for (int i = 0; i < 100; i++) { tray_loop(0); std::this_thread::sleep_for(std::chrono::milliseconds(5)); } } else { std::this_thread::sleep_for(std::chrono::milliseconds(1000)); } #endif } }; TEST_F(TrayTest, TestTrayInit) { int result = tray_init(&testTray); trayRunning = (result == 0); EXPECT_EQ(result, 0); WaitForTrayReady(); EXPECT_TRUE(captureScreenshot("tray_icon_initial")); } TEST_F(TrayTest, TestTrayLoop) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Test non-blocking loop (blocking=0) since blocking would hang without events int result = tray_loop(0); EXPECT_EQ(result, 0); } TEST_F(TrayTest, TestTrayUpdate) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); EXPECT_EQ(testTray.icon, TRAY_ICON1); // update the values testTray.icon = TRAY_ICON2; testTray.tooltip = "TestTray2"; tray_update(&testTray); EXPECT_EQ(testTray.icon, TRAY_ICON2); // put back the original values testTray.icon = TRAY_ICON1; testTray.tooltip = "TestTray"; tray_update(&testTray); EXPECT_EQ(testTray.icon, TRAY_ICON1); EXPECT_EQ(testTray.tooltip, "TestTray"); } TEST_F(TrayTest, TestToggleCallback) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); bool initialCheckedState = testTray.menu[1].checked; toggle_cb(&testTray.menu[1]); EXPECT_EQ(testTray.menu[1].checked, !initialCheckedState); } TEST_F(TrayTest, TestMenuItemCallback) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Test hello callback - it should work without crashing ASSERT_NE(testTray.menu[0].cb, nullptr); testTray.menu[0].cb(&testTray.menu[0]); } TEST_F(TrayTest, TestDisabledMenuItem) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Verify disabled menu item EXPECT_EQ(testTray.menu[2].disabled, 1); EXPECT_STREQ(testTray.menu[2].text, "Disabled"); } TEST_F(TrayTest, TestMenuSeparator) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Verify separator exists EXPECT_STREQ(testTray.menu[3].text, "-"); EXPECT_EQ(testTray.menu[3].cb, nullptr); } TEST_F(TrayTest, TestSubmenuStructure) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Verify submenu structure EXPECT_STREQ(testTray.menu[4].text, "SubMenu"); ASSERT_NE(testTray.menu[4].submenu, nullptr); // Verify nested submenu levels EXPECT_STREQ(testTray.menu[4].submenu[0].text, "THIRD"); ASSERT_NE(testTray.menu[4].submenu[0].submenu, nullptr); EXPECT_STREQ(testTray.menu[4].submenu[0].submenu[0].text, "7"); } TEST_F(TrayTest, TestSubmenuCallback) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Test submenu callback ASSERT_NE(testTray.menu[4].submenu[0].submenu[0].cb, nullptr); testTray.menu[4].submenu[0].submenu[0].cb(&testTray.menu[4].submenu[0].submenu[0]); } TEST_F(TrayTest, TestNotificationDisplay) { #if !(defined(_WIN32) || defined(__linux__) || defined(__APPLE__)) GTEST_SKIP() << "Notifications only supported on desktop platforms"; #endif #if defined(_WIN32) QUERY_USER_NOTIFICATION_STATE notification_state; if (HRESULT ns = SHQueryUserNotificationState(¬ification_state); ns != S_OK || notification_state != QUNS_ACCEPTS_NOTIFICATIONS) { GTEST_SKIP() << "Notifications not accepted in this environment. SHQueryUserNotificationState result: " << ns << ", state: " << notification_state; } #endif int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Set notification properties testTray.notification_title = "Test Notification"; testTray.notification_text = "This is a test notification message"; testTray.notification_icon = TRAY_ICON1; tray_update(&testTray); WaitForTrayReady(); EXPECT_TRUE(captureScreenshot("tray_notification_displayed")); // Clear notification testTray.notification_title = nullptr; testTray.notification_text = nullptr; testTray.notification_icon = nullptr; tray_update(&testTray); } TEST_F(TrayTest, TestNotificationCallback) { #if !(defined(_WIN32) || defined(__linux__) || defined(__APPLE__)) GTEST_SKIP() << "Notifications only supported on desktop platforms"; #endif #if defined(_WIN32) QUERY_USER_NOTIFICATION_STATE notification_state; if (HRESULT ns = SHQueryUserNotificationState(¬ification_state); ns != S_OK || notification_state != QUNS_ACCEPTS_NOTIFICATIONS) { GTEST_SKIP() << "Notifications not accepted in this environment. SHQueryUserNotificationState result: " << ns << ", state: " << notification_state; } #endif static bool callbackInvoked = false; auto notification_callback = []() { callbackInvoked = true; }; int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Set notification with callback testTray.notification_title = "Clickable Notification"; testTray.notification_text = "Click this notification to test callback"; testTray.notification_icon = TRAY_ICON1; testTray.notification_cb = notification_callback; tray_update(&testTray); // Note: callback would be invoked by user interaction in real scenario // In test environment, we verify it's set correctly EXPECT_NE(testTray.notification_cb, nullptr); // Clear notification testTray.notification_title = nullptr; testTray.notification_text = nullptr; testTray.notification_icon = nullptr; testTray.notification_cb = nullptr; tray_update(&testTray); } TEST_F(TrayTest, TestTooltipUpdate) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Test initial tooltip EXPECT_STREQ(testTray.tooltip, "TestTray"); // Update tooltip testTray.tooltip = "Updated Tooltip Text"; tray_update(&testTray); EXPECT_STREQ(testTray.tooltip, "Updated Tooltip Text"); // Restore original tooltip testTray.tooltip = "TestTray"; tray_update(&testTray); } TEST_F(TrayTest, TestMenuItemContext) { static int contextValue = 42; static bool contextCallbackInvoked = false; auto context_callback = [](struct tray_menu *item) { // NOSONAR(cpp:S995) - must match tray_menu.cb signature void(*)(struct tray_menu*) if (item->context != nullptr) { const auto *value = static_cast(item->context); contextCallbackInvoked = (*value == 42); } }; // Create menu with context std::array context_menu_arr = {{{.text = "Context Item", .cb = context_callback, .context = &contextValue}, {.text = nullptr}}}; struct tray_menu *context_menu = context_menu_arr.data(); testTray.menu = context_menu; int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Verify context is set EXPECT_EQ(testTray.menu[0].context, &contextValue); // Invoke callback with context testTray.menu[0].cb(&testTray.menu[0]); EXPECT_TRUE(contextCallbackInvoked); // Restore original menu testTray.menu = submenu; } TEST_F(TrayTest, TestCheckboxStates) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); EXPECT_EQ(testTray.menu[1].checkbox, 1); EXPECT_EQ(testTray.menu[1].checked, 1); // Show menu open with checkbox in checked state captureMenuStateAndExit("tray_menu_checkbox_checked"); // NOSONAR(cpp:S6168) - helper uses std::thread for AppleClang 17 compatibility // Re-initialize tray with checkbox unchecked trayRunning = false; testTray.menu[1].checked = 0; initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Show menu open with checkbox in unchecked state captureMenuStateAndExit("tray_menu_checkbox_unchecked"); // NOSONAR(cpp:S6168) - helper uses std::thread for AppleClang 17 compatibility // Restore initial checked state testTray.menu[1].checked = 1; } TEST_F(TrayTest, TestMultipleIconUpdates) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Update icon multiple times testTray.icon = TRAY_ICON2; tray_update(&testTray); testTray.icon = TRAY_ICON1; tray_update(&testTray); } TEST_F(TrayTest, TestCompleteMenuHierarchy) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Verify complete menu structure int menuCount = 0; for (const struct tray_menu *m = testTray.menu; m->text != nullptr; m++) { menuCount++; } EXPECT_EQ(menuCount, 7); // Hello, Checked, Disabled, Sep, SubMenu, Sep, Quit // Verify all nested submenus ASSERT_NE(testTray.menu[4].submenu, nullptr); ASSERT_NE(testTray.menu[4].submenu[0].submenu, nullptr); ASSERT_NE(testTray.menu[4].submenu[1].submenu, nullptr); } TEST_F(TrayTest, TestIconPathArray) { #if defined(TRAY_WINAPI) // Test icon path array caching (Windows-specific feature) // The tray struct has a flexible array member, so we allocate a raw buffer // and use memcpy to initialize const fields before the object is used. const size_t icon_count = 2; const size_t buf_size = sizeof(struct tray) + icon_count * sizeof(const char *); std::vector buf(buf_size, std::byte {0}); auto *iconCacheTray = reinterpret_cast(buf.data()); // NOSONAR(cpp:S3630) - reinterpret_cast required to overlay struct onto raw buffer for flexible array member iconCacheTray->icon = TRAY_ICON1; iconCacheTray->tooltip = "Icon Cache Test"; iconCacheTray->notification_icon = nullptr; iconCacheTray->notification_text = nullptr; iconCacheTray->notification_title = nullptr; iconCacheTray->notification_cb = nullptr; iconCacheTray->menu = submenu; // Write const fields via memcpy — const_cast is required to initialize const members in a C struct flexible array allocation auto count_val = static_cast(icon_count); std::memcpy(const_cast(&iconCacheTray->iconPathCount), &count_val, sizeof(count_val)); // NOSONAR(cpp:S859) - required to initialize const member in C struct allocated via raw buffer const char *icon1 = TRAY_ICON1; const char *icon2 = TRAY_ICON2; std::memcpy(const_cast(&iconCacheTray->allIconPaths[0]), &icon1, sizeof(icon1)); // NOSONAR(cpp:S859) - required to initialize const member in C struct allocated via raw buffer std::memcpy(const_cast(&iconCacheTray->allIconPaths[1]), &icon2, sizeof(icon2)); // NOSONAR(cpp:S859) - required to initialize const member in C struct allocated via raw buffer int initResult = tray_init(iconCacheTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Verify initial icon EXPECT_EQ(iconCacheTray->icon, TRAY_ICON1); // Switch to cached icon iconCacheTray->icon = TRAY_ICON2; tray_update(iconCacheTray); // buf goes out of scope, no manual free needed #else // On non-Windows platforms, just test basic icon switching int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); EXPECT_EQ(testTray.icon, TRAY_ICON1); testTray.icon = TRAY_ICON2; tray_update(&testTray); #endif } TEST_F(TrayTest, TestQuitCallback) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Verify quit callback exists ASSERT_NE(testTray.menu[6].cb, nullptr); EXPECT_STREQ(testTray.menu[6].text, "Quit"); // Note: Actually calling quit_cb would terminate the tray, // which is tested separately in TestTrayExit } TEST_F(TrayTest, TestTrayShowMenu) { int initResult = tray_init(&testTray); trayRunning = (initResult == 0); ASSERT_EQ(initResult, 0); // Screenshot shows the full menu open, including the SubMenu entry that leads to nested items captureMenuStateAndExit("tray_menu_shown"); // NOSONAR(cpp:S6168) - helper uses std::thread for AppleClang 17 compatibility } TEST_F(TrayTest, TestTrayExit) { tray_exit(); }