1
0

test_user_simulation.py 17 KB

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