Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to test two way data-binding? #117

Closed
Tracked by #296
yjp20 opened this issue Jan 15, 2021 · 10 comments · Fixed by testing-library/testing-library-docs#1366
Closed
Tracked by #296

How to test two way data-binding? #117

yjp20 opened this issue Jan 15, 2021 · 10 comments · Fixed by testing-library/testing-library-docs#1366

Comments

@yjp20
Copy link

yjp20 commented Jan 15, 2021

As far as I can tell, just passing in a variable into the render function doesn't enable two way data binding. Despite being an anti-pattern, two-way data binding is actually useful in a lot of cases, so it would be nice if there was a way to ensure that it works.

Edit:
I suppose you could also do something like

const utils = render(Input, { value: "" });
const component = utils.component.$$;
/*
* Update component such that value = asdf
*/
expect(component.ctx[component.props["value"]]).toBe("asdf")

But it's a bit repetitive and there's probably at least some justification in putting in a helper function into the library (since the framework supports it after all)

@MirrorBytes
Copy link
Collaborator

I'm not sure how you're setting up two-way binding, but the easiest way to test it would be to use a store that you pass to the component via render, then update it. You may need to throw an await tick() prior to an expect statement though.

@yjp20
Copy link
Author

yjp20 commented Jan 31, 2021

Just to be clear, what I mean by two-way bindings are the bind:value={value} kind of statements that svelte supports.

I don't think passing in a store would work for me. I don't want to change how my code works to accommodate tests.

@MirrorBytes
Copy link
Collaborator

MirrorBytes commented Jan 31, 2021 via email

@yjp20
Copy link
Author

yjp20 commented Feb 1, 2021

The binding mechanism itself is indeed something that "just works," but there are underlying mechanisms to that binding that may require testing. For example, in my codebase I have an input component (InputDate) which accepts a date and supports two way binding. Because the input itself is a text input, the component parses the text and changes the date accordingly, which then via the two-way binding will make whatever necessary state change in the parent component.

Again I can use events and listeners, but I would like to keep a consistent API among all my input components, and having a two-way binding provides great UX especially for the base components within forms and controls. If I have no way of checking what the value of the prop is of the component, then I also essentially have no way of guaranteeing that this functionality (not the binding itself, but the translation process under the hood) works.

@janosh
Copy link

janosh commented Feb 27, 2022

@yjp20 Did you ever find a good way to test 2-way binding?

@yjp20
Copy link
Author

yjp20 commented Feb 27, 2022

Sorry to say I havent yet.

@shaun-wild
Copy link

Following, hopefully, someone finds a nice way to approach this problem.

@marcosmercuri
Copy link

I had a similar problem, and I solved it by creating a dummy wrapper on the SUT, that dispatches an event when the binded property changes:

<script lang="ts">
    import { createEventDispatcher } from 'svelte';
    export let prop;
    const dispatch = createEventDispatcher();

    $: {
        dispatch('prop-changed', prop);
    }
</script>

<ComponentToTest bind:prop />

And then in the test, I render the wrapper and add a listener on the event, and do assertions on that.

Hope this helps someone.

@janosh
Copy link

janosh commented Oct 22, 2022

Also worth checking out: svelte-component-test-recipes by @davipon has a section on testing 2-way binding.

If anyone finds a way to make the prop's name dynamic in @marcosmercuri wrapper component (so you don't need a new Test2WayBind.svelte for every reactive prop you want to test), let me know. This of course doesn't work:

<script lang="ts">
  import { createEventDispatcher, type SvelteComponent } from 'svelte'

  export let component: SvelteComponent
  export let prop_name: string

  const dispatch = createEventDispatcher()
  let prop: unknown

  $: dispatch(`${prop_name}-changed`, prop)
</script>

<!-- Svelte does not support dynamic binding bind:{prop_name} (yet) -->
<svelte:component this={component} bind:{prop_name}={prop} />

@denuser
Copy link

denuser commented Dec 26, 2023

I have it like this. Works fine to me.

import { bind, binding_callbacks } from 'svelte/internal';
import { get, writable, type Writable } from 'svelte/store';
import type { SvelteComponent } from 'svelte';
          
 it('should bind value properly', async () => {
    const bindingProps = {
	    value: writable('')
    } as Record<string, Writable<unknown>>;
    
    const props = Object.fromEntries(
	    Object.keys(bindingProps).map((x) => [x, get(bindingProps[x])])
    );
    
    const { component } = render(Input, {
	    placeholder: 'Basic',
	    ...props
    });
    
    pushBindingCallbacks(component, bindingProps);
    
    expect(screen.getByPlaceholderText('Basic')).toBeInTheDocument();
    fireEvent.input(screen.getByPlaceholderText('Basic'), { target: { value: 'login' } });
    
    await waitFor(() => {
	    expect(get(bindingProps['value'])).toBe('login');
    });
});

function pushBindingCallbacks(
    component: SvelteComponent,
    bindingProps: Record<string, Writable<unknown>>
) {
    Object.entries(bindingProps).forEach(([key, valueStore]) => {
        binding_callbacks.push(() =>
	        bind(component, key, (value: string) => {
		        valueStore.set(value);
	        })
        );
    });
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants