Skip to content

Commit bdc4dd8

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 bdc4dd8

2 files changed

Lines changed: 142 additions & 91 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: 107 additions & 64 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,6 +248,14 @@ class TrayTest: public BaseTest { // NOSONAR(cpp:S3656) - fixture members must
201248
}
202249
};
203250

251+
class TrayIconTest:
252+
public TrayTest,
253+
public ::testing::WithParamInterface<TrayIconParam> {};
254+
255+
class TrayNotificationIconTest:
256+
public TrayTest,
257+
public ::testing::WithParamInterface<TrayIconParam> {};
258+
204259
TEST_F(TrayTest, TestTrayInit) {
205260
int result = tray_init(&testTray);
206261
trayRunning = (result == 0);
@@ -209,6 +264,17 @@ TEST_F(TrayTest, TestTrayInit) {
209264
EXPECT_TRUE(captureScreenshot("tray_icon_initial"));
210265
}
211266

267+
TEST_P(TrayIconTest, TestTrayIconDisplay) {
268+
const auto &iconParam = GetParam();
269+
testTray.icon = iconParam.icon;
270+
271+
int result = tray_init(&testTray);
272+
trayRunning = (result == 0);
273+
EXPECT_EQ(result, 0);
274+
WaitForTrayReady();
275+
EXPECT_TRUE(captureScreenshot(std::string("tray_icon_") + iconParam.name));
276+
}
277+
212278
TEST_F(TrayTest, TestTrayLoop) {
213279
int initResult = tray_init(&testTray);
214280
trayRunning = (initResult == 0);
@@ -302,14 +368,12 @@ TEST_F(TrayTest, TestSubmenuCallback) {
302368
testTray.menu[4].submenu[0].submenu[0].cb(&testTray.menu[4].submenu[0].submenu[0]);
303369
}
304370

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;
371+
TEST_P(TrayNotificationIconTest, TestNotificationDisplay) {
372+
if (const std::string skipReason = nativeNotificationSkipReason(); !skipReason.empty()) {
373+
GTEST_SKIP() << skipReason;
311374
}
312-
#endif
375+
376+
const auto &iconParam = GetParam();
313377

314378
int initResult = tray_init(&testTray);
315379
trayRunning = (initResult == 0);
@@ -318,12 +382,12 @@ TEST_F(TrayTest, TestNotificationDisplay) {
318382
// Set notification properties
319383
testTray.notification_title = "Test Notification";
320384
testTray.notification_text = "This is a test notification message";
321-
testTray.notification_icon = TRAY_ICON1;
385+
testTray.notification_icon = iconParam.icon;
322386

323387
tray_update(&testTray);
324388

325389
WaitForTrayReady();
326-
EXPECT_TRUE(captureScreenshot("tray_notification_displayed"));
390+
EXPECT_TRUE(captureScreenshot(std::string("tray_notification_") + iconParam.name + "_icon"));
327391

328392
// Clear notification
329393
testTray.notification_title = nullptr;
@@ -333,14 +397,12 @@ TEST_F(TrayTest, TestNotificationDisplay) {
333397
waitForNativeNotificationTimeout();
334398
}
335399

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;
400+
TEST_P(TrayNotificationIconTest, TestNotificationCallback) {
401+
if (const std::string skipReason = nativeNotificationSkipReason(); !skipReason.empty()) {
402+
GTEST_SKIP() << skipReason;
342403
}
343-
#endif
404+
405+
const auto &iconParam = GetParam();
344406

345407
static bool callbackInvoked = false;
346408
auto notification_callback = []() {
@@ -354,7 +416,7 @@ TEST_F(TrayTest, TestNotificationCallback) {
354416
// Set notification with callback
355417
testTray.notification_title = "Clickable Notification";
356418
testTray.notification_text = "Click this notification to test callback";
357-
testTray.notification_icon = TRAY_ICON1;
419+
testTray.notification_icon = iconParam.icon;
358420
testTray.notification_cb = notification_callback;
359421

360422
tray_update(&testTray);
@@ -423,6 +485,8 @@ TEST_F(TrayTest, TestMenuItemContext) {
423485
}
424486

425487
TEST_F(TrayTest, TestCheckboxStates) {
488+
testTray.icon = TRAY_ICON_SVG;
489+
426490
int initResult = tray_init(&testTray);
427491
trayRunning = (initResult == 0);
428492
ASSERT_EQ(initResult, 0);
@@ -503,6 +567,8 @@ TEST_F(TrayTest, TestQuitCallback) {
503567
}
504568

505569
TEST_F(TrayTest, TestTrayShowMenu) {
570+
testTray.icon = TRAY_ICON_SVG;
571+
506572
int initResult = tray_init(&testTray);
507573
trayRunning = (initResult == 0);
508574
ASSERT_EQ(initResult, 0);
@@ -515,61 +581,24 @@ TEST_F(TrayTest, TestTrayExit) {
515581
tray_exit();
516582
}
517583

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-
558584
TEST_F(TrayTest, TestMenuAppearsOnLeftClick) {
559585
// Regression test for: clicking the tray icon did not bring up the menu.
560586
// The activated(Trigger) signal was not connected to the menu popup logic.
561587
// tray_show_menu() exercises the same code path that the activated handler calls.
588+
testTray.icon = TRAY_ICON_SVG;
589+
562590
int initResult = tray_init(&testTray);
563591
trayRunning = (initResult == 0);
564592
ASSERT_EQ(initResult, 0);
565593

566594
captureMenuStateAndExit("tray_menu_left_click"); // NOSONAR(cpp:S6168) - helper uses std::thread for AppleClang 17 compatibility
567595
}
568596

569-
TEST_F(TrayTest, TestNotificationCallbackFiredOnClick) {
597+
TEST_P(TrayNotificationIconTest, TestNotificationCallbackFiredOnClick) {
570598
// Regression test for: clicking a notification did not invoke the callback.
571599
// On the D-Bus path, QSystemTrayIcon::messageClicked is never emitted; the
572600
// callback must be routed through TrayNotificationHandler::onActionInvoked.
601+
const auto &iconParam = GetParam();
573602
static bool callbackInvoked = false;
574603
callbackInvoked = false;
575604

@@ -579,7 +608,7 @@ TEST_F(TrayTest, TestNotificationCallbackFiredOnClick) {
579608

580609
testTray.notification_title = "Clickable Notification";
581610
testTray.notification_text = "Click to test callback";
582-
testTray.notification_icon = TRAY_ICON1;
611+
testTray.notification_icon = iconParam.icon;
583612
testTray.notification_cb = []() {
584613
callbackInvoked = true;
585614
};
@@ -623,7 +652,7 @@ TEST_F(TrayTest, TestMenuCallbackAfterNotificationUpdate) {
623652

624653
testTray.notification_title = "Menu Callback Regression";
625654
testTray.notification_text = "Notification update should not break menu callbacks";
626-
testTray.notification_icon = TRAY_ICON1;
655+
testTray.notification_icon = TRAY_ICON_SVG;
627656
tray_update(&testTray);
628657
WaitForTrayReady();
629658

@@ -639,3 +668,17 @@ TEST_F(TrayTest, TestMenuCallbackAfterNotificationUpdate) {
639668

640669
testTray.menu[0].cb = original_cb;
641670
}
671+
672+
INSTANTIATE_TEST_SUITE_P(
673+
TrayIcons,
674+
TrayIconTest,
675+
::testing::ValuesIn(TRAY_ICON_PARAMS),
676+
trayIconParamName
677+
);
678+
679+
INSTANTIATE_TEST_SUITE_P(
680+
TrayNotificationIcons,
681+
TrayNotificationIconTest,
682+
::testing::ValuesIn(TRAY_ICON_PARAMS),
683+
trayIconParamName
684+
);

0 commit comments

Comments
 (0)