Skip to content

Commit a72f0a4

Browse files
authored
OP#10323: Do not clear the remark label before setting its text
The form outlives the page it is built into, so once that page's browser has left, NiceGUI deletes its client and the form holds a deleted label. clear() resolves the element's client and raised, failing every otconfig loaded after the first visit to the page. set_text() goes through update(), which skips a deleted element. A label has no children, so the clear() did nothing on a live page either.
1 parent 0660ac8 commit a72f0a4

3 files changed

Lines changed: 84 additions & 1 deletion

File tree

‎OTAnalytics/plugin_ui/nicegui_gui/pages/remarks_form/container.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ def build(self) -> None:
3030
def load_remark(self) -> None:
3131
if not self._remark_label:
3232
return
33-
self._remark_label.clear()
3433
if remark := self._viewmodel.get_remark():
3534
self._remark_label.set_text(remark)
3635
else:

‎tests/unit/OTAnalytics/plugin_ui/nicegui_gui/pages/remarks_form/__init__.py‎

Whitespace-only changes.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Tests the form once the browser that showed its page has gone away."""
2+
3+
import asyncio
4+
import gc
5+
import weakref
6+
from dataclasses import dataclass
7+
from unittest.mock import Mock
8+
9+
from nicegui import Client, context, ui
10+
from nicegui.testing import User
11+
12+
from OTAnalytics.plugin_ui.nicegui_gui.pages.remarks_form.container import RemarkForm
13+
14+
A_REMARK = "Counted at the northern approach"
15+
PAGE_WITH_FORM = "/with-form"
16+
PAGE_ELSEWHERE = "/elsewhere"
17+
COLLECTION_ATTEMPTS = 50
18+
COLLECTION_INTERVAL_SECONDS = 0.01
19+
20+
21+
class TestAfterThePageIsGone:
22+
async def test_loading_a_remark_does_not_raise(self, user: User) -> None:
23+
"""The form outlives the page it was built into: it is created once and
24+
rebuilt on every visit. Once that visit's browser has left, NiceGUI
25+
deletes the client, and loading a remark reached for it and raised
26+
`RuntimeError`, failing every otconfig loaded after the first page visit.
27+
28+
@bug by randy-seng
29+
"""
30+
target = create_target(create_given())
31+
client_ids: list[str] = []
32+
33+
@ui.page(PAGE_WITH_FORM)
34+
def page_with_form() -> None:
35+
target.build()
36+
client_ids.append(context.client.id)
37+
38+
@ui.page(PAGE_ELSEWHERE)
39+
def page_elsewhere() -> None:
40+
ui.label("elsewhere")
41+
42+
await user.open(PAGE_WITH_FORM)
43+
await leave_page(user, client_ids[0])
44+
45+
target.load_remark()
46+
47+
48+
async def leave_page(user: User, client_id: str) -> None:
49+
"""Navigate away and let NiceGUI delete the page's client, as it does once the
50+
browser has left.
51+
52+
Elements hold their client only weakly, so they notice it is gone only once it
53+
has been collected. Opening another page first makes the test user let go of
54+
it; the client's outbox loop lets go once it notices the deletion.
55+
"""
56+
await user.open(PAGE_ELSEWHERE)
57+
client = Client.instances[client_id]
58+
collected = weakref.ref(client)
59+
client.delete()
60+
del client
61+
for _ in range(COLLECTION_ATTEMPTS):
62+
await asyncio.sleep(COLLECTION_INTERVAL_SECONDS)
63+
gc.collect()
64+
if collected() is None:
65+
return
66+
raise AssertionError("The deleted client was never collected")
67+
68+
69+
@dataclass
70+
class Given:
71+
resource_manager: Mock
72+
view_model: Mock
73+
74+
75+
def create_given() -> Given:
76+
resource_manager = Mock()
77+
resource_manager.get.return_value = "label"
78+
view_model = Mock()
79+
view_model.get_remark.return_value = A_REMARK
80+
return Given(resource_manager=resource_manager, view_model=view_model)
81+
82+
83+
def create_target(given: Given) -> RemarkForm:
84+
return RemarkForm(given.resource_manager, given.view_model)

0 commit comments

Comments
 (0)