Skip to content

Component stubs should render default slots #658

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

Closed
trollepierre opened this issue May 25, 2018 · 30 comments
Closed

Component stubs should render default slots #658

trollepierre opened this issue May 25, 2018 · 30 comments

Comments

@trollepierre
Copy link
Contributor

Version

1.0.0-beta.16

Reproduction link

http://google.com

Steps to reproduce

<template>
  <v-layout>
    My text
  </v-layout>
</template>

<script>

  export default {
    name: 'Component',
  }
</script>
import { createLocalVue, shallowMount } from '@vue/test-utils'
import Vuetify from 'vuetify'
import Vuex from 'vuex'
import Component from './Component.vue'

describe('components | Component', () => {
  let localVue

  beforeEach(() => {
    localVue = createLocalVue()
    localVue.use(Vuetify)
    localVue.use(Vuex)
  })

  describe('template', () => {
    it('should match snapshot', () => {
      // When
      const wrapper = shallowMount(Component, { localVue })

      // Then
      expect(wrapper.element).toMatchSnapshot()
    })
  })
})

When I comment localVue.use(Vuex) , it shouldn't affect my snapshot, because I am not using any store in my component

What is expected?

Green test

What is actually happening?

Jest Snapshot result:


- <div
  class="layout"
>
  
    My text

</div>
  <!---->

could you provide a template of CodePen, CodeSandbox if you want a link, please?

@eddyerburgh
Copy link
Member

Can you add a reproduction on CodeSandBox—https://codesandbox.io/s/m4qk02vjoy?

@trollepierre
Copy link
Contributor Author

Yes : https://codesandbox.io/s/vvk1pw1w3l
in TestComponent.spec.js, I commented
localVue.use(Vuex)
and the snapshot test fails

But I am not using Vuex inside this component

@lmiller1990
Copy link
Member

lmiller1990 commented Jun 4, 2018

I don't think this is a bug, @trollepierre . I was able to get your test passing using this code:

import { createLocalVue, mount } from '@vue/test-utils'
import TestComponent from "./TestComponent";
import Vuetify from 'vuetify'

describe('components | Component', () => {
  // let localVue

  beforeEach(() => {
    localVue = createLocalVue()
    localVue.use(Vuetify)
    //localVue.use(Vuex)
  })

  describe('template', () => {
    it('should match snapshot', () => {
      // When
      const wrapper = mount(TestComponent)

      // Then
      expect(wrapper.vm.$el).toMatchSnapshot()
    })
  })
})

What I did

  1. wrapper.element is not valid. For snapshot, you should can use wrapper.vm.$el or wrapper.htmI()). I recommend wrapper.html(), it gives a nicer output. In your case, I had to write wrapper.vm.$el to match the snapshot.

  2. Use mount instead of shallowMount. I think you should almost always use mount for snapshots, otherwise you end up stubbing everything out. Since you were using shallowMount, the <v-layout> component was stubbed out, so vm.$el was empty.

Vuex does not appear to have any relationship to this test and doing localVue.use(Vuex) should not have any impact on the test. You can include it or not, and the test still passes if you make the above changes.

@eddyerburgh
Copy link
Member

eddyerburgh commented Jun 4, 2018 via email

@trollepierre
Copy link
Contributor Author

trollepierre commented Jun 4, 2018

I checked. wrapper.element is the same as vm.$el.
using .html() doesn't help with this problem. It just gives another overview of the rendered html.

=> I don't want to mount component in snapshot. Because I don't want to update all my parents component when updating a grand-grand-children...

@lmiller1990 : it looks like Vuex makes a change in the test. I don't know why. That is why I opened this bug. I supposed this bug depends on Vuetify and VueTestUtils, but I don't see how.

@lmiller1990
Copy link
Member

lmiller1990 commented Jun 4, 2018

If you use shallowMount, the v-layout component will be stubbed out, that's why your snapshot is empty.

Re @eddyerburgh , wrapper.element also works fine.

@trollepierre Vuex does not make a change in the test from what I can see - how did you come to this conclusion?

Using shallowMount, there should be no way to generate that snapshot, because it will stub out v-layout by default, or at least that is my understanding.

I forked your repo and commented out all the Vuex related stuff, and it seems to be passing: https://codesandbox.io/s/m7xw536p9j

Something odd is going on, having a closer look.

Edit, this seems okay too:

Edit, this appears to be fine, too:

import { shallowMount } from '@vue/test-utils'
import TestComponent from "./TestComponent";
import Vuetify from 'vuetify'
//import Vuex from 'vuex'

