瀏覽代碼

Testing complex elements with `User` simulation (#3635)

* 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>
Rodja Trappe 8 月之前
父節點
當前提交
608b79b042
共有 3 個文件被更改,包括 107 次插入20 次删除
  1. 27 18
      nicegui/elements/upload.py
  2. 28 1
      tests/test_user_simulation.py
  3. 52 1
      website/documentation/content/user_documentation.py

+ 27 - 18
nicegui/elements/upload.py

@@ -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
@@ -60,28 +60,37 @@ class Upload(DisableableElement, component='upload.js'):
         @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)

+ 28 - 1
tests/test_user_simulation.py

@@ -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
@@ -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'},
+    ]

+ 52 - 1
website/documentation/content/user_documentation.py

@@ -78,7 +78,7 @@ doc.text('Querying', '''
 
 @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
@@ -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.