Skip to content

Commit 64a3ef2

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 64a3ef2

2 files changed

Lines changed: 145 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: 110 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,13 @@ 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();
377+
testTray.icon = iconParam.icon;
313378

314379
int initResult = tray_init(&testTray);
315380
trayRunning = (initResult == 0);
@@ -318,12 +383,12 @@ TEST_F(TrayTest, TestNotificationDisplay) {
318383
// Set notification properties
319384
testTray.notification_title = "Test Notification";
320385
testTray.notification_text = "This is a test notification message";
321-
testTray.notification_icon = TRAY_ICON1;
386+
testTray.notification_icon = iconParam.icon;
322387

323388
tray_update(&testTray);
324389

325390
WaitForTrayReady();
326-
EXPECT_TRUE(captureScreenshot("tray_notification_displayed"));
391+
EXPECT_TRUE(captureScreenshot(std::string("tray_notification_") + iconParam.name + "_icon"));
327392

328393
// Clear notification
329394
testTray.notification_title = nullptr;
@@ -333,14 +398,13 @@ TEST_F(TrayTest, TestNotificationDisplay) {
333398
waitForNativeNotificationTimeout();
334399
}
335400

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;
401+
TEST_P(TrayNotificationIconTest, TestNotificationCallback) {
402+
if (const std::string skipReason = nativeNotificationSkipReason(); !skipReason.empty()) {
403+
GTEST_SKIP() << skipReason;
342404
}
343-
#endif
405+
406+
const auto &iconParam = GetParam();
407+
testTray.icon = iconParam.icon;
344408

345409
static bool callbackInvoked = false;
346410
auto notification_callback = []() {
@@ -354,7 +418,7 @@ TEST_F(TrayTest, TestNotificationCallback) {
354418
// Set notification with callback
355419
testTray.notification_title = "Clickable Notification";
356420
testTray.notification_text = "Click this notification to test callback";
357-
testTray.notification_icon = TRAY_ICON1;
421+
testTray.notification_icon = iconParam.icon;
358422
testTray.notification_cb = notification_callback;
359423

360424
tray_update(&testTray);
@@ -423,6 +487,8 @@ TEST_F(TrayTest, TestMenuItemContext) {
423487
}
424488

425489
TEST_F(TrayTest, TestCheckboxStates) {
490+
testTray.icon = TRAY_ICON_SVG;
491+
426492
int initResult = tray_init(&testTray);
427493
trayRunning = (initResult == 0);
428494
ASSERT_EQ(initResult, 0);
@@ -503,6 +569,8 @@ TEST_F(TrayTest, TestQuitCallback) {
503569
}
504570

505571
TEST_F(TrayTest, TestTrayShowMenu) {
572+
testTray.icon = TRAY_ICON_SVG;
573+
506574
int initResult = tray_init(&testTray);
507575
trayRunning = (initResult == 0);
508576
ASSERT_EQ(initResult, 0);
@@ -515,61 +583,25 @@ TEST_F(TrayTest, TestTrayExit) {
515583
tray_exit();
516584
}
517585

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-
558586
TEST_F(TrayTest, TestMenuAppearsOnLeftClick) {
559587
// Regression test for: clicking the tray icon did not bring up the menu.
560588
// The activated(Trigger) signal was not connected to the menu popup logic.
561589
// tray_show_menu() exercises the same code path that the activated handler calls.
590+
testTray.icon = TRAY_ICON_SVG;
591+
562592
int initResult = tray_init(&testTray);
563593
trayRunning = (initResult == 0);
564594
ASSERT_EQ(initResult, 0);
565595

566596
captureMenuStateAndExit("tray_menu_left_click"); // NOSONAR(cpp:S6168) - helper uses std::thread for AppleClang 17 compatibility
567597
}
568598

569-
TEST_F(TrayTest, TestNotificationCallbackFiredOnClick) {
599+
TEST_P(TrayNotificationIconTest, TestNotificationCallbackFiredOnClick) {
570600
// Regression test for: clicking a notification did not invoke the callback.
571601
// On the D-Bus path, QSystemTrayIcon::messageClicked is never emitted; the
572602
// callback must be routed through TrayNotificationHandler::onActionInvoked.
603+
const auto &iconParam = GetParam();
604+
testTray.icon = iconParam.icon;
573605
static bool callbackInvoked = false;
574606
callbackInvoked = false;
575607

@@ -579,7 +611,7 @@ TEST_F(TrayTest, TestNotificationCallbackFiredOnClick) {
579611

580612
testTray.notification_title = "Clickable Notification";
581613
testTray.notification_text = "Click to test callback";
582-
testTray.notification_icon = TRAY_ICON1;
614+
testTray.notification_icon = iconParam.icon;
583615
testTray.notification_cb = []() {
584616
callbackInvoked = true;
585617
};
@@ -623,7 +655,7 @@ TEST_F(TrayTest, TestMenuCallbackAfterNotificationUpdate) {
623655

624656
testTray.notification_title = "Menu Callback Regression";
625657
testTray.notification_text = "Notification update should not break menu callbacks";
626-
testTray.notification_icon = TRAY_ICON1;
658+
testTray.notification_icon = TRAY_ICON_SVG;
627659
tray_update(&testTray);
628660
WaitForTrayReady();
629661

@@ -639,3 +671,17 @@ TEST_F(TrayTest, TestMenuCallbackAfterNotificationUpdate) {
639671

640672
testTray.menu[0].cb = original_cb;
641673
}
674+
675+
INSTANTIATE_TEST_SUITE_P(
676+
TrayIcons,
677+
TrayIconTest,
678+
::testing::ValuesIn(TRAY_ICON_PARAMS),
679+
trayIconParamName
680+
);
681+
682+
INSTANTIATE_TEST_SUITE_P(
683+
TrayNotificationIcons,
684+
TrayNotificationIconTest,
685+
::testing::ValuesIn(TRAY_ICON_PARAMS),
686+
trayIconParamName
687+
);

0 commit comments

Comments
 (0)