Esempio n. 1
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
Esempio n. 2
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
Esempio n. 3
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
Esempio n. 4
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
Esempio n. 5
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
Esempio n. 6
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"
        }]
Esempio n. 7
0
async def test_use_location(display: DisplayFixture):
    location = idom.Ref()

    @poll
    async def poll_location():
        """This needs to be async to allow the server to respond"""
        return location.current

    @idom.component
    def ShowRoute():
        location.current = display.backend.implementation.use_location()
        return html.pre({"id": "scope"}, str(location.current))

    await display.show(ShowRoute)

    await poll_location.until_equals(Location("/", ""))

    for loc in [
            Location("/something"),
            Location("/something/file.txt"),
            Location("/another/something"),
            Location("/another/something/file.txt"),
            Location("/another/something/file.txt", "?key=value"),
            Location("/another/something/file.txt",
                     "?key1=value1&key2=value2"),
    ]:
        await display.goto(loc.pathname + loc.search)
        await poll_location.until_equals(loc)
Esempio n. 8
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"
Esempio n. 9
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()
Esempio n. 10
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
Esempio n. 11
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()
Esempio n. 12
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()
Esempio n. 13
0
def test_automatic_reconnect(create_driver):
    # we need to wait longer here because the automatic reconnect is not instance
    driver = create_driver(implicit_wait_timeout=10)

    @idom.component
    def OldComponent():
        return idom.html.p({"id": "old-component"}, "old")

    mount_point = ServerMountPoint()

    with mount_point:
        mount_point.mount(OldComponent)
        driver.get(mount_point.url())

    # the server is disconnected but the last view state is still shown
    driver.find_element_by_id("old-component")

    set_state = idom.Ref(None)

    @idom.component
    def NewComponent():
        state, set_state.current = idom.hooks.use_state(0)
        return idom.html.p({"id": f"new-component-{state}"}, f"new-{state}")

    with mount_point:
        mount_point.mount(NewComponent)

        # Note the lack of a page refresh before looking up this new component. The
        # client should attempt to reconnect and display the new view automatically.
        driver.find_element_by_id("new-component-0")

        # check that we can resume normal operation
        set_state.current(1)
        driver.find_element_by_id("new-component-1")
Esempio n. 14
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]
Esempio n. 15
0
async def test_that_js_module_unmount_is_called(display: DisplayFixture):
    SomeComponent = idom.web.export(
        idom.web.module_from_file(
            "set-flag-when-unmount-is-called",
            JS_FIXTURES_DIR / "set-flag-when-unmount-is-called.js",
        ),
        "SomeComponent",
    )

    set_current_component = idom.Ref(None)

    @idom.component
    def ShowCurrentComponent():
        current_component, set_current_component.current = idom.hooks.use_state(
            lambda: SomeComponent({
                "id": "some-component",
                "text": "initial component"
            }))
        return current_component

    await display.show(ShowCurrentComponent)

    await display.page.wait_for_selector("#some-component", state="attached")

    set_current_component.current(
        idom.html.h1({"id": "some-other-component"}, "some other component"))

    # the new component has been displayed
    await display.page.wait_for_selector("#some-other-component",
                                         state="attached")

    # the unmount callback for the old component was called
    await display.page.wait_for_selector("#unmount-flag", state="attached")
Esempio n. 16
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
Esempio n. 17
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
Esempio n. 18
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
Esempio n. 19
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]
Esempio n. 20
0
def test_basic_ref_behavior():
    r = idom.Ref(1)
    assert r.current == 1

    r.current = 2
    assert r.current == 2

    assert r.set_current(3) == 2
    assert r.current == 3

    r = idom.Ref()
    with pytest.raises(AttributeError):
        r.current

    r.current = 4
    assert r.current == 4
Esempio n. 21
0
def test_module_from_source(
        driver,
        driver_wait,
        display,
        htm,  # we need this in order to run the test js module
):
    test_module = Module("test-module", source_file=HERE / "test_js_module.js")

    response_data = idom.Ref(None)

    @idom.component
    def ShowButton():
        return test_module.TestButton({
            "id":
            "test-button",
            "onClick":
            lambda event: response_data.set_current(event["data"]),
            "eventResponseData":
            10,
        })

    display(ShowButton)

    client_button = driver.find_element_by_id("test-button")
    client_button.click()
    driver_wait.until(lambda dvr: response_data.current == 10)
Esempio n. 22
0
def test_simple_click_event(driver, display):
    clicked = idom.Ref(False)

    @idom.component
    def Button():
        hook = hooks.current_hook()

        async def on_click(event):
            clicked.current = True
            hook.schedule_render()

        if not clicked.current:
            return idom.html.button({
                "onClick": on_click,
                "id": "click"
            }, ["Click Me!"])
        else:
            return idom.html.p({"id": "complete"}, ["Complete"])

    display(Button)

    button = driver.find_element_by_id("click")
    button.click()
    driver.find_element_by_id("complete")

    # we care what happens in the final delete when there's no value
    assert clicked.current
