Example #1
0
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"
Example #2
0
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"
                },
            ],
        )
Example #3
0
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()
Example #4
0
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,
        )
Example #5
0
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
Example #6
0
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
Example #7
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
Example #8
0
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,
    )
Example #9
0
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": ""}
Example #10
0
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
Example #11
0
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()
Example #12
0
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
Example #13
0
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
Example #14
0
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()
Example #15
0
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": ""
                    },
                ],
            )
Example #16
0
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
Example #17
0
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
Example #18
0
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
Example #19
0
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": ""
                },
            ],
        )
Example #20
0
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)
Example #21
0
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]
Example #22
0
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
Example #23
0
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()
Example #24
0
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]
Example #25
0
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
Example #26
0
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))}))"
Example #27
0
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"
        }]
Example #28
0
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()
Example #29
0
 async def main() -> None:
     await serve_json_patch(
         idom.Layout(
             ConnectionContext(component,
                               value=Connection(request, websocket,
                                                path))),
         send_coro,
         recv_coro,
     )
Example #30
0
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