async def test_use_context_default_value(): Context = idom.create_context("something") value = idom.Ref() @idom.component def ComponentProvidesContext(): return Context(ComponentUsesContext()) @idom.component def ComponentUsesContext(): value.current = idom.use_context(Context) return html.div() async with idom.Layout(ComponentProvidesContext()) as layout: await layout.render() assert value.current == "something" @idom.component def ComponentUsesContext(): value.current = idom.use_context(Context) return html.div() async with idom.Layout(ComponentUsesContext()) as layout: await layout.render() assert value.current == "something"
async def test_render_raw_vdom_dict_with_single_component_object_as_children(): @idom.component def Main(): return {"tagName": "div", "children": Child()} @idom.component def Child(): return {"tagName": "div", "children": {"tagName": "h1"}} async with idom.Layout(Main()) as layout: patch = await layout.render() assert_same_items( patch.changes, [ { "op": "add", "path": "/children", "value": [{ "tagName": "div", "children": [{ "tagName": "h1" }] }], }, { "op": "add", "path": "/tagName", "value": "div" }, ], )
async def test_layout_does_not_copy_element_children_by_key(): # this is a regression test for a subtle bug: # https://github.com/idom-team/idom/issues/556 set_items = idom.Ref() @idom.component def SomeComponent(): items, set_items.current = idom.use_state([1, 2, 3]) return idom.html.div([ idom.html.div( idom.html.input({"onChange": lambda event: None}), key=str(i), ) for i in items ]) async with idom.Layout(SomeComponent()) as layout: await layout.render() set_items.current([2, 3]) await layout.render() set_items.current([3]) await layout.render() set_items.current([]) await layout.render()
async def test_layout_render_error_has_partial_update(): history = RenderHistory() @history.track("main") @idom.element async def Main(self): return idom.html.div([OkChild(), BadChild()]) @history.track("ok_child") @idom.element async def OkChild(self): return idom.html.div(["hello"]) @idom.element async def BadChild(self): raise ValueError("Something went wrong :(") async with idom.Layout(Main()) as layout: update = await layout.render() assert isinstance(update.error, ValueError) assert update == LayoutUpdate( src=history.main_1.id, new={ history.ok_child_1.id: { "tagName": "div", "children": [{"type": "str", "data": "hello"}], } }, old=[], error=update.error, )
async def test_use_memo(): component_hook = HookCatcher() set_state_hook = idom.Ref(None) used_values = [] @idom.component @component_hook.capture def ComponentWithMemo(): state, set_state_hook.current = idom.hooks.use_state(0) value = idom.hooks.use_memo( lambda: idom.Ref( state), # use a Ref here just to ensure it's a unique obj [state], ) used_values.append(value) return idom.html.div() async with idom.Layout(ComponentWithMemo()) as layout: await layout.render() set_state_hook.current(1) await layout.render() component_hook.schedule_render() await layout.render() assert used_values[0] is not used_values[1] assert used_values[1] is used_values[2] assert len(used_values) == 3
async def test_use_reducer(): saved_count = idom.Ref(None) saved_dispatch = idom.Ref(None) def reducer(count, action): if action == "increment": return count + 1 elif action == "decrement": return count - 1 else: raise ValueError(f"Unknown action '{action}'") @idom.component def Counter(initial_count): saved_count.current, saved_dispatch.current = idom.hooks.use_reducer( reducer, initial_count) return idom.html.div() async with idom.Layout(Counter(0)) as layout: await layout.render() assert saved_count.current == 0 saved_dispatch.current("increment") await layout.render() assert saved_count.current == 1 saved_dispatch.current("decrement") await layout.render() assert saved_count.current == 0
async def test_use_effect_cleanup_occurs_on_will_unmount(): outer_component_hook = HookCatcher() cleanup_triggered = idom.Ref(False) cleanup_triggered_before_next_render = idom.Ref(False) @idom.component @outer_component_hook.capture def OuterComponent(): if cleanup_triggered.current: cleanup_triggered_before_next_render.current = True return ComponentWithEffect() @idom.component def ComponentWithEffect(): @idom.hooks.use_effect def effect(): def cleanup(): cleanup_triggered.current = True return cleanup return idom.html.div() async with idom.Layout(OuterComponent()) as layout: await layout.render() assert not cleanup_triggered.current outer_component_hook.schedule_render() await layout.render() assert cleanup_triggered.current assert cleanup_triggered_before_next_render.current
async def test_render_raw_vdom_dict_with_single_element_object_as_children(): history = RenderHistory() @history.track("main") @idom.element async def Main(self): return {"tagName": "div", "children": Child()} @history.track("child") @idom.element async def Child(self): return {"tagName": "div", "children": {"tagName": "h1"}} async with idom.Layout(Main()) as layout: render = await layout.render() assert render == LayoutUpdate( src=history.main_1.id, new={ history.child_1.id: { "tagName": "div", "children": [{"type": "obj", "data": {"tagName": "h1"}}], }, history.main_1.id: { "tagName": "div", "children": [{"type": "ref", "data": history.child_1.id}], }, }, old=[], error=None, )
async def test_component_can_return_none(): @idom.component def SomeComponent(): return None async with idom.Layout(SomeComponent()) as layout: assert (await layout.render()).new == {"tagName": ""}
async def test_use_context_only_renders_for_value_change(): Context = idom.create_context(None) provider_hook = HookCatcher() render_count = idom.Ref(0) set_state = idom.Ref() @idom.component @provider_hook.capture def ComponentProvidesContext(): state, set_state.current = idom.use_state(0) return Context(ComponentInContext(), value=state) @idom.component def ComponentInContext(): render_count.current += 1 return html.div() async with idom.Layout(ComponentProvidesContext()) as layout: await layout.render() assert render_count.current == 1 set_state.current(1) await layout.render() assert render_count.current == 2 provider_hook.latest.schedule_render() await layout.render() assert render_count.current == 2
async def test_use_memo_automatically_infers_closure_values(): set_count = idom.Ref() did_memo = asyncio.Event() @idom.component def CounterWithEffect(): count, set_count.current = idom.hooks.use_state(0) @idom.hooks.use_memo def some_memo_func_that_uses_count(): """should automatically trigger on count change""" count # use count in this closure did_memo.set() return idom.html.div() async with idom.Layout(CounterWithEffect()) as layout: await layout.render() await did_memo.wait() did_memo.clear() for i in range(1, 3): set_count.current(i) await layout.render() await did_memo.wait() did_memo.clear()
async def test_error_in_effect_pre_unmount_cleanup_is_gracefully_handled(): set_key = idom.Ref() @idom.component def OuterComponent(): key, set_key.current = idom.use_state("first") return ComponentWithEffect(key=key) @idom.component def ComponentWithEffect(): @idom.hooks.use_effect def ok_effect(): def bad_cleanup(): raise ValueError("Something went wong :(") return bad_cleanup return idom.html.div() with assert_idom_did_log( match_message=r"Pre-unmount effect .*? failed", error_type=ValueError, ): async with idom.Layout(OuterComponent()) as layout: await layout.render() set_key.current("second") await layout.render() # no error
async def test_use_effect_cleanup_occurs_before_next_effect(): component_hook = HookCatcher() cleanup_triggered = idom.Ref(False) cleanup_triggered_before_next_effect = idom.Ref(False) @idom.component @component_hook.capture def ComponentWithEffect(): @idom.hooks.use_effect(dependencies=None) def effect(): if cleanup_triggered.current: cleanup_triggered_before_next_effect.current = True def cleanup(): cleanup_triggered.current = True return cleanup return idom.html.div() async with idom.Layout(ComponentWithEffect()) as layout: await layout.render() assert not cleanup_triggered.current component_hook.latest.schedule_render() await layout.render() assert cleanup_triggered.current assert cleanup_triggered_before_next_effect.current
async def test_use_debug_mode_with_factory(): set_message = idom.Ref() component_hook = HookCatcher() @idom.component @component_hook.capture def SomeComponent(): message, set_message.current = idom.use_state("hello") idom.use_debug_value(lambda: f"message is {message!r}") return idom.html.div() async with idom.Layout(SomeComponent()) as layout: with assert_idom_did_log(r"SomeComponent\(.*?\) message is 'hello'"): await layout.render() set_message.current("bye") with assert_idom_did_log(r"SomeComponent\(.*?\) message is 'bye'"): await layout.render() component_hook.latest.schedule_render() with assert_idom_did_not_log(r"SomeComponent\(.*?\) message is 'bye'"): await layout.render()
async def test_layout_render_error_has_partial_update_with_error_message(): @idom.component def Main(): return idom.html.div([OkChild(), BadChild(), OkChild()]) @idom.component def OkChild(): return idom.html.div(["hello"]) @idom.component def BadChild(): raise ValueError("error from bad child") with assert_idom_did_log(match_error="error from bad child"): async with idom.Layout(Main()) as layout: patch = await render_json_patch(layout) assert_same_items( patch.changes, [ { "op": "add", "path": "/children", "value": [{ "tagName": "div", "children": [ { "tagName": "", "children": [{ "tagName": "div", "children": ["hello"] }], }, { "tagName": "", "error": "ValueError: error from bad child", }, { "tagName": "", "children": [{ "tagName": "div", "children": ["hello"] }], }, ], }], }, { "op": "add", "path": "/tagName", "value": "" }, ], )
async def test_double_updated_component_is_not_double_rendered(): hook = HookCatcher() run_count = idom.Ref(0) @idom.component @hook.capture def AnyComponent(): run_count.current += 1 return idom.html.div() async with idom.Layout(AnyComponent()) as layout: await layout.render() assert run_count.current == 1 hook.schedule_render() hook.schedule_render() await layout.render() try: await asyncio.wait_for( layout.render(), timeout=0.1, # this should have been plenty of time ) except asyncio.TimeoutError: pass # the render should still be rendering since we only update once assert run_count.current == 2
async def test_root_component_life_cycle_hook_is_garbage_collected(): live_hooks = set() def add_to_live_hooks(constructor): def wrapper(*args, **kwargs): result = constructor(*args, **kwargs) hook = idom.hooks.current_hook() hook_id = id(hook) live_hooks.add(hook_id) finalize(hook, live_hooks.discard, hook_id) return result return wrapper @idom.component @add_to_live_hooks def Root(): return idom.html.div() async with idom.Layout(Root()) as layout: await layout.render() assert len(live_hooks) == 1 # The layout still holds a reference to the root so that's only deleted once we # release our reference to the layout. del layout assert not live_hooks
async def test_use_effect_callback_occurs_after_full_render_is_complete(): effect_triggered = idom.Ref(False) effect_triggers_after_final_render = idom.Ref(None) @idom.component def OuterComponent(): return idom.html.div( ComponentWithEffect(), CheckNoEffectYet(), ) @idom.component def ComponentWithEffect(): @idom.hooks.use_effect def effect(): effect_triggered.current = True return idom.html.div() @idom.component def CheckNoEffectYet(): effect_triggers_after_final_render.current = not effect_triggered.current return idom.html.div() async with idom.Layout(OuterComponent()) as layout: await layout.render() assert effect_triggered.current assert effect_triggers_after_final_render.current is not None assert effect_triggers_after_final_render.current
async def test_component_can_return_another_component_directly(): @idom.component def Outer(): return Inner() @idom.component def Inner(): return idom.html.div("hello") async with idom.Layout(Outer()) as layout: update = await render_json_patch(layout) assert_same_items( update.changes, [ { "op": "add", "path": "/children", "value": [{ "children": [{ "children": ["hello"], "tagName": "div" }], "tagName": "", }], }, { "op": "add", "path": "/tagName", "value": "" }, ], )
async def test_error_in_effect_pre_unmount_cleanup_is_gracefully_handled( caplog): outer_component_hook = HookCatcher() @idom.component @outer_component_hook.capture def OuterComponent(): return ComponentWithEffect() @idom.component def ComponentWithEffect(): @idom.hooks.use_effect def ok_effect(): def bad_cleanup(): raise ValueError("Something went wong :(") return bad_cleanup return idom.html.div() async with idom.Layout(OuterComponent()) as layout: await layout.render() outer_component_hook.schedule_render() await layout.render() # no error first_log_line = next(iter(caplog.records)).msg.split("\n", 1)[0] assert re.match("Pre-unmount effect .*? failed for .*?", first_log_line)
async def test_hooks_for_keyed_components_get_garbage_collected(): pop_item = idom.Ref(None) garbage_collect_items = [] registered_finalizers = set() @idom.component def Outer(): items, set_items = idom.hooks.use_state([1, 2, 3]) pop_item.current = lambda: set_items(items[:-1]) return idom.html.div(Inner(key=k, finalizer_id=k) for k in items) @idom.component def Inner(finalizer_id): if finalizer_id not in registered_finalizers: hook = idom.hooks.current_hook() finalize(hook, lambda: garbage_collect_items.append(finalizer_id)) registered_finalizers.add(finalizer_id) return idom.html.div(finalizer_id) async with idom.Layout(Outer()) as layout: await layout.render() pop_item.current() await layout.render() assert garbage_collect_items == [3] pop_item.current() await layout.render() assert garbage_collect_items == [3, 2] pop_item.current() await layout.render() assert garbage_collect_items == [3, 2, 1]
async def test_use_callback_memoization(): component_hook = HookCatcher() set_state_hook = idom.Ref(None) used_callbacks = [] @idom.component @component_hook.capture def ComponentWithRef(): state, set_state_hook.current = idom.hooks.use_state(0) @idom.hooks.use_callback(args=[state] ) # use the deco form for coverage def cb(): return None used_callbacks.append(cb) return idom.html.div() async with idom.Layout(ComponentWithRef()) as layout: await layout.render() set_state_hook.current(1) await layout.render() component_hook.schedule_render() await layout.render() assert used_callbacks[0] is not used_callbacks[1] assert used_callbacks[1] is used_callbacks[2] assert len(used_callbacks) == 3
async def test_duplicate_sibling_keys_causes_error(caplog): hook = HookCatcher() should_error = True @idom.component @hook.capture def ComponentReturnsDuplicateKeys(): if should_error: return idom.html.div( idom.html.div(key="duplicate"), idom.html.div(key="duplicate"), ) else: return idom.html.div() async with idom.Layout(ComponentReturnsDuplicateKeys()) as layout: with assert_idom_did_log( error_type=ValueError, match_error=r"Duplicate keys \['duplicate'\] at '/children/0'", ): await layout.render() hook.latest.schedule_render() should_error = False await layout.render() should_error = True hook.latest.schedule_render() with assert_idom_did_log( error_type=ValueError, match_error=r"Duplicate keys \['duplicate'\] at '/children/0'", ): await layout.render()
async def test_use_memo_with_stored_args_is_empty_tuple_after_args_are_none(): component_hook = HookCatcher() used_values = [] iter_values = iter([1, 2, 3]) args_used_in_memo = idom.Ref(()) @idom.component @component_hook.capture def ComponentWithMemo(): value = idom.hooks.use_memo( lambda: next(iter_values), args_used_in_memo.current, # noqa: ROH202 ) used_values.append(value) return idom.html.div() async with idom.Layout(ComponentWithMemo()) as layout: await layout.render() component_hook.schedule_render() args_used_in_memo.current = None await layout.render() component_hook.schedule_render() args_used_in_memo.current = () await layout.render() assert used_values == [1, 2, 2]
async def test_element_parents_must_exist_unless_is_root(): history = RenderHistory() @history.track("main") @idom.element async def Main(self): return Child() @history.track("child") @idom.element async def Child(self): return idom.html.div() @idom.element async def element_not_in_layout(self): ... async with idom.Layout(Main()) as layout: await layout.render() state = layout._element_state with pytest.raises(KeyError): layout._element_parent(element_not_in_layout()) assert not state
def test_layout_repr(): @idom.component def MyComponent(): ... my_component = MyComponent() layout = idom.Layout(my_component) assert str(layout) == f"Layout(MyComponent({hex(id(my_component))}))"
async def test_nested_component_layout(): parent_set_state = idom.Ref(None) child_set_state = idom.Ref(None) @idom.component def Parent(): state, parent_set_state.current = idom.hooks.use_state(0) return idom.html.div(state, Child()) @idom.component def Child(): state, child_set_state.current = idom.hooks.use_state(0) return idom.html.div(state) async with idom.Layout(Parent()) as layout: path, changes = await layout.render() assert path == "" assert_same_items( changes, [ { "op": "add", "path": "/children", "value": ["0", { "tagName": "div", "children": ["0"] }], }, { "op": "add", "path": "/tagName", "value": "div" }, ], ) parent_set_state.current(1) path, changes = await layout.render() assert path == "" assert changes == [{ "op": "replace", "path": "/children/0", "value": "1" }] child_set_state.current(1) path, changes = await layout.render() assert path == "/children/1" assert changes == [{ "op": "replace", "path": "/children/0", "value": "1" }]
async def test_layout_has_event_loop(event_loop): @idom.element async def MyElement(self): ... async with idom.Layout(MyElement()) as layout: assert layout.loop is event_loop # await the render since creating the layout schedules a render task await layout.render()
async def main() -> None: await serve_json_patch( idom.Layout( ConnectionContext(component, value=Connection(request, websocket, path))), send_coro, recv_coro, )
async def test_neighboring_contexts_do_not_conflict(): LeftContext = idom.create_context(None, name="Left") RightContext = idom.create_context(None, name="Right") set_left = idom.Ref() set_right = idom.Ref() left_used_value = idom.Ref() right_used_value = idom.Ref() left_render_count = idom.Ref(0) right_render_count = idom.Ref(0) @idom.component def Root(): left_value, set_left.current = idom.use_state(1) right_value, set_right.current = idom.use_state(1) return idom.html.div( LeftContext(Left(), value=left_value), RightContext(Right(), value=right_value), ) @idom.component def Left(): left_render_count.current += 1 left_used_value.current = idom.use_context(LeftContext) return idom.html.div() @idom.component def Right(): right_render_count.current += 1 right_used_value.current = idom.use_context(RightContext) return idom.html.div() async with idom.Layout(Root()) as layout: await layout.render() assert left_render_count.current == 1 assert right_render_count.current == 1 assert left_used_value.current == 1 assert right_used_value.current == 1 for i in range(2, 5): set_left.current(i) await layout.render() assert left_render_count.current == i assert right_render_count.current == 1 assert left_used_value.current == i assert right_used_value.current == 1 for j in range(2, 5): set_right.current(j) await layout.render() assert left_render_count.current == i assert right_render_count.current == j assert left_used_value.current == i assert right_used_value.current == j