Exemple #1
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.latest.schedule_render()
        hook.latest.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
Exemple #2
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()
Exemple #3
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(dependencies=[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.latest.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
Exemple #4
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()
Exemple #5
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
Exemple #6
0
async def test_changing_key_of_component_resets_state():
    set_key = Ref()
    did_init_state = Ref(0)
    hook = HookCatcher()

    @component
    @hook.capture
    def Root():
        key, set_key.current = use_state("key-1")
        return Child(key=key)

    @component
    def Child():
        use_state(
            lambda: did_init_state.set_current(did_init_state.current + 1))

    async with Layout(Root()) as layout:
        await layout.render()
        assert did_init_state.current == 1

        set_key.current("key-2")
        await layout.render()
        assert did_init_state.current == 2

        hook.latest.schedule_render()
        await layout.render()
        assert did_init_state.current == 2
Exemple #7
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
Exemple #8
0
async def test_use_memo_with_stored_deps_is_empty_tuple_after_deps_are_none():
    component_hook = HookCatcher()
    used_values = []

    iter_values = iter([1, 2, 3])
    deps_used_in_memo = idom.Ref(())

    @idom.component
    @component_hook.capture
    def ComponentWithMemo():
        value = idom.hooks.use_memo(
            lambda: next(iter_values),
            deps_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.latest.schedule_render()
        deps_used_in_memo.current = None
        await layout.render()
        component_hook.latest.schedule_render()
        deps_used_in_memo.current = ()
        await layout.render()

    assert used_values == [1, 2, 2]
Exemple #9
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.latest.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
Exemple #10
0
async def test_keyed_components_preserve_hook_on_parent_update():
    outer_hook = HookCatcher()
    inner_hook = HookCatcher()

    @idom.component
    @outer_hook.capture
    def Outer():
        return Inner(key=1)

    @idom.component
    @inner_hook.capture
    def Inner():
        return idom.html.div()

    async with idom.Layout(Outer()) as layout:
        await layout.render()
        old_inner_hook = inner_hook.latest

        outer_hook.latest.schedule_render()
        await layout.render()
        assert old_inner_hook is inner_hook.latest
Exemple #11
0
async def test_components_are_garbage_collected():
    live_components = set()
    outer_component_hook = HookCatcher()

    def add_to_live_components(constructor):
        def wrapper(*args, **kwargs):
            component = constructor(*args, **kwargs)
            component_id = id(component)
            live_components.add(component_id)
            finalize(component, live_components.discard, component_id)
            return component

        return wrapper

    @add_to_live_components
    @idom.component
    @outer_component_hook.capture
    def Outer():
        return Inner()

    @add_to_live_components
    @idom.component
    def Inner():
        return idom.html.div()

    async with idom.Layout(Outer()) as layout:
        await layout.render()

        assert len(live_components) == 2

        last_live_components = live_components.copy()
        # The existing `Outer` component rerenders. A new `Inner` component is created and
        # the the old `Inner` component should be deleted. Thus there should be one
        # changed component in the set of `live_components` the old `Inner` deleted and new
        # `Inner` added.
        outer_component_hook.latest.schedule_render()
        await layout.render()

        assert len(live_components - last_live_components) == 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
    # the hook also contains a reference to the root component
    del outer_component_hook

    assert not live_components
Exemple #12
0
async def test_use_ref():
    component_hook = HookCatcher()
    used_refs = []

    @idom.component
    @component_hook.capture
    def ComponentWithRef():
        used_refs.append(idom.hooks.use_ref(1))
        return idom.html.div()

    async with idom.Layout(ComponentWithRef()) as layout:
        await layout.render()
        component_hook.latest.schedule_render()
        await layout.render()

    assert used_refs[0] is used_refs[1]
    assert len(used_refs) == 2
Exemple #13
0
async def test_update_path_to_component_that_is_not_direct_child_is_correct():
    hook = HookCatcher()

    @idom.component
    def Parent():
        return idom.html.div(idom.html.div(Child()))

    @idom.component
    @hook.capture
    def Child():
        return idom.html.div()

    async with idom.Layout(Parent()) as layout:
        await layout.render()

        hook.latest.schedule_render()

        update = await render_json_patch(layout)
        assert update.path == "/children/0/children/0/children/0"
Exemple #14
0
async def test_component_should_render_always_false():
    render_count = idom.Ref(0)
    root_hook = HookCatcher()

    @idom.component
    @root_hook.capture
    def Root():
        return ComponentShouldRender(SomeComponent(),
                                     should_render=lambda new: False)

    @idom.component
    def SomeComponent():
        render_count.current += 1
        return html.div()

    async with idom.Layout(Root()) as layout:
        for _ in range(4):
            await layout.render()
            root_hook.latest.schedule_render()

    assert render_count.current == 1
Exemple #15
0
async def test_use_memo_never_runs_if_deps_is_empty_list():
    component_hook = HookCatcher()
    used_values = []

    iter_values = iter([1, 2, 3])

    @idom.component
    @component_hook.capture
    def ComponentWithMemo():
        value = idom.hooks.use_memo(lambda: next(iter_values), ())
        used_values.append(value)
        return idom.html.div()

    async with idom.Layout(ComponentWithMemo()) as layout:
        await layout.render()
        component_hook.latest.schedule_render()
        await layout.render()
        component_hook.latest.schedule_render()
        await layout.render()

    assert used_values == [1, 1, 1]
Exemple #16
0
async def test_component_error_in_should_render_is_handled_gracefully():
    root_hook = HookCatcher()

    @idom.component
    @root_hook.capture
    def Root():
        def bad_should_render(new):
            raise ValueError("The error message")

        return ComponentShouldRender(html.div(),
                                     should_render=bad_should_render)

    with assert_idom_did_log(
            match_message=
            r".* component failed to check if .* should be rendered",
            error_type=ValueError,
            match_error="The error message",
    ):
        async with idom.Layout(Root()) as layout:
            await layout.render()
            root_hook.latest.schedule_render()
            await layout.render()
Exemple #17
0
async def test_memoized_effect_on_recreated_if_dependencies_change():
    component_hook = HookCatcher()
    set_state_callback = idom.Ref(None)
    effect_run_count = idom.Ref(0)

    first_value = 1
    second_value = 2

    @idom.component
    @component_hook.capture
    def ComponentWithMemoizedEffect():
        state, set_state_callback.current = idom.hooks.use_state(first_value)

        @idom.hooks.use_effect(dependencies=[state])
        def effect():
            effect_run_count.current += 1

        return idom.html.div()

    async with idom.Layout(ComponentWithMemoizedEffect()) as layout:
        await layout.render()

        assert effect_run_count.current == 1

        component_hook.latest.schedule_render()
        await layout.render()

        assert effect_run_count.current == 1

        set_state_callback.current(second_value)
        await layout.render()

        assert effect_run_count.current == 2

        component_hook.latest.schedule_render()
        await layout.render()

        assert effect_run_count.current == 2
Exemple #18
0
async def test_error_in_effect_cleanup_is_gracefully_handled(caplog):
    caplog.clear()
    component_hook = HookCatcher()

    @idom.component
    @component_hook.capture
    def ComponentWithEffect():
        @idom.hooks.use_effect(dependencies=None
                               )  # force this to run every time
        def ok_effect():
            def bad_cleanup():
                raise ValueError("Something went wong :(")

            return bad_cleanup

        return idom.html.div()

    with assert_idom_did_log(
            match_error=r"Layout post-render effect .* failed"):
        async with idom.Layout(ComponentWithEffect()) as layout:
            await layout.render()
            component_hook.latest.schedule_render()
            await layout.render()  # no error
Exemple #19
0
async def test_error_in_effect_cleanup_is_gracefully_handled():
    component_hook = HookCatcher()

    @idom.component
    @component_hook.capture
    def ComponentWithEffect():
        hook = current_hook()

        def bad_effect():
            raise ValueError("The error message")

        hook.add_effect(COMPONENT_DID_RENDER_EFFECT, bad_effect)
        return idom.html.div()

    with assert_idom_did_log(
            match_message="Component post-render effect .*? failed",
            error_type=ValueError,
            match_error="The error message",
    ):
        async with idom.Layout(ComponentWithEffect()) as layout:
            await layout.render()
            component_hook.latest.schedule_render()
            await layout.render()  # no error
Exemple #20
0
async def test_use_async_effect_cancel(caplog):
    component_hook = HookCatcher()
    effect_ran = asyncio.Event()
    effect_was_cancelled = asyncio.Event()

    event_that_never_occurs = asyncio.Event()

    @idom.component
    @component_hook.capture
    def ComponentWithLongWaitingEffect():
        @idom.hooks.use_effect(dependencies=None
                               )  # force this to run every time
        async def effect():
            effect_ran.set()
            try:
                await event_that_never_occurs.wait()
            except asyncio.CancelledError:
                effect_was_cancelled.set()
                raise

        return idom.html.div()

    async with idom.Layout(ComponentWithLongWaitingEffect()) as layout:
        await layout.render()

        await effect_ran.wait()
        component_hook.latest.schedule_render()

        await layout.render()

    await asyncio.wait_for(effect_was_cancelled.wait(), 1)

    # So I know we said the event never occurs but... to ensure the effect's future is
    # cancelled before the test is cleaned up we need to set the event. This is because
    # the cancellation doesn't propogate before the test is resolved which causes
    # delayed log messages that impact other tests.
    event_that_never_occurs.set()
Exemple #21
0
async def test_changing_key_of_parent_element_unmounts_children():
    random.seed(0)

    root_hook = HookCatcher()
    state = idom.Ref(None)

    @idom.component
    @root_hook.capture
    def Root():
        return idom.html.div(HasState(), key=str(random.random()))

    @idom.component
    def HasState():
        state.current = idom.hooks.use_state(random.random)[0]
        return idom.html.div()

    async with idom.Layout(Root()) as layout:
        await layout.render()

        for i in range(5):
            last_state = state.current
            root_hook.latest.schedule_render()
            await layout.render()
            assert last_state != state.current
Exemple #22
0
async def test_schedule_render_from_unmounted_hook():
    parent_set_state = idom.Ref()

    @idom.component
    def Parent():
        state, parent_set_state.current = idom.hooks.use_state(1)
        return Child(key=state, state=state)

    child_hook = HookCatcher()

    @idom.component
    @child_hook.capture
    def Child(state):
        idom.hooks.use_effect(lambda: lambda: print("unmount", state))
        return idom.html.div(state)

    with assert_idom_did_log(
            r"Did not render component with model state ID .*? - component already unmounted",
    ):
        async with idom.Layout(Parent()) as layout:
            await layout.render()

            old_hook = child_hook.latest

            # cause initial child to be unmounted
            parent_set_state.current(2)
            await layout.render()

            # trigger render for hook that's been unmounted
            old_hook.schedule_render()

            # schedule one more render just to make it so `layout.render()` doesn't hang
            # when the scheduled render above gets skipped
            parent_set_state.current(3)

            await layout.render()
Exemple #23
0
async def test_memoized_effect_cleanup_only_triggered_before_new_effect():
    component_hook = HookCatcher()
    set_state_callback = idom.Ref(None)
    cleanup_trigger_count = idom.Ref(0)

    first_value = 1
    second_value = 2

    @idom.component
    @component_hook.capture
    def ComponentWithEffect():
        state, set_state_callback.current = idom.hooks.use_state(first_value)

        @idom.hooks.use_effect(dependencies=[state])
        def effect():
            def cleanup():
                cleanup_trigger_count.current += 1

            return cleanup

        return idom.html.div()

    async with idom.Layout(ComponentWithEffect()) as layout:
        await layout.render()

        assert cleanup_trigger_count.current == 0

        component_hook.latest.schedule_render()
        await layout.render()

        assert cleanup_trigger_count.current == 0

        set_state_callback.current(second_value)
        await layout.render()

        assert cleanup_trigger_count.current == 1
Exemple #24
0
async def test_use_async_effect_cleanup():
    component_hook = HookCatcher()
    effect_ran = asyncio.Event()
    cleanup_ran = asyncio.Event()

    @idom.component
    @component_hook.capture
    def ComponentWithAsyncEffect():
        @idom.hooks.use_effect(dependencies=None
                               )  # force this to run every time
        async def effect():
            effect_ran.set()
            return cleanup_ran.set

        return idom.html.div()

    async with idom.Layout(ComponentWithAsyncEffect()) as layout:
        await layout.render()

        component_hook.latest.schedule_render()

        await layout.render()

    await asyncio.wait_for(cleanup_ran.wait(), 1)