Skip to content

Commit 6805fc3

Browse files
Add Windows toast cleanup to test utils
Implement Windows-specific notification cleanup for tests: add WinRT/UIAutomation helpers (clearToastNotificationHistory, closeVisibleToastNotifications, releaseCom, isCloseButtonInToastArea, invokeButton) and update dismissNativeNotifications to use them. Add required Windows headers/includes and link additional Windows libs (ole32, oleaut32, runtimeobject, uiautomationcore) in tests/CMakeLists.txt. Also simplify the README icon support line and replace an explicit sleep in waitForNativeNotificationTimeout with a call to dismissNativeNotifications.
1 parent 6fdb6b6 commit 6805fc3

5 files changed

Lines changed: 197 additions & 9 deletions

File tree

README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,7 @@ The `icon` and `notification_icon` fields can be a path to an image file or an i
133133
are resolved from the process working directory, so applications should copy or install icon files where the running
134134
process can find them.
135135

136-
| Component | Backend | Supported inputs | Notes |
137-
|-----------------------------------------|--------------------------------------------------------------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
138-
| Tray icon (`icon`) | Qt `QSystemTrayIcon` / `QIcon` on all platforms | SVG, ICO, PNG, Qt theme icon names | Loaded through Qt's `QIcon` path; SVG, ICO, and PNG are tested. Theme icon names are resolved by Qt when the platform/theme supports them. |
139-
| Notification icon (`notification_icon`) | Qt `QSystemTrayIcon::showMessage` / `QIcon` on all platforms | SVG, ICO, PNG, Qt theme icon names | Loaded through Qt's `QIcon` path; SVG, ICO, and PNG are tested. Theme icon names are resolved by Qt when the platform/theme supports them. |
136+
SVG, ICO, PNG, and Qt theme icon names are supported.
140137

141138
For the most predictable cross-platform behavior, use SVG or PNG files for both tray and notification icons. ICO is
142139
supported by the Qt-backed paths tested by this project.

src/QtTrayMenu.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
// qt includes
99
#include <QApplication>
10+
#include <QByteArray>
1011
#include <QCursor>
1112
#include <QDebug>
1213
#include <QMouseEvent>
@@ -19,6 +20,12 @@ namespace {
1920
int defaultArgc = 1; // NOSONAR(cpp:S5421): This is required for QApplication's argc/argv constructor
2021
char defaultArgv0[] = "TrayMenuApp"; // NOSONAR(cpp:S5421): This is required for QApplication's argc/argv constructor
2122
char *defaultArgv[] = {defaultArgv0, nullptr}; // NOSONAR(cpp:S5421,cpp:S5954): This is required for QApplication's argc/argv constructor
23+
24+
#ifdef TRAY_ENABLE_TEST_HOOKS
25+
bool nativeNotificationsSuppressed() {
26+
return qgetenv("TRAY_TEST_SUPPRESS_NATIVE_NOTIFICATIONS") == QByteArrayLiteral("1");
27+
}
28+
#endif
2229
} // namespace
2330

2431
QtTrayMenu::QtTrayMenu(QObject *parent, const bool debug):
@@ -310,6 +317,11 @@ void QtTrayMenu::showMessage(const QString &title, const QString &msg, std::func
310317
}
311318
if (QSystemTrayIcon::supportsMessages()) {
312319
notificationCallback = std::move(callback);
320+
#ifdef TRAY_ENABLE_TEST_HOOKS
321+
if (nativeNotificationsSuppressed()) {
322+
return;
323+
}
324+
#endif
313325
emit trayIcon->showMessage(title, msg, icon, msecs);
314326
}
315327
}
@@ -320,6 +332,11 @@ void QtTrayMenu::showMessage(const QString &title, const QString &msg, const QSt
320332
}
321333
if (QSystemTrayIcon::supportsMessages()) {
322334
notificationCallback = std::move(callback);
335+
#ifdef TRAY_ENABLE_TEST_HOOKS
336+
if (nativeNotificationsSuppressed()) {
337+
return;
338+
}
339+
#endif
323340
emit trayIcon->showMessage(title, msg, lookupIcon(iconPath), msecs);
324341
}
325342
}

