瀏覽代碼

Pytest: JavaScript calls should not time out when using the user fixture (#4512)

This PR tries to fix #4508 which discovered that a javascript call is
required to create `ui.drawer`. If the test lasted longer than 1 sec the
error handler was called with a javascript timeout. To fix this this PR
does the following:

- implement a general test failure if there are error logs (this makes
the original issue visible in the first place)
- provide a generic mechanism in the user fixture to define what the
simulated response to a certain javascript call should be
- use the mechanism to provide a proper javascript response for a value
check inside `ui.drawer`

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
Rodja Trappe 1 月之前
父節點
當前提交
745167c81c

+ 3 - 2
nicegui/page_layout.py

@@ -132,8 +132,9 @@ class Drawer(ValueElement, default_classes='nicegui-drawer'):
 
         if value is None and not self.client.is_auto_index_client:
             async def _request_value() -> None:
-                js_code = f'!getHtmlElement({self.id}).parentElement.classList.contains("q-layout--prevent-focus")'
-                self.value = await context.client.run_javascript(js_code)
+                self.value = await context.client.run_javascript(
+                    f'!getHtmlElement({self.id}).parentElement.classList.contains("q-layout--prevent-focus")  // __IS_DRAWER_OPEN__'
+                )
             self.client.on_connect(_request_value)
 
     def toggle(self) -> None:

+ 23 - 1
nicegui/testing/user.py

@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import asyncio
 import re
-from typing import Any, List, Optional, Set, Type, TypeVar, Union, overload
+from typing import Any, Callable, Dict, List, Optional, Set, Type, TypeVar, Union, overload
 from uuid import uuid4
 
 import httpx
@@ -11,6 +11,7 @@ import socketio
 from nicegui import Client, ElementFilter, ui
 from nicegui.element import Element
 from nicegui.nicegui import _on_handshake
+from nicegui.outbox import Message
 
 from .user_download import UserDownload
 from .user_interaction import UserInteraction
@@ -35,6 +36,9 @@ class User:
         self.navigate = UserNavigate(self)
         self.notify = UserNotify()
         self.download = UserDownload(self)
+        self.javascript_rules: Dict[re.Pattern, Callable[[re.Match], Any]] = {
+            re.compile('.*__IS_DRAWER_OPEN__'): lambda _: True,  # see https://github.com/zauberzeug/nicegui/issues/4508
+        }
 
     @property
     def _client(self) -> Client:
@@ -69,8 +73,26 @@ class User:
         self.back_history.append(path)
         if clear_forward_history:
             self.forward_history.clear()
+        self._patch_outbox_emit_function()
         return self.client
 
+    def _patch_outbox_emit_function(self) -> None:
+        original_emit = self._client.outbox._emit
+
+        async def simulated_emit(message: Message) -> None:
+            await original_emit(message)
+            _, type_, data = message
+            if type_ == 'run_javascript':
+                for rule, result in self.javascript_rules.items():
+                    match = rule.match(data['code'])
+                    if match:
+                        self._client.handle_javascript_response({
+                            'request_id': data['request_id'],
+                            'result': result(match),
+                        })
+
+        self._client.outbox._emit = simulated_emit  # type: ignore
+
     @overload
     async def should_see(self,
                          target: Union[str, Type[T]],

+ 4 - 0
nicegui/testing/user_plugin.py

@@ -38,6 +38,7 @@ def prepare_simulated_auto_index_client(request):
 @pytest.fixture
 async def user(nicegui_reset_globals,  # noqa: F811, pylint: disable=unused-argument
                prepare_simulated_auto_index_client,  # pylint: disable=unused-argument
+               caplog: pytest.LogCaptureFixture,
                request: pytest.FixtureRequest,
                ) -> AsyncGenerator[User, None]:
     """Create a new user fixture."""
@@ -48,6 +49,9 @@ async def user(nicegui_reset_globals,  # noqa: F811, pylint: disable=unused-argu
     ui.navigate = Navigate()
     ui.notify = notify
     ui.download = download
+    logs = caplog.get_records('call')
+    if logs:
+        pytest.fail('There were unexpected logs.', pytrace=False)
 
 
 @pytest.fixture

+ 25 - 0
tests/test_user_simulation.py

@@ -1,4 +1,5 @@
 import csv
+import re
 from io import BytesIO
 from typing import Callable, Dict, Type, Union
 
@@ -540,3 +541,27 @@ async def test_typing_to_disabled_element(user: User) -> None:
     assert target.value == initial_value
     await user.should_see(initial_value)
     await user.should_not_see(given_new_input)
+
+
+async def test_drawer(user: User):
+    @ui.page('/')
+    def test_page():
+        with ui.left_drawer() as drawer:
+            ui.label('Hello')
+        ui.label().bind_text_from(drawer, 'value', lambda v: f'Drawer: {v}')
+
+    await user.open('/')
+    await user.should_see('Hello')
+    await user.should_see('Drawer: True')
+
+
+async def test_run_javascript(user: User):
+    @ui.page('/')
+    async def page():
+        await ui.context.client.connected()
+        date = await ui.run_javascript('Math.sqrt(1764)')
+        ui.label(date)
+
+    user.javascript_rules[re.compile(r'Math.sqrt\((\d+)\)')] = lambda match: int(match.group(1))**0.5
+    await user.open('/')
+    await user.should_see('42')

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

@@ -248,11 +248,46 @@ def multiple_users():
         ''')
 
 
+doc.text('Simulate JavasScript', '''
+    The `User` class has a `javascript_rules` dictionary to simulate JavaScript execution.
+    The key is a compiled regular expression and the value is a function that returns the JavaScript response.
+    The function will be called with the match object of the regular expression on the JavaScript command.
+
+    *Added in version 2.14.0*
+''')
+
+
+@doc.ui
+def simulate_javascript():
+    with ui.row().classes('gap-4 items-stretch'):
+        with python_window(classes='w-[500px]', title='some UI code'):
+            ui.markdown('''
+                ```python
+                @ui.page('/')
+                async def page():
+                    await context.client.connected()
+                    date = await ui.run_javascript('Math.sqrt(1764)')
+                    ui.label(date)
+                ```
+            ''')
+
+        with python_window(classes='w-[500px]', title='user assertions'):
+            ui.markdown('''
+                ```python
+                user.javascript_rules[re.compile(r'Math.sqrt\\((\\d+)\\)')] = \\
+                    lambda match: int(match.group(1))**0.5
+                await user.open('/')
+                await user.should_see('42')
+                ```
+            ''')
+
+
 doc.text('Comparison with the screen fixture', '''
     By cutting out the browser, test execution becomes much faster than the [`screen` fixture](/documentation/screen).
-    Of course, some features like screenshots or browser-specific behavior are not available.
     See our [pytests example](https://github.com/zauberzeug/nicegui/tree/main/examples/pytests)
     which implements the same tests with both fixtures.
+    Of course, some features like screenshots or browser-specific behavior are not available,
+    but in most cases the speed of the `user` fixture makes it the first choice.
 ''')
 
 doc.reference(User, title='User Reference')