test_page.py 8.5 KB

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