Skip to content

Commit

Permalink
replace hacky docs.pytest and Demo.raw with generic docs.part
Browse files Browse the repository at this point in the history
  • Loading branch information
rodja committed Jul 31, 2024
1 parent b929ef8 commit 3e11494
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 65 deletions.
4 changes: 2 additions & 2 deletions website/documentation/content/doc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .api import demo, extra_column, get_page, intro, redirects, reference, registry, text, title, ui, pytest
from .api import demo, extra_column, get_page, intro, redirects, reference, registry, text, title, ui, part

__all__ = [
'demo',
Expand All @@ -11,5 +11,5 @@
'ui',
'get_page',
'extra_column',
'pytest',
'part',
]
34 changes: 13 additions & 21 deletions website/documentation/content/doc/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from nicegui import app as nicegui_app
from nicegui import ui as nicegui_ui
import nicegui
from nicegui.elements.markdown import remove_indentation

from .page import DocumentationPage
Expand Down Expand Up @@ -112,30 +113,21 @@ def decorator(function: Callable) -> Callable:
return decorator


def pytest(*args, **kwargs) -> Callable[[Callable], Callable]:
"""Add a pytest demo section to the current documentation page."""
if len(args) == 2:
title_, description = args
is_markdown = True
else:
obj = args[0]
doc = obj.__doc__
if isinstance(obj, type) and not doc:
doc = obj.__init__.__doc__ # type: ignore
title_, description = doc.split('\n', 1)
title_ = title_.rstrip('.')
is_markdown = False

description = remove_indentation(description)
def part(title_: str) -> Callable:
"""Add a custom part to the current documentation page."""
page = _get_current_page()

def decorator(function: Callable) -> Callable:
page.parts.append(DocumentationPart(
title=title_,
description=description,
description_format='md' if is_markdown else 'rst',
demo=Demo(function=function, lazy=kwargs.get('lazy', True), tab=kwargs.get('tab'), raw=True),
))
task_id = nicegui.slot.get_task_id()
orig = nicegui.slot.Slot.stacks[task_id]
# NOTE we create an empty context so the function is not rendered as elements but available to ElementFilter
del nicegui.slot.Slot.stacks[task_id]
with nicegui.ui.element(_client=nicegui.Client(nicegui.page.page(''), request=None)):
function()
elements = nicegui.ElementFilter(kind=nicegui.ui.markdown, local_scope=True)
description = ''.join(e.content for e in elements if '```' not in e.content)
nicegui.slot.Slot.stacks[task_id] = orig
page.parts.append(DocumentationPart(title=title_, search_text=description, ui=function))
return function
return decorator

Expand Down
2 changes: 1 addition & 1 deletion website/documentation/content/doc/part.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ class Demo:
function: Callable
lazy: bool = True
tab: Optional[Union[str, Callable]] = None
raw: bool = False


@dataclass(**KWONLY_SLOTS)
Expand All @@ -24,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
28 changes: 26 additions & 2 deletions website/documentation/content/screen_documentation.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
from nicegui.testing import Screen
from . import doc
from nicegui.testing import Screen
from ..windows import python_window
from nicegui import ui


@doc.part('Screen Fixture')
def screen_fixture():

doc.text('Screen Fixture', '''
The `screen` fixture starts a real(headless) browser to interact with your application.
ui.markdown('''
The `screen` fixture starts a real (headless) browser to interact with your application.
This is only necessary if you have browser specific behavior to test.
NiceGUI itself is thoroughly tested with this fixture to ensure each component works as expected.
So only use it if you have to.
''')

with python_window(classes='w-[600px]', title='example'):
ui.markdown('''
```python
from selenium.webdriver.common.keys import Keys
screen.open('/')
screen.type(Keys.TAB) # to focus on the first input
screen.type('user1')
screen.type(Keys.TAB) # to focus the second input
screen.type('pass1')
screen.click('Log in')
screen.should_contain('Hello user1!')
screen.click('logout')
screen.should_contain('Log in')
```''')


doc.reference(Screen)
37 changes: 21 additions & 16 deletions website/documentation/content/user_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,28 @@
from ..windows import python_window


@doc.pytest('User Fixture', '''
We recommend you utilize 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.
The `user` fixture cuts away the browser and replaces it by a light weight simulation which entirely runs in Python.
You can assert for 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.
''', preview=False)
@doc.part('User Fixture')
def user_fixture():
async def test_login_logoff(user: User) -> None:
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('''
We recommend you utilize 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.
The `user` fixture cuts away the browser and replaces it by a light weight simulation which entirely runs in Python.
You can assert for 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.
''')

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')
```''')


