Exemplo n.º 1
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
Exemplo n.º 2
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
Exemplo n.º 3
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
Exemplo n.º 4
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)
Exemplo n.º 5
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
Exemplo n.º 6
0
async def test_components_are_garbage_collected():
    live_components = set()
    outer_component_hook = HookCatcher()

    @idom.component
    @outer_component_hook.capture
    def Outer():
        component = idom.hooks.current_hook().component
        live_components.add(id(component))
        finalize(component, live_components.remove, id(component))

        hook = idom.hooks.current_hook()

        @idom.event(target_id="force-update")
        async def force_update():
            hook.schedule_render()

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

    @idom.component
    def Inner():
        component = idom.hooks.current_hook().component
        live_components.add(id(component))
        finalize(component, live_components.remove, id(component))
        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.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 a reference to it.
    del layout
    # the hook also contains a reference to the root component
    del outer_component_hook

    gc.collect()
    assert not live_components
Exemplo n.º 7
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.schedule_render()
        await layout.render()

    assert used_refs[0] is used_refs[1]
    assert len(used_refs) == 2
Exemplo n.º 8
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]
Exemplo n.º 9
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
        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.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()
Exemplo n.º 10
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
        async def effect():
            effect_ran.set()
            return cleanup_ran.set

        return idom.html.div()

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

        await effect_ran.wait()
        component_hook.schedule_render()

        await layout.render()

    await asyncio.wait_for(cleanup_ran.wait(), 1)
Exemplo n.º 11
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(args=[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.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
Exemplo n.º 12
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.current.schedule_render()

        update = await layout.render()
        assert update.path == "/children/0/children/0"
Exemplo n.º 13
0
async def test_use_memo_never_runs_if_args_args_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.schedule_render()
        await layout.render()
        component_hook.schedule_render()
        await layout.render()

    assert used_values == [1, 1, 1]
Exemplo n.º 14
0
async def test_memoized_effect_on_recreated_if_args_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(args=[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.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.schedule_render()
        await layout.render()

        assert effect_run_count.current == 2