describe('components | Component', () => {
  describe('template', () => {
    it('should match snapshot', () => {
      // When
      const wrapper = shallowMount(TestComponent)
      // Then
      expect(wrapper.element).toMatchSnapshot()
    })
  })
})

Edit again:

this passes:

localVue.use(Vuetify)
locaVue.use(Vuex)

and this seems to fail for me:

locaVue.use(Vuex)
localVue.use(Vuetify)

Replacing Vuex with VueRouter is the same - nothing to do with Vuex, but seems like localVue is doing something odd. Doesn't make a whole lot of sense so far to me. Hm. Also, simply using mount allows the test to pass. I can't understand shallowMount was working or why using Vuex would allow it to pass.

@eddyerburgh
Copy link
Member

eddyerburgh commented Jun 4, 2018

Hey so the problem is that shallow stubs components and doesn't render slots by default. Thinking about it, I think we should add this. There have been a few other people who have been stung by this, and the slot content really is part of the component you're testing.

shallow only stubs registered components, so when you don't use a localVue, it it doesn't stub the v-layout component, because Vuetify hasn't been installed.

For now, you can fix it by using a custom stub:

const VLayoutStub = {
  render: function(h) {
    return h("div", { class: "layout" }, this.$slots.default);
  }
};
const wrapper = shallowMount(TestComponent, {
  localVue,
  stubs: {
    "v-layout": VCardStub
  }
});

Or by using mount as @lmiller1990 suggested

@eddyerburgh eddyerburgh changed the title Problem in generated Snapshot when using Vuetify: I need to add localVue.use(Vuex) to render my component that is not using Vuex Component stubs should render default slots Jun 4, 2018
@lmiller1990
Copy link
Member

lmiller1990 commented Jun 4, 2018

Your suggested workaround (passing this.$slots.default) it something I should add to the createStub object as part of #678 . We could have a renderSlots option, where the default is true.

createStub('v-layout', { renderDefaultSlot: true })

This could be then used by users, and/or internally as well to handle this case.

Any idea why adding localVue.use(SomeLib) after .use(Vuetify) caused the test to pass?

@lambdalisue
Copy link

lambdalisue commented Jun 27, 2018

Hey so the problem is that shallow stubs components and doesn't render slots by default. Thinking about it, I think we should add this. There have been a few other people who have been stung by this, and the slot content really is part of the component you're testing.

Yes, please 👍 Using mount() renders child components too much. I don't want to care about the internal DOM structure of child components but DOM structure which I wrote as a slot of a target component.

@lambdalisue
Copy link

#658 (comment) shows Unknown custom element: ... warnings so I used the following for now.

// XXX: Create a general stub with its default slot
// https://github.com/vuejs/vue-test-utils/issues/658
const createSlotStub = (name) => {
  // Prevent 'Unknown custom element: ...'
  const config = require('vue').config;
  if (!config.ignoredElements) {
    config.ignoredElements = [];
  }
  config.ignoredElements.push(name);
  // Return functional component
  return {
    render: function(h) {
      return h(name, {}, this.$slots.default);
    },
  };
};

Then

const wrapper = shallowMount(TestComponent, {
  localVue,
  stubs: {
    "v-layout": createSlotStub('layout-stub'),
  }
});

@eddyerburgh
Copy link
Member

beta.21 will render default slots 👍

@miriamgreis
Copy link

miriamgreis commented Jul 17, 2018

@eddyerburgh I know that this was closed quite a while ago, but we're still wondering about

localVue.use(Vuetify)
locaVue.use(Vuex)

vs.

locaVue.use(Vuex)
localVue.use(Vuetify)

We run exactly the same test case (besides changing the order of the use commands) shallowMounting a simple Vue component looking like this:

<div>
  <v-container>
    <v-layout
      <v-flex>
        <h1>hello</h1>
      </v-flex>
    </v-layout>
  </v-container>
</div>

This is what wrapper.html() looks like in the first case:

 console.log test.js:16
    -- Vuetify first --
  console.log test.js:17
    <div><div class="container"><div class="layout"><div class="flex"><h1>hello</h1></div></div></div></div>

This is what wrapper.html() looks like in the second case:

  console.log test.js:27
    -- Vuetify second --
  console.log test.js:28
    <div><vuecomponent-stub></vuecomponent-stub></div>

Shouldn't this be the same? And will the changes solve this problem?

@lmiller1990
Copy link
Member

lmiller1990 commented Jul 17, 2018

That's odd. It looks like the first output is rendered with mount, and the second with shallowMount.

