Browse Source

Merge branch 'main' of github.com:zauberzeug/nicegui

Falko Schindler 2 năm trước cách đây
mục cha
commit
ea759d213c

+ 1 - 1
nicegui/binding.py

@@ -40,7 +40,7 @@ async def update_views_async(views: Set[HTMLBaseComponent]):
 
 
 def update_views(views: Set[HTMLBaseComponent]):
-    if asyncio._get_running_loop() is None:
+    if globals.loop is None:
         return  # NOTE: no need to update view if event loop is not running, yet
     create_task(update_views_async(views), name='update_views_async')
 

+ 3 - 5
nicegui/elements/element.py

@@ -5,6 +5,7 @@ from typing import Dict, Optional
 
 import justpy as jp
 
+from .. import globals
 from ..binding import BindableProperty, bind_from, bind_to
 from ..page import Page, get_current_view
 from ..task_logger import create_task
@@ -117,8 +118,5 @@ class Element:
         return self
 
     def update(self) -> None:
-        try:
-            asyncio.get_running_loop()
-        except RuntimeError:
-            return
-        create_task(self.view.update())
+        if globals.loop is not None:
+            create_task(self.view.update())

+ 2 - 1
nicegui/elements/table.py

@@ -3,6 +3,7 @@ from typing import Dict
 
 import justpy as jp
 
+from .. import globals
 from ..task_logger import create_task
 from .element import Element
 
@@ -22,6 +23,6 @@ class Table(Element):
         view.options = self.options = jp.Dict(**options)
         super().__init__(view)
 
-        if not jp.template_options['aggrid'] and asyncio.get_event_loop().is_running():
+        if not jp.template_options['aggrid'] and globals.loop and globals.loop.is_running():
             create_task(self.page.run_javascript('location.reload()'))
         jp.template_options['aggrid'] = True

+ 1 - 1
nicegui/events.py

@@ -232,7 +232,7 @@ def handle_event(handler: Optional[Callable], arguments: EventArguments) -> Opti
         no_arguments = not signature(handler).parameters
         result = handler() if no_arguments else handler(arguments)
         if is_coroutine(handler):
-            if asyncio.get_event_loop().is_running():
+            if globals.loop and globals.loop.is_running():
                 create_task(result, name=str(handler))
             else:
                 on_startup(None, result)

+ 1 - 0
nicegui/globals.py

@@ -16,6 +16,7 @@ if TYPE_CHECKING:
 app: 'Starlette'
 config: Optional['Config'] = None
 server: Optional[Server] = None
+loop: Optional[asyncio.AbstractEventLoop] = None
 page_builders: Dict[str, 'PageBuilder'] = {}
 view_stack: List['jp.HTMLBaseComponent'] = []
 tasks: List[asyncio.tasks.Task] = []

+ 2 - 0
nicegui/nicegui.py

@@ -1,4 +1,5 @@
 # isort:skip_file
+import asyncio
 from typing import Awaitable, Callable
 
 if True:  # NOTE: prevent formatter from mixing up these lines
@@ -27,6 +28,7 @@ async def patched_justpy_startup():
 
 @jp.app.on_event('startup')
 def startup():
+    globals.loop = asyncio.get_running_loop()
     init_auto_index_page()
     create_page_routes()
     globals.tasks.extend(create_task(t.coro, name=t.name) for t in Timer.prepared_coroutines)

+ 1 - 1
nicegui/routes.py

@@ -84,7 +84,7 @@ def add_dependencies(py_filepath: str, dependencies: List[str] = []) -> None:
         jp.app.routes.insert(0, Route(f'/{filename}', lambda _: FileResponse(vue_filepath)))
         jp.component_file_list += [filename]
 
-    if asyncio.get_event_loop().is_running():
+    if globals.loop and globals.loop.is_running():
         # NOTE: if new dependencies are added after starting the server, we need to reload the page on connected clients
         async def reload(view: jp.HTMLBaseComponent) -> None:
             for page in view.pages.values():

+ 3 - 1
nicegui/task_logger.py

@@ -6,6 +6,8 @@ import logging
 import sys
 from typing import Any, Awaitable, Optional, Tuple, TypeVar
 
+from . import globals
+
 T = TypeVar('T')
 
 