tests/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ include_directories("${GTEST_SOURCE_DIR}/googletest/include" "${GTEST_SOURCE_DIR
2020
if (APPLE)
2121
set(TEST_LIBS "-framework Cocoa")
2222
elseif (WIN32)
23-
set(TEST_LIBS gdi32 gdiplus)
23+
set(TEST_LIBS gdi32 gdiplus ole32 oleaut32 runtimeobject uiautomationcore)
2424
endif()
2525

2626
file(GLOB_RECURSE TEST_SOURCES

tests/unit/test_tray_qt.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class TrayQtCoverageTest: public BaseTest { // NOSONAR(cpp:S3656) - fixture mem
4444
void SetUp() override {
4545
BaseTest::SetUp();
4646

47+
setEnv("TRAY_TEST_SUPPRESS_NATIVE_NOTIFICATIONS", "1");
48+
4749
tray_set_log_callback(nullptr);
4850
tray_set_app_info(nullptr, nullptr, nullptr);
4951

@@ -76,6 +78,8 @@ class TrayQtCoverageTest: public BaseTest { // NOSONAR(cpp:S3656) - fixture mem
7678
trayRunning = false;
7779
}
7880

81+
setEnv("TRAY_TEST_SUPPRESS_NATIVE_NOTIFICATIONS", "0");
82+
7983
tray_set_log_callback(nullptr);
8084
BaseTest::TearDown();
8185
}

tests/utils.cpp

Lines changed: 174 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,175 @@
88
// standard includes
99
#include <chrono>
1010
#include <cstdlib>
11+
#include <cwchar>
1112
#include <thread>
1213

13-
#ifdef __linux__
14+
#ifdef _WIN32
15+
#ifndef NOMINMAX
16+
#define NOMINMAX
17+
#endif
18+
#include <roapi.h>
19+
#include <uiautomationclient.h>
20+
#include <Windows.h>
21+
#include <windows.ui.notifications.h>
22+
#include <winstring.h>
23+
#endif
24+
25+
#if defined(_WIN32) || defined(__linux__)
1426
namespace {
27+
#ifdef _WIN32
28+
namespace notifications = ABI::Windows::UI::Notifications;
29+
30+
template<typename T>
31+
void releaseCom(T *&value) {
32+
if (value != nullptr) {
33+
value->Release();
34+
value = nullptr;
35+
}
36+
}
37+
38+
void clearToastNotificationHistory() {
39+
const HRESULT init_result = RoInitialize(RO_INIT_MULTITHREADED);
40+
const bool should_uninitialize = SUCCEEDED(init_result);
41+
42+
HSTRING class_id = nullptr;
43+
HRESULT result = WindowsCreateString(
44+
RuntimeClass_Windows_UI_Notifications_ToastNotificationManager,
45+
static_cast<UINT32>(std::wcslen(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager)),
46+
&class_id
47+
);
48+
if (FAILED(result)) {
49+
if (should_uninitialize) {
50+
RoUninitialize();
51+
}
52+
return;
53+
}
54+
55+
notifications::IToastNotificationManagerStatics2 *manager = nullptr;
56+
result = RoGetActivationFactory(
57+
class_id,
58+
__uuidof(notifications::IToastNotificationManagerStatics2),
59+
reinterpret_cast<void **>(&manager)
60+
);
61+
WindowsDeleteString(class_id);
62+
if (FAILED(result) || manager == nullptr) {
63+
if (should_uninitialize) {
64+
RoUninitialize();
65+
}
66+
return;
67+
}
68+
69+
notifications::IToastNotificationHistory *history = nullptr;
70+
result = manager->get_History(&history);
71+
manager->Release();
72+
if (SUCCEEDED(result) && history != nullptr) {
73+
(void) history->Clear();
74+
history->Release();
75+
}
76+
77+
if (should_uninitialize) {
78+
RoUninitialize();
79+
}
80+
}
81+
82+
bool isCloseButtonInToastArea(IUIAutomationElement *button) {
83+
BSTR name = nullptr;
84+
if (FAILED(button->get_CurrentName(&name))) {
85+
if (name != nullptr) {
86+
SysFreeString(name);
87+
}
88+
return false;
89+
}
90+
if (name == nullptr) {
91+
return false;
92+
}
93+
94+
const bool is_close_button = std::wcscmp(name, L"Close") == 0;
95+
SysFreeString(name);
96+
if (!is_close_button) {
97+
return false;
98+
}
99+
100+
RECT rect {};
101+
if (FAILED(button->get_CurrentBoundingRectangle(&rect))) {
102+
return false;
103+
}
104+
105+
const int screen_width = GetSystemMetrics(SM_CXSCREEN);
106+
const int screen_height = GetSystemMetrics(SM_CYSCREEN);
107+
return rect.left > screen_width / 2 && rect.top > screen_height / 2 && rect.right <= screen_width && rect.bottom <= screen_height;
108+
}
109+
110+
void invokeButton(IUIAutomationElement *button) {
111+
IUnknown *pattern = nullptr;
112+
if (FAILED(button->GetCurrentPattern(UIA_InvokePatternId, &pattern)) || pattern == nullptr) {
113+
return;
114+
}
115+
116+
IUIAutomationInvokePattern *invoke_pattern = nullptr;
117+
if (SUCCEEDED(pattern->QueryInterface(IID_PPV_ARGS(&invoke_pattern))) && invoke_pattern != nullptr) {
118+
(void) invoke_pattern->Invoke();
119+
}
120+
releaseCom(invoke_pattern);
121+
releaseCom(pattern);
122+
}
123+
124+
void closeVisibleToastNotifications() {
125+
const HRESULT init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
126+
if (FAILED(init_result) && init_result != RPC_E_CHANGED_MODE) {
127+
return;
128+
}
129+
const bool should_uninitialize = SUCCEEDED(init_result);
130+
131+
IUIAutomation *automation = nullptr;
132+
HRESULT result = CoCreateInstance(CLSID_CUIAutomation, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&automation));
133+
if (FAILED(result) || automation == nullptr) {
134+
if (should_uninitialize) {
135+
CoUninitialize();
136+
}
137+
return;
138+
}
139+
140+
IUIAutomationElement *root = nullptr;
141+
IUIAutomationCondition *button_condition = nullptr;
142+
IUIAutomationElementArray *buttons = nullptr;
143+
VARIANT button_type {};
144+
button_type.vt = VT_I4;
145+
button_type.lVal = UIA_ButtonControlTypeId;
146+
147+
result = automation->GetRootElement(&root);
148+
if (SUCCEEDED(result) && root != nullptr) {
149+
result = automation->CreatePropertyCondition(UIA_ControlTypePropertyId, button_type, &button_condition);
150+
}
151+
if (SUCCEEDED(result) && button_condition != nullptr) {
152+
result = root->FindAll(TreeScope_Descendants, button_condition, &buttons);
153+
}
154+
155+
int button_count = 0;
156+
if (SUCCEEDED(result) && buttons != nullptr && SUCCEEDED(buttons->get_Length(&button_count))) {
157+
for (int i = 0; i < button_count; ++i) {
158+
IUIAutomationElement *button = nullptr;
159+
if (SUCCEEDED(buttons->GetElement(i, &button)) && button != nullptr) {
160+
if (isCloseButtonInToastArea(button)) {
161+
invokeButton(button);
162+
}
163+
releaseCom(button);
164+
}
165+
}
166+
}
167+
168+
releaseCom(buttons);
169+
releaseCom(button_condition);
170+
releaseCom(root);
171+
releaseCom(automation);
172+
173+
if (should_uninitialize) {
174+
CoUninitialize();
175+
}
176+
}
177+
#endif
178+
179+
#ifdef __linux__
15180
void closeFreedesktopNotifications() {
16181
constexpr const char *close_notifications =
17182
"if command -v dbus-send >/dev/null 2>&1; then "
@@ -24,6 +189,7 @@ namespace {
24189
"fi";
25190
(void) std::system(close_notifications); // NOSONAR(cpp:S4721) - test-only cleanup of desktop notifications
26191
}
192+
#endif
27193
} // namespace
28194
#endif
29195

@@ -42,7 +208,12 @@ int setEnv(const std::string &name, const std::string &value) {
42208
}
43209

44210
void dismissNativeNotifications() {
45-
#ifdef __linux__
211+
#ifdef _WIN32
212+
clearToastNotificationHistory();
213+
closeVisibleToastNotifications();
214+
constexpr auto wait_timeout = std::chrono::milliseconds(500);
215+
std::this_thread::sleep_for(wait_timeout);
216+
#elif defined(__linux__)
46217
closeFreedesktopNotifications();
47218
constexpr auto wait_timeout = std::chrono::milliseconds(500);
48219
std::this_thread::sleep_for(wait_timeout);
@@ -51,8 +222,7 @@ void dismissNativeNotifications() {
51222

52223
void waitForNativeNotificationTimeout() {
53224
#ifdef _WIN32
54-
constexpr auto wait_timeout = std::chrono::milliseconds(6000);
55-
std::this_thread::sleep_for(wait_timeout);
225+
dismissNativeNotifications();
56226
#elif defined(__linux__)
57227
dismissNativeNotifications();
58228
#endif

0 commit comments

Comments
 (0)