doc.text('Querying', '''
Expand Down
40 changes: 19 additions & 21 deletions website/documentation/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def _uncomment(text: str) -> str:
return UNCOMMENT_PATTERN.sub(r'\1', text) # NOTE: non-executed lines should be shown in the code examples


def demo(f: Callable, *, lazy: bool = True, tab: Optional[Union[str, Callable]] = None, raw: bool = False) -> Callable:
def demo(f: Callable, *, lazy: bool = True, tab: Optional[Union[str, Callable]] = None) -> Callable:
"""Render a callable as a demo with Python code and browser window."""
with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
code = inspect.getsource(f).split('# END OF DEMO', 1)[0].strip().splitlines()
Expand All @@ -29,11 +29,10 @@ def demo(f: Callable, *, lazy: bool = True, tab: Optional[Union[str, Callable]]
del code[0]
del code[0]
indentation = len(code[0]) - len(code[0].lstrip())
if not raw:
code = [line[indentation:] for line in code]
code = ['from nicegui import ui'] + [_uncomment(line) for line in code]
code = [line[indentation:] for line in code]
code = ['from nicegui import ui'] + [_uncomment(line) for line in code]
code = ['' if line == '#' else line for line in code]
if not code[-1].startswith('ui.run(') and not raw:
if not code[-1].startswith('ui.run('):
code.append('')
code.append('ui.run()')
full_code = isort.code('\n'.join(code), no_sections=True, lines_after_imports=1)
Expand All @@ -43,21 +42,20 @@ def demo(f: Callable, *, lazy: bool = True, tab: Optional[Union[str, Callable]]
.classes('absolute right-2 top-10 opacity-10 hover:opacity-80 cursor-pointer') \
.on('click', js_handler=f'() => navigator.clipboard.writeText({json.dumps(full_code)})') \
.on('click', lambda: ui.notify('Copied to clipboard', type='positive', color='primary'), [])
if not raw:
with browser_window(title=tab,
classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window') as window:
if lazy:
spinner = ui.spinner(size='lg').props('thickness=2')

async def handle_intersection():
window.remove(spinner)
if helpers.is_coroutine_function(f):
await f()
else:
f()
intersection_observer(on_intersection=handle_intersection)
else:
assert not helpers.is_coroutine_function(f), 'async functions are not supported in non-lazy demos'
f()
with browser_window(title=tab,
classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window') as window:
if lazy:
spinner = ui.spinner(size='lg').props('thickness=2')

async def handle_intersection():
window.remove(spinner)
if helpers.is_coroutine_function(f):
await f()
else:
f()
intersection_observer(on_intersection=handle_intersection)
else:
assert not helpers.is_coroutine_function(f), 'async functions are not supported in non-lazy demos'
f()

return f
2 changes: 1 addition & 1 deletion website/documentation/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def render_content():
if part.ui:
part.ui()
if part.demo:
demo(part.demo.function, lazy=part.demo.lazy, tab=part.demo.tab, raw=part.demo.raw)
demo(part.demo.function, lazy=part.demo.lazy, tab=part.demo.tab)
if part.reference:
generate_class_doc(part.reference, part.title)
if part.link:
Expand Down
2 changes: 1 addition & 1 deletion website/documentation/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def build_search_index() -> None:
search_index.extend([
{
'title': f'{documentation.heading.replace("*", "")}: {part.title}',
'content': part.description or '',
'content': part.description or part.search_text or '',
'format': part.description_format,
'url': f'/documentation/{documentation.name}#{part.link_target}',
}
Expand Down

0 comments on commit 3e11494

Please sign in to comment.