vuejs / vue-test-utils

Component Test Utils for Vue 2
https://vue-test-utils.vuejs.org
MIT License
3.57k stars 670 forks source link

JSX $scopedSlots support #657

Closed panganibanpj closed 5 years ago

panganibanpj commented 6 years ago

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)

38elements commented 6 years ago

What is benefit for supporting JSX $scopedSlots?

davestaab commented 6 years ago

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.

kayandra commented 6 years ago

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!

panganibanpj commented 6 years ago

I've worked around it by wrapping the component like

shallowMount({
  render() {
    return (
      <Component
        scopedSlots={{
          default: <p></p>
        }}
      />
    );
  }
})
kayandra commented 6 years ago

hi @panganibanpj I still get

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

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

davestaab commented 6 years ago

@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);
kayandra commented 6 years ago

@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

davestaab commented 6 years ago

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.

kayandra commented 6 years ago

@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?

davestaab commented 6 years ago

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?

kayandra commented 6 years ago

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

davestaab commented 6 years ago

@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.

kayandra commented 6 years ago

@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.

38elements commented 6 years ago

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); },
},
aweber1 commented 5 years ago

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>`);