Skip to content

Commit 3c558f8

Browse files
Parametrize tray tests and top-level icon config
CMake: Only define default icon variables and install/copy helper when the project is top-level (TRAY_IS_TOP_LEVEL). Add a second set of default icons (icon2.*) and include them in TRAY_ICON_FILES. Gate the example target copying behind TRAY_IS_TOP_LEVEL as well. Tests: Convert many unit tests to parameterized tests over icon types (svg/ico/png/themed). Add icon constants for .ico and secondary icons, a TrayIconParam struct, helpers to print and name params, and a nativeNotificationSkipReason helper to conditionally skip notification tests on unsupported environments. Ensure test assets are copied only when an extension is present, update screenshot names to include the icon param, and instantiate the parameterized suites. Remove several redundant single-file icon tests and adjust a few tests to explicitly set SVG icons where needed. Overall this makes tests cover multiple icon formats and avoids top-level-only icon behavior when used as a subproject.
1 parent 6dd53c9 commit 3c558f8

2 files changed

Lines changed: 136 additions & 93 deletions

File tree

CMakeLists.txt

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,33 +39,41 @@ file(GLOB TRAY_SOURCES
3939
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.h"
4040
)
4141

42-
set(TRAY_ICON_ICO "${CMAKE_CURRENT_SOURCE_DIR}/icons/icon.ico")
43-
set(TRAY_ICON_PNG "${CMAKE_CURRENT_SOURCE_DIR}/icons/icon.png")
44-
set(TRAY_ICON_SVG "${CMAKE_CURRENT_SOURCE_DIR}/icons/icon.svg")
45-
set(TRAY_ICON_FILES
46-
"${TRAY_ICON_ICO}"
47-
"${TRAY_ICON_PNG}"
48-
"${TRAY_ICON_SVG}"
49-
)
50-
51-
set(_TRAY_ICON_ICO "${TRAY_ICON_ICO}" CACHE INTERNAL "Default tray ICO icon path")
52-
set(_TRAY_ICON_PNG "${TRAY_ICON_PNG}" CACHE INTERNAL "Default tray PNG icon path")
53-
set(_TRAY_ICON_SVG "${TRAY_ICON_SVG}" CACHE INTERNAL "Default tray SVG icon path")
54-
55-
# Copy default tray icon files into the output directory of the specified target.
56-
function(tray_copy_default_icons target_name)
57-
if(NOT TARGET "${target_name}")
58-
message(FATAL_ERROR "tray_copy_default_icons expected an existing target: ${target_name}")
59-
endif()
42+
if(TRAY_IS_TOP_LEVEL)
43+
set(TRAY_ICON_ICO "${CMAKE_CURRENT_SOURCE_DIR}/icons/icon.ico")
44+
set(TRAY_ICON_PNG "${CMAKE_CURRENT_SOURCE_DIR}/icons/icon.png")
45+
set(TRAY_ICON_SVG "${CMAKE_CURRENT_SOURCE_DIR}/icons/icon.svg")
46+
set(TRAY_ICON2_ICO "${CMAKE_CURRENT_SOURCE_DIR}/icons/icon2.ico")
47+
set(TRAY_ICON2_PNG "${CMAKE_CURRENT_SOURCE_DIR}/icons/icon2.png")
48+
set(TRAY_ICON2_SVG "${CMAKE_CURRENT_SOURCE_DIR}/icons/icon2.svg")
49+
set(TRAY_ICON_FILES
50+
"${TRAY_ICON_ICO}"
51+
"${TRAY_ICON_PNG}"
52+
"${TRAY_ICON_SVG}"
53+
"${TRAY_ICON2_ICO}"
54+
"${TRAY_ICON2_PNG}"
55+
"${TRAY_ICON2_SVG}"
56+
)
57+
58+
set(_TRAY_ICON_ICO "${TRAY_ICON_ICO}" CACHE INTERNAL "Default tray ICO icon path")
59+
set(_TRAY_ICON_PNG "${TRAY_ICON_PNG}" CACHE INTERNAL "Default tray PNG icon path")
60+
set(_TRAY_ICON_SVG "${TRAY_ICON_SVG}" CACHE INTERNAL "Default tray SVG icon path")
61+
62+
# Copy default tray icon files into the output directory of the specified target.
63+
function(tray_copy_default_icons target_name)
64+
if(NOT TARGET "${target_name}")
65+
message(FATAL_ERROR "tray_copy_default_icons expected an existing target: ${target_name}")
66+
endif()
6067

