123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- from nicegui import ui
- from nicegui.testing import User, UserInteraction
- from ..windows import python_window
- from . import doc
- @doc.part('User Fixture')
- def user_fixture():
- ui.markdown('''
- We recommend utilizing the `user` fixture instead of the [`screen` fixture](/documentation/screen) wherever possible
- because execution is as fast as unit tests and it does not need Selenium as a dependency
- when loaded via `pytest_plugins = ['nicegui.testing.user_plugin']`.
- The `user` fixture cuts away the browser and replaces it by a lightweight simulation entirely in Python.
- See [project structure](/documentation/project_structure) for a description of the setup.
- You can assert to "see" specific elements or content, click buttons, type into inputs and trigger events.
- We aimed for a nice API to write acceptance tests which read like a story and are easy to understand.
- Due to the fast execution, the classical [test pyramid](https://martinfowler.com/bliki/TestPyramid.html),
- where UI tests are considered slow and expensive, does not apply anymore.
- ''').classes('bold-links arrow-links')
- with python_window(classes='w-[600px]', title='example'):
- ui.markdown('''
- ```python
- await user.open('/')
- user.find('Username').type('user1')
- user.find('Password').type('pass1').trigger('keydown.enter')
- await user.should_see('Hello user1!')
- user.find('logout').click()
- await user.should_see('Log in')
- ```
- ''')
- ui.markdown('''
- **NOTE:** The `user` fixture is quite new and still misses some features.
- Please let us know in separate feature requests
- [over on GitHub](https://github.com/zauberzeug/nicegui/discussions/new?category=ideas-feature-requests).
- ''').classes('bold-links arrow-links')
- @doc.part('Async execution')
- def async_execution():
- ui.markdown('''
- The user simulation runs in the same async context as your app
- to make querying and interaction as easy as possible.
- But that also means that your tests must be `async`.
- We suggest to activate the [pytest-asyncio auto-mode](https://pytest-asyncio.readthedocs.io/en/latest/concepts.html#auto-mode)
- by either creating a `pytest.ini` file in your project root
- or adding the activation directly to your `pyproject.toml`.
- ''').classes('bold-links arrow-links')
- with ui.row(wrap=False).classes('gap-4 items-center'):
- with python_window(classes='w-[300px] h-42', title='pytest.ini'):
- ui.markdown('''
- ```ini
- [pytest]
- asyncio_mode = auto
- ```
- ''')
- ui.label('or').classes('text-2xl')
- with python_window(classes='w-[300px] h-42', title='pyproject.toml'):
- ui.markdown('''
- ```toml
- [tool.pytest.ini_options]
- asyncio_mode = "auto"
- ```
- ''')
- doc.text('Querying', '''
- The querying capabilities of the `User` are built upon the [ElementFilter](/documentation/element_filter).
- The `user.should_see(...)` method and `user.find(...)` method
- provide parameters to filter for content, [markers](/documentation/element_filter#markers), types, etc.
- If you do not provide a named property, the string will match against the text content and markers.
- ''')
- @doc.ui
- def querying():
- with ui.row().classes('gap-4 items-stretch'):
- with python_window(classes='w-[400px]', title='some UI code'):
- ui.markdown('''
- ```python
- with ui.row():
- ui.label('Hello World!').mark('greeting')
- ui.icon('star')
- with ui.row():
- ui.label('Hello Universe!')
- ui.input(placeholder='Type here')
- ```
- ''')
- with python_window(classes='w-[600px]', title='user assertions'):
- ui.markdown('''
- ```python
- await user.should_see('greeting')
- await user.should_see('star')
- await user.should_see('Hello Universe!')
- await user.should_see('Type here')
- await user.should_see('Hello')
- await user.should_see(marker='greeting')
- await user.should_see(kind=ui.icon)
- ```
- ''')
- doc.text('Using an ElementFilter', '''
- It may be desirable to use an [`ElementFilter`](/documentation/element_filter) to
- - preserve the order of elements to check their order on the page, and
- - more granular filtering options, such as `ElementFilter(...).within(...)`.
- By entering the `user` context and iterating over `ElementFilter`,
- you can preserve the natural document order of matching elements:
- ''')
- @doc.ui
- def using_an_elementfilter():
- with ui.row().classes('gap-4 items-stretch'):
- with python_window(classes='w-[400px]', title='UI code'):
- ui.markdown('''
- ```python
- ui.label('1').mark('number')
- ui.label('2').mark('number')
- ui.label('3').mark('number')
- ```
- ''')
- with python_window(classes='w-[600px]', title='user assertions'):
- ui.markdown('''
- ```python
- with user:
- elements = list(ElementFilter(marker='number'))
- assert len(elements) == 3
- assert elements[0].text == '1'
- assert elements[1].text == '2'
- assert elements[2].text == '3'
- ```
- ''')
- doc.text('Complex elements', '''
- There are some elements with complex visualization and interaction behaviors (`ui.upload`, `ui.table`, ...).
- Not every aspect of these elements can be tested with `should_see` and `UserInteraction`.
- Still, you can grab them with `user.find(...)` and do the testing on the elements themselves.
- ''')
- @doc.ui
- def upload_table():
- with ui.row().classes('gap-4 items-stretch'):
- with python_window(classes='w-[500px]', title='some UI code'):
- ui.markdown('''
- ```python
- def receive_file(e: events.UploadEventArguments):
- content = e.content.read().decode('utf-8')
- reader = csv.DictReader(content.splitlines())
- ui.table(
- columns=[{
- 'name': h,
- 'label': h.capitalize(),
- 'field': h,
- } for h in reader.fieldnames or []],
- rows=list(reader),
- )
- ui.upload(on_upload=receive_file)
- ```
- ''')
- with python_window(classes='w-[500px]', title='user assertions'):
- ui.markdown('''
- ```python
- upload = user.find(ui.upload).elements.pop()
- upload.handle_uploads([UploadFile(
- BytesIO(b'name,age\\nAlice,30\\nBob,28'),
- filename='data.csv',
- headers=Headers(raw=[(b'content-type', b'text/csv')]),
- )])
- table = user.find(ui.table).elements.pop()
- assert table.columns == [
- {'name': 'name', 'label': 'Name', 'field': 'name'},
- {'name': 'age', 'label': 'Age', 'field': 'age'},
- ]
- assert table.rows == [
- {'name': 'Alice', 'age': '30'},
- {'name': 'Bob', 'age': '28'},
- ]
- ```
- ''')
- doc.text('Autocomplete', '''
- The `UserInteraction` object returned by `user.find(...)` provides methods to trigger events on the found elements.
- This demo shows how to trigger a "keydown.tab" event to autocomplete an input field.
- *Added in version 2.7.0*
- ''')
- @doc.ui
- def trigger_events():
- with ui.row().classes('gap-4 items-stretch'):
- with python_window(classes='w-[500px]', title='some UI code'):
- ui.markdown('''
- ```python
- fruits = ['apple', 'banana', 'cherry']
- ui.input(label='fruit', autocomplete=fruits)
- ```
- ''')
- with python_window(classes='w-[500px]', title='user assertions'):
- ui.markdown('''
- ```python
- await user.open('/')
- user.find('fruit').type('a').trigger('keydown.tab')
- await user.should_see('apple')
- ```
- ''')
- doc.text('Test Downloads', '''
- You can verify that a download was triggered by checking `user.downloads.http_responses`.
- By awaiting `user.downloads.next()` you can get the next download response.
- *Added in version 2.1.0*
- ''')
- @doc.ui
- def check_outbox():
- with ui.row().classes('gap-4 items-stretch'):
- with python_window(classes='w-[500px]', title='some UI code'):
- ui.markdown('''
- ```python
- @ui.page('/')
- def page():
- def download():
- ui.download(b'Hello', filename='hello.txt')
- ui.button('Download', on_click=download)
- ```
- ''')
- with python_window(classes='w-[500px]', title='user assertions'):
- ui.markdown('''
- ```python
- await user.open('/')
- assert len(user.download.http_responses) == 0
- user.find('Download').click()
- response = await user.download.next()
- assert response.text == 'Hello'
- ```
- ''')
- doc.text('Multiple Users', '''
- Sometimes it is not enough to just interact with the UI as a single user.
- Besides the `user` fixture, we also provide the `create_user` fixture which is a factory function to create users.
- The `User` instances are independent from each other and can interact with the UI in parallel.
- See our [Chat App example](https://github.com/zauberzeug/nicegui/blob/main/examples/chat_app/test_chat_app.py)
- for a full demonstration.
- ''')
- @doc.ui
- def multiple_users():
- with python_window(classes='w-[600px]', title='example'):
- ui.markdown('''
- ```python
- async def test_chat(create_user: Callable[[], User]) -> None:
- userA = create_user()
- await userA.open('/')
- userB = create_user()
- await userB.open('/')
- userA.find(ui.input).type('from A').trigger('keydown.enter')
- await userB.should_see('from A')
- userB.find(ui.input).type('from B').trigger('keydown.enter')
- await userA.should_see('from A')
- await userA.should_see('from B')
- ```
- ''')
- doc.text('Simulate JavasScript', '''
- The `User` class has a `javascript_rules` dictionary to simulate JavaScript execution.
- The key is a compiled regular expression and the value is a function that returns the JavaScript response.
- The function will be called with the match object of the regular expression on the JavaScript command.
- *Added in version 2.14.0*
- ''')
- @doc.ui
- def simulate_javascript():
- with ui.row().classes('gap-4 items-stretch'):
- with python_window(classes='w-[500px]', title='some UI code'):
- ui.markdown('''
- ```python
- @ui.page('/')
- async def page():
- await context.client.connected()
- date = await ui.run_javascript('Math.sqrt(1764)')
- ui.label(date)
- ```
- ''')
- with python_window(classes='w-[500px]', title='user assertions'):
- ui.markdown('''
- ```python
- user.javascript_rules[re.compile(r'Math.sqrt\\((\\d+)\\)')] = \\
- lambda match: int(match.group(1))**0.5
- await user.open('/')
- await user.should_see('42')
- ```
- ''')
- doc.text('Comparison with the screen fixture', '''
- By cutting out the browser, test execution becomes much faster than the [`screen` fixture](/documentation/screen).
- See our [pytests example](https://github.com/zauberzeug/nicegui/tree/main/examples/pytests)
- which implements the same tests with both fixtures.
- Of course, some features like screenshots or browser-specific behavior are not available,
- but in most cases the speed of the `user` fixture makes it the first choice.
- ''')
- doc.reference(User, title='User Reference')
- doc.reference(UserInteraction, title='UserInteraction Reference')
|