Skip to content

Commit

Permalink
Add documentation of pytest fixtures and pytest configuration (#3413)
Browse files Browse the repository at this point in the history
* begin with documentation of fixtures and pytest setup

* code review

* also prepare simulation for screen fixture

* replace hacky docs.pytest and Demo.raw with generic docs.part

* better split of testing topics in the docs

* improve user docs

* add UserInteraction reference

* review

* simplify doc.part

* fix storage test

* web driver info

* review

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
  • Loading branch information
rodja and falkoschindler authored Aug 2, 2024
1 parent 7f20ecb commit 66f0430
Show file tree
Hide file tree
Showing 14 changed files with 473 additions and 9 deletions.
2 changes: 2 additions & 0 deletions nicegui/testing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from .screen import Screen
from .user import User
from .user_interaction import UserInteraction

__all__ = [
'Screen',
'User',
'UserInteraction',
]
6 changes: 5 additions & 1 deletion nicegui/testing/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def screen(nicegui_reset_globals, # pylint: disable=unused-argument
caplog: pytest.LogCaptureFixture,
) -> Generator[Screen, None, None]:
"""Create a new SeleniumScreen fixture."""
prepare_simulation(request)
screen_ = Screen(nicegui_driver, caplog)
yield screen_
logs = screen_.caplog.get_records('call')
Expand Down Expand Up @@ -176,7 +177,10 @@ def wrapped_test(*args, **kwargs):


def prepare_simulation(request: pytest.FixtureRequest) -> None:
"""Prepare a simulation to be started -- by using the "module_under_test" marker you can specify the main entry point of the app."""
"""Prepare a simulation to be started.
By using the "module_under_test" marker you can specify the main entry point of the app.
"""
marker = request.node.get_closest_marker('module_under_test')
if marker is not None:
with Client.auto_index_client:
Expand Down
19 changes: 16 additions & 3 deletions nicegui/testing/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ async def open(self, path: str, *, clear_forward_history: bool = True) -> None:
self.activate()

def activate(self) -> Self:
"""Activate the user for interaction."""
"""Activate the user for interaction.
This can be used if you have multiple users and want to switch between them.
"""
if self.current_user:
self.current_user.deactivate()
self.current_user = self
Expand All @@ -62,7 +65,10 @@ def activate(self) -> Self:
return self

def deactivate(self, *_) -> None:
"""Deactivate the user."""
"""Deactivate the user.
This can be used if you have multiple users and want to switch between them.
"""
assert self.client
self.client.__exit__()
self.current_user = None
Expand Down Expand Up @@ -93,7 +99,14 @@ async def should_see(self,
content: Union[str, list[str], None] = None,
retries: int = 3,
) -> None:
"""Assert that the page contains an input with the given value."""
"""Assert that the page contains an element fulfilling certain filter rules.
Note that there is no scrolling in the user simulation -- the entire page is always *visible*.
Due to asynchronous execution, sometimes the expected elements only appear after a short delay.
By default `should_see` makes three attempts to find the element before failing.
This can be adjusted with the `retries` parameter.
"""
assert self.client
for _ in range(retries):
with self.client:
Expand Down
3 changes: 2 additions & 1 deletion tests/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import httpx
import pytest

from nicegui import app, background_tasks, context, ui
from nicegui import app, background_tasks, context, core, ui
from nicegui import storage as storage_module
from nicegui.testing import Screen

Expand Down Expand Up @@ -285,6 +285,7 @@ def test_missing_storage_secret(screen: Screen):
def page():
ui.label(app.storage.user.get('message', 'no message'))

core.app.user_middleware.clear() # remove the session middlewares added by prepare_simulation by default
screen.open('/')
screen.assert_py_logger('ERROR', 'app.storage.user needs a storage_secret passed in ui.run()')

Expand Down
7 changes: 4 additions & 3 deletions website/documentation/content/doc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from .api import demo, extra_column, get_page, intro, redirects, reference, registry, text, title, ui
from .api import demo, extra_column, get_page, intro, part, redirects, reference, registry, text, title, ui

__all__ = [
'demo',
'extra_column',
'get_page',
'intro',
'part',
'redirects',
'reference',
'registry',
'text',
'title',
'ui',
'get_page',
'extra_column',
]
19 changes: 19 additions & 0 deletions website/documentation/content/doc/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from types import ModuleType
from typing import Any, Callable, Dict, Optional, Union, overload

import nicegui
from nicegui import app as nicegui_app
from nicegui import ui as nicegui_ui
from nicegui.elements.markdown import remove_indentation
Expand Down Expand Up @@ -112,6 +113,24 @@ def decorator(function: Callable) -> Callable:
return decorator


def part(title_: str) -> Callable:
"""Add a custom part with arbitrary UI and descriptive markdown elements to the current documentation page.
The content of any contained markdown elements will be used for search indexing.
"""
page = _get_current_page()

def decorator(function: Callable) -> Callable:
with nicegui_ui.element() as container:
function()
elements = nicegui.ElementFilter(kind=nicegui.ui.markdown, local_scope=True)
description = ''.join(e.content for e in elements if '```' not in e.content)
container.delete()
page.parts.append(DocumentationPart(title=title_, search_text=description, ui=function))
return function
return decorator


def ui(function: Callable) -> Callable:
"""Add arbitrary UI to the current documentation page."""
_get_current_page().parts.append(DocumentationPart(ui=function))
Expand Down
1 change: 1 addition & 0 deletions website/documentation/content/doc/part.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class DocumentationPart:
ui: Optional[Callable] = None
demo: Optional[Demo] = None
reference: Optional[type] = None
search_text: Optional[str] = None

@property
def link_target(self) -> Optional[str]:
Expand Down
22 changes: 22 additions & 0 deletions website/documentation/content/element_filter_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,26 @@ def text_element() -> None:
ui.label(', '.join(b.text for b in ElementFilter(kind=TextElement, local_scope=True)))


@doc.demo('Markers', '''
Markers are a simple way to tag elements with a string which can be queried by an `ElementFilter`.
''')
def marker_demo() -> None:
from nicegui import ElementFilter

with ui.card().mark('red'):
ui.label('label A')
with ui.card().mark('strong'):
ui.label('label B')
with ui.card().mark('red strong'):
ui.label('label C')

# ElementFilter(marker='red').classes('bg-red-200')
# ElementFilter(marker='strong').classes('text-bold')
# ElementFilter(marker='red strong').classes('bg-red-600 text-white')
# END OF DEMO
ElementFilter(marker='red', local_scope=True).classes('bg-red-200')
ElementFilter(marker='strong', local_scope=True).classes('text-bold')
ElementFilter(marker='red strong', local_scope=True).classes('bg-red-600 text-white')


doc.reference(ElementFilter)
17 changes: 17 additions & 0 deletions website/documentation/content/overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
section_page_layout,
section_pages_routing,
section_styling_appearance,
section_testing,
section_text_elements,
)