61-
foreach(icon_file IN LISTS TRAY_ICON_FILES)
62-
add_custom_command(TARGET "${target_name}" POST_BUILD
63-
COMMAND ${CMAKE_COMMAND} -E copy_if_different
64-
"${icon_file}"
65-
"$<TARGET_FILE_DIR:${target_name}>"
66-
COMMENT "Copying ${icon_file} to $<TARGET_FILE_DIR:${target_name}>")
67-
endforeach()
68-
endfunction()
68+
foreach(icon_file IN LISTS TRAY_ICON_FILES)
69+
add_custom_command(TARGET "${target_name}" POST_BUILD
70+
COMMAND ${CMAKE_COMMAND} -E copy_if_different
71+
"${icon_file}"
72+
"$<TARGET_FILE_DIR:${target_name}>"
73+
COMMENT "Copying ${icon_file} to $<TARGET_FILE_DIR:${target_name}>")
74+
endforeach()
75+
endfunction()
76+
endif()
6977

7078
find_package(Qt6 COMPONENTS Widgets Svg)
7179
if(Qt6_FOUND)
@@ -128,7 +136,7 @@ endif()
128136

129137
add_library(tray::tray ALIAS ${PROJECT_NAME})
130138

131-
if(BUILD_EXAMPLE)
139+
if(TRAY_IS_TOP_LEVEL AND BUILD_EXAMPLE)
132140
add_executable(tray_example "${CMAKE_CURRENT_SOURCE_DIR}/src/example.c")
133141
target_link_libraries(tray_example tray::tray)
134142
tray_copy_default_icons(tray_example)

tests/unit/test_tray.cpp

Lines changed: 101 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
#include <array>
66
#include <atomic>
77
#include <chrono>
8+
#include <ostream>
9+
#include <string>
810
#include <thread>
911

1012
#if defined(_WIN32) || defined(_WIN64)
@@ -15,13 +17,50 @@
1517
#include "src/tray.h"
1618
#include "tests/screenshot_utils.h"
1719

18-
constexpr const char *TRAY_ICON1 = "icon.png";
19-
constexpr const char *TRAY_ICON2 = "icon2.png";
20+
constexpr const char *TRAY_ICON_ICO = "icon.ico";
21+
constexpr const char *TRAY_ICON_PNG = "icon.png";
2022
constexpr const char *TRAY_ICON_SVG = "icon.svg";
23+
constexpr const char *TRAY_ICON2_ICO = "icon2.ico";
24+
constexpr const char *TRAY_ICON2_PNG = "icon2.png";
25+
constexpr const char *TRAY_ICON2_SVG = "icon2.svg";
2126
constexpr const char *TRAY_ICON_THEMED = "mail-message-new";
27+
constexpr const char *TRAY_ICON1 = TRAY_ICON_PNG;
28+
constexpr const char *TRAY_ICON2 = TRAY_ICON2_PNG;
2229

