1
0

test_user_simulation.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. import csv
  2. from io import BytesIO
  3. from typing import Callable, Dict, Type, Union
  4. import pytest
  5. from fastapi import UploadFile
  6. from fastapi.datastructures import Headers
  7. from fastapi.responses import PlainTextResponse
  8. from nicegui import app, events, ui
  9. from nicegui.testing import User
  10. # pylint: disable=missing-function-docstring
  11. async def test_auto_index_page(user: User) -> None:
  12. ui.label('Main page')
  13. await user.open('/')
  14. await user.should_see('Main page')
  15. async def test_multiple_pages(create_user: Callable[[], User]) -> None:
  16. @ui.page('/')
  17. def index():
  18. ui.label('Main page')
  19. @ui.page('/other')
  20. def other():
  21. ui.label('Other page')
  22. userA = create_user()
  23. userB = create_user()
  24. await userA.open('/')
  25. await userA.should_see('Main page')
  26. await userA.should_not_see('Other page')
  27. await userB.open('/other')
  28. await userB.should_see('Other page')
  29. await userB.should_not_see('Main page')
  30. async def test_source_element(user: User) -> None:
  31. @ui.page('/')
  32. def index():
  33. ui.image('https://via.placeholder.com/150')
  34. await user.open('/')
  35. await user.should_see('placeholder.com')
  36. async def test_button_click(user: User) -> None:
  37. @ui.page('/')
  38. def index():
  39. ui.button('click me', on_click=lambda: ui.label('clicked'))
  40. await user.open('/')
  41. user.find('click me').click()
  42. await user.should_see('clicked')
  43. async def test_assertion_raised_when_no_nicegui_page_is_returned(user: User) -> None:
  44. @app.get('/plain')
  45. def index() -> PlainTextResponse:
  46. return PlainTextResponse('Hello')
  47. with pytest.raises(ValueError):
  48. await user.open('/plain')
  49. async def test_assertion_raised_when_element_not_found(user: User) -> None:
  50. @ui.page('/')
  51. def index():
  52. ui.label('Hello')
  53. await user.open('/')
  54. with pytest.raises(AssertionError):
  55. await user.should_see('World')
  56. @pytest.mark.parametrize('storage_builder', [lambda: app.storage.browser, lambda: app.storage.user])
  57. async def test_storage(user: User, storage_builder: Callable[[], Dict]) -> None:
  58. @ui.page('/')
  59. def page():
  60. storage = storage_builder()
  61. storage['count'] = storage.get('count', 0) + 1
  62. ui.label().bind_text_from(storage, 'count')
  63. await user.open('/')
  64. await user.should_see('1')
  65. await user.open('/')
  66. await user.should_see('2')
  67. async def test_navigation(user: User) -> None:
  68. @ui.page('/')
  69. def page():
  70. ui.label('Main page')
  71. ui.button('go to other', on_click=lambda: ui.navigate.to('/other'))
  72. ui.button('forward', on_click=ui.navigate.forward)
  73. @ui.page('/other')
  74. def other():
  75. ui.label('Other page')
  76. ui.button('back', on_click=ui.navigate.back)
  77. await user.open('/')
  78. await user.should_see('Main page')
  79. user.find('go to other').click()
  80. await user.should_see('Other page')
  81. user.find('back').click()
  82. await user.should_see('Main page')
  83. user.find('forward').click()
  84. await user.should_see('Other page')
  85. async def test_multi_user_navigation(create_user: Callable[[], User]) -> None:
  86. @ui.page('/')
  87. def page():
  88. ui.label('Main page')
  89. ui.button('go to other', on_click=lambda: ui.navigate.to('/other'))
  90. ui.button('forward', on_click=ui.navigate.forward)
  91. @ui.page('/other')
  92. def other():
  93. ui.label('Other page')
  94. ui.button('back', on_click=ui.navigate.back)
  95. userA = create_user()
  96. userB = create_user()
  97. await userA.open('/')
  98. await userA.should_see('Main page')
  99. await userB.open('/')
  100. await userB.should_see('Main page')
  101. userA.find('go to other').click()
  102. await userA.should_see('Other page')
  103. await userB.should_see('Main page')
  104. userA.find('back').click()
  105. await userA.should_see('Main page')
  106. await userB.should_see('Main page')
  107. userA.find('forward').click()
  108. await userA.should_see('Other page')
  109. await userB.should_see('Main page')
  110. async def test_reload(user: User) -> None:
  111. @ui.page('/')
  112. def page():
  113. ui.input('test input')
  114. ui.button('reload', on_click=ui.navigate.reload)
  115. await user.open('/')
  116. await user.should_not_see('Hello')
  117. user.find('test input').type('Hello')
  118. await user.should_see('Hello')
  119. user.find('reload').click()
  120. await user.should_not_see('Hello')
  121. async def test_notification(user: User) -> None:
  122. @ui.page('/')
  123. def page():
  124. ui.button('notify', on_click=lambda: ui.notify('Hello'))
  125. await user.open('/')
  126. user.find('notify').click()
  127. await user.should_see('Hello')
  128. @pytest.mark.parametrize('kind', [ui.checkbox, ui.switch])
  129. async def test_checkbox_and_switch(user: User, kind: Type) -> None:
  130. element = kind('my element', on_change=lambda e: ui.notify(f'Changed: {e.value}'))
  131. ui.label().bind_text_from(element, 'value', lambda v: 'enabled' if v else 'disabled')
  132. await user.open('/')
  133. await user.should_see('disabled')
  134. user.find('element').click()
  135. await user.should_see('enabled')
  136. await user.should_see('Changed: True')
  137. user.find('element').click()
  138. await user.should_see('disabled')
  139. await user.should_see('Changed: False')
  140. @pytest.mark.parametrize('kind', [ui.input, ui.editor, ui.codemirror])
  141. async def test_input(user: User, kind: Type) -> None:
  142. element = kind(on_change=lambda e: ui.notify(f'Changed: {e.value}'))
  143. ui.label().bind_text_from(element, 'value', lambda v: f'Value: {v}')
  144. await user.open('/')
  145. await user.should_see('Value: ')
  146. user.find(kind).type('Hello')
  147. await user.should_see('Value: Hello')
  148. await user.should_see('Changed: Hello')
  149. user.find(kind).type(' World')
  150. await user.should_see('Value: Hello World')
  151. await user.should_see('Changed: Hello World')
  152. user.find(kind).clear()
  153. user.find(kind).type('Test')
  154. await user.should_see('Value: Test')
  155. await user.should_see('Changed: Test')
  156. async def test_should_not_see(user: User) -> None:
  157. @ui.page('/')
  158. def page():
  159. ui.label('Hello')
  160. await user.open('/')
  161. await user.should_not_see('World')
  162. await user.should_see('Hello')
  163. async def test_should_not_see_notification(user: User) -> None:
  164. @ui.page('/')
  165. def page():
  166. ui.button('Notify', on_click=lambda: ui.notification('Hello'))
  167. await user.open('/')
  168. await user.should_not_see('Hello')
  169. user.find('Notify').click()
  170. await user.should_see('Hello')
  171. with pytest.raises(AssertionError):
  172. await user.should_not_see('Hello')
  173. user.find('Hello').trigger('dismiss')
  174. await user.should_not_see('Hello')
  175. async def test_trigger_event(user: User) -> None:
  176. @ui.page('/')
  177. def page():
  178. ui.input().on('keydown.enter', lambda: ui.notify('Enter pressed'))
  179. await user.open('/')
  180. user.find(ui.input).trigger('keydown.enter')
  181. await user.should_see('Enter pressed')
  182. async def test_click_link(user: User) -> None:
  183. @ui.page('/')
  184. def page():
  185. ui.link('go to other', '/other')
  186. @ui.page('/other')
  187. def other():
  188. ui.label('Other page')
  189. await user.open('/')
  190. user.find('go to other').click()
  191. await user.should_see('Other page')
  192. async def test_kind_content_marker_combinations(user: User) -> None:
  193. @ui.page('/')
  194. def page():
  195. ui.label('One')
  196. ui.button('Two')
  197. ui.button('Three').mark('three')
  198. await user.open('/')
  199. await user.should_see(content='One')
  200. await user.should_see(kind=ui.button)
  201. await user.should_see(kind=ui.button, content='Two')
  202. with pytest.raises(AssertionError):
  203. await user.should_see(kind=ui.button, content='One')
  204. await user.should_see(marker='three')
  205. await user.should_see(kind=ui.button, marker='three')
  206. with pytest.raises(AssertionError):
  207. await user.should_see(marker='three', content='One')
  208. async def test_page_to_string_output_used_in_error_messages(user: User) -> None:
  209. @ui.page('/')
  210. def page():
  211. ui.label('Hello').mark('first')
  212. with ui.row():
  213. with ui.column():
  214. ui.button('World').mark('second')
  215. ui.icon('thumbs-up').mark('third')
  216. ui.avatar('star')
  217. ui.input('some input', placeholder='type here', value='typed')
  218. ui.markdown('''## Markdown
  219. - A
  220. - B
  221. - C
  222. ''')
  223. with ui.card().tight():
  224. ui.image('https://via.placeholder.com/150')
  225. await user.open('/')
  226. output = str(user.current_layout)
  227. assert output == '''
  228. q-layout
  229. q-page-container
  230. q-page
  231. div
  232. Label [markers=first, text=Hello]
  233. Row
  234. Column
  235. Button [markers=second, label=World]
  236. Icon [markers=third, name=thumbs-up]
  237. Avatar [icon=star]
  238. Input [value=typed, label=some input, placeholder=type here, type=text]
  239. Markdown [content=## Markdown...]
  240. Card
  241. Image [src=https://via.placehol...]
  242. '''.strip()
  243. async def test_combined_filter_parameters(user: User) -> None:
  244. ui.input(placeholder='x', value='y')
  245. await user.open('/')
  246. await user.should_see('x')
  247. await user.should_see('y')
  248. await user.should_not_see('x y')
  249. async def test_typing(user: User) -> None:
  250. @ui.page('/')
  251. def page():
  252. ui.label('Hello!')
  253. ui.button('World!')
  254. await user.open('/')
  255. # NOTE we have not yet found a way to test the typing suggestions automatically
  256. # to test, hover over the variable and verify that your IDE inferres the correct type
  257. _ = user.find(kind=ui.label).elements # Set[ui.label]
  258. _ = user.find(ui.label).elements # Set[ui.label]
  259. _ = user.find('World').elements # Set[ui.element]
  260. _ = user.find('Hello').elements # Set[ui.element]
  261. _ = user.find('!').elements # Set[ui.element]
  262. async def test_select(user: User) -> None:
  263. ui.select(options=['A', 'B', 'C'], on_change=lambda e: ui.notify(f'Value: {e.value}'))
  264. await user.open('/')
  265. await user.should_not_see('A')
  266. await user.should_not_see('B')
  267. await user.should_not_see('C')
  268. user.find(ui.select).click()
  269. await user.should_see('B')
  270. await user.should_see('C')
  271. user.find('A').click()
  272. await user.should_see('Value: A')
  273. await user.should_see('A')
  274. await user.should_not_see('B')
  275. await user.should_not_see('C')
  276. async def test_select_from_dict(user: User) -> None:
  277. ui.select(options={'value A': 'label A', 'value B': 'label B', 'value C': 'label C'},
  278. on_change=lambda e: ui.notify(f'Notify: {e.value}'))
  279. await user.open('/')
  280. await user.should_not_see('label A')
  281. await user.should_not_see('label B')
  282. await user.should_not_see('label C')
  283. user.find(ui.select).click()
  284. await user.should_see('label A')
  285. await user.should_see('label B')
  286. await user.should_see('label C')
  287. user.find('label A').click()
  288. await user.should_see('Notify: value A')
  289. async def test_select_multiple_from_dict(user: User) -> None:
  290. ui.select(options={'value A': 'label A', 'value B': 'label B', 'value C': 'label C'},
  291. multiple=True, on_change=lambda e: ui.notify(f'Notify: {e.value}'))
  292. await user.open('/')
  293. await user.should_not_see('label A')
  294. await user.should_not_see('label B')
  295. await user.should_not_see('label C')
  296. user.find(ui.select).click()
  297. await user.should_see('label A')
  298. await user.should_see('label B')
  299. await user.should_see('label C')
  300. user.find('label A').click()
  301. await user.should_see("Notify: ['value A']")
  302. user.find(ui.select).click()
  303. user.find('label B').click()
  304. await user.should_see("Notify: ['value A', 'value B']")
  305. async def test_select_multiple_values(user: User):
  306. select = ui.select(['A', 'B'], value='A',
  307. multiple=True, on_change=lambda e: ui.notify(f'Notify: {e.value}'))
  308. ui.label().bind_text_from(select, 'value', backward=lambda v: f'value = {v}')
  309. await user.open('/')
  310. await user.should_see("value = ['A']")
  311. user.find(ui.select).click()
  312. user.find('B').click()
  313. await user.should_see("Notify: ['A', 'B']")
  314. await user.should_see("value = ['A', 'B']")
  315. assert select.value == ['A', 'B']
  316. user.find(ui.select).click()
  317. user.find('A').click()
  318. await user.should_see("Notify: ['B']")
  319. await user.should_see("value = ['B']")
  320. assert select.value == ['B']
  321. async def test_upload_table(user: User) -> None:
  322. def receive_file(e: events.UploadEventArguments) -> None:
  323. reader = csv.DictReader(e.content.read().decode('utf-8').splitlines())
  324. ui.table(columns=[{'name': h, 'label': h.capitalize(), 'field': h} for h in reader.fieldnames or []],
  325. rows=list(reader))
  326. ui.upload(on_upload=receive_file)
  327. await user.open('/')
  328. upload = user.find(ui.upload).elements.pop()
  329. headers = Headers(raw=[(b'content-type', b'text/csv')])
  330. upload.handle_uploads([UploadFile(BytesIO(b'name,age\nAlice,30\nBob,28'), headers=headers)])
  331. table = user.find(ui.table).elements.pop()
  332. assert table.columns == [
  333. {'name': 'name', 'label': 'Name', 'field': 'name'},
  334. {'name': 'age', 'label': 'Age', 'field': 'age'},
  335. ]
  336. assert table.rows == [
  337. {'name': 'Alice', 'age': '30'},
  338. {'name': 'Bob', 'age': '28'},
  339. ]
  340. @pytest.mark.parametrize('data', ['/data', b'Hello'])
  341. async def test_download_file(user: User, data: Union[str, bytes]) -> None:
  342. @app.get('/data')
  343. def get_data() -> PlainTextResponse:
  344. return PlainTextResponse('Hello')
  345. @ui.page('/')
  346. def page():
  347. ui.button('Download', on_click=lambda: ui.download(data))
  348. await user.open('/')
  349. assert len(user.download.http_responses) == 0
  350. user.find('Download').click()
  351. response = await user.download.next()
  352. assert len(user.download.http_responses) == 1
  353. assert response.status_code == 200
  354. assert response.text == 'Hello'
  355. async def test_validation(user: User) -> None:
  356. ui.input('Number', validation={'Not a number': lambda value: value.isnumeric()})
  357. await user.open('/')
  358. await user.should_not_see('Not a number')
  359. user.find(ui.input).type('some invalid entry')
  360. await user.should_see('Not a number')
  361. async def test_trigger_autocomplete(user: User) -> None:
  362. ui.input(label='fruit', autocomplete=['apple', 'banana', 'cherry'])
  363. await user.open('/')
  364. await user.should_not_see('apple')
  365. user.find('fruit').type('a').trigger('keydown.tab')
  366. await user.should_see('apple')
  367. async def test_seeing_invisible_elements(user: User) -> None:
  368. visible_label = ui.label('Visible')
  369. hidden_label = ui.label('Hidden')
  370. hidden_label.visible = False
  371. await user.open('/')
  372. with pytest.raises(AssertionError):
  373. await user.should_see('Hidden')
  374. with pytest.raises(AssertionError):
  375. await user.should_not_see('Visible')
  376. visible_label.visible = False
  377. hidden_label.visible = True
  378. await user.should_see('Hidden')
  379. await user.should_not_see('Visible')
  380. async def test_finding_invisible_elements(user: User) -> None:
  381. button = ui.button('click me', on_click=lambda: ui.label('clicked'))
  382. button.visible = False
  383. await user.open('/')
  384. with pytest.raises(AssertionError):
  385. user.find('click me').click()
  386. button.visible = True
  387. user.find('click me').click()
  388. await user.should_see('clicked')
  389. async def test_page_to_string_output_for_invisible_elements(user: User) -> None:
  390. ui.label('Visible')
  391. ui.label('Hidden').set_visibility(False)
  392. await user.open('/')
  393. output = str(user.current_layout)
  394. assert output == '''
  395. q-layout
  396. q-page-container
  397. q-page
  398. div
  399. Label [text=Visible]
  400. Label [text=Hidden, visible=False]
  401. '''.strip()
  402. async def test_typing_to_disabled_element(user: User) -> None:
  403. initial_value = 'Hello first'
  404. given_new_input = 'Hello second'
  405. target = ui.input(value=initial_value)
  406. target.disable()
  407. await user.open('/')
  408. user.find(initial_value).type(given_new_input)
  409. assert target.value == initial_value
  410. await user.should_see(initial_value)
  411. await user.should_not_see(given_new_input)