1
0

user_documentation.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. from nicegui import ui
  2. from nicegui.testing import User, UserInteraction
  3. from ..windows import python_window
  4. from . import doc
  5. @doc.part('User Fixture')
  6. def user_fixture():
  7. ui.markdown('''
  8. We recommend utilizing the `user` fixture instead of the [`screen` fixture](/documentation/screen) wherever possible
  9. because execution is as fast as unit tests and it does not need Selenium as a dependency
  10. when loaded via `pytest_plugins = ['nicegui.testing.user_plugin']`.
  11. The `user` fixture cuts away the browser and replaces it by a lightweight simulation entirely in Python.
  12. See [project structure](/documentation/project_structure) for a description of the setup.
  13. You can assert to "see" specific elements or content, click buttons, type into inputs and trigger events.
  14. We aimed for a nice API to write acceptance tests which read like a story and are easy to understand.
  15. Due to the fast execution, the classical [test pyramid](https://martinfowler.com/bliki/TestPyramid.html),
  16. where UI tests are considered slow and expensive, does not apply anymore.
  17. ''').classes('bold-links arrow-links')
  18. with python_window(classes='w-[600px]', title='example'):
  19. ui.markdown('''
  20. ```python
  21. await user.open('/')
  22. user.find('Username').type('user1')
  23. user.find('Password').type('pass1').trigger('keydown.enter')
  24. await user.should_see('Hello user1!')
  25. user.find('logout').click()
  26. await user.should_see('Log in')
  27. ```
  28. ''')
  29. ui.markdown('''
  30. **NOTE:** The `user` fixture is quite new and still misses some features.
  31. Please let us know in separate feature requests
  32. [over on GitHub](https://github.com/zauberzeug/nicegui/discussions/new?category=ideas-feature-requests).
  33. ''').classes('bold-links arrow-links')
  34. @doc.part('Async execution')
  35. def async_execution():
  36. ui.markdown('''
  37. The user simulation runs in the same async context as your app
  38. to make querying and interaction as easy as possible.
  39. But that also means that your tests must be `async`.
  40. We suggest to activate the [pytest-asyncio auto-mode](https://pytest-asyncio.readthedocs.io/en/latest/concepts.html#auto-mode)
  41. by either creating a `pytest.ini` file in your project root
  42. or adding the activation directly to your `pyproject.toml`.
  43. ''').classes('bold-links arrow-links')
  44. with ui.row(wrap=False).classes('gap-4 items-center'):
  45. with python_window(classes='w-[300px] h-42', title='pytest.ini'):
  46. ui.markdown('''
  47. ```ini
  48. [pytest]
  49. asyncio_mode = auto
  50. ```
  51. ''')
  52. ui.label('or').classes('text-2xl')
  53. with python_window(classes='w-[300px] h-42', title='pyproject.toml'):
  54. ui.markdown('''
  55. ```toml
  56. [tool.pytest.ini_options]
  57. asyncio_mode = "auto"
  58. ```
  59. ''')
  60. doc.text('Querying', '''
  61. The querying capabilities of the `User` are built upon the [ElementFilter](/documentation/element_filter).
  62. The `user.should_see(...)` method and `user.find(...)` method
  63. provide parameters to filter for content, [markers](/documentation/element_filter#markers), types, etc.
  64. If you do not provide a named property, the string will match against the text content and markers.
  65. ''')
  66. @doc.ui
  67. def querying():
  68. with ui.row().classes('gap-4 items-stretch'):
  69. with python_window(classes='w-[400px]', title='some UI code'):
  70. ui.markdown('''
  71. ```python
  72. with ui.row():
  73. ui.label('Hello World!').mark('greeting')
  74. ui.icon('star')
  75. with ui.row():
  76. ui.label('Hello Universe!')
  77. ui.input(placeholder='Type here')
  78. ```
  79. ''')
  80. with python_window(classes='w-[600px]', title='user assertions'):
  81. ui.markdown('''
  82. ```python
  83. await user.should_see('greeting')
  84. await user.should_see('star')
  85. await user.should_see('Hello Universe!')
  86. await user.should_see('Type here')
  87. await user.should_see('Hello')
  88. await user.should_see(marker='greeting')
  89. await user.should_see(kind=ui.icon)
  90. ```
  91. ''')
  92. doc.text('Complex elements', '''
  93. There are some elements with complex visualization and interaction behaviors (`ui.upload`, `ui.table`, ...).
  94. Not every aspect of these elements can be tested with `should_see` and `UserInteraction`.
  95. Still, you can grab them with `user.find(...)` and do the testing on the elements themselves.
  96. ''')
  97. @doc.ui
  98. def upload_table():
  99. with ui.row().classes('gap-4 items-stretch'):
  100. with python_window(classes='w-[500px]', title='some UI code'):
  101. ui.markdown('''
  102. ```python
  103. def receive_file(e: events.UploadEventArguments):
  104. content = e.content.read().decode('utf-8')
  105. reader = csv.DictReader(content.splitlines())
  106. ui.table(
  107. columns=[{
  108. 'name': h,
  109. 'label': h.capitalize(),
  110. 'field': h,
  111. } for h in reader.fieldnames or []],
  112. rows=list(reader),
  113. )
  114. ui.upload(on_upload=receive_file)
  115. ```
  116. ''')
  117. with python_window(classes='w-[500px]', title='user assertions'):
  118. ui.markdown('''
  119. ```python
  120. upload = user.find(ui.upload).elements.pop()
  121. upload.handle_uploads([UploadFile(
  122. BytesIO(b'name,age\\nAlice,30\\nBob,28'),
  123. filename='data.csv',
  124. headers=Headers(raw=[(b'content-type', b'text/csv')]),
  125. )])
  126. table = user.find(ui.table).elements.pop()
  127. assert table.columns == [
  128. {'name': 'name', 'label': 'Name', 'field': 'name'},
  129. {'name': 'age', 'label': 'Age', 'field': 'age'},
  130. ]
  131. assert table.rows == [
  132. {'name': 'Alice', 'age': '30'},
  133. {'name': 'Bob', 'age': '28'},
  134. ]
  135. ```
  136. ''')
  137. doc.text('Autocomplete', '''
  138. The `UserInteraction` object returned by `user.find(...)` provides methods to trigger events on the found elements.
  139. This demo shows how to trigger a "keydown.tab" event to autocomplete an input field.
  140. *Added in version 2.7.0*
  141. ''')
  142. @doc.ui
  143. def trigger_events():
  144. with ui.row().classes('gap-4 items-stretch'):
  145. with python_window(classes='w-[500px]', title='some UI code'):
  146. ui.markdown('''
  147. ```python
  148. fruits = ['apple', 'banana', 'cherry']
  149. ui.input(label='fruit', autocomplete=fruits)
  150. ```
  151. ''')
  152. with python_window(classes='w-[500px]', title='user assertions'):
  153. ui.markdown('''
  154. ```python
  155. await user.open('/')
  156. user.find('fruit').type('a').trigger('keydown.tab')
  157. await user.should_see('apple')
  158. ```
  159. ''')
  160. doc.text('Test Downloads', '''
  161. You can verify that a download was triggered by checking `user.downloads.http_responses`.
  162. By awaiting `user.downloads.next()` you can get the next download response.
  163. *Added in version 2.1.0*
  164. ''')
  165. @doc.ui
  166. def check_outbox():
  167. with ui.row().classes('gap-4 items-stretch'):
  168. with python_window(classes='w-[500px]', title='some UI code'):
  169. ui.markdown('''
  170. ```python
  171. @ui.page('/')
  172. def page():
  173. def download():
  174. ui.download(b'Hello', filename='hello.txt')
  175. ui.button('Download', on_click=download)
  176. ```
  177. ''')
  178. with python_window(classes='w-[500px]', title='user assertions'):
  179. ui.markdown('''
  180. ```python
  181. await user.open('/')
  182. assert len(user.download.http_responses) == 0
  183. user.find('Download').click()
  184. response = await user.download.next()
  185. assert response.text == 'Hello'
  186. ```
  187. ''')
  188. doc.text('Multiple Users', '''
  189. Sometimes it is not enough to just interact with the UI as a single user.
  190. Besides the `user` fixture, we also provide the `create_user` fixture which is a factory function to create users.
  191. The `User` instances are independent from each other and can interact with the UI in parallel.
  192. See our [Chat App example](https://github.com/zauberzeug/nicegui/blob/main/examples/chat_app/test_chat_app.py)
  193. for a full demonstration.
  194. ''')
  195. @doc.ui
  196. def multiple_users():
  197. with python_window(classes='w-[600px]', title='example'):
  198. ui.markdown('''
  199. ```python
  200. async def test_chat(create_user: Callable[[], User]) -> None:
  201. userA = create_user()
  202. await userA.open('/')
  203. userB = create_user()
  204. await userB.open('/')
  205. userA.find(ui.input).type('from A').trigger('keydown.enter')
  206. await userB.should_see('from A')
  207. userB.find(ui.input).type('from B').trigger('keydown.enter')
  208. await userA.should_see('from A')
  209. await userA.should_see('from B')
  210. ```
  211. ''')
  212. doc.text('Comparison with the screen fixture', '''
  213. By cutting out the browser, test execution becomes much faster than the [`screen` fixture](/documentation/screen).
  214. Of course, some features like screenshots or browser-specific behavior are not available.
  215. See our [pytests example](https://github.com/zauberzeug/nicegui/tree/main/examples/pytests)
  216. which implements the same tests with both fixtures.
  217. ''')
  218. doc.reference(User, title='User Reference')
  219. doc.reference(UserInteraction, title='UserInteraction Reference')