Vue-test-utils: setData does not force a re-render of the component when using v-model, data(), and a computed property

Created on 6 Apr 2018  ·  18Comments  ·  Source: vuejs/vue-test-utils

Version

1.0.0-beta.13

Reproduction link

https://github.com/s-robertson/vtu-setdata

Steps to reproduce

Create a new Vue component with the following criteria:

  1. Have data() return an object containing an empty string, e.g. msg
  2. Have a computed property that returns a boolean depending on if msg is empty or not. E.g. hasMsg
  3. Have an <input> in the component template that uses v-model, bound to msg
  4. Have an element, such as an <h1> in the component template that uses v-if, bound to hasMsg. This element must come AFTER the <input> in the template

Now write a unit test that:

  1. Mounts the component via shallow()
  2. Calls setData and sets msg to a non-empty string
  3. Checks to see if the template contains the <h1>

Finally, run the unit test.

What is expected?

Calling setData will cause the component to re-render, making hasMsg be true and the test will pass.

What is actually happening?

Calling setData does _not_ cause the component to re-render. The unit test cannot find the <h1> element and fails:

 FAIL  tests/unit/HelloWorld.spec.js
  HelloWorld.vue
    ✕ renders props.msg when passed (36ms)

  ● HelloWorld.vue › renders props.msg when passed

    expect(received).toBe(expected) // Object.is equality

    Expected value to be:
      true
    Received:
      false

       9 |     wrapper.setData({ msg });
      10 | 
    > 11 |     expect(wrapper.contains('h1')).toBe(true);
      12 |   })
      13 | });
      14 | 

      at Object.<anonymous> (tests/unit/HelloWorld.spec.js:11:36)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.062s
Ran all test suites.
 ERROR  jest exited with code 1.
error Command failed with exit code 1.

What's interesting is that the order of the <input> and <h1> in the template matters. If the <h1> comes before the <input> the test will also pass.

You can also get the test to pass by forcefully updating the component via wrapper.vm.$forceUpdate(); after you've called setData.

bug

Most helpful comment

@onomojo @rafaoliverce what do your tests look like? Are you using await wrapper.vm.$nextTick() in your tests? There was a large breaking
change in v30 to remove sync mode. This requires you to await for Vue to
finish the next event loop after each change that will affect the DOM.

Here are the current docs on it https://vue-test-utils.vuejs.org/guides/testing-async-components.html#updates-applied-by-vue

All 18 comments

I believe this has been fixed in the dev branch. The test case you provided passes.

Hi @eddyerburgh,

I switched to the dev branch in my example project, but am still seeing the unit test fail:

https://github.com/s-robertson/vtu-setdata/commit/7758fa2468489d815b99d69a7b895a95b1e45e73

That's because the dev branch did not have the updated built file.

I've released the fixes in 1.0.0-beta.14

@eddyerburgh

version: @vue/test-utils": "^1.0.0-beta.16"

I have a v-if pointing to a data property called errors.recordType on an element with the attribute data-test-id="record-type-error"

this fails...

const wrapper = mount(Modal)
wrapper.setData({ errors: { 'recordType': 'Error Message' } })
expect(wrapper.contains('[data-test-id="record-type-error"]')).toBe(true)

this does not...

const wrapper = mount(Modal)
wrapper.setData({ errors: { 'recordType': 'Error Message' } })
wrapper.vm.$forceUpdate()
expect(wrapper.contains('[data-test-id="record-type-error"]')).toBe(true)

Seems to not be fixed yet, unless I'm doing something wrong.

I have a similar problem as @An-AngryBear with 1.0.0-beta.16

<template>
  <div>
    <div>
      <h2>Active stubs</h2>
      <div
        v-if="!activeStubs.length"
        data-qa="no active stubs info"
      >
        No stubs enabled!
      </div>
      <table
        v-else
        data-qa="active stubs"
      >
        <tr
          v-for="(stub, index) in activeStubs"
          :key="index"
          data-qa="active stub"
        >
          <td>{{ stub.url && stub.url.toString() }}</td>
          <td>{{ stub.type }}</td>
          <td>{{ stub.code }}</td>
        </tr>
      </table>
    </div>
  </div>