2330
// File-scope tray data shared across all TrayTest instances
2431
namespace {
32+
struct TrayIconParam {
33+
const char *name;
34+
const char *icon;
35+
const char *alternateIcon;
36+
};
37+
38+
constexpr std::array<TrayIconParam, 4> TRAY_ICON_PARAMS {
39+
{{"svg", TRAY_ICON_SVG, TRAY_ICON2_SVG},
40+
{"ico", TRAY_ICON_ICO, TRAY_ICON2_ICO},
41+
{"png", TRAY_ICON_PNG, TRAY_ICON2_PNG},
42+
{"themed", TRAY_ICON_THEMED, TRAY_ICON_THEMED}}
43+
};
44+
45+
std::string trayIconParamName(const ::testing::TestParamInfo<TrayIconParam> &info) {
46+
return info.param.name;
47+
}
48+
49+
void PrintTo(const TrayIconParam &param, std::ostream *os) {
50+
*os << param.name;
51+
}
52+
53+
std::string nativeNotificationSkipReason() {
54+
#if defined(_WIN32)
55+
QUERY_USER_NOTIFICATION_STATE notification_state;
56+
if (const HRESULT ns = SHQueryUserNotificationState(&notification_state); ns != S_OK || notification_state != QUNS_ACCEPTS_NOTIFICATIONS) {
57+
return "Notifications not accepted in this environment. SHQueryUserNotificationState result: " + std::to_string(ns) + ", state: " + std::to_string(notification_state);
58+
}
59+
#endif
60+
61+
return {};
62+
}
63+
2564
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
2665
{.text = "7", .cb = nullptr},
2766
{.text = "-"},
@@ -172,8 +211,16 @@ class TrayTest: public BaseTest { // NOSONAR(cpp:S3656) - fixture members must
172211
}
173212
};
174213

175-
ensureIconInTestDir(TRAY_ICON1);
176-
ensureIconInTestDir(TRAY_ICON_SVG);
214+
auto ensureFileIconInTestDir = [&ensureIconInTestDir](const char *iconName) {
215+
if (std::filesystem::path(iconName).has_extension()) {
216+
ensureIconInTestDir(iconName);
217+
}
218+
};
219+
220+
for (const auto &iconParam : TRAY_ICON_PARAMS) {
221+
ensureFileIconInTestDir(iconParam.icon);
222+
ensureFileIconInTestDir(iconParam.alternateIcon);
223+
}
177224

178225
trayRunning = false;
179226
testTray.icon = TRAY_ICON1;
@@ -201,12 +248,23 @@ class TrayTest: public BaseTest { // NOSONAR(cpp:S3656) - fixture members must
201248
}
202249
};
203250

