Skip to content

Commit

Permalink
Introduce proper download simulation for the user fixture (#3689)
Browse files Browse the repository at this point in the history
* check outbox messages (see #3686)

* allow user fixture to test file downloads

* introduce proper download simulation

* update docs

* code review

* use constant for HTTP 200

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
  • Loading branch information
rodja and falkoschindler committed Sep 6, 2024
1 parent e27064e commit 2934267
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 3 deletions.
8 changes: 6 additions & 2 deletions nicegui/testing/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from nicegui.element import Element
from nicegui.nicegui import _on_handshake

from .user_download import UserDownload
from .user_interaction import UserInteraction
from .user_navigate import UserNavigate
from .user_notify import UserNotify
Expand All @@ -33,14 +34,16 @@ def __init__(self, client: httpx.AsyncClient) -> None:
self.forward_history: List[str] = []
self.navigate = UserNavigate(self)
self.notify = UserNotify()
self.download = UserDownload(self)

def __getattribute__(self, name: str) -> Any:
if name not in {'notify', 'navigate'}: # NOTE: avoid infinite recursion
if name not in {'notify', 'navigate', 'download'}: # NOTE: avoid infinite recursion
ui.navigate = self.navigate
ui.notify = self.notify
ui.download = self.download
return super().__getattribute__(name)

async def open(self, path: str, *, clear_forward_history: bool = True) -> None:
async def open(self, path: str, *, clear_forward_history: bool = True) -> Client:
"""Open the given path."""
response = await self.http_client.get(path, follow_redirects=True)
assert response.status_code == 200, f'Expected status code 200, got {response.status_code}'
Expand All @@ -56,6 +59,7 @@ async def open(self, path: str, *, clear_forward_history: bool = True) -> None:
self.back_history.append(path)
if clear_forward_history:
self.forward_history.clear()
return self.client

@overload
async def should_see(self,
Expand Down
46 changes: 46 additions & 0 deletions nicegui/testing/user_download.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from __future__ import annotations

import asyncio
import time
from pathlib import Path
from typing import TYPE_CHECKING, Any, List, Optional, Union

import httpx

from .. import background_tasks

if TYPE_CHECKING:
from .user import User


class UserDownload:

def __init__(self, user: User) -> None:
self.http_responses: List[httpx.Response] = []
self.user = user

def __call__(self, src: Union[str, Path, bytes], filename: Optional[str] = None, media_type: str = '') -> Any:
background_tasks.create(self._get(src))

async def _get(self, src: Union[str, Path, bytes]) -> None:
if isinstance(src, bytes):
await asyncio.sleep(0)
response = httpx.Response(httpx.codes.OK, content=src)
else:
response = await self.user.http_client.get(str(src))
self.http_responses.append(response)

async def next(self, *, timeout: float = 1.0) -> httpx.Response:
"""Wait for a new download to happen.
:param timeout: the maximum time to wait (default: 1.0)
:returns: the HTTP response
"""
assert self.user.client
downloads = len(self.http_responses)
deadline = time.time() + timeout
while len(self.http_responses) < downloads + 1:
await asyncio.sleep(0.1)
if time.time() > deadline:
raise TimeoutError('Download did not happen')
return self.http_responses[-1]
21 changes: 20 additions & 1 deletion tests/test_user_simulation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import csv
from io import BytesIO
from typing import Callable, Dict, Type
from typing import Callable, Dict, Type, Union

import pytest
from fastapi import UploadFile
Expand Down Expand Up @@ -377,3 +377,22 @@ def receive_file(e: events.UploadEventArguments) -> None:
{'name': 'Alice', 'age': '30'},
{'name': 'Bob', 'age': '28'},
]


@pytest.mark.parametrize('data', ['/data', b'Hello'])
async def test_download_file(user: User, data: Union[str, bytes]) -> None:
@app.get('/data')
def get_data() -> PlainTextResponse:
return PlainTextResponse('Hello')

@ui.page('/')
def page():
ui.button('Download', on_click=lambda: ui.download(data))

await user.open('/')
assert len(user.download.http_responses) == 0
user.find('Download').click()
response = await user.download.next()
assert len(user.download.http_responses) == 1
assert response.status_code == 200
assert response.text == 'Hello'
33 changes: 33 additions & 0 deletions website/documentation/content/user_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,39 @@ def receive_file(e: events.UploadEventArguments):
''')


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


@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.
Expand Down

0 comments on commit 2934267

Please sign in to comment.