test_page.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import asyncio
  2. import re
  3. from typing import Optional
  4. from uuid import uuid4
  5. from fastapi.responses import PlainTextResponse
  6. from selenium.webdriver.common.by import By
  7. from nicegui import app, background_tasks, ui
  8. from nicegui.testing import Screen
  9. def test_page(screen: Screen):
  10. @ui.page('/')
  11. def page():
  12. ui.label('Hello, world!')
  13. screen.open('/')
  14. screen.should_contain('NiceGUI')
  15. screen.should_contain('Hello, world!')
  16. def test_auto_index_page(screen: Screen):
  17. ui.label('Hello, world!')
  18. screen.open('/')
  19. screen.should_contain('NiceGUI')
  20. screen.should_contain('Hello, world!')
  21. def test_custom_title(screen: Screen):
  22. @ui.page('/', title='My Custom Title')
  23. def page():
  24. ui.label('Hello, world!')
  25. screen.open('/')
  26. screen.should_contain('My Custom Title')
  27. screen.should_contain('Hello, world!')
  28. def test_route_with_custom_path(screen: Screen):
  29. @ui.page('/test_route')
  30. def page():
  31. ui.label('page with custom path')
  32. screen.open('/test_route')
  33. screen.should_contain('page with custom path')
  34. def test_auto_index_page_with_link_to_subpage(screen: Screen):
  35. ui.link('link to subpage', '/subpage')
  36. @ui.page('/subpage')
  37. def page():
  38. ui.label('the subpage')
  39. screen.open('/')
  40. screen.click('link to subpage')
  41. screen.should_contain('the subpage')
  42. def test_link_to_page_by_passing_function(screen: Screen):
  43. @ui.page('/subpage')
  44. def page():
  45. ui.label('the subpage')
  46. ui.link('link to subpage', page)
  47. screen.open('/')
  48. screen.click('link to subpage')
  49. screen.should_contain('the subpage')
  50. def test_creating_new_page_after_startup(screen: Screen):
  51. screen.start_server()
  52. @ui.page('/late_page')
  53. def page():
  54. ui.label('page created after startup')
  55. screen.open('/late_page')
  56. screen.should_contain('page created after startup')
  57. def test_shared_and_private_pages(screen: Screen):
  58. @ui.page('/private_page')
  59. def private_page():
  60. ui.label(f'private page with uuid {uuid4()}')
  61. ui.label(f'shared page with uuid {uuid4()}')
  62. screen.open('/private_page')
  63. uuid1 = screen.find('private page').text.split()[-1]
  64. screen.open('/private_page')
  65. uuid2 = screen.find('private page').text.split()[-1]
  66. assert uuid1 != uuid2
  67. screen.open('/')
  68. uuid1 = screen.find('shared page').text.split()[-1]
  69. screen.open('/')
  70. uuid2 = screen.find('shared page').text.split()[-1]
  71. assert uuid1 == uuid2
  72. def test_wait_for_connected(screen: Screen):
  73. label: Optional[ui.label] = None
  74. async def load() -> None:
  75. assert label
  76. label.text = 'loading...'
  77. # NOTE we can not use asyncio.create_task() here because we are on a different thread than the NiceGUI event loop
  78. background_tasks.create(takes_a_while())
  79. async def takes_a_while() -> None:
  80. await asyncio.sleep(0.1)
  81. assert label
  82. label.text = 'delayed data has been loaded'
  83. @ui.page('/')
  84. async def page():
  85. nonlocal label
  86. label = ui.label()
  87. await ui.context.client.connected()
  88. await load()
  89. screen.open('/')
  90. screen.should_contain('delayed data has been loaded')
  91. def test_wait_for_disconnect(screen: Screen):
  92. events = []
  93. @ui.page('/', reconnect_timeout=0)
  94. async def page():
  95. await ui.context.client.connected()
  96. events.append('connected')
  97. await ui.context.client.disconnected()
  98. events.append('disconnected')
  99. screen.open('/')
  100. screen.wait(0.5)
  101. screen.open('/')
  102. screen.wait(0.5)
  103. assert events == ['connected', 'disconnected', 'connected']
  104. def test_wait_for_disconnect_without_awaiting_connected(screen: Screen):
  105. events = []
  106. @ui.page('/', reconnect_timeout=0)
  107. async def page():
  108. await ui.context.client.disconnected()
  109. events.append('disconnected')
  110. screen.open('/')
  111. screen.wait(0.5)
  112. screen.open('/')
  113. screen.wait(0.5)
  114. assert events == ['disconnected']
  115. def test_adding_elements_after_connected(screen: Screen):
  116. @ui.page('/')
  117. async def page():
  118. ui.label('before')
  119. await ui.context.client.connected()
  120. ui.label('after')
  121. screen.open('/')
  122. screen.should_contain('before')
  123. screen.should_contain('after')
  124. def test_exception(screen: Screen):
  125. @ui.page('/')
  126. def page():
  127. raise RuntimeError('some exception')
  128. screen.open('/')
  129. screen.should_contain('500')
  130. screen.should_contain('Server error')
  131. screen.assert_py_logger('ERROR', 'some exception')
  132. def test_exception_after_connected(screen: Screen):
  133. @ui.page('/')
  134. async def page():
  135. await ui.context.client.connected()
  136. ui.label('this is shown')
  137. raise RuntimeError('some exception')
  138. screen.open('/')
  139. screen.should_contain('this is shown')
  140. screen.assert_py_logger('ERROR', 'some exception')
  141. def test_page_with_args(screen: Screen):
  142. @ui.page('/page/{id_}')
  143. def page(id_: int):
  144. ui.label(f'Page {id_}')
  145. screen.open('/page/42')
  146. screen.should_contain('Page 42')
  147. def test_adding_elements_during_onconnect(screen: Screen):
  148. @ui.page('/')
  149. def page():
  150. ui.label('Label 1')
  151. ui.context.client.on_connect(lambda: ui.label('Label 2'))
  152. screen.open('/')
  153. screen.should_contain('Label 2')
  154. def test_async_connect_handler(screen: Screen):
  155. @ui.page('/')
  156. def page():
  157. async def run_js():
  158. result.text = await ui.run_javascript('41 + 1')
  159. result = ui.label()
  160. ui.context.client.on_connect(run_js)
  161. screen.open('/')
  162. screen.should_contain('42')
  163. def test_dark_mode(screen: Screen):
  164. @ui.page('/auto', dark=None)
  165. def page():
  166. ui.label('A').classes('text-blue-400 dark:text-red-400')
  167. @ui.page('/light', dark=False)
  168. def light_page():
  169. ui.label('B').classes('text-blue-400 dark:text-red-400')
  170. @ui.page('/dark', dark=True)
  171. def dark_page():
  172. ui.label('C').classes('text-blue-400 dark:text-red-400')
  173. blue = 'rgba(96, 165, 250, 1)'
  174. red = 'rgba(248, 113, 113, 1)'
  175. white = 'rgba(0, 0, 0, 0)'
  176. black = 'rgba(18, 18, 18, 1)'
  177. screen.open('/auto')
  178. assert screen.find('A').value_of_css_property('color') == blue
  179. assert screen.find_by_tag('body').value_of_css_property('background-color') == white
  180. screen.open('/light')
  181. assert screen.find('B').value_of_css_property('color') == blue
  182. assert screen.find_by_tag('body').value_of_css_property('background-color') == white
  183. screen.open('/dark')
  184. assert screen.find('C').value_of_css_property('color') == red
  185. assert screen.find_by_tag('body').value_of_css_property('background-color') == black
  186. def test_returning_custom_response(screen: Screen):
  187. @ui.page('/')
  188. def page(plain: bool = False):
  189. if plain:
  190. return PlainTextResponse('custom response')
  191. else:
  192. ui.label('normal NiceGUI page')
  193. screen.open('/')
  194. screen.should_contain('normal NiceGUI page')
  195. screen.should_not_contain('custom response')
  196. screen.open('/?plain=true')
  197. screen.should_contain('custom response')
  198. screen.should_not_contain('normal NiceGUI page')
  199. def test_returning_custom_response_async(screen: Screen):
  200. @ui.page('/')
  201. async def page(plain: bool = False):
  202. await asyncio.sleep(0.01) # simulates a db request or similar
  203. if plain:
  204. return PlainTextResponse('custom response')
  205. else:
  206. ui.label('normal NiceGUI page')
  207. screen.open('/')
  208. screen.should_contain('normal NiceGUI page')
  209. screen.should_not_contain('custom response')
  210. screen.open('/?plain=true')
  211. screen.should_contain('custom response')
  212. screen.should_not_contain('normal NiceGUI page')
  213. def test_warning_about_to_late_responses(screen: Screen):
  214. @ui.page('/')
  215. async def page():
  216. await ui.context.client.connected()
  217. ui.label('NiceGUI page')
  218. return PlainTextResponse('custom response')
  219. screen.open('/')
  220. screen.should_contain('NiceGUI page')
  221. screen.assert_py_logger('ERROR', re.compile('it was returned after the HTML had been delivered to the client'))
  222. def test_reconnecting_without_page_reload(screen: Screen):
  223. @ui.page('/', reconnect_timeout=3.0)
  224. def page():
  225. ui.input('Input').props('autofocus')
  226. ui.button('drop connection', on_click=lambda: ui.run_javascript('socket.io.engine.close()'))
  227. screen.open('/')
  228. screen.type('hello')
  229. screen.click('drop connection')
  230. screen.wait(2.0)
  231. element = screen.selenium.find_element(By.XPATH, '//*[@aria-label="Input"]')
  232. assert element.get_attribute('value') == 'hello', 'input should be preserved after reconnect (i.e. no page reload)'
  233. def test_ip(screen: Screen):
  234. @ui.page('/')
  235. def page():
  236. ui.label(ui.context.client.ip or 'unknown')
  237. screen.open('/')
  238. screen.should_contain('127.0.0.1')
  239. def test_multicast(screen: Screen):
  240. def update():
  241. for client in app.clients('/'):
  242. with client:
  243. ui.label('added')
  244. @ui.page('/')
  245. def page():
  246. ui.button('add label', on_click=update)
  247. screen.open('/')
  248. screen.switch_to(1)
  249. screen.open('/')
  250. screen.click('add label')
  251. screen.should_contain('added')
  252. screen.switch_to(0)
  253. screen.should_contain('added')