Expand Down Expand Up @@ -84,6 +85,19 @@
Out of the box, NiceGUI provides everything you need to make modern, stylish, responsive user interfaces.
''')

doc.text('Testing', '''
NiceGUI provides a comprehensive testing framework based on [pytest](https://docs.pytest.org/)
which allows you to automate the testing of your user interface.
You can utilize the `screen` fixture which starts a real (headless) browser to interact with your application.
This is great if you have browser-specific behavior to test.
But most of the time, NiceGUI's newly introduced `user` fixture is more suited:
It only simulates the user interaction on a Python level and, hence, is blazing fast.
That way the classical [test pyramid](https://martinfowler.com/bliki/TestPyramid.html),
where UI tests are considered slow and expensive, does not apply anymore.
This can have a huge impact on your development speed, quality and confidence.
''')

tiles = [
(section_text_elements, '''
Elements like `ui.label`, `ui.markdown`, `ui.restructured_text` and `ui.html` can be used to display text and other content.
Expand Down Expand Up @@ -115,6 +129,9 @@
(section_configuration_deployment, '''
Whether you want to run your app locally or on a server, native or in a browser, we got you covered.
'''),
(section_testing, '''
Write automated UI tests which run in a headless browser (slow) or fully simulated in Python (fast).
'''),
]


Expand Down
Loading

0 comments on commit 66f0430

Please sign in to comment.