</template>

<script>
export default {
  name: `NetworkTabContent`,
  data() {
    return {
      activeStubs: [],
    };
  },
};
</script>
test(`It should render a table of active stubs.`, () => {
  wrapper.setData({ activeStubs: [{}, {}] });
  // Without the next line, the test fails because
  // the component is not rerendered.
  wrapper.vm.$forceUpdate();

  expect(wrapper.contains(`[data-qa="active stubs"]`)).toBe(true);
  expect(wrapper.findAll(`[data-qa="active stub"]`).length).toBe(2);
});

@maoberlehner Similarly I'm using v-if = "show" property, Initially it was set to false and I'm making it as true in a method. I've tried set the {show = true} using (setData) and called the particular method But got a _isDestroyed of undefined. please see the code below.

Test.vue
<template>
<div>some data</div>
<child v-if="show"></child>
</template>

<script>
export default {
   name : 'test',
   data() => {
   return {
         show: false
     }
  },
    methods: {
       toggleShow() {
          this.show = !this.show
       }
    }
}
</script>
Test.test.js
describe('Methods ', () => {
    const mountComponent = () => {
        return shallowMount(Test, { 
        stubs: {
           'child': true
        }
        })
     }

    it('test toggle',  done => {
       const wrapper = mountComponent()
        wrapper.setData({ show: true });
        wrapper.vm.$forceUpdate()
       wrapper.vm.toggleShow()
       expect(wrapper.vm.show).toBe(false) // should to toggle
       done()
     })
})

While running this test got following error ~~

Test.test.js › Methods › should call toggleShow

TypeError: Cannot read property '_isDestroyed' of undefined

  57 |       const wrapper = mountComponent()
> 58 |       wrapper.setData({show: true})
     |               ^
  59 |       wrapper.vm.$forceUpdate()
  60 |       wrapper.vm.toggleShow()
  61 |       expect(wrapper.vm.show).toBe(false)

  at destroy (node_modules/vue/dist/vue.runtime.common.js:4174:28)
  at invokeDestroyHook (node_modules/vue/dist/vue.runtime.common.js:5741:59)
  at removeVnodes (node_modules/vue/dist/vue.runtime.common.js:5757:11)
  at VueComponent.patch [as __patch__] (node_modules/vue/dist/vue.runtime.common.js:6170:11)
  at VueComponent.Vue._update (node_modules/vue/dist/vue.runtime.common.js:2668:19)
  at VueComponent.vm._update (node_modules/@vue/test-utils/dist/vue-test-utils.js:2095:12)
  at VueComponent.updateComponent (node_modules/vue/dist/vue.runtime.common.js:2786:10)
  at Watcher.get (node_modules/vue/dist/vue.runtime.common.js:3140:25)
  at Watcher.run (node_modules/vue/dist/vue.runtime.common.js:3217:22)
  at Watcher.update (node_modules/vue/dist/vue.runtime.common.js:3205:10)
  at VueComponent.Vue.$forceUpdate (node_modules/vue/dist/vue.runtime.common.js:2689:19)
  at updateChildComponent (node_modules/vue/dist/vue.runtime.common.js:2863:8)
  at prepatch (node_modules/vue/dist/vue.runtime.common.js:4142:5)
  at patchVnode (node_modules/vue/dist/vue.runtime.common.js:5923:7)
  at updateChildren (node_modules/vue/dist/vue.runtime.common.js:5820:9)
  at patchVnode (node_modules/vue/dist/vue.runtime.common.js:5934:29)
  at VueComponent.patch [as __patch__] (node_modules/vue/dist/vue.runtime.common.js:6094:9)
  at VueComponent.Vue._update (node_modules/vue/dist/vue.runtime.common.js:2668:19)
  at VueComponent.vm._update (node_modules/@vue/test-utils/dist/vue-test-utils.js:2095:12)
  at VueComponent.updateComponent (node_modules/vue/dist/vue.runtime.common.js:2786:10)
  at Watcher.get (node_modules/vue/dist/vue.runtime.common.js:3140:25)
  at Watcher.run (node_modules/vue/dist/vue.runtime.common.js:3217:22)
  at Watcher.update (node_modules/vue/dist/vue.runtime.common.js:3205:10)
  at Dep.notify (node_modules/vue/dist/vue.runtime.common.js:695:13)
  at Object.reactiveSetter [as show]

