@flighthq/tray 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/tray.d.ts +26 -0
- package/dist/tray.d.ts.map +1 -0
- package/dist/tray.js +203 -0
- package/dist/tray.js.map +1 -0
- package/package.json +37 -0
- package/src/tray.test.ts +647 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC"}
|
package/dist/tray.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MenuItemTemplate, RectangleLike, TrayBackend, TrayBalloonOptions, TrayCapabilities, TrayEventData, TrayIcon, TrayIconOptions } from '@flighthq/types';
|
|
2
|
+
import type { Vector2Like } from '@flighthq/types';
|
|
3
|
+
export declare function createTrayIcon(options?: Readonly<TrayIconOptions>): TrayIcon | null;
|
|
4
|
+
export declare function createWebTrayBackend(): TrayBackend;
|
|
5
|
+
export declare function destroyTrayIcon(tray: TrayIcon): void;
|
|
6
|
+
export declare function displayTrayBalloon(tray: TrayIcon, options: Readonly<TrayBalloonOptions>): void;
|
|
7
|
+
export declare function getTrayBackend(): TrayBackend;
|
|
8
|
+
export declare function getTrayCapabilities(): Readonly<TrayCapabilities>;
|
|
9
|
+
export declare function getTrayIconBounds(tray: TrayIcon): Readonly<RectangleLike> | null;
|
|
10
|
+
export declare function getTrayIcons(): readonly TrayIcon[];
|
|
11
|
+
export declare function getTrayIconTitle(tray: TrayIcon): string;
|
|
12
|
+
export declare function getTrayIconTooltip(tray: TrayIcon): string;
|
|
13
|
+
export declare function isTrayDestroyed(tray: TrayIcon): boolean;
|
|
14
|
+
export declare function onTrayEvent(listener: (event: Readonly<TrayEventData>) => void): () => void;
|
|
15
|
+
export declare function popupTrayContextMenu(tray: TrayIcon, position?: Readonly<Vector2Like>): void;
|
|
16
|
+
export declare function removeTrayBalloon(tray: TrayIcon): void;
|
|
17
|
+
export declare function setTrayBackend(backend: TrayBackend | null): void;
|
|
18
|
+
export declare function setTrayIcon(tray: TrayIcon, icon: string): void;
|
|
19
|
+
export declare function setTrayIconContextMenu(tray: TrayIcon, items: readonly MenuItemTemplate[]): void;
|
|
20
|
+
export declare function setTrayIconTemplate(tray: TrayIcon, isTemplate: boolean): void;
|
|
21
|
+
export declare function setTrayIconTitle(tray: TrayIcon, title: string): void;
|
|
22
|
+
export declare function setTrayIconTooltip(tray: TrayIcon, tooltip: string): void;
|
|
23
|
+
export declare function setTrayIgnoreDoubleClickEvents(tray: TrayIcon, ignore: boolean): void;
|
|
24
|
+
export declare function setTrayPressedIcon(tray: TrayIcon, icon: string): void;
|
|
25
|
+
export declare function startTrayIconAnimation(tray: TrayIcon, frames: readonly string[], intervalMs: number): () => void;
|
|
26
|
+
//# sourceMappingURL=tray.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tray.d.ts","sourceRoot":"","sources":["../src/tray.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,QAAQ,EACR,eAAe,EAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAcnD,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,QAAQ,GAAG,IAAI,CAGnF;AAKD,wBAAgB,oBAAoB,IAAI,WAAW,CAmElD;AAGD,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAEpD;AAID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAE9F;AAGD,wBAAgB,cAAc,IAAI,WAAW,CAG5C;AAKD,wBAAgB,mBAAmB,IAAI,QAAQ,CAAC,gBAAgB,CAAC,CAEhE;AAID,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,IAAI,CAEhF;AAGD,wBAAgB,YAAY,IAAI,SAAS,QAAQ,EAAE,CAIlD;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEvD;AAGD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEzD;AAID,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAEvD;AAKD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAE1F;AAKD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,CAE3F;AAGD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAEtD;AAGD,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAEhE;AAID,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE9D;AAID,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAE/F;AAKD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,GAAG,IAAI,CAE7E;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAEpE;AAGD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAExE;AAID,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAEpF;AAID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAErE;AAOD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,IAAI,CAShH"}
|
package/dist/tray.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Web tray capability constants. Web has no system tray — all capabilities are false.
|
|
2
|
+
const WEB_CAPABILITIES = {
|
|
3
|
+
balloon: false,
|
|
4
|
+
bounds: false,
|
|
5
|
+
clickEvents: false,
|
|
6
|
+
dropFiles: false,
|
|
7
|
+
pressedIcon: false,
|
|
8
|
+
title: false,
|
|
9
|
+
};
|
|
10
|
+
// Creates a tray icon, or null when the host has no system tray (e.g. web). The backend returns -1 to
|
|
11
|
+
// signal no tray; this translates that to a null sentinel for the caller.
|
|
12
|
+
export function createTrayIcon(options) {
|
|
13
|
+
const id = getTrayBackend().create(options ?? {});
|
|
14
|
+
return id < 0 ? null : { id };
|
|
15
|
+
}
|
|
16
|
+
// Builds the default web backend. Web has no system tray, so create returns -1 and the tray mutators
|
|
17
|
+
// are no-ops — a native host (Electron's Tray, Tauri) is required for the tray icon itself. (The
|
|
18
|
+
// application/dock badge lives in @flighthq/app's setAppBadgeCount, not here.)
|
|
19
|
+
export function createWebTrayBackend() {
|
|
20
|
+
return {
|
|
21
|
+
create() {
|
|
22
|
+
// No tray on web. -1 signals "unsupported"; createTrayIcon maps it to null.
|
|
23
|
+
return -1;
|
|
24
|
+
},
|
|
25
|
+
destroy() {
|
|
26
|
+
// No-op: web has no tray icon to destroy.
|
|
27
|
+
},
|
|
28
|
+
displayBalloon() {
|
|
29
|
+
// No-op: balloon notifications require a native host (Windows only).
|
|
30
|
+
},
|
|
31
|
+
getBounds() {
|
|
32
|
+
// No tray on web; null signals unavailable.
|
|
33
|
+
return null;
|
|
34
|
+
},
|
|
35
|
+
getCapabilities() {
|
|
36
|
+
return WEB_CAPABILITIES;
|
|
37
|
+
},
|
|
38
|
+
getTitle() {
|
|
39
|
+
// No tray on web.
|
|
40
|
+
return '';
|
|
41
|
+
},
|
|
42
|
+
getTooltip() {
|
|
43
|
+
// No tray on web.
|
|
44
|
+
return '';
|
|
45
|
+
},
|
|
46
|
+
isDestroyed() {
|
|
47
|
+
// No tray icons exist on web; treat every id as destroyed.
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
listIds() {
|
|
51
|
+
// No tray icons exist on web.
|
|
52
|
+
return [];
|
|
53
|
+
},
|
|
54
|
+
popUpContextMenu() {
|
|
55
|
+
// No-op: web has no context menu to pop up.
|
|
56
|
+
},
|
|
57
|
+
removeBalloon() {
|
|
58
|
+
// No-op: balloon notifications require a native host (Windows only).
|
|
59
|
+
},
|
|
60
|
+
setContextMenu() {
|
|
61
|
+
// No-op: web has no tray icon — a native host (Electron/Tauri) is required.
|
|
62
|
+
},
|
|
63
|
+
setIcon() {
|
|
64
|
+
// No-op: web has no tray icon to update.
|
|
65
|
+
},
|
|
66
|
+
setIgnoreDoubleClickEvents() {
|
|
67
|
+
// No-op: web has no tray icon double-click behavior to configure.
|
|
68
|
+
},
|
|
69
|
+
setPressedIcon() {
|
|
70
|
+
// No-op: web has no tray icon; pressed icon is macOS-specific.
|
|
71
|
+
},
|
|
72
|
+
setTemplate() {
|
|
73
|
+
// No-op: template images are a macOS menu-bar concept; irrelevant on web.
|
|
74
|
+
},
|
|
75
|
+
setTitle() {
|
|
76
|
+
// No-op: web has no tray icon to update.
|
|
77
|
+
},
|
|
78
|
+
setTooltip() {
|
|
79
|
+
// No-op: web has no tray icon to update.
|
|
80
|
+
},
|
|
81
|
+
subscribe() {
|
|
82
|
+
// No tray on web — a native host (Electron/Tauri) is required to emit tray events.
|
|
83
|
+
return () => { };
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Destroys a tray icon and frees its host resource. No-op when the host has no tray.
|
|
88
|
+
export function destroyTrayIcon(tray) {
|
|
89
|
+
getTrayBackend().destroy(tray.id);
|
|
90
|
+
}
|
|
91
|
+
// Displays a Windows balloon notification from the tray icon. No-op on macOS/Linux and on web.
|
|
92
|
+
// Balloon lifecycle events (balloonShow/balloonClick/balloonClose) are emitted via onTrayEvent.
|
|
93
|
+
export function displayTrayBalloon(tray, options) {
|
|
94
|
+
getTrayBackend().displayBalloon(tray.id, options);
|
|
95
|
+
}
|
|
96
|
+
// The active tray backend, or a lazily-created web default. There is always a backend.
|
|
97
|
+
export function getTrayBackend() {
|
|
98
|
+
if (_backend === null)
|
|
99
|
+
_backend = createWebTrayBackend();
|
|
100
|
+
return _backend;
|
|
101
|
+
}
|
|
102
|
+
// Returns the capability flags for the active tray backend. Use before calling APIs that may
|
|
103
|
+
// silently no-op — for example, check capabilities.balloon before displayTrayBalloon, or
|
|
104
|
+
// capabilities.bounds before getTrayIconBounds. On web all flags are false.
|
|
105
|
+
export function getTrayCapabilities() {
|
|
106
|
+
return getTrayBackend().getCapabilities();
|
|
107
|
+
}
|
|
108
|
+
// Returns the screen bounds of the tray icon (x/y/width/height), or null when the platform does not
|
|
109
|
+
// expose icon geometry (Linux/AppIndicator, web). Use for anchoring popovers or windows to the icon.
|
|
110
|
+
export function getTrayIconBounds(tray) {
|
|
111
|
+
return getTrayBackend().getBounds(tray.id);
|
|
112
|
+
}
|
|
113
|
+
// Returns all live tray icon handles known to the active backend. On web this is always empty.
|
|
114
|
+
export function getTrayIcons() {
|
|
115
|
+
return getTrayBackend()
|
|
116
|
+
.listIds()
|
|
117
|
+
.map((id) => ({ id }));
|
|
118
|
+
}
|
|
119
|
+
// Returns the current title text of a tray icon, or an empty string when unavailable (web, non-macOS).
|
|
120
|
+
export function getTrayIconTitle(tray) {
|
|
121
|
+
return getTrayBackend().getTitle(tray.id);
|
|
122
|
+
}
|
|
123
|
+
// Returns the current hover tooltip text of a tray icon, or an empty string when unavailable (web).
|
|
124
|
+
export function getTrayIconTooltip(tray) {
|
|
125
|
+
return getTrayBackend().getTooltip(tray.id);
|
|
126
|
+
}
|
|
127
|
+
// Returns whether a tray icon has been destroyed. Returns true on web (no trays exist).
|
|
128
|
+
// Use this to guard calls after destroyTrayIcon when the tray lifecycle is unclear.
|
|
129
|
+
export function isTrayDestroyed(tray) {
|
|
130
|
+
return getTrayBackend().isDestroyed(tray.id);
|
|
131
|
+
}
|
|
132
|
+
// Subscribes to tray icon events, delivering a rich TrayEventData payload (id, type, bounds,
|
|
133
|
+
// position, modifier keys, and drop payloads). Returns an unsubscribe function. On web this
|
|
134
|
+
// never fires (no tray); a native host is required.
|
|
135
|
+
export function onTrayEvent(listener) {
|
|
136
|
+
return getTrayBackend().subscribe(listener);
|
|
137
|
+
}
|
|
138
|
+
// Programmatically shows the attached context menu, optionally at a specific screen position.
|
|
139
|
+
// On web this is a no-op. Useful for showing the menu in response to a custom gesture or shortcut
|
|
140
|
+
// without waiting for the user to right-click the tray icon.
|
|
141
|
+
export function popupTrayContextMenu(tray, position) {
|
|
142
|
+
getTrayBackend().popUpContextMenu(tray.id, position);
|
|
143
|
+
}
|
|
144
|
+
// Dismisses the currently-displayed Windows balloon notification. No-op on macOS/Linux and web.
|
|
145
|
+
export function removeTrayBalloon(tray) {
|
|
146
|
+
getTrayBackend().removeBalloon(tray.id);
|
|
147
|
+
}
|
|
148
|
+
// Installs a native host tray backend; pass null to fall back to the web default.
|
|
149
|
+
export function setTrayBackend(backend) {
|
|
150
|
+
_backend = backend;
|
|
151
|
+
}
|
|
152
|
+
// Sets the image for the tray icon. Accepts the same icon path/data-URI accepted by createTrayIcon.
|
|
153
|
+
// Use this for runtime status updates (indicators, spinners, theme changes).
|
|
154
|
+
export function setTrayIcon(tray, icon) {
|
|
155
|
+
getTrayBackend().setIcon(tray.id, icon);
|
|
156
|
+
}
|
|
157
|
+
// Sets the context menu attached to a tray icon. Shown on right-click (or popupTrayContextMenu).
|
|
158
|
+
// No-op when the host has no tray.
|
|
159
|
+
export function setTrayIconContextMenu(tray, items) {
|
|
160
|
+
getTrayBackend().setContextMenu(tray.id, items);
|
|
161
|
+
}
|
|
162
|
+
// Marks the tray icon as a macOS template image. Template images auto-invert for light/dark menu
|
|
163
|
+
// bars. No-op on Windows/Linux and on web. Set iconTemplate:true on TrayIconOptions at creation to
|
|
164
|
+
// combine with the initial icon, or call this after creation to update the flag dynamically.
|
|
165
|
+
export function setTrayIconTemplate(tray, isTemplate) {
|
|
166
|
+
getTrayBackend().setTemplate(tray.id, isTemplate);
|
|
167
|
+
}
|
|
168
|
+
// Sets the title text displayed next to the tray icon (macOS menu bar only). No-op on other platforms.
|
|
169
|
+
export function setTrayIconTitle(tray, title) {
|
|
170
|
+
getTrayBackend().setTitle(tray.id, title);
|
|
171
|
+
}
|
|
172
|
+
// Sets the hover tooltip for the tray icon. No-op when the host has no tray.
|
|
173
|
+
export function setTrayIconTooltip(tray, tooltip) {
|
|
174
|
+
getTrayBackend().setTooltip(tray.id, tooltip);
|
|
175
|
+
}
|
|
176
|
+
// Sets whether the host should collapse double-click events into individual click events (macOS).
|
|
177
|
+
// No-op on Windows/Linux and on web.
|
|
178
|
+
export function setTrayIgnoreDoubleClickEvents(tray, ignore) {
|
|
179
|
+
getTrayBackend().setIgnoreDoubleClickEvents(tray.id, ignore);
|
|
180
|
+
}
|
|
181
|
+
// Sets the image shown when the tray icon is pressed (macOS only). Electron calls this
|
|
182
|
+
// setPressedImage. No-op on Windows/Linux and on web.
|
|
183
|
+
export function setTrayPressedIcon(tray, icon) {
|
|
184
|
+
getTrayBackend().setPressedIcon(tray.id, icon);
|
|
185
|
+
}
|
|
186
|
+
// Starts an animated icon sequence by cycling through the given frames at the specified interval.
|
|
187
|
+
// The caller owns the timer — this function is a thin helper over setTrayIcon that starts an
|
|
188
|
+
// interval and returns a stop function. Call the returned function (or stopTrayIconAnimation) to
|
|
189
|
+
// cancel. The tray icon is not destroyed when the animation stops.
|
|
190
|
+
// Note: interval timing is best-effort; the actual frame rate depends on the host event loop.
|
|
191
|
+
export function startTrayIconAnimation(tray, frames, intervalMs) {
|
|
192
|
+
if (frames.length === 0)
|
|
193
|
+
return () => { };
|
|
194
|
+
let index = 0;
|
|
195
|
+
setTrayIcon(tray, frames[index]);
|
|
196
|
+
const handle = setInterval(() => {
|
|
197
|
+
index = (index + 1) % frames.length;
|
|
198
|
+
setTrayIcon(tray, frames[index]);
|
|
199
|
+
}, intervalMs);
|
|
200
|
+
return () => clearInterval(handle);
|
|
201
|
+
}
|
|
202
|
+
let _backend = null;
|
|
203
|
+
//# sourceMappingURL=tray.js.map
|
package/dist/tray.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tray.js","sourceRoot":"","sources":["../src/tray.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/tray",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/types": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"typescript": "^5.3.0"
|
|
34
|
+
},
|
|
35
|
+
"description": "Tray icon and application badge over a swappable web/native backend",
|
|
36
|
+
"sideEffects": false
|
|
37
|
+
}
|
package/src/tray.test.ts
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MenuItemTemplate,
|
|
3
|
+
TrayBackend,
|
|
4
|
+
TrayBalloonOptions,
|
|
5
|
+
TrayCapabilities,
|
|
6
|
+
TrayEventData,
|
|
7
|
+
TrayEventType,
|
|
8
|
+
TrayIconOptions,
|
|
9
|
+
} from '@flighthq/types';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createTrayIcon,
|
|
13
|
+
createWebTrayBackend,
|
|
14
|
+
destroyTrayIcon,
|
|
15
|
+
displayTrayBalloon,
|
|
16
|
+
getTrayBackend,
|
|
17
|
+
getTrayCapabilities,
|
|
18
|
+
getTrayIconBounds,
|
|
19
|
+
getTrayIcons,
|
|
20
|
+
getTrayIconTitle,
|
|
21
|
+
getTrayIconTooltip,
|
|
22
|
+
isTrayDestroyed,
|
|
23
|
+
onTrayEvent,
|
|
24
|
+
popupTrayContextMenu,
|
|
25
|
+
removeTrayBalloon,
|
|
26
|
+
setTrayBackend,
|
|
27
|
+
setTrayIcon,
|
|
28
|
+
setTrayIconContextMenu,
|
|
29
|
+
setTrayIconTemplate,
|
|
30
|
+
setTrayIconTitle,
|
|
31
|
+
setTrayIconTooltip,
|
|
32
|
+
setTrayIgnoreDoubleClickEvents,
|
|
33
|
+
setTrayPressedIcon,
|
|
34
|
+
startTrayIconAnimation,
|
|
35
|
+
} from './tray';
|
|
36
|
+
|
|
37
|
+
// Fake backend state per icon.
|
|
38
|
+
interface FakeTray {
|
|
39
|
+
balloon: TrayBalloonOptions | null;
|
|
40
|
+
contextMenu: readonly MenuItemTemplate[] | null;
|
|
41
|
+
destroyed: boolean;
|
|
42
|
+
icon: string;
|
|
43
|
+
id: number;
|
|
44
|
+
ignoreDoubleClick: boolean;
|
|
45
|
+
isTemplate: boolean;
|
|
46
|
+
options: TrayIconOptions;
|
|
47
|
+
pressedIcon: string;
|
|
48
|
+
title: string;
|
|
49
|
+
tooltip: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Full fake backend implementing every TrayBackend method for test control.
|
|
53
|
+
function fakeBackend(caps: Partial<TrayCapabilities> = {}): TrayBackend & {
|
|
54
|
+
capabilities: TrayCapabilities;
|
|
55
|
+
fireEvent(event: TrayEventData): void;
|
|
56
|
+
lastPopupPosition: { x: number; y: number } | null;
|
|
57
|
+
trays: Map<number, FakeTray>;
|
|
58
|
+
} {
|
|
59
|
+
const capabilities: TrayCapabilities = {
|
|
60
|
+
balloon: caps.balloon ?? true,
|
|
61
|
+
bounds: caps.bounds ?? true,
|
|
62
|
+
clickEvents: caps.clickEvents ?? true,
|
|
63
|
+
dropFiles: caps.dropFiles ?? false,
|
|
64
|
+
pressedIcon: caps.pressedIcon ?? true,
|
|
65
|
+
title: caps.title ?? true,
|
|
66
|
+
};
|
|
67
|
+
let nextId = 1;
|
|
68
|
+
let eventListener: ((event: Readonly<TrayEventData>) => void) | null = null;
|
|
69
|
+
const trays = new Map<number, FakeTray>();
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
capabilities,
|
|
73
|
+
trays,
|
|
74
|
+
lastPopupPosition: null,
|
|
75
|
+
create(options) {
|
|
76
|
+
const id = nextId++;
|
|
77
|
+
trays.set(id, {
|
|
78
|
+
balloon: null,
|
|
79
|
+
contextMenu: null,
|
|
80
|
+
destroyed: false,
|
|
81
|
+
icon: options.icon ?? '',
|
|
82
|
+
id,
|
|
83
|
+
ignoreDoubleClick: false,
|
|
84
|
+
isTemplate: options.iconTemplate ?? false,
|
|
85
|
+
options,
|
|
86
|
+
pressedIcon: '',
|
|
87
|
+
title: options.title ?? '',
|
|
88
|
+
tooltip: options.tooltip ?? '',
|
|
89
|
+
});
|
|
90
|
+
return id;
|
|
91
|
+
},
|
|
92
|
+
destroy(id) {
|
|
93
|
+
const tray = trays.get(id);
|
|
94
|
+
if (tray) tray.destroyed = true;
|
|
95
|
+
},
|
|
96
|
+
displayBalloon(id, options) {
|
|
97
|
+
const tray = trays.get(id);
|
|
98
|
+
if (tray) tray.balloon = options;
|
|
99
|
+
},
|
|
100
|
+
getBounds(id) {
|
|
101
|
+
if (!trays.has(id)) return null;
|
|
102
|
+
return { height: 22, width: 22, x: 100, y: 0 };
|
|
103
|
+
},
|
|
104
|
+
getCapabilities() {
|
|
105
|
+
return capabilities;
|
|
106
|
+
},
|
|
107
|
+
getTitle(id) {
|
|
108
|
+
return trays.get(id)?.title ?? '';
|
|
109
|
+
},
|
|
110
|
+
getTooltip(id) {
|
|
111
|
+
return trays.get(id)?.tooltip ?? '';
|
|
112
|
+
},
|
|
113
|
+
isDestroyed(id) {
|
|
114
|
+
const tray = trays.get(id);
|
|
115
|
+
return tray === undefined || tray.destroyed;
|
|
116
|
+
},
|
|
117
|
+
listIds() {
|
|
118
|
+
return Array.from(trays.keys()).filter((id) => !trays.get(id)!.destroyed);
|
|
119
|
+
},
|
|
120
|
+
popUpContextMenu(id, position) {
|
|
121
|
+
this.lastPopupPosition = position ? { x: position.x, y: position.y } : null;
|
|
122
|
+
},
|
|
123
|
+
removeBalloon(id) {
|
|
124
|
+
const tray = trays.get(id);
|
|
125
|
+
if (tray) tray.balloon = null;
|
|
126
|
+
},
|
|
127
|
+
setContextMenu(id, items) {
|
|
128
|
+
const tray = trays.get(id);
|
|
129
|
+
if (tray) tray.contextMenu = items;
|
|
130
|
+
},
|
|
131
|
+
setIcon(id, icon) {
|
|
132
|
+
const tray = trays.get(id);
|
|
133
|
+
if (tray) tray.icon = icon;
|
|
134
|
+
},
|
|
135
|
+
setIgnoreDoubleClickEvents(id, ignore) {
|
|
136
|
+
const tray = trays.get(id);
|
|
137
|
+
if (tray) tray.ignoreDoubleClick = ignore;
|
|
138
|
+
},
|
|
139
|
+
setPressedIcon(id, icon) {
|
|
140
|
+
const tray = trays.get(id);
|
|
141
|
+
if (tray) tray.pressedIcon = icon;
|
|
142
|
+
},
|
|
143
|
+
setTemplate(id, isTemplate) {
|
|
144
|
+
const tray = trays.get(id);
|
|
145
|
+
if (tray) tray.isTemplate = isTemplate;
|
|
146
|
+
},
|
|
147
|
+
setTitle(id, title) {
|
|
148
|
+
const tray = trays.get(id);
|
|
149
|
+
if (tray) tray.title = title;
|
|
150
|
+
},
|
|
151
|
+
setTooltip(id, tooltip) {
|
|
152
|
+
const tray = trays.get(id);
|
|
153
|
+
if (tray) tray.tooltip = tooltip;
|
|
154
|
+
},
|
|
155
|
+
subscribe(listener) {
|
|
156
|
+
eventListener = listener;
|
|
157
|
+
return () => {
|
|
158
|
+
eventListener = null;
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
fireEvent(event) {
|
|
162
|
+
eventListener?.(event);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function makeTrayEvent(overrides: Partial<TrayEventData> = {}): TrayEventData {
|
|
168
|
+
return {
|
|
169
|
+
altKey: false,
|
|
170
|
+
bounds: null,
|
|
171
|
+
ctrlKey: false,
|
|
172
|
+
dropFiles: null,
|
|
173
|
+
dropText: null,
|
|
174
|
+
id: 1,
|
|
175
|
+
metaKey: false,
|
|
176
|
+
position: null,
|
|
177
|
+
shiftKey: false,
|
|
178
|
+
type: 'click',
|
|
179
|
+
...overrides,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
afterEach(() => setTrayBackend(null));
|
|
184
|
+
|
|
185
|
+
describe('createTrayIcon', () => {
|
|
186
|
+
it('creates a tray via the active backend', () => {
|
|
187
|
+
const backend = fakeBackend();
|
|
188
|
+
setTrayBackend(backend);
|
|
189
|
+
const tray = createTrayIcon({ tooltip: 'App' });
|
|
190
|
+
expect(tray).not.toBeNull();
|
|
191
|
+
expect(backend.trays.get(tray!.id)!.options.tooltip).toBe('App');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('passes iconTemplate option to backend', () => {
|
|
195
|
+
const backend = fakeBackend();
|
|
196
|
+
setTrayBackend(backend);
|
|
197
|
+
const tray = createTrayIcon({ icon: 'icon.png', iconTemplate: true });
|
|
198
|
+
expect(backend.trays.get(tray!.id)!.isTemplate).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('returns null on the web backend (no tray)', () => {
|
|
202
|
+
expect(createTrayIcon()).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('createWebTrayBackend', () => {
|
|
207
|
+
it('returns sentinels without throwing (web has no tray)', () => {
|
|
208
|
+
const backend = createWebTrayBackend();
|
|
209
|
+
expect(backend.create({})).toBe(-1);
|
|
210
|
+
expect(() => backend.destroy(0)).not.toThrow();
|
|
211
|
+
expect(() => backend.setTooltip(0, 'x')).not.toThrow();
|
|
212
|
+
expect(() => backend.setTitle(0, 'x')).not.toThrow();
|
|
213
|
+
expect(() => backend.setContextMenu(0, [])).not.toThrow();
|
|
214
|
+
expect(() => backend.setIcon(0, 'icon.png')).not.toThrow();
|
|
215
|
+
expect(() => backend.setTemplate(0, true)).not.toThrow();
|
|
216
|
+
expect(() => backend.setPressedIcon(0, 'pressed.png')).not.toThrow();
|
|
217
|
+
expect(() => backend.setIgnoreDoubleClickEvents(0, true)).not.toThrow();
|
|
218
|
+
expect(() => backend.popUpContextMenu(0)).not.toThrow();
|
|
219
|
+
expect(() => backend.displayBalloon(0, { title: 'T', text: 'B' })).not.toThrow();
|
|
220
|
+
expect(() => backend.removeBalloon(0)).not.toThrow();
|
|
221
|
+
expect(backend.getBounds(0)).toBeNull();
|
|
222
|
+
expect(backend.getTitle(0)).toBe('');
|
|
223
|
+
expect(backend.getTooltip(0)).toBe('');
|
|
224
|
+
expect(backend.isDestroyed(0)).toBe(true);
|
|
225
|
+
expect(backend.listIds()).toEqual([]);
|
|
226
|
+
expect(typeof backend.subscribe(() => {})).toBe('function');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('web capabilities are all false', () => {
|
|
230
|
+
const backend = createWebTrayBackend();
|
|
231
|
+
const caps = backend.getCapabilities();
|
|
232
|
+
expect(caps.balloon).toBe(false);
|
|
233
|
+
expect(caps.bounds).toBe(false);
|
|
234
|
+
expect(caps.clickEvents).toBe(false);
|
|
235
|
+
expect(caps.dropFiles).toBe(false);
|
|
236
|
+
expect(caps.pressedIcon).toBe(false);
|
|
237
|
+
expect(caps.title).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('destroyTrayIcon', () => {
|
|
242
|
+
it('destroys the tray via the active backend', () => {
|
|
243
|
+
const backend = fakeBackend();
|
|
244
|
+
setTrayBackend(backend);
|
|
245
|
+
const tray = createTrayIcon()!;
|
|
246
|
+
destroyTrayIcon(tray);
|
|
247
|
+
expect(backend.trays.get(tray.id)!.destroyed).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('displayTrayBalloon', () => {
|
|
252
|
+
it('passes balloon options to the backend', () => {
|
|
253
|
+
const backend = fakeBackend();
|
|
254
|
+
setTrayBackend(backend);
|
|
255
|
+
const tray = createTrayIcon()!;
|
|
256
|
+
const opts: TrayBalloonOptions = { title: 'Alert', text: 'Something happened' };
|
|
257
|
+
displayTrayBalloon(tray, opts);
|
|
258
|
+
expect(backend.trays.get(tray.id)!.balloon).toStrictEqual(opts);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('accepts optional balloon fields', () => {
|
|
262
|
+
const backend = fakeBackend();
|
|
263
|
+
setTrayBackend(backend);
|
|
264
|
+
const tray = createTrayIcon()!;
|
|
265
|
+
const opts: TrayBalloonOptions = {
|
|
266
|
+
title: 'Alert',
|
|
267
|
+
text: 'Body',
|
|
268
|
+
iconType: 'warning',
|
|
269
|
+
largeIcon: true,
|
|
270
|
+
noSound: true,
|
|
271
|
+
respectQuietTime: true,
|
|
272
|
+
};
|
|
273
|
+
displayTrayBalloon(tray, opts);
|
|
274
|
+
expect(backend.trays.get(tray.id)!.balloon?.iconType).toBe('warning');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('is a no-op on the web backend', () => {
|
|
278
|
+
expect(() => displayTrayBalloon({ id: 0 }, { title: 'T', text: 'B' })).not.toThrow();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('getTrayBackend', () => {
|
|
283
|
+
it('falls back to a web backend', () => {
|
|
284
|
+
expect(getTrayBackend()).not.toBeNull();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('returns the registered backend', () => {
|
|
288
|
+
const backend = fakeBackend();
|
|
289
|
+
setTrayBackend(backend);
|
|
290
|
+
expect(getTrayBackend()).toBe(backend);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('getTrayCapabilities', () => {
|
|
295
|
+
it('returns capabilities from the active backend', () => {
|
|
296
|
+
const backend = fakeBackend({ balloon: true, bounds: false });
|
|
297
|
+
setTrayBackend(backend);
|
|
298
|
+
const caps = getTrayCapabilities();
|
|
299
|
+
expect(caps.balloon).toBe(true);
|
|
300
|
+
expect(caps.bounds).toBe(false);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('returns all-false capabilities on web', () => {
|
|
304
|
+
const caps = getTrayCapabilities();
|
|
305
|
+
expect(Object.values(caps).every((v) => v === false)).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('getTrayIconBounds', () => {
|
|
310
|
+
it('returns bounds from the backend', () => {
|
|
311
|
+
const backend = fakeBackend();
|
|
312
|
+
setTrayBackend(backend);
|
|
313
|
+
const tray = createTrayIcon()!;
|
|
314
|
+
const bounds = getTrayIconBounds(tray);
|
|
315
|
+
expect(bounds).not.toBeNull();
|
|
316
|
+
expect(bounds!.width).toBeGreaterThan(0);
|
|
317
|
+
expect(bounds!.height).toBeGreaterThan(0);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('returns null on web', () => {
|
|
321
|
+
expect(getTrayIconBounds({ id: 0 })).toBeNull();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('getTrayIcons', () => {
|
|
326
|
+
it('returns all live tray icons', () => {
|
|
327
|
+
const backend = fakeBackend();
|
|
328
|
+
setTrayBackend(backend);
|
|
329
|
+
const t1 = createTrayIcon()!;
|
|
330
|
+
const t2 = createTrayIcon()!;
|
|
331
|
+
const icons = getTrayIcons();
|
|
332
|
+
expect(icons.map((t) => t.id)).toContain(t1.id);
|
|
333
|
+
expect(icons.map((t) => t.id)).toContain(t2.id);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('excludes destroyed tray icons', () => {
|
|
337
|
+
const backend = fakeBackend();
|
|
338
|
+
setTrayBackend(backend);
|
|
339
|
+
const t1 = createTrayIcon()!;
|
|
340
|
+
destroyTrayIcon(t1);
|
|
341
|
+
const icons = getTrayIcons();
|
|
342
|
+
expect(icons.map((t) => t.id)).not.toContain(t1.id);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('returns empty array on web', () => {
|
|
346
|
+
expect(getTrayIcons()).toEqual([]);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe('getTrayIconTitle', () => {
|
|
351
|
+
it('returns the current title', () => {
|
|
352
|
+
const backend = fakeBackend();
|
|
353
|
+
setTrayBackend(backend);
|
|
354
|
+
const tray = createTrayIcon()!;
|
|
355
|
+
setTrayIconTitle(tray, 'MyApp');
|
|
356
|
+
expect(getTrayIconTitle(tray)).toBe('MyApp');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('returns empty string on web', () => {
|
|
360
|
+
expect(getTrayIconTitle({ id: 0 })).toBe('');
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe('getTrayIconTooltip', () => {
|
|
365
|
+
it('returns the current tooltip', () => {
|
|
366
|
+
const backend = fakeBackend();
|
|
367
|
+
setTrayBackend(backend);
|
|
368
|
+
const tray = createTrayIcon()!;
|
|
369
|
+
setTrayIconTooltip(tray, 'Hover tip');
|
|
370
|
+
expect(getTrayIconTooltip(tray)).toBe('Hover tip');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('returns empty string on web', () => {
|
|
374
|
+
expect(getTrayIconTooltip({ id: 0 })).toBe('');
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('isTrayDestroyed', () => {
|
|
379
|
+
it('returns false for a live tray', () => {
|
|
380
|
+
const backend = fakeBackend();
|
|
381
|
+
setTrayBackend(backend);
|
|
382
|
+
const tray = createTrayIcon()!;
|
|
383
|
+
expect(isTrayDestroyed(tray)).toBe(false);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('returns true after destroyTrayIcon', () => {
|
|
387
|
+
const backend = fakeBackend();
|
|
388
|
+
setTrayBackend(backend);
|
|
389
|
+
const tray = createTrayIcon()!;
|
|
390
|
+
destroyTrayIcon(tray);
|
|
391
|
+
expect(isTrayDestroyed(tray)).toBe(true);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('returns true on web (no trays exist)', () => {
|
|
395
|
+
expect(isTrayDestroyed({ id: 0 })).toBe(true);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('onTrayEvent', () => {
|
|
400
|
+
it('delivers rich TrayEventData via the active backend', () => {
|
|
401
|
+
const backend = fakeBackend();
|
|
402
|
+
setTrayBackend(backend);
|
|
403
|
+
let received: TrayEventData | null = null;
|
|
404
|
+
onTrayEvent((event) => {
|
|
405
|
+
received = event;
|
|
406
|
+
});
|
|
407
|
+
const evt = makeTrayEvent({
|
|
408
|
+
id: 7,
|
|
409
|
+
type: 'rightClick',
|
|
410
|
+
bounds: { x: 100, y: 0, width: 22, height: 22 },
|
|
411
|
+
shiftKey: true,
|
|
412
|
+
});
|
|
413
|
+
backend.fireEvent(evt);
|
|
414
|
+
expect(received).not.toBeNull();
|
|
415
|
+
expect(received!.id).toBe(7);
|
|
416
|
+
expect(received!.type).toBe('rightClick');
|
|
417
|
+
expect(received!.bounds).toMatchObject({ x: 100, y: 0 });
|
|
418
|
+
expect(received!.shiftKey).toBe(true);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('delivers drop events with file payloads', () => {
|
|
422
|
+
const backend = fakeBackend();
|
|
423
|
+
setTrayBackend(backend);
|
|
424
|
+
let received: TrayEventData | null = null;
|
|
425
|
+
onTrayEvent((e) => {
|
|
426
|
+
received = e;
|
|
427
|
+
});
|
|
428
|
+
backend.fireEvent(makeTrayEvent({ type: 'dropFiles', dropFiles: ['/path/to/file.txt'] }));
|
|
429
|
+
expect(received!.dropFiles).toEqual(['/path/to/file.txt']);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('returns an unsubscribe that stops delivery', () => {
|
|
433
|
+
const backend = fakeBackend();
|
|
434
|
+
setTrayBackend(backend);
|
|
435
|
+
let count = 0;
|
|
436
|
+
const unsubscribe = onTrayEvent(() => {
|
|
437
|
+
count += 1;
|
|
438
|
+
});
|
|
439
|
+
backend.fireEvent(makeTrayEvent({ type: 'click' }));
|
|
440
|
+
unsubscribe();
|
|
441
|
+
backend.fireEvent(makeTrayEvent({ type: 'click' }));
|
|
442
|
+
expect(count).toBe(1);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('delivers balloon events', () => {
|
|
446
|
+
const backend = fakeBackend();
|
|
447
|
+
setTrayBackend(backend);
|
|
448
|
+
const types: TrayEventType[] = [];
|
|
449
|
+
onTrayEvent((e) => {
|
|
450
|
+
types.push(e.type);
|
|
451
|
+
});
|
|
452
|
+
backend.fireEvent(makeTrayEvent({ type: 'balloonShow' }));
|
|
453
|
+
backend.fireEvent(makeTrayEvent({ type: 'balloonClick' }));
|
|
454
|
+
backend.fireEvent(makeTrayEvent({ type: 'balloonClose' }));
|
|
455
|
+
expect(types).toEqual(['balloonShow', 'balloonClick', 'balloonClose']);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe('popupTrayContextMenu', () => {
|
|
460
|
+
it('pops up the context menu without a position', () => {
|
|
461
|
+
const backend = fakeBackend();
|
|
462
|
+
setTrayBackend(backend);
|
|
463
|
+
const tray = createTrayIcon()!;
|
|
464
|
+
popupTrayContextMenu(tray);
|
|
465
|
+
// No error thrown; position was unset
|
|
466
|
+
expect(backend.lastPopupPosition).toBeNull();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('pops up the context menu at a given position', () => {
|
|
470
|
+
const backend = fakeBackend();
|
|
471
|
+
setTrayBackend(backend);
|
|
472
|
+
const tray = createTrayIcon()!;
|
|
473
|
+
popupTrayContextMenu(tray, { x: 200, y: 50 });
|
|
474
|
+
expect(backend.lastPopupPosition).toEqual({ x: 200, y: 50 });
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('is a no-op on web', () => {
|
|
478
|
+
expect(() => popupTrayContextMenu({ id: 0 }, { x: 0, y: 0 })).not.toThrow();
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe('removeTrayBalloon', () => {
|
|
483
|
+
it('removes the active balloon', () => {
|
|
484
|
+
const backend = fakeBackend();
|
|
485
|
+
setTrayBackend(backend);
|
|
486
|
+
const tray = createTrayIcon()!;
|
|
487
|
+
displayTrayBalloon(tray, { title: 'T', text: 'B' });
|
|
488
|
+
removeTrayBalloon(tray);
|
|
489
|
+
expect(backend.trays.get(tray.id)!.balloon).toBeNull();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('is a no-op on web', () => {
|
|
493
|
+
expect(() => removeTrayBalloon({ id: 0 })).not.toThrow();
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
describe('setTrayBackend', () => {
|
|
498
|
+
it('clears back to the web fallback when passed null', () => {
|
|
499
|
+
setTrayBackend(fakeBackend());
|
|
500
|
+
setTrayBackend(null);
|
|
501
|
+
expect(getTrayBackend()).not.toBeNull();
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
describe('setTrayIcon', () => {
|
|
506
|
+
it('updates the icon at runtime', () => {
|
|
507
|
+
const backend = fakeBackend();
|
|
508
|
+
setTrayBackend(backend);
|
|
509
|
+
const tray = createTrayIcon({ icon: 'idle.png' })!;
|
|
510
|
+
setTrayIcon(tray, 'active.png');
|
|
511
|
+
expect(backend.trays.get(tray.id)!.icon).toBe('active.png');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('is a no-op on web', () => {
|
|
515
|
+
expect(() => setTrayIcon({ id: 0 }, 'icon.png')).not.toThrow();
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe('setTrayIconContextMenu', () => {
|
|
520
|
+
it('sets the context menu via the active backend', () => {
|
|
521
|
+
const backend = fakeBackend();
|
|
522
|
+
setTrayBackend(backend);
|
|
523
|
+
const tray = createTrayIcon()!;
|
|
524
|
+
const items: readonly MenuItemTemplate[] = [{ id: 'quit', label: 'Quit' }];
|
|
525
|
+
setTrayIconContextMenu(tray, items);
|
|
526
|
+
expect(backend.trays.get(tray.id)!.contextMenu).toBe(items);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
describe('setTrayIconTemplate', () => {
|
|
531
|
+
it('marks the icon as a template image', () => {
|
|
532
|
+
const backend = fakeBackend();
|
|
533
|
+
setTrayBackend(backend);
|
|
534
|
+
const tray = createTrayIcon()!;
|
|
535
|
+
setTrayIconTemplate(tray, true);
|
|
536
|
+
expect(backend.trays.get(tray.id)!.isTemplate).toBe(true);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('clears the template flag', () => {
|
|
540
|
+
const backend = fakeBackend();
|
|
541
|
+
setTrayBackend(backend);
|
|
542
|
+
const tray = createTrayIcon({ iconTemplate: true })!;
|
|
543
|
+
setTrayIconTemplate(tray, false);
|
|
544
|
+
expect(backend.trays.get(tray.id)!.isTemplate).toBe(false);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('is a no-op on web', () => {
|
|
548
|
+
expect(() => setTrayIconTemplate({ id: 0 }, true)).not.toThrow();
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
describe('setTrayIconTitle', () => {
|
|
553
|
+
it('sets the title via the active backend', () => {
|
|
554
|
+
const backend = fakeBackend();
|
|
555
|
+
setTrayBackend(backend);
|
|
556
|
+
const tray = createTrayIcon()!;
|
|
557
|
+
setTrayIconTitle(tray, 'Hello');
|
|
558
|
+
expect(backend.trays.get(tray.id)!.title).toBe('Hello');
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
describe('setTrayIconTooltip', () => {
|
|
563
|
+
it('sets the tooltip via the active backend', () => {
|
|
564
|
+
const backend = fakeBackend();
|
|
565
|
+
setTrayBackend(backend);
|
|
566
|
+
const tray = createTrayIcon()!;
|
|
567
|
+
setTrayIconTooltip(tray, 'Tip');
|
|
568
|
+
expect(backend.trays.get(tray.id)!.tooltip).toBe('Tip');
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
describe('setTrayIgnoreDoubleClickEvents', () => {
|
|
573
|
+
it('sets the ignore double-click flag', () => {
|
|
574
|
+
const backend = fakeBackend();
|
|
575
|
+
setTrayBackend(backend);
|
|
576
|
+
const tray = createTrayIcon()!;
|
|
577
|
+
setTrayIgnoreDoubleClickEvents(tray, true);
|
|
578
|
+
expect(backend.trays.get(tray.id)!.ignoreDoubleClick).toBe(true);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('is a no-op on web', () => {
|
|
582
|
+
expect(() => setTrayIgnoreDoubleClickEvents({ id: 0 }, true)).not.toThrow();
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
describe('setTrayPressedIcon', () => {
|
|
587
|
+
it('sets the pressed icon image', () => {
|
|
588
|
+
const backend = fakeBackend();
|
|
589
|
+
setTrayBackend(backend);
|
|
590
|
+
const tray = createTrayIcon()!;
|
|
591
|
+
setTrayPressedIcon(tray, 'pressed.png');
|
|
592
|
+
expect(backend.trays.get(tray.id)!.pressedIcon).toBe('pressed.png');
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('is a no-op on web', () => {
|
|
596
|
+
expect(() => setTrayPressedIcon({ id: 0 }, 'pressed.png')).not.toThrow();
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
describe('startTrayIconAnimation', () => {
|
|
601
|
+
beforeEach(() => {
|
|
602
|
+
vi.useFakeTimers();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
afterEach(() => {
|
|
606
|
+
vi.useRealTimers();
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('cycles through frames at the given interval', () => {
|
|
610
|
+
const backend = fakeBackend();
|
|
611
|
+
setTrayBackend(backend);
|
|
612
|
+
const tray = createTrayIcon({ icon: 'frame0.png' })!;
|
|
613
|
+
const frames = ['frame0.png', 'frame1.png', 'frame2.png'];
|
|
614
|
+
startTrayIconAnimation(tray, frames, 100);
|
|
615
|
+
// Initial frame set immediately
|
|
616
|
+
expect(backend.trays.get(tray.id)!.icon).toBe('frame0.png');
|
|
617
|
+
vi.advanceTimersByTime(100);
|
|
618
|
+
expect(backend.trays.get(tray.id)!.icon).toBe('frame1.png');
|
|
619
|
+
vi.advanceTimersByTime(100);
|
|
620
|
+
expect(backend.trays.get(tray.id)!.icon).toBe('frame2.png');
|
|
621
|
+
vi.advanceTimersByTime(100);
|
|
622
|
+
// Wraps around
|
|
623
|
+
expect(backend.trays.get(tray.id)!.icon).toBe('frame0.png');
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('stop function cancels animation', () => {
|
|
627
|
+
const backend = fakeBackend();
|
|
628
|
+
setTrayBackend(backend);
|
|
629
|
+
const tray = createTrayIcon()!;
|
|
630
|
+
const frames = ['a.png', 'b.png'];
|
|
631
|
+
const stop = startTrayIconAnimation(tray, frames, 50);
|
|
632
|
+
vi.advanceTimersByTime(50);
|
|
633
|
+
expect(backend.trays.get(tray.id)!.icon).toBe('b.png');
|
|
634
|
+
stop();
|
|
635
|
+
vi.advanceTimersByTime(200);
|
|
636
|
+
// Frame stays at b.png after stop
|
|
637
|
+
expect(backend.trays.get(tray.id)!.icon).toBe('b.png');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('returns a no-op stop for empty frames', () => {
|
|
641
|
+
const backend = fakeBackend();
|
|
642
|
+
setTrayBackend(backend);
|
|
643
|
+
const tray = createTrayIcon()!;
|
|
644
|
+
const stop = startTrayIconAnimation(tray, [], 100);
|
|
645
|
+
expect(() => stop()).not.toThrow();
|
|
646
|
+
});
|
|
647
|
+
});
|