Skip to content

Commit 00eb5ae

Browse files
committed
begin testing layout
1 parent 4631a48 commit 00eb5ae

File tree

5 files changed

+91
-127
lines changed

5 files changed

+91
-127
lines changed

idom/core/layout.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class ElementState(NamedTuple):
4949

5050
class Layout(HasAsyncResources):
5151

52-
__slots__ = ["_root", "_event_handlers"]
52+
__slots__ = ["root", "_event_handlers"]
5353

5454
if not hasattr(abc.ABC, "__weakref__"): # pragma: no cover
5555
__slots__.append("__weakref__")
@@ -60,14 +60,9 @@ def __init__(
6060
super().__init__()
6161
if not isinstance(root, AbstractElement):
6262
raise TypeError("Expected an AbstractElement, not %r" % root)
63-
self._root = root
63+
self.root = root
6464
self._event_handlers: Dict[str, EventHandler] = {}
6565

66-
@property
67-
def root(self) -> str:
68-
"""Id of the root element."""
69-
return self._root.id
70-
7166
def update(self, element: "AbstractElement") -> None:
7267
try:
7368
self._rendering_queue.put(element)
@@ -92,14 +87,14 @@ async def render(self) -> Dict[str, Any]:
9287
@async_resource
9388
async def _rendering_queue(self) -> AsyncIterator["_ElementQueue"]:
9489
queue = _ElementQueue()
95-
queue.put(self._root)
90+
queue.put(self.root)
9691
yield queue
9792

9893
@async_resource
9994
async def _element_states(self) -> AsyncIterator[ElementState]:
100-
root_element_state = self._create_element_state(self._root, "")
95+
root_element_state = self._create_element_state(self.root, "")
10196
try:
102-
yield {self._root.id: root_element_state}
97+
yield {self.root.id: root_element_state}
10398
finally:
10499
self._delete_element_state(root_element_state)
105100

@@ -255,6 +250,9 @@ def _iter_element_states_from_root(
255250
for i in visited_element_state.child_elements_ids
256251
)
257252

253+
def __repr__(self) -> str:
254+
return f"{type(self).__name__}({self.root})"
255+
258256

259257
@coroutine
260258
def _render_with_life_cycle_hook(element_state: ElementState) -> Iterator[None]:

tests/test_core/test_hooks.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from tests.general_utils import assert_unordered_equal
66

7+
from .utils import HookCatcher
8+
79

810
async def test_must_be_rendering_in_layout_to_use_hooks():
911
@idom.element
@@ -284,13 +286,13 @@ async def CheckNoEffectYet():
284286

285287

286288
async def test_use_effect_cleanup_occurs_on_will_render():
287-
element_hook = idom.Ref(None)
289+
element_hook = HookCatcher()
288290
cleanup_triggered = idom.Ref(False)
289291
cleanup_triggered_before_next_render = idom.Ref(False)
290292

291293
@idom.element
294+
@element_hook.capture
292295
async def ElementWithEffect():
293-
element_hook.current = idom.hooks.current_hook()
294296
if cleanup_triggered.current:
295297
cleanup_triggered_before_next_render.current = True
296298

@@ -308,21 +310,21 @@ def cleanup():
308310

309311
assert not cleanup_triggered.current
310312

311-
element_hook.current.schedule_render()
313+
element_hook.schedule_render()
312314
await layout.render()
313315

314316
assert cleanup_triggered.current
315317
assert cleanup_triggered_before_next_render.current
316318

317319

318320
async def test_use_effect_cleanup_occurs_on_will_unmount():
319-
outer_element_hook = idom.Ref(None)
321+
outer_element_hook = HookCatcher()
320322
cleanup_triggered = idom.Ref(False)
321323
cleanup_triggered_before_next_render = idom.Ref(False)
322324

323325
@idom.element
326+
@outer_element_hook.capture
324327
async def OuterElement():
325-
outer_element_hook.current = idom.hooks.current_hook()
326328
if cleanup_triggered.current:
327329
cleanup_triggered_before_next_render.current = True
328330
return ElementWithEffect()
@@ -343,24 +345,24 @@ def cleanup():
343345

344346
assert not cleanup_triggered.current
345347

346-
outer_element_hook.current.schedule_render()
348+
outer_element_hook.schedule_render()
347349
await layout.render()
348350

349351
assert cleanup_triggered.current
350352
assert cleanup_triggered_before_next_render.current
351353

352354

353355
async def test_use_effect_memoization():
354-
element_hook = idom.Ref(None)
356+
element_hook = HookCatcher()
355357
set_state_callback = idom.Ref(None)
356358
effect_run_count = idom.Ref(0)
357359

358360
first_value = 1
359361
second_value = 2
360362

361363
@idom.element
364+
@element_hook.capture
362365
async def ElementWithMemoizedEffect():
363-
element_hook.current = idom.hooks.current_hook()
364366
state, set_state_callback.current = idom.hooks.use_state(first_value)
365367

366368
@idom.hooks.use_effect(args=[state])
@@ -374,7 +376,7 @@ def effect():
374376

375377
assert effect_run_count.current == 1
376378

377-
element_hook.current.schedule_render()
379+
element_hook.schedule_render()
378380
await layout.render()
379381

380382
assert effect_run_count.current == 1
@@ -384,7 +386,7 @@ def effect():
384386

385387
assert effect_run_count.current == 2
386388

387-
element_hook.current.schedule_render()
389+
element_hook.schedule_render()
388390
await layout.render()
389391

390392
assert effect_run_count.current == 2

tests/test_core/test_layout.py

Lines changed: 52 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import gc
2-
import asyncio
32
from weakref import finalize
43

54
import pytest
65

76
import idom
87
from idom.core.layout import LayoutUpdate
98

10-
from .utils import RenderHistory
9+
from tests.general_utils import assert_unordered_equal
10+
11+
from .utils import HookCatcher
1112

1213

1314
def test_layout_repr():
@@ -27,104 +28,79 @@ def test_layout_expects_abstract_element():
2728
idom.Layout(idom.html.div())
2829

2930

30-
async def test_layout_has_event_loop(event_loop):
31-
@idom.element
32-
async def MyElement():
33-
return idom.html.div()
34-
35-
async with idom.Layout(MyElement()) as layout:
36-
assert layout.loop is event_loop
37-
# await the render since creating the layout schedules a render task
38-
await layout.render()
39-
40-
41-
async def test_layout_cancels_renders_on_close():
42-
event_that_is_never_set = asyncio.Event()
43-
render_is_cancelled = asyncio.Event()
44-
45-
@idom.element
46-
async def MyElement():
47-
try:
48-
await event_that_is_never_set.wait()
49-
finally:
50-
render_is_cancelled.set()
51-
52-
async with idom.Layout(MyElement()):
53-
await asyncio.sleep(0.1)
54-
55-
await render_is_cancelled.wait()
56-
57-
5831
async def test_simple_layout():
5932
set_state_hook = idom.Ref(None)
6033

6134
@idom.element
62-
async def SimpleElement(tag):
63-
tag, set_tag = idom.hooks.use_state(tag)
64-
set_state_hook.current = set_tag
35+
async def SimpleElement():
36+
tag, set_state_hook.current = idom.hooks.use_state("div")
6537
return idom.vdom(tag)
6638

67-
element = SimpleElement("div")
68-
async with idom.Layout(element) as layout:
39+
async with idom.Layout(SimpleElement()) as layout:
40+
path, changes = await layout.render()
6941

70-
src, new, old, errors = await layout.render()
71-
assert src == element.id
72-
assert new == {element.id: {"tagName": "div"}}
73-
assert old == []
74-
assert errors == []
42+
assert path == ""
43+
assert_unordered_equal(
44+
changes,
45+
[
46+
{"op": "add", "path": "/eventHandlers", "value": {}},
47+
{"op": "add", "path": "/tagName", "value": "div"},
48+
],
49+
)
7550

7651
set_state_hook.current("table")
52+
path, changes = await layout.render()
7753

78-
src, new, old, errors = await layout.render()
79-
assert src == element.id
80-
assert new == {element.id: {"tagName": "table"}}
81-
assert old == []
82-
assert errors == []
54+
assert path == ""
55+
assert changes == [{"op": "replace", "path": "/tagName", "value": "table"}]
8356

8457

8558
async def test_nested_element_layout():
59+
parent_set_state = idom.Ref(None)
60+
child_set_state = idom.Ref(None)
8661

87-
history = RenderHistory()
88-
89-
@history.track("parent")
9062
@idom.element
9163
async def Parent():
92-
return idom.html.div([Child()])
64+
state, parent_set_state.current = idom.hooks.use_state(0)
65+
return idom.html.div(state, Child())
9366

94-
@history.track("child")
9567
@idom.element
9668
async def Child():
97-
return idom.html.div()
69+
state, child_set_state.current = idom.hooks.use_state(0)
70+
return idom.html.div(state)
9871

9972
async with idom.Layout(Parent()) as layout:
10073

101-
src, new, old, errors = await layout.render()
74+
path, changes = await layout.render()
10275

103-
assert src == history.parent_1.id
104-
assert new == {
105-
history.parent_1.id: {
106-
"tagName": "div",
107-
"children": [{"data": history.child_1.id, "type": "ref"}],
108-
},
109-
history.child_1.id: {"tagName": "div"},
110-
}
111-
assert old == []
112-
assert errors == []
76+
assert path == ""
77+
assert_unordered_equal(
78+
changes,
79+
[
80+
{"op": "add", "path": "/tagName", "value": "div"},
81+
{
82+
"op": "add",
83+
"path": "/children",
84+
"value": [
85+
"0",
86+
{"children": ["0"], "eventHandlers": {}, "tagName": "div"},
87+
],
88+
},
89+
{"op": "add", "path": "/eventHandlers", "value": {}},
90+
],
91+
)
11392

114-
layout.update(history.parent_1)
93+
parent_set_state.current(1)
94+
path, changes = await layout.render()
11595

116-
src, new, old, errors = await layout.render()
96+
assert path == ""
97+
assert changes == [{"op": "replace", "path": "/children/0", "value": "1"}]
11798

118-
assert src == history.parent_1.id
119-
assert new == {
120-
history.parent_1.id: {
121-
"tagName": "div",
122-
"children": [{"data": history.child_2.id, "type": "ref"}],
123-
},
124-
history.child_2.id: {"tagName": "div"},
125-
}
126-
assert old == [history.child_1.id]
127-
assert errors == []
99+
child_set_state.current(1)
100+
path, changes = await layout.render()
101+
102+
assert path == "/children/1"
103+
assert changes == [{"op": "replace", "path": "/children/0", "value": "1"}]
128104

129105

130106
async def test_layout_render_error_has_partial_update():
@@ -220,11 +196,11 @@ async def Outer():
220196
live_elements.add(element.id)
221197
finalize(element, live_elements.remove, element.id)
222198

223-
update = idom.hooks.use_update()
199+
hook = idom.hooks.current_hook()
224200

225201
@idom.event(target_id="force-update")
226202
async def force_update():
227-
update()
203+
hook.schedule_render()
228204

229205
return idom.html.div({"onEvent": force_update}, Inner())
230206

tests/test_core/test_render.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,11 @@ async def recv():
9494

9595
@idom.element
9696
async def Outer():
97-
update = idom.hooks.current_hook().use_update()
97+
hook = idom.hooks.current_hook()
9898

9999
@idom.event(target_id=target_id)
100100
async def an_event():
101-
update()
101+
hook.schedule_render()
102102

103103
return idom.html.div({"onEvent": an_event}, Inner())
104104

tests/test_core/utils.py

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,20 @@
11
from functools import wraps
22

33

4-
class RenderHistory:
5-
def __init__(self):
6-
self.__counters = {}
7-
self.__elements = set()
8-
9-
def clear(self):
10-
for k in self.__dict__.items():
11-
if not k.startswith("_"):
12-
del self.__dict__[k]
13-
14-
def track(self, name):
15-
if name.startswith("_"):
16-
raise ValueError(f"Name {name!r} startswith '_'.")
17-
18-
self.__counters[name] = 0
19-
20-
def setup(element_function):
21-
@wraps(element_function)
22-
def wrapper(*args, **kwargs):
23-
elmt = element_function(*args, **kwargs)
24-
if elmt.id not in self.__elements:
25-
self.__elements.add(elmt.id)
26-
self.__counters[name] += 1
27-
setattr(self, f"{name}_{self.__counters[name]}", elmt)
28-
return elmt
29-
30-
return wrapper
31-
32-
return setup
4+
import idom
5+
6+
7+
class HookCatcher:
8+
9+
current: idom.hooks.LifeCycleHook
10+
11+
def capture(self, render_function):
12+
@wraps(render_function)
13+
async def wrapper(*args, **kwargs):
14+
self.current = idom.hooks.current_hook()
15+
return await render_function(*args, **kwargs)
16+
17+
return wrapper
18+
19+
def schedule_render(self) -> None:
20+
self.current.schedule_render()

0 commit comments

Comments
 (0)