Vue-test-utils: JSX $scopedSlots support

Created on 25 May 2018  路  15Comments  路  Source: vuejs/vue-test-utils

What problem does this feature solve?

Being able to use $scopedSlots with render function (specifically for JSX usage)

shallowMount({
    render() {
        return this.$scopedSlots.foo({ bar: 'baz' });
    },
}, {
    scopedSlots: {
        foo: '<div>{{ bar }}</div>',
    },
})

It will complain that $scopedSlots.foo is not a function, but if you make it a function, it will say $scopedSlots.foo.trim() is not a function

What does the proposed API look like?

I prefer if scopedSlots can be passed function values like:

scopedSlots: {
  foo: (createElement, props) => { return createElement('div', props); },
},

Otherwise, at least make it so the above syntax would still work (with string scopedSlot.foo values but the this.$scopedSlots.foo() in the render function still work)

feature request intend to implement

Most helpful comment

I've worked around it by wrapping the component like

shallowMount({
  render() {
    return (
      <Component
        scopedSlots={{
          default: <p></p>
        }}
      />
    );
  }
})

All 15 comments

What is benefit for supporting JSX $scopedSlots?

This feature would be useful for making renderless components such as outlined by Adam here: https://adamwathan.me/renderless-components-in-vuejs/

It would be helpful to test the component api.
Currently I get the above error in jest in my unit test.

Hi, please can someone help me with how I can test scoped slots?

Currently, my render function looks like this

  render() {
    return this.$scopedSlots.default({ ...this.computedStateAndHelpers })
  },

In my test (with Jest), I'm trying to mount like this:

    const wrapper = shallowMount(Component, {
      scopedSlots: {
        default: '<p></p>'
      }
    })
    expect(wrapper).toBeAViewComponent()

But no luck.

I've also tried making default property a method and no luck there either. I believe I followed the docs properly. How do I do this? Thanks!

I've worked around it by wrapping the component like

shallowMount({
  render() {
    return (
      <Component
        scopedSlots={{
          default: <p></p>
        }}
      />
    );
  }
})

hi @panganibanpj I still get

TypeError: this.$scopedSlots.default is not a function

Also, won't I lose direct access to the components vm?

@kayandrae07 You should be able to call find to get the nested component (may have to mount instead of shallowMount)

Something like this:

const wrapper = mount({
  render() {
    return (
      <Component
        scopedSlots={{
          default: <p></p>
        }}
      />
    );
  }
});
const componentWrapper = wrapper.find(Component);

@davestaab thank you. This was what worked for me

    const wrapper = mount({
      render() {
        return <Component scopedSlots={{ default: () => <p></p> }}/>
      }
    })
    const componentWrapper = wrapper.find(Component)
    expect(componentWrapper.isVueInstance()).toBeTruthy()

I had to make the scoped slot parameter a function otherwise it failed for me

So that method works until I need to mock dependencies.

When I pass options to mount/shallowMount they are set on the outer component and not on the true component I'm trying to test. (Component in the above examples)

Looking forward to a first class solution to this.

@davestaab 馃槃 I created a helper function that sends all the options to the Component

import { mount } from '@vue/test-utils'

export default (component, options = {}) => {
  return mount({
    render(h) {
      return h(component, {
        scopedSlots: { default: () => <p /> },
        ...options,
      })
    },
  }).find(component)
}

I'm also looking for a solution and will love to contribute in my spare time. Also, do you know how I can access the values exported from a scoped slot?

I'm able to do this:

  const wrapper = shallowMount(Component, {
    scopedSlots: {
        default:
          `<div slot-scope="props" class="content">
            slot content {{ props.message }} 
          </div>`
       }
    });
    expect(wrapper.text()).toContain("slot content hi");

where "hi" is bound to the slot as message inside Component.

So message is passed from Component to the scoped slot. Is that what you mean?

Yeah, it's what I'm also doing. But assuming I'm exposing a method from the slot-scope. How do I access it?

@kayandrae07 I've ended up using a template based approach like in this project:
https://github.com/posva/vue-promised

Basically I make a test helper component that uses the component in question. To test methods, I use them and assert they had the desired results.

Hope this helps.

@davestaab Thank you! Taking a look at Vue-promised and its tests and I found a suitable solution to my problem. Since it uses scoped-slots too, I can just mirror their implementation.

This feature would be useful for making renderless components such as outlined by Adam here: https://adamwathan.me/renderless-components-in-vuejs/

It would be helpful to test the component api.
Currently I get the above error in jest in my unit test.

It seems that this example does not use createElement().
I think the API as below using createElement() is not absolutely necessary.

scopedSlots: {
  foo: (createElement, props) => { return createElement('div', props); },
},

I was encountering similar scoped slot testing issues when using a functional component or component with a render function. After #808 landed, the final key for me ended up being to ensure that the scoped slot in a test component returns a VNode as that is what render functions ultimately need to return. The following sample code demonstrates returning a VNode from a scoped slot in a test mounted component. Hope someone else finds value in it!

// component

const MyComponent = {
  functional: true,
  props: {
    someComponentProp: { type: String },
  }
  render(createElement, context) {
    const { someComponentProp } = context.props;
    if (context.data.scopedSlots && context.data.scopedSlots.default) {
      // scoped slot should return a VNode
      return context.data.scopedSlots.default(someComponentProp);
    }
    return createElement('div', {}, 'default value if slot isn't defined');
  }
};
// test

const scopedComponent = {
  props: { someProp: { type: String } },
  render(createElement) {
    return createElement('em', {}, this.someProp);
  },
};

const props = { someComponentProp: 'somePropValue' };

const rendered = mount(MyComponent, {
  context: {
    props,
    scopedSlots: {
      default: (someProp) => {
        const scoped = mount(scopedComponent, { propsData: { someProp } });
        // NOTE: `render` function needs VNode
        return scoped.vnode;
      },
    },
  },
});

expect(rendered.html()).toBe(`<em>${props.someComponentProp}</em>`);
Was this page helpful?
0 / 5 - 0 ratings

Related issues

38elements picture 38elements  路  3Comments

jonyoder picture jonyoder  路  3Comments

dlumbrer picture dlumbrer  路  3Comments

vilarinholeo picture vilarinholeo  路  3Comments

vwxyutarooo picture vwxyutarooo  路  3Comments