|
| 1 | +# pylint:disable=redefined-outer-name |
| 2 | +# pylint:disable=unused-argument |
| 3 | + |
| 4 | +from functools import cached_property |
| 5 | + |
| 6 | +import nicegui |
| 7 | +import pytest |
| 8 | +from fastapi import FastAPI |
| 9 | +from helpers import assert_contains_text |
| 10 | +from nicegui import APIRouter, ui |
| 11 | +from nicegui.element import Element |
| 12 | +from playwright.async_api import Page |
| 13 | +from pytest_mock import MockerFixture |
| 14 | +from simcore_service_dynamic_scheduler.api.frontend._common.base_display_model import ( |
| 15 | + BaseUpdatableDisplayModel, |
| 16 | +) |
| 17 | +from simcore_service_dynamic_scheduler.api.frontend._common.updatable_component import ( |
| 18 | + BaseUpdatableComponent, |
| 19 | +) |
| 20 | +from simcore_service_dynamic_scheduler.api.frontend._utils import ( |
| 21 | + get_settings, |
| 22 | + set_parent_app, |
| 23 | +) |
| 24 | +from simcore_service_dynamic_scheduler.core.settings import ApplicationSettings |
| 25 | + |
| 26 | +pytest_simcore_core_services_selection = [ |
| 27 | + "postgres", |
| 28 | + "rabbit", |
| 29 | + "redis", |
| 30 | +] |
| 31 | + |
| 32 | +pytest_simcore_ops_services_selection = [ |
| 33 | + "redis-commander", |
| 34 | +] |
| 35 | + |
| 36 | + |
| 37 | +class Pet(BaseUpdatableDisplayModel): |
| 38 | + name: str |
| 39 | + species: str |
| 40 | + |
| 41 | + |
| 42 | +class Friend(BaseUpdatableDisplayModel): |
| 43 | + name: str |
| 44 | + age: int |
| 45 | + |
| 46 | + |
| 47 | +class Person(BaseUpdatableDisplayModel): |
| 48 | + @cached_property |
| 49 | + def rerender_on_type_change(self) -> set[str]: |
| 50 | + return {"companion"} |
| 51 | + |
| 52 | + name: str |
| 53 | + age: int |
| 54 | + companion: Pet | Friend |
| 55 | + |
| 56 | + |
| 57 | +class PersonDisplay(BaseUpdatableComponent[Person]): |
| 58 | + def _get_parent(self) -> Element: |
| 59 | + return ui.column() |
| 60 | + |
| 61 | + def _draw(self) -> None: |
| 62 | + print("calling _draw") |
| 63 | + ui.label(f"Name: {self.display_model.name}") |
| 64 | + ui.label(f"Age: {self.display_model.age}") |
| 65 | + |
| 66 | + @ui.refreshable |
| 67 | + def comp_ui() -> None: |
| 68 | + if isinstance(self.display_model.companion, Friend): |
| 69 | + ui.label().bind_text_from( |
| 70 | + self.display_model.companion, |
| 71 | + "name", |
| 72 | + backward=lambda name: f"Friend Name: {name}", |
| 73 | + ) |
| 74 | + ui.label(f"Friend Age: {self.display_model.companion.age}") |
| 75 | + |
| 76 | + if isinstance(self.display_model.companion, Pet): |
| 77 | + ui.label().bind_text_from( |
| 78 | + self.display_model.companion, |
| 79 | + "name", |
| 80 | + backward=lambda name: f"Pet Name: {name}", |
| 81 | + ) |
| 82 | + ui.label(f"Pet Species: {self.display_model.companion.species}") |
| 83 | + |
| 84 | + comp_ui() |
| 85 | + |
| 86 | + # self.display_model.on_value_change("companion", comp_ui.refresh) |
| 87 | + self.display_model.on_type_change("companion", comp_ui.refresh) |
| 88 | + |
| 89 | + |
| 90 | +@pytest.fixture |
| 91 | +def use_internal_scheduler() -> bool: |
| 92 | + return True |
| 93 | + |
| 94 | + |
| 95 | +@pytest.fixture |
| 96 | +def person() -> Person: |
| 97 | + return Person(name="Alice", age=30, companion=Pet(name="Fluffy", species="cat")) |
| 98 | + |
| 99 | + |
| 100 | +@pytest.fixture |
| 101 | +def router(person: Person) -> APIRouter: |
| 102 | + |
| 103 | + router = APIRouter() |
| 104 | + |
| 105 | + @ui.page("/", api_router=router) |
| 106 | + async def index(): |
| 107 | + person_display = PersonDisplay(person) |
| 108 | + |
| 109 | + ui.label("BEFORE_LABEL") |
| 110 | + person_display.add_to_ui() |
| 111 | + ui.label("AFTER_LABEL") |
| 112 | + |
| 113 | + return router |
| 114 | + |
| 115 | + |
| 116 | +@pytest.fixture |
| 117 | +def not_initialized_app(not_initialized_app: FastAPI, router: APIRouter) -> FastAPI: |
| 118 | + minimal_app = FastAPI() |
| 119 | + |
| 120 | + settings = ApplicationSettings.create_from_envs() |
| 121 | + minimal_app.state.settings = settings |
| 122 | + |
| 123 | + nicegui.app.include_router(router) |
| 124 | + |
| 125 | + nicegui.ui.run_with( |
| 126 | + minimal_app, |
| 127 | + mount_path=settings.DYNAMIC_SCHEDULER_UI_MOUNT_PATH, |
| 128 | + storage_secret=settings.DYNAMIC_SCHEDULER_UI_STORAGE_SECRET.get_secret_value(), |
| 129 | + ) |
| 130 | + set_parent_app(minimal_app) |
| 131 | + return minimal_app |
| 132 | + |
| 133 | + |
| 134 | +async def test_updatable_component( |
| 135 | + mocker: MockerFixture, |
| 136 | + app_runner: None, |
| 137 | + async_page: Page, |
| 138 | + server_host_port: str, |
| 139 | + person: Person, |
| 140 | + # person_display: PersonDisplay, |
| 141 | +): |
| 142 | + await async_page.goto( |
| 143 | + f"{server_host_port}{get_settings().DYNAMIC_SCHEDULER_UI_MOUNT_PATH}" |
| 144 | + ) |
| 145 | + |
| 146 | + # ensre render worked |
| 147 | + await assert_contains_text(async_page, "BEFORE_LABEL") |
| 148 | + await assert_contains_text(async_page, "AFTER_LABEL") |
| 149 | + |
| 150 | + await assert_contains_text(async_page, "Name: Alice") |
| 151 | + await assert_contains_text(async_page, "Age: 30") |
| 152 | + |
| 153 | + # _draw_spy = mocker.spy(person_display, "_draw") |
| 154 | + # _recreate_ui_spy = mocker.spy(person_display, "_recrate_ui") |
| 155 | + |
| 156 | + # # TODO: test that it changes when accessing the propeties directly |
| 157 | + |
| 158 | + await assert_contains_text(async_page, "Pet Name: Fluffy") |
| 159 | + person.companion.name = "Buddy" |
| 160 | + # : TODO: bidn property |
| 161 | + await assert_contains_text(async_page, "Pet Name: Buddy", timeout=2) |
| 162 | + |
| 163 | + # # TODO: check that UI was rerendered with new pet name |
| 164 | + |
| 165 | + # person.name = "Bob" |
| 166 | + # # TODO: check that UI was rerendered with new name |
| 167 | + |
| 168 | + # person.companion = Pet(name="Buddy", species="dog") |
| 169 | + # # TODO: check that ui has no changes only values changed |
| 170 | + |
| 171 | + person.companion = Friend(name="Charlie", age=25) |
| 172 | + await assert_contains_text(async_page, "Friend Name: Charlie2", timeout=2) |
| 173 | + |
| 174 | + # # TODO: test that it changes if we apply the updates via the update method on the object? |
| 175 | + |
| 176 | + # # TODO: check that ui has changed as expected |
| 177 | + # assert person.requires_rerender({"age": 31}) is False |
| 178 | + # person_display.update_model({"age": 31}) |
| 179 | + |
| 180 | + # # TODO: check that ui has changed as expected |
| 181 | + # assert person.requires_rerender({"companion": {"name": "Daisy"}}) is False |
| 182 | + # person_display.update_model({"companion": {"name": "Daisy"}}) |
| 183 | + |
| 184 | + # # TODO: check that ui has changed as expected |
| 185 | + # assert person.requires_rerender({"companion": {"age": 28}}) is False |
| 186 | + # person_display.update_model({"companion": {"name": "Eve", "age": 28}}) |
0 commit comments