icon_tests.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. # coding=utf-8
  2. # pystray
  3. # Copyright (C) 2016-2020 Moses Palmér
  4. #
  5. # This program is free software: you can redistribute it and/or modify it under
  6. # the terms of the GNU Lesser General Public License as published by the Free
  7. # Software Foundation, either version 3 of the License, or (at your option) any
  8. # later version.
  9. #
  10. # This program is distributed in the hope that it will be useful, but WITHOUT
  11. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  12. # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  13. # details.
  14. #
  15. # You should have received a copy of the GNU Lesser General Public License
  16. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. import sys
  18. import unittest
  19. import pystray
  20. from six.moves import queue
  21. from six import reraise
  22. from time import sleep
  23. from pystray import Menu as menu, MenuItem as item
  24. from . import action, confirm, icon, image, say, separator, true
  25. #: The number of seconds to wait for interactive commands
  26. TIMEOUT = 10
  27. def test(icon):
  28. """A decorator to mark an inner function as the actual test code.
  29. The decorated function will be run in a separate thread as the ``setup``
  30. argument to :meth:`pystray.Icon.run`.
  31. This decorator actually runs the decorated method, and does not return
  32. anything.
  33. """
  34. def inner(f):
  35. q = queue.Queue()
  36. def setup(icon):
  37. try:
  38. f()
  39. q.put(True)
  40. except:
  41. q.put(sys.exc_info())
  42. finally:
  43. icon.visible = False
  44. icon.stop()
  45. icon.run(setup=setup)
  46. result = q.get()
  47. if result is not True:
  48. reraise(*result)
  49. return inner
  50. def for_default_action(test):
  51. """Prevents a test from being run on implementations not supporting default
  52. action on click.
  53. :param test: The test.
  54. """
  55. if pystray.Icon.HAS_DEFAULT_ACTION:
  56. return test
  57. else:
  58. return lambda *a: None
  59. def for_menu(test):
  60. """Prevents a test from being run on implementations not supporting a menu.
  61. :param test: The test.
  62. """
  63. if pystray.Icon.HAS_MENU:
  64. return test
  65. else:
  66. return lambda *a: None
  67. def for_menu_radio(test):
  68. """Prevents a test from being run on implementations not supporting mutually
  69. exclusive menu item groups.
  70. :param test: The test.
  71. """
  72. if pystray.Icon.HAS_MENU_RADIO:
  73. return test
  74. else:
  75. return lambda *a: None
  76. def for_notification(test):
  77. """Prevents a test from being run on implementations not supporting notifications
  78. :param test: The test.
  79. """
  80. if pystray.Icon.HAS_NOTIFICATION:
  81. return test
  82. else:
  83. return lambda *a: None
  84. class IconTest(unittest.TestCase):
  85. def test_set_icon(self):
  86. """Tests that updating the icon works.
  87. """
  88. ico, colors1 = icon()
  89. original = ico.icon
  90. alternative, colors2 = image()
  91. @test(ico)
  92. def _():
  93. ico.visible = True
  94. for i in range(8):
  95. ico.icon = (alternative, original)[i % 2]
  96. sleep(0.5)
  97. confirm(
  98. self,
  99. 'Did an alternating %s, and %s icon appear?', colors1, colors2)
  100. def test_set_icon_after_constructor(self):
  101. """Tests that updating the icon works.
  102. """
  103. ico, colors1 = icon(no_image=True)
  104. alternative, colors2 = image()
  105. @test(ico)
  106. def _():
  107. ico.icon = alternative
  108. ico.visible = True
  109. confirm(
  110. self,
  111. 'Did an icon appear?')
  112. def test_set_icon_to_none(self):
  113. """Tests that setting the icon to None hides it.
  114. """
  115. ico, colors = icon()
  116. @test(ico)
  117. def _():
  118. ico.visible = True
  119. sleep(1.0)
  120. ico.icon = None
  121. self.assertFalse(ico.visible)
  122. confirm(
  123. self,
  124. 'Did the %s icon disappear?', colors)
  125. def test_title(self):
  126. """Tests that initialising with a title works.
  127. """
  128. title = 'pystray test icon'
  129. ico, colors = icon(title=title)
  130. @test(ico)
  131. def _():
  132. ico.visible = True
  133. confirm(
  134. self,
  135. 'Did an %s icon with the title "%s" appear?', colors, title)
  136. def test_title_set_hidden(self):
  137. """Tests that setting the title of a hidden icon works.
  138. """
  139. title = 'pystray test icon'
  140. ico, colors = icon(title='this is incorrect')
  141. @test(ico)
  142. def _():
  143. ico.title = title
  144. ico.visible = True
  145. confirm(
  146. self,
  147. 'Did a %s icon with the title "%s" appear?', colors, title)
  148. def test_title_set_visible(self):
  149. """Tests that setting the title of a visible icon works.
  150. """
  151. title = 'pystray test icon'
  152. ico, colors = icon(title='this is incorrect')
  153. @test(ico)
  154. def _():
  155. ico.visible = True
  156. ico.title = title
  157. confirm(
  158. self,
  159. 'Did a %s icon with the title "%s" appear?', colors, title)
  160. def test_visible(self):
  161. """Tests that the ``visible`` attribute reflects the visibility.
  162. """
  163. ico, colors = icon(title='this is incorrect')
  164. @test(ico)
  165. def _():
  166. self.assertFalse(ico.visible)
  167. ico.visible = True
  168. self.assertTrue(ico.visible)
  169. def test_visible_set(self):
  170. """Tests that showing a simple icon works.
  171. """
  172. ico, colors = icon()
  173. @test(ico)
  174. def _():
  175. ico.visible = True
  176. confirm(
  177. self,
  178. 'Did a %s icon appear?', colors)
  179. def test_visible_set_no_icon(self):
  180. """Tests that setting the icon when none is set shows the icon.
  181. """
  182. ico = pystray.Icon('test')
  183. @test(ico)
  184. def _():
  185. try:
  186. with self.assertRaises(ValueError):
  187. ico.visible = True
  188. finally:
  189. ico.visible = False
  190. def test_show_hide(self):
  191. """Tests that showing and hiding the icon works.
  192. """
  193. ico, colors = icon()
  194. @test(ico)
  195. def _():
  196. for i in range(4):
  197. ico.visible = True
  198. sleep(0.5)
  199. ico.visible = False
  200. sleep(0.5)
  201. confirm(
  202. self,
  203. 'Did a flashing %s icon appear?', colors)
  204. @for_default_action
  205. def test_activate(self):
  206. """Tests that ``on_activate`` is correctly called.
  207. """
  208. q = queue.Queue()
  209. def on_activate(icon):
  210. q.put(True)
  211. ico, colors = icon(menu=menu(
  212. action(on_activate),))
  213. @test(ico)
  214. def _():
  215. ico.visible = True
  216. say('Click the icon')
  217. q.get(timeout=TIMEOUT)
  218. def test_activate_with_default(self):
  219. """Tests that the default menu item is activated when activating icon.
  220. """
  221. q = queue.Queue()
  222. def on_activate(icon):
  223. q.put(True)
  224. ico, colors = icon(menu=menu(
  225. item('Item 1', None),
  226. item('Default', on_activate, default=True)))
  227. @test(ico)
  228. def _():
  229. ico.visible = True
  230. say('Click the icon or select the default menu item')
  231. q.get(timeout=TIMEOUT)
  232. @for_menu
  233. def test_menu_construct(self):
  234. """Tests that the menu is constructed.
  235. """
  236. ico, colors = icon(menu=menu(
  237. item('Item 1', None),
  238. item('Item 2', None)))
  239. @test(ico)
  240. def _():
  241. ico.visible = True
  242. say('Expand the popup menu')
  243. confirm(
  244. self,
  245. 'Was it\n%s?' % str(ico.menu))
  246. @for_menu
  247. def test_menu_activate(self):
  248. """Tests that the menu can be activated.
  249. """
  250. q = queue.Queue()
  251. def on_activate():
  252. q.put(True)
  253. ico, colors = icon(menu=(
  254. item('Item 1', on_activate),
  255. item('Item 2', None)))
  256. @test(ico)
  257. def _():
  258. ico.visible = True
  259. say('Click Item 1')
  260. q.get(timeout=TIMEOUT)
  261. @for_menu
  262. def test_menu_activate_method(self):
  263. """Tests that the menu can be activated and a method can be used.
  264. """
  265. q = queue.Queue()
  266. class C:
  267. def on_activate(self):
  268. q.put(True)
  269. c = C()
  270. ico, colors = icon(menu=(
  271. item('Item 1', c.on_activate),
  272. item('Item 2', None)))
  273. @test(ico)
  274. def _():
  275. ico.visible = True
  276. say('Click Item 1')
  277. q.get(timeout=TIMEOUT)
  278. @for_menu
  279. def test_menu_activate_submenu(self):
  280. """Tests that an item in a submenu can be activated.
  281. """
  282. q = queue.Queue()
  283. def on_activate():
  284. q.put(True)
  285. ico, colors = icon(menu=(
  286. item('Item 1', None),
  287. item('Submenu', menu(
  288. item('Item 2', None),
  289. item('Item 3', on_activate)))))
  290. @test(ico)
  291. def _():
  292. ico.visible = True
  293. say('Click Item 3 in the submenu')
  294. q.get(timeout=TIMEOUT)
  295. @for_default_action
  296. def test_menu_invisble(self):
  297. """Tests that a menu consisting of only empty items does not show.
  298. """
  299. q = queue.Queue()
  300. def on_activate():
  301. q.put(True)
  302. ico, colors = icon(menu=menu(
  303. item('Item1', None, visible=False),
  304. item('Item2', on_activate, default=True, visible=False)))
  305. @test(ico)
  306. def _():
  307. ico.visible = True
  308. say('Ensure that the menu does not show and then click the icon')
  309. q.get(timeout=TIMEOUT)
  310. @for_menu
  311. def test_menu_dynamic(self):
  312. """Tests that a dynamic menu works.
  313. """
  314. q = queue.Queue()
  315. q.ticks = 0
  316. def on_activate():
  317. q.put(True)
  318. q.ticks += 1
  319. ico, colors = icon(menu=menu(
  320. item('Item 1', on_activate),
  321. item('Item 2', None),
  322. item(lambda _:'Item ' + str(q.ticks + 3), None)))
  323. @test(ico)
  324. def _():
  325. ico.visible = True
  326. say('Click Item 1')
  327. q.get(timeout=TIMEOUT)
  328. say('Expand the popup menu')
  329. confirm(
  330. self,
  331. 'Was it\n%s?' % str(ico.menu))
  332. @for_default_action
  333. @for_menu
  334. def test_menu_dynamic_show_hide(self):
  335. """Tests that a dynamic menu that is hidden works as expected.
  336. """
  337. q = queue.Queue()
  338. q.ticks = 0
  339. def on_activate():
  340. q.put(True)
  341. q.ticks += 1
  342. def visible(menu_item):
  343. return q.ticks % 2 == 0
  344. ico, colors = icon(menu=menu(
  345. item('Default', on_activate, default=True, visible=visible),
  346. item('Item 2', None, visible=visible)))
  347. @test(ico)
  348. def _():
  349. ico.visible = True
  350. say('Click the icon or select the default menu item')
  351. q.get(timeout=TIMEOUT)
  352. say('Ensure that the menu does not show and then click the icon')
  353. q.get(timeout=TIMEOUT)
  354. say('Expand the popup menu')
  355. confirm(
  356. self,
  357. 'Was it\n%s?' % str(ico.menu))
  358. @for_menu_radio
  359. def test_menu_radio(self):
  360. """Tests that mutually exclusive items are displayed separately.
  361. """
  362. ico, colors = icon(menu=menu(
  363. item('Item 1', None, checked=true),
  364. item('Item 2', None, checked=true, radio=True)))
  365. @test(ico)
  366. def _():
  367. ico.visible = True
  368. say('Expand the popup menu')
  369. confirm(
  370. self,
  371. 'Was <Item 2> displayed differently from <Item 1>?')
  372. @for_menu_radio
  373. def test_menu_enabled(self):
  374. """Tests that menu items can be disabled.
  375. """
  376. ico, colors = icon(menu=menu(
  377. item('Item 1', None, enabled=true),
  378. item('Item 2', None, enabled=False)))
  379. @test(ico)
  380. def _():
  381. ico.visible = True
  382. say('Expand the popup menu')
  383. confirm(
  384. self,
  385. 'Was <Item 1> enabled and <Item 2> disabled?')
  386. @for_notification
  387. def test_show_notification(self):
  388. """Tests that generation of a notification works.
  389. """
  390. ico, colors = icon()
  391. @test(ico)
  392. def _():
  393. ico.notify(title='Title: Test', message='This is a message!')
  394. confirm(
  395. self,
  396. 'Did a notification appear?')
  397. @for_notification
  398. def test_hide_notification(self):
  399. """Tests that a notification can be removed again.
  400. """
  401. ico, colors = icon()
  402. @test(ico)
  403. def _():
  404. ico.notify(title='Title: Test', message='This is a message!')
  405. sleep(5.0)
  406. ico.remove_notification()
  407. confirm(
  408. self,
  409. 'Was the notification removed?')