createLocalVue seems to be doing something strange, maybe. createLocalVue or createInstance could be suspect.

@miriamgreis
Copy link

@lmiller1990 Yes! I thought you found out the same as you stated this in one of your posts above? That's why I added it here and didn't create a new bug report.

I can upload the minimal example to my GitHub account and link it here if that would help.

@lmiller1990
Copy link
Member

lmiller1990 commented Jul 17, 2018

Hm. Eddy mentioned this above:

shallow only stubs registered components, so when you don't use a localVue, it it doesn't stub the v-layout component, because Vuetify hasn't been installed.

It looks like when you call localVue.use(Vuex) it somehow replaces? overrides? localVue.use(Vuetify), so it is as if localVue.use(Vuetify) never happened. This could be incorrect. I'm going to repro locally and play around a little.

Do you have an actual example of what you are testing? Sometimes lots of dependencies can make unit tests tricky. What is your actual goal of the test you are writing? PS: this is still a bug, I think, and needs to be addressed. I think localVue has some other related bugs, maybe there is another way to implement createLocalVue... 🤔

@miriamgreis
Copy link

@lmiller1990 Here is a repo for you to play around: https://github.com/miriamgreis/vue-test-utils-vuetify-and-vuex

We use Vuetify for our layout and Vuex to store our data. So why shouldn't we use both of them in our tests? We need to mock the store in order to test our components.

@lmiller1990
Copy link
Member

Thanks for the repo. Sure, you can use both if you need it for your unit test. The test you posted above doesn't have Vuex in the test, so I was wondering what kind of test you are writing.

@miriamgreis
Copy link

miriamgreis commented Jul 17, 2018

I really just created a quick and easy example because the real code is from a customer project with quite complex data stored with Vuex. We do use it in these tests, but the problem happens anyway, so it's not really relevant here, I guess.

@lmiller1990
Copy link
Member

lmiller1990 commented Jul 17, 2018

Since you are just testing the component, the complexity of the store shouldn't be too much trouble. I used to have trouble writing tests for the same reason. I learned this from my experience and wrote about it here.

  1. if you are testing if you component renders something based on Vuex, eg using state or getters, you should mock those. Since you most likely are using a computed property to return this.$state or this.$getters.someGetter, you can test it like this:
const wrapper = shallowMount(Foo, {
  computed: {
    myGetter() => 'whatever value you want'
  }
})

This gives you control over the state. You can easily test edges cases. The component is simply a function of the current state of the app, regardless of whether it is local state, Vuex, or from props. Use the mounting options like propsData and computed to test your component correctly represents the state. Test Vuex getters/actions/mutations in isolation, see here.

  1. commit and dispatch

The other thing you often do with Vuex is commit or dispatch something. In this case, what you are really testing is:

  1. did I commit/dispatch the right handler?
  2. did it have the right payload

In that case, you can use mock functions and a mock store (or a real store, with mock mutations and action). The man himself, Edd, wrote about it in the documentation's guides section here.

Since I learned to use mocks and stubs and minimize dependencies, my unit tests became more simple and easier to write. Hopefully that can help you.

PS, I still think that this is a problem and am investigating. People should be able to write more end to end like unit tests if they want, and in some cases those tests are more useful.

I am trying to collect my experience in a series of articles here to help others learn more about testing Vue (I am still learning myself). It's a WIP but maybe that can help you as well.

I'll post if I make some progress on this. I would like to be able to use any Vue plugin without problems in tests.

@miriamgreis
Copy link

@lmiller1990 Thanks for providing all the help and the links, but what you describe in your article is exactly what we already do in our tests. ;-) We use mocks and stubs for almost everything. Nevertheless, it requires us to have: Vue.use(Vuex).

@lmiller1990
Copy link
Member

lmiller1990 commented Jul 17, 2018

@miriamgreis sure, no problem.

If possible it would be great to get a full reproduction of your problem, after all. After trying some stuff out now, I am not having the same problem you appear to be.

  • When I use shallowMount, everything stubs out nicely (expected, as of beta 20).
  • When i use mount, the entire DOM tree renders nicely.

One problem I encountered on the way, and I suspect a lot of people encounter is Vuetify appears to require a v-app component as the route. If you are testing components in isolation, you probably do not have a v-app. The way I handled it is like this:

import { createLocalVue, mount, shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
import Vuex from "vuex"
import Vuetify from "vuetify"
import RootApp from "@/components/RootApp.vue"


const VAppRoot = (Child) => ({
  components: { Child, RootApp },
  template: "<root-app><child /></root-app>"
})

describe('HelloWorld.vue', () => {
  const localVue = createLocalVue()

  localVue.use(Vuetify)
  localVue.use(Vuex)

  it('shallowMounts', () => {
    const comp = VAppRoot(HelloWorld)

    const wrapper = shallowMount(comp, {
      localVue
    })

    console.log(wrapper.html())
  })

  it('mounts', () => {
    const comp = VAppRoot(HelloWorld)

    const wrapper = mount(comp, {
      localVue
    })

    console.log(wrapper.html())
  })
})

Here is RootApp:

<template>
  <v-app class="outer">
    <slot />
  </v-app>
</template>

<script>
export default {

}
</script>

HelloWorld.vue:

<template>
  <div class="hello--world">
    <v-container>
      <v-layout
        <v-flex>
          <h1>hello</h1>
          <v-btn color="success">Success</v-btn>

        </v-flex>
      </v-layout>
    </v-container>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  }
}
</script>

console.log outputs:

shallowMount:

<rootapp-stub></rootapp-stub>

Makes sense, it just stubbed out the root-app. The slot will be rendered in beta.21 thanks to Edd.

mount:

 <div data-app="true" class="application outer theme--light" id="app">
   <div class="application--wrap">
     <div class="hello--world">
       <div class="container">
         <div class="flex"><h1>hello</h1> 
         <button type="button" class="v-btn success">
          <div class="v-btn__content">Success</div>
        </button>
        </div>
      </div>
    </div>
  </div>
</div>

Looks good, I think.

Maybe that can help. Did you have a v-app in your tests? I was able to put Vue.use in any order and it was fine.

@miriamgreis
Copy link

miriamgreis commented Jul 17, 2018

Thanks for taking so much time to try to solve the problem. Your example obviously works because you put your own component around the Vuetify components. Our example has a Vuetify component as a root component as you can see in the provided example. You find the reproduction code in the repository I linked above: https://github.com/miriamgreis/vue-test-utils-vuetify-and-vuex

There is also another solution to the v-app problem which doesn't blow up the test code as it doesn't require extra components in your test: vuetifyjs/vuetify#1210

@lmiller1990
Copy link
Member

Hm, did you link the right repro? It doesn't have Vue or Vuetify (or maybe I misunderstood something).

Thanks for the link to the Vuetify issue, I can use it to repro the issue more accurately.

Not sure if I can help more, I still want to look more into the potential Vue.use issue. Hopefully beta.21 will help with people using Vuetify.

@miriamgreis
Copy link

miriamgreis commented Jul 18, 2018

@lmiller1990 Sorry, it's my fault that I didn't doublecheck the repository. Looks like I pushed to the wrong one yesterday. :-( You will find the right code in there now.

Maybe it's a language issue, but just to state this more clear: I don't need any of your explanation. I really appreciate that you provided these explanations on how to use Vuex and Vuetify in test, but I already now all of this. We already use Vuex and Vuetify successfully in our unit tests. So you don't need to explain anything else, just look into the potential Vue.use issue, because this is what we are interested in. ;-)

@lmiller1990
Copy link
Member

Ok, thanks for the repo. I tried it and can repro the problem locally. I'll use this to find the problem. Thanks for clarifying.

@lmiller1990
Copy link
Member

lmiller1990 commented Jul 18, 2018

Okay, I isolated the problem. The problem is any plugin that calls Vue.mixin seems to introduce the bug. Here is a more minimal reproduction: https://github.com/lmiller1990/vue-test-utils-vuetify-and-vuex . This is most likely related to how createLocalVue is implemented.

Basically

const myPlugin = {
  install: function (Vue, opts) {
    Vue.mixin({ 
      // doing Vue.mixin breaks it
    })
  }
}

I think this causes the problem in this issue, too: #819 . I'll keep working on it this week, and let you know if/when I solve it 👍

@miriamgreis
Copy link

Thanks very much. :-)

@Mourdraug
Copy link

@lmiller1990 I have a test case that requires me to make full mount and it crashed without encasing it in v-app, so I used method you shown here and got test to run with full mount. How can I access child component's instance tho?

@lmiller1990
Copy link
Member

@Mourdraug I am not sure if I understood correctly or not but can you do something like:

import Child from "./Child.vue"

const wrapper = mount(Foo)
const child = wrapper.find(Child)
const childVm = child.vm // this is the child component instance

@Mourdraug
Copy link

I was sure I tried find() before and failed. But it works now. Thanks a lot.

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

No branches or pull requests

6 participants