@@ -26,7 +28,7 @@ def create_task(
     message = 'Task raised an exception'
     message_args = ()
     if loop is None:
-        loop = asyncio.get_running_loop()
+        loop = globals.loop
     if sys.version_info[1] < 8:
         task = loop.create_task(coroutine)  # name parameter is only supported from 3.8 onward
     else:

+ 2 - 1
nicegui/update.py

@@ -1,12 +1,13 @@
 import asyncio
 from typing import List
 
+from . import globals
 from .elements.element import Element
 from .task_logger import create_task
 
 
 def update(self, *elements: List[Element]) -> None:
-    if not asyncio.get_event_loop().is_running():
+    if not (gobals.loop and globals.loop.is_running()):
         return
     for element in elements:
         create_task(element.view.update())

+ 14 - 9
tests/test_element.py

@@ -5,27 +5,32 @@ from .user import User
 
 def test_classes(user: User):
     label = ui.label('label')
+    user.open('/')
 
-    def classes() -> str:
-        user.open('/')
-        return user.find('label').get_attribute('class')
+    def assert_classes(classes: str) -> None:
+        for i in range(20):
+            if user.find('label').get_attribute('class') == classes:
+                return
+            user.sleep(0.01)
+        else:
+            raise AssertionError(f'Expected {classes}, got {user.find("label").get_attribute("class")}')
 
-    assert classes() == ''
+    assert_classes('')
 
     label.classes('one')
-    assert classes() == 'one'
+    assert_classes('one')
 
     label.classes('one')
-    assert classes() == 'one'
+    assert_classes('one')
 
     label.classes('two three')
-    assert classes() == 'one two three'
+    assert_classes('one two three')
 
     label.classes(remove='two')
-    assert classes() == 'one three'
+    assert_classes('one three')
 
     label.classes(replace='four')
-    assert classes() == 'four'
+    assert_classes('four')
 
 
 def test_style(user: User):

+ 25 - 0
tests/test_user.py

@@ -0,0 +1,25 @@
+from nicegui import ui
+
+from .user import User
+
+
+def test_rendering_page(user: User):
+    ui.label('test label')
+    with ui.row().classes('positive'):
+        ui.input('test input', placeholder='some placeholder')
+    with ui.column():
+        ui.label('1')
+        ui.label('2')
+        ui.label('3')
+
+    user.open('/')
+    assert user.page() == '''Title: NiceGUI
+
+test label
+row [class: items-start positive]
+  test input: some placeholder [class: no-wrap items-start standard labeled]
+column [class: items-start]
+  1
+  2
+  3
+'''

+ 42 - 3
tests/user.py

@@ -7,6 +7,7 @@ from selenium.common.exceptions import NoSuchElementException
 from selenium.webdriver.remote.webelement import WebElement
 
 PORT = 3392
+IGNORED_CLASSES = ['row', 'column', 'q-field', 'q-field__label', 'q-input']
 
 
 class User():
@@ -51,10 +52,48 @@ class User():
         try:
             return self.selenium.find_element_by_xpath(f'//*[contains(text(),"{text}")]')
         except NoSuchElementException:
-            raise AssertionError(f'Could not find "{text}" on:\n{self.get_body()}')
+            raise AssertionError(f'Could not find "{text}" on:\n{self.page()}')
 
-    def get_body(self) -> str:
-        return self.selenium.find_element_by_tag_name('body').text
+    def page(self) -> str:
+        return f'Title: {self.selenium.title}\n\n' + self.content(self.selenium.find_element_by_tag_name('body'))
+
+    def content(self, element: WebElement, indent: str = '') -> str:
+        content = ''
+        classes: list[str] = []
+        for child in element.find_elements_by_xpath('./*'):
+            is_element = False
+            is_group = False
+            render_children = True
+            assert isinstance(child, WebElement)
+            if not child.find_elements_by_xpath('./*') and child.text:
+                is_element = True
+                content += f'{indent}{child.text}'
+            classes = child.get_attribute('class').strip().split()
+            if classes:
+                if classes[0] in ['row', 'column']:
+                    content += classes[0]
+                    is_element = True
+                    is_group = True
+                if classes[0] == 'q-field':
+                    try:
+                        name = child.find_element_by_class_name('q-field__label').text
+                    except NoSuchElementException:
+                        name = ''
+                    input = child.find_element_by_tag_name('input')
+                    value = input.get_attribute('value') or input.get_attribute('placeholder')
+                    content += f'{indent}{name}: {value}'
+                    render_children = False
+                    is_element = True
+                [classes.remove(c) for c in IGNORED_CLASSES if c in classes]
+                for i, c in enumerate(classes):
+                    classes[i] = c.removeprefix('q-field--')
+                if is_element:
+                    content += f' [class: {" ".join(classes)}]'
+            if is_element:
+                content += '\n'
+            if render_children:
+                content += self.content(child, indent + ('  ' if is_group else ''))
+        return content
 
     def get_tags(self, name: str) -> list[WebElement]:
         return self.selenium.find_elements_by_tag_name(name)