vuejs / vue-test-utils

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

Component stubs should render default slots #658

Closed trollepierre closed 6 years ago

trollepierre commented 6 years ago

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 commented 6 years ago

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

trollepierre commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

Actually wrapper.element should be the same as vm. $el. If it's not then that's a bug

On Mon, 4 Jun 2018, 14:16 Lachlan, notifications@github.com wrote:

I don't think this is a bug, @trollepierre https://github.com/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 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.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/vuejs/vue-test-utils/issues/658#issuecomment-394350590, or mute the thread https://github.com/notifications/unsubscribe-auth/AMlbWwcs1_kHq7MTDZj33qnDbKWFBlUGks5t5TMxgaJpZM4UOYwV .

trollepierre commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

lmiller1990 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

https://github.com/vuejs/vue-test-utils/issues/658#issuecomment-394386042 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 commented 6 years ago

beta.21 will render default slots 👍

miriamgreis commented 6 years ago

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

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 commented 6 years ago

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

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 commented 6 years ago

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

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

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

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 commented 6 years ago

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: https://github.com/vuetifyjs/vuetify/issues/1210

lmiller1990 commented 6 years ago

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 commented 6 years ago

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

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 commented 6 years ago

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 commented 6 years ago

Thanks very much. :-)

Mourdraug commented 6 years ago

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

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

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