(node_modules/vue/dist/vue.runtime.common.js:1012:11)
at VueComponent.proxySetter [as show]
(node_modules/vue/dist/vue.runtime.common.js:3298:26)
at VueComponent.set [as $set] (node_modules/vue/dist/vue.runtime.common.js:1034:17)
at node_modules/@vue/test-utils/dist/vue-test-utils.js:1162:10
at Array.forEach ()
at recursivelySetData (node_modules/@vue/test-utils/dist/vue-test-utils.js:1155:21)
at VueWrapper.setData (node_modules/@vue/test-utils/dist/vue-test-utils.js:1654:3)
at Object.setData (test/unit/views/tdi/HFE.test.js:59:15)

@narenderv7 Have you tried setting sync to false and using Vue.nextTick?

test('use Vue.nextTick', (done) => {
  const wrapper = mount(TestComponent, { sync: false })
  wrapper.trigger('click')
  Vue.nextTick(() => {
    expect(wrapper.text()).toBe('updated')
    done()
  })
})

I just came across this issue and I believe 1.0.0-beta.25 mitigates the original issue. I had a similar experience of not seeing a DOM update that would encourage a correct test assertion whereas now the assertion is returning the expected value.

I would kindly suggest @s-robertson to update to the latest version of the toolbelt and test it out. :+1:

I've just ugrade from 1.0.0-beta.22 to 1.0.0-beta.25 and get a similar issue.

modal-container.vue

<template>
  <transition name="modal">
    <div id="modal" class="modal__mask" v-if="config.show" :config="config">
    <slot></slot>
    </div>
  </transition>
</template>

modal-container.specs.js

it('closes the modal when the close button is clicked', () => {
    let modalConfig = { show: true }
    const propsData = { config: modalConfig }
    const wrapper = mount(ModalContainer, {
      localVue,
      propsData,
      slots: {
        default: 'Hello world',
      },
    })

    const closeButton = wrapper.find('.modal__close')
    closeButton.trigger('click')
    expect(wrapper.isEmpty()).toBe(true)
  })

This test fail, I still have to use wrapper.vm.forceUpdate() in order to make it pass.

This seems like a seperate issue @rlecellier , could you create a new issue with a reproduction in either a GitHub repo or codesandbox?

I've found that sometimes I have to use an async function and call await on setData and setProps

I ended up stubbing it, It worked for me

This seems to have regressed starting with 1.0.0-beta.30 and still broken in 1.0.0-beta.31. 1.0.0-beta.29 seems to work fine for me though.

Went back to 1.0.0-beta.29 as @onomojo said, and everything started working again.
The components for some reason were not* being updated after setData() for example.

@onomojo @rafaoliverce what do your tests look like? Are you using await wrapper.vm.$nextTick() in your tests? There was a large breaking
change in v30 to remove sync mode. This requires you to await for Vue to
finish the next event loop after each change that will affect the DOM.

Here are the current docs on it https://vue-test-utils.vuejs.org/guides/testing-async-components.html#updates-applied-by-vue

Yes, probably that's the cause of the problems.
Thanks @JessicaSachs !

Thanks @JessicaSachs !! You saved my day! I forget about the nextTick event always for Vue components.

Thanks @JessicaSachs , your comment saved my day, cheers!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vilarinholeo picture vilarinholeo  ·  3Comments

jonyoder picture jonyoder  ·  3Comments

dlumbrer picture dlumbrer  ·  3Comments

maerteijn picture maerteijn  ·  3Comments

matt-sanders picture matt-sanders  ·  3Comments