Esempio n. 23
0
def test_input_ignore_empty(driver, driver_wait, display):
    # ignore empty since that's an invalid float
    inp_ingore_ref = idom.Ref("1")
    inp_not_ignore_ref = idom.Ref("1")

    @idom.component
    def InputWrapper():
        return idom.html.div(
            idom.widgets.Input(
                lambda value: setattr(inp_ingore_ref, "current", value),
                "number",
                inp_ingore_ref.current,
                {"id": "inp-ignore"},
                ignore_empty=True,
            ),
            idom.widgets.Input(
                lambda value: setattr(inp_not_ignore_ref, "current", value),
                "number",
                inp_not_ignore_ref.current,
                {"id": "inp-not-ignore"},
                ignore_empty=False,
            ),
        )

    display(InputWrapper)

    client_inp_ignore = driver.find_element_by_id("inp-ignore")
    client_inp_not_ignore = driver.find_element_by_id("inp-not-ignore")

    send_keys(client_inp_ignore, Keys.BACKSPACE)
    time.sleep(0.1)  # waiting and deleting again seems to decrease flakiness
    send_keys(client_inp_ignore, Keys.BACKSPACE)

    send_keys(client_inp_not_ignore, Keys.BACKSPACE)
    time.sleep(0.1)  # waiting and deleting again seems to decrease flakiness
    send_keys(client_inp_not_ignore, Keys.BACKSPACE)

    driver_wait.until(
        lambda drv: client_inp_ignore.get_attribute("value") == "")
    driver_wait.until(
        lambda drv: client_inp_not_ignore.get_attribute("value") == "")

    # ignored empty value on change
    assert inp_ingore_ref.current == "1"
    # did not ignore empty value on change
    assert inp_not_ignore_ref.current == ""
Esempio n. 24
0
 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()
Esempio n. 25
0
def test_basic_ref_behavior():
    r = idom.Ref(1)
    assert r.current == 1

    r.current = 2
    assert r.current == 2

    assert r.set_current(3) == 2
    assert r.current == 3
Esempio n. 26
0
async def test_switching_component_definition():
    toggle_component = idom.Ref()
    first_used_state = idom.Ref(None)
    second_used_state = idom.Ref(None)

    @idom.component
    def Root():
        toggle, toggle_component.current = use_toggle(True)
        if toggle:
            return FirstComponent()
        else:
            return SecondComponent()

    @idom.component
    def FirstComponent():
        first_used_state.current = use_state("first")[0]
        # reset state after unmount
        use_effect(lambda: lambda: first_used_state.set_current(None))
        return html.div()

    @idom.component
    def SecondComponent():
        second_used_state.current = use_state("second")[0]
        # reset state after unmount
        use_effect(lambda: lambda: second_used_state.set_current(None))
        return html.div()

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

        assert first_used_state.current == "first"
        assert second_used_state.current is None

        toggle_component.current()
        await layout.render()

        assert first_used_state.current is None
        assert second_used_state.current == "second"

        toggle_component.current()
        await layout.render()

        assert first_used_state.current == "first"
        assert second_used_state.current is None
Esempio n. 27
0
async def test_set_state_with_reducer_instead_of_value():
    count = idom.Ref()
    set_count = idom.Ref()

    def increment(count):
        return count + 1

    @idom.component
    def Counter():
        count.current, set_count.current = idom.hooks.use_state(0)
        return idom.html.div(count.current)

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

        for i in range(4):
            assert count.current == i
            set_count.current(increment)
            await layout.render()
Esempio n. 28
0
async def test_use_state_with_constructor():
    constructor_call_count = idom.Ref(0)

    set_outer_state = idom.Ref()
    set_inner_key = idom.Ref()
    set_inner_state = idom.Ref()

    def make_default():
        constructor_call_count.current += 1
        return 0

    @idom.component
    def Outer():
        state, set_outer_state.current = idom.use_state(0)
        inner_key, set_inner_key.current = idom.use_state("first")
        return idom.html.div(state, Inner(key=inner_key))

    @idom.component
    def Inner():
        state, set_inner_state.current = idom.use_state(make_default)
        return idom.html.div(state)

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

        assert constructor_call_count.current == 1

        set_outer_state.current(1)
        await layout.render()

        assert constructor_call_count.current == 1

        set_inner_state.current(1)
        await layout.render()

        assert constructor_call_count.current == 1

        set_inner_key.current("second")
        await layout.render()

        assert constructor_call_count.current == 2
Esempio n. 29
0
async def test_use_scope(display: DisplayFixture):
    scope = idom.Ref()

    @idom.component
    def ShowScope():
        scope.current = display.backend.implementation.use_scope()
        return html.pre({"id": "scope"}, str(scope.current))

    await display.show(ShowScope)

    await display.page.wait_for_selector("#scope")
    assert isinstance(scope.current, MutableMapping)
Esempio n. 30
0
async def test_model_key_preserves_callback_identity_for_common_elements(
        caplog):
    called_good_trigger = idom.Ref(False)
    good_handler = StaticEventHandler()
    bad_handler = StaticEventHandler()

    @idom.component
    def MyComponent():
        reverse_children, set_reverse_children = use_toggle()

        @good_handler.use
        def good_trigger():
            called_good_trigger.current = True
            set_reverse_children()

        @bad_handler.use
        def bad_trigger():
            raise ValueError("Called bad trigger")

        children = [
            idom.html.button({
                "onClick": good_trigger,
                "id": "good"
            },
                             "good",
                             key="good"),
            idom.html.button({
                "onClick": bad_trigger,
                "id": "bad"
            },
                             "bad",
                             key="bad"),
        ]

        if reverse_children:
            children.reverse()

        return idom.html.div(children)

    async with idom.Layout(MyComponent()) as layout:
        await layout.render()
        for i in range(3):
            event = LayoutEvent(good_handler.target, [])
            await layout.deliver(event)

            assert called_good_trigger.current
            # reset after checking
            called_good_trigger.current = False

            await layout.render()

    assert not caplog.records