204-
TEST_F(TrayTest, TestTrayInit) {
251+
class TrayIconTest:
252+
public TrayTest,
253+
public ::testing::WithParamInterface<TrayIconParam> {};
254+
255+
class TrayNotificationIconTest:
256+
public TrayTest,
257+
public ::testing::WithParamInterface<TrayIconParam> {};
258+
259+
TEST_P(TrayIconTest, TestTrayInit) {
260+
const auto &iconParam = GetParam();
261+
testTray.icon = iconParam.icon;
262+
205263
int result = tray_init(&testTray);
206264
trayRunning = (result == 0);
207265
EXPECT_EQ(result, 0);
208266
WaitForTrayReady();
209-
EXPECT_TRUE(captureScreenshot("tray_icon_initial"));
267+
EXPECT_TRUE(captureScreenshot(std::string("tray_icon_") + iconParam.name));
210268
}
211269

212270
TEST_F(TrayTest, TestTrayLoop) {
@@ -302,14 +360,12 @@ TEST_F(TrayTest, TestSubmenuCallback) {
302360
testTray.menu[4].submenu[0].submenu[0].cb(&testTray.menu[4].submenu[0].submenu[0]);
303361
}
304362

305-
TEST_F(TrayTest, TestNotificationDisplay) {
306-
#if defined(_WIN32)
307-
QUERY_USER_NOTIFICATION_STATE notification_state;
308-
if (HRESULT ns = SHQueryUserNotificationState(&notification_state);
309-
ns != S_OK || notification_state != QUNS_ACCEPTS_NOTIFICATIONS) {
310-
GTEST_SKIP() << "Notifications not accepted in this environment. SHQueryUserNotificationState result: " << ns << ", state: " << notification_state;
363+
TEST_P(TrayNotificationIconTest, TestNotificationDisplay) {
364+
if (const std::string skipReason = nativeNotificationSkipReason(); !skipReason.empty()) {
365+
GTEST_SKIP() << skipReason;
311366
}
312-
#endif
367+
368+
const auto &iconParam = GetParam();
313369

314370
int initResult = tray_init(&testTray);
315371
trayRunning = (initResult == 0);
@@ -318,12 +374,12 @@ TEST_F(TrayTest, TestNotificationDisplay) {
318374
// Set notification properties
319375
testTray.notification_title = "Test Notification";
320376
testTray.notification_text = "This is a test notification message";
321-
testTray.notification_icon = TRAY_ICON1;
377+
testTray.notification_icon = iconParam.icon;
322378

323379
tray_update(&testTray);
324380

325381
WaitForTrayReady();
326-
EXPECT_TRUE(captureScreenshot("tray_notification_displayed"));
382+
EXPECT_TRUE(captureScreenshot(std::string("tray_notification_") + iconParam.name + "_icon"));
327383

328384
// Clear notification
329385
testTray.notification_title = nullptr;
@@ -333,14 +389,12 @@ TEST_F(TrayTest, TestNotificationDisplay) {
333389
waitForNativeNotificationTimeout();
334390
}
335391

336-
TEST_F(TrayTest, TestNotificationCallback) {
337-
#if defined(_WIN32)
338-
QUERY_USER_NOTIFICATION_STATE notification_state;
339-
if (HRESULT ns = SHQueryUserNotificationState(&notification_state);
340-
ns != S_OK || notification_state != QUNS_ACCEPTS_NOTIFICATIONS) {
341-
GTEST_SKIP() << "Notifications not accepted in this environment. SHQueryUserNotificationState result: " << ns << ", state: " << notification_state;
392+
TEST_P(TrayNotificationIconTest, TestNotificationCallback) {
393+
if (const std::string skipReason = nativeNotificationSkipReason(); !skipReason.empty()) {
394+
GTEST_SKIP() << skipReason;
342395
}
343-
#endif
396+
397+
const auto &iconParam = GetParam();
344398

345399
static bool callbackInvoked = false;
346400
auto notification_callback = []() {
@@ -354,7 +408,7 @@ TEST_F(TrayTest, TestNotificationCallback) {
354408
// Set notification with callback
355409
testTray.notification_title = "Clickable Notification";
356410
testTray.notification_text = "Click this notification to test callback";
357-
testTray.notification_icon = TRAY_ICON1;
411+
testTray.notification_icon = iconParam.icon;
358412
testTray.notification_cb = notification_callback;
359413

360414
tray_update(&testTray);
@@ -423,6 +477,8 @@ TEST_F(TrayTest, TestMenuItemContext) {
423477
}
424478

425479
TEST_F(TrayTest, TestCheckboxStates) {
480+
testTray.icon = TRAY_ICON_SVG;
481+
426482
int initResult = tray_init(&testTray);
427483
trayRunning = (initResult == 0);
428484
ASSERT_EQ(initResult, 0);
@@ -503,6 +559,8 @@ TEST_F(TrayTest, TestQuitCallback) {
503559
}
504560

505561
TEST_F(TrayTest, TestTrayShowMenu) {
562+
testTray.icon = TRAY_ICON_SVG;
563+
506564
int initResult = tray_init(&testTray);
507565
trayRunning = (initResult == 0);
508566
ASSERT_EQ(initResult, 0);
@@ -515,61 +573,24 @@ TEST_F(TrayTest, TestTrayExit) {
515573
tray_exit();
516574
}
517575

518-
TEST_F(TrayTest, TestTrayIconThemed) {
519-
testTray.icon = TRAY_ICON_THEMED;
520-
int result = tray_init(&testTray);
521-
trayRunning = (result == 0);
522-
ASSERT_EQ(result, 0);
523-
WaitForTrayReady();
524-
EXPECT_TRUE(captureScreenshot("tray_icon_themed"));
525-
testTray.icon = TRAY_ICON1;
526-
}
527-
528-
TEST_F(TrayTest, TestTrayIconSvgFile) {
529-
testTray.icon = TRAY_ICON_SVG;
530-
int result = tray_init(&testTray);
531-
trayRunning = (result == 0);
532-
ASSERT_EQ(result, 0);
533-
WaitForTrayReady();
534-
EXPECT_TRUE(captureScreenshot("tray_icon_svg"));
535-
testTray.icon = TRAY_ICON1;
536-
}
537-
538-
TEST_F(TrayTest, TestNotificationWithThemedIcon) {
539-
int initResult = tray_init(&testTray);
540-
trayRunning = (initResult == 0);
541-
ASSERT_EQ(initResult, 0);
542-
543-
testTray.notification_title = "Test Notification";
544-
testTray.notification_text = "This is a test notification message";
545-
testTray.notification_icon = TRAY_ICON_THEMED;
546-
tray_update(&testTray);
547-
548-
WaitForTrayReady();
549-
EXPECT_TRUE(captureScreenshot("tray_notification_themed_icon"));
550-
551-
testTray.notification_title = nullptr;
552-
testTray.notification_text = nullptr;
553-
testTray.notification_icon = nullptr;
554-
tray_update(&testTray);
555-
waitForNativeNotificationTimeout();
556-
}
557-
558576
TEST_F(TrayTest, TestMenuAppearsOnLeftClick) {
559577
// Regression test for: clicking the tray icon did not bring up the menu.
560578
// The activated(Trigger) signal was not connected to the menu popup logic.
561579
// tray_show_menu() exercises the same code path that the activated handler calls.
580+
testTray.icon = TRAY_ICON_SVG;
581+
562582
int initResult = tray_init(&testTray);
563583
trayRunning = (initResult == 0);
564584
ASSERT_EQ(initResult, 0);
565585

566586
captureMenuStateAndExit("tray_menu_left_click"); // NOSONAR(cpp:S6168) - helper uses std::thread for AppleClang 17 compatibility
567587
}
568588

569-
TEST_F(TrayTest, TestNotificationCallbackFiredOnClick) {
589+
TEST_P(TrayNotificationIconTest, TestNotificationCallbackFiredOnClick) {
570590
// Regression test for: clicking a notification did not invoke the callback.
571591
// On the D-Bus path, QSystemTrayIcon::messageClicked is never emitted; the
572592
// callback must be routed through TrayNotificationHandler::onActionInvoked.
593+
const auto &iconParam = GetParam();
573594
static bool callbackInvoked = false;
574595
callbackInvoked = false;
575596

@@ -579,7 +600,7 @@ TEST_F(TrayTest, TestNotificationCallbackFiredOnClick) {
579600

580601
testTray.notification_title = "Clickable Notification";
581602
testTray.notification_text = "Click to test callback";
582-
testTray.notification_icon = TRAY_ICON1;
603+
testTray.notification_icon = iconParam.icon;
583604
testTray.notification_cb = []() {
584605
callbackInvoked = true;
585606
};
@@ -623,7 +644,7 @@ TEST_F(TrayTest, TestMenuCallbackAfterNotificationUpdate) {
623644

624645
testTray.notification_title = "Menu Callback Regression";
625646
testTray.notification_text = "Notification update should not break menu callbacks";
626-
testTray.notification_icon = TRAY_ICON1;
647+
testTray.notification_icon = TRAY_ICON_SVG;
627648
tray_update(&testTray);
628649
WaitForTrayReady();
629650

@@ -639,3 +660,17 @@ TEST_F(TrayTest, TestMenuCallbackAfterNotificationUpdate) {
639660

640661
testTray.menu[0].cb = original_cb;
641662
}
663+
664+
INSTANTIATE_TEST_SUITE_P(
665+
TrayIcons,
666+
TrayIconTest,
667+
::testing::ValuesIn(TRAY_ICON_PARAMS),
668+
trayIconParamName
669+
);
670+
671+
INSTANTIATE_TEST_SUITE_P(
672+
TrayNotificationIcons,
673+
TrayNotificationIconTest,
674+
::testing::ValuesIn(TRAY_ICON_PARAMS),
675+
trayIconParamName
676+
);

0 commit comments

Comments
 (0)