Skip to content

Commit

Permalink
Testing complex elements with User simulation (#3635)
Browse files Browse the repository at this point in the history
* allow simulated uploads

* about testing complex elements

* testing upload table in one scenario
because it is also a demo in the docs and not as "artifical"

* code review

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
  • Loading branch information
rodja and falkoschindler committed Aug 29, 2024
1 parent 252a111 commit 608b79b
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 20 deletions.
45 changes: 27 additions & 18 deletions nicegui/elements/upload.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Callable, Dict, Optional, cast
from typing import Any, Callable, Dict, List, Optional, cast

from fastapi import Request
from starlette.datastructures import UploadFile
Expand Down Expand Up @@ -60,28 +60,37 @@ def __init__(self, *,
@app.post(self._props['url'])
async def upload_route(request: Request) -> Dict[str, str]:
form = await request.form()
for data in form.values():
for upload_handler in self._upload_handlers:
handle_event(upload_handler, UploadEventArguments(
sender=self,
client=self.client,
content=cast(UploadFile, data).file,
name=cast(UploadFile, data).filename or '',
type=cast(UploadFile, data).content_type or '',
))
for multi_upload_handler in self._multi_upload_handlers:
handle_event(multi_upload_handler, MultiUploadEventArguments(
sender=self,
client=self.client,
contents=[cast(UploadFile, data).file for data in form.values()],
names=[cast(UploadFile, data).filename or '' for data in form.values()],
types=[cast(UploadFile, data).content_type or '' for data in form.values()],
))
uploads = [cast(UploadFile, data) for data in form.values()]
self.handle_uploads(uploads)
return {'upload': 'success'}

if on_rejected:
self.on_rejected(on_rejected)

def handle_uploads(self, uploads: List[UploadFile]) -> None:
"""Handle the uploaded files.
This method is primarily intended for internal use and for simulating file uploads in tests.
"""
for upload in uploads:
for upload_handler in self._upload_handlers:
handle_event(upload_handler, UploadEventArguments(
sender=self,
client=self.client,
content=upload.file,
name=upload.filename or '',
type=upload.content_type or '',
))
multi_upload_args = MultiUploadEventArguments(
sender=self,
client=self.client,
contents=[upload.file for upload in uploads],
names=[upload.filename or '' for upload in uploads],
types=[upload.content_type or '' for upload in uploads],
)
for multi_upload_handler in self._multi_upload_handlers:
handle_event(multi_upload_handler, multi_upload_args)

def on_upload(self, callback: Callable[..., Any]) -> Self:
"""Add a callback to be invoked when a file is uploaded."""
self._upload_handlers.append(callback)
Expand Down
29 changes: 28 additions & 1 deletion tests/test_user_simulation.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import csv
from io import BytesIO
from typing import Callable, Dict, Type

import pytest
from fastapi import UploadFile
from fastapi.datastructures import Headers
from fastapi.responses import PlainTextResponse

from nicegui import app, ui
from nicegui import app, events, ui
from nicegui.testing import User

# pylint: disable=missing-function-docstring
Expand Down Expand Up @@ -350,3 +354,26 @@ async def test_select(user: User) -> None:
await user.should_see('A')
await user.should_not_see('B')
await user.should_not_see('C')


async def test_upload_table(user: User) -> None:
def receive_file(e: events.UploadEventArguments) -> None:
reader = csv.DictReader(e.content.read().decode('utf-8').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)

await user.open('/')
upload = user.find(ui.upload).elements.pop()
headers = Headers(raw=[(b'content-type', b'text/csv')])
upload.handle_uploads([UploadFile(BytesIO(b'name,age\nAlice,30\nBob,28'), headers=headers)])

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'},
]
53 changes: 52 additions & 1 deletion website/documentation/content/user_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def async_execution():

@doc.ui
def querying():
with ui.row(wrap=False).classes('gap-4 items-stretch'):
with ui.row().classes('gap-4 items-stretch'):
with python_window(classes='w-[400px]', title='some UI code'):
ui.markdown('''
```python
Expand All @@ -105,6 +105,57 @@ def querying():
''')


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('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.
Expand Down

0 comments on commit 608b79b

Please sign in to comment.