Skip to content

Mock $refs #271

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
charliekassel opened this issue Dec 15, 2017 · 34 comments
Closed

Mock $refs #271

charliekassel opened this issue Dec 15, 2017 · 34 comments

Comments

@charliekassel
Copy link

It would be useful to be able to mock $refs.

Currently, as far as I understand there is no way to test a method that calls a childs methods via $refs.

methodToTest ( ) {
  this.a = 1
  this.$refs.childComponent.childsMethod()
}
---
undefined is not an object (evaluating 'this.$refs.childComponent.childsMethod')
@Austio
Copy link
Contributor

Austio commented Dec 20, 2017

@charliekassel could you provide an example where you would want to use this functionality?

@charliekassel
Copy link
Author

Perhaps it's actually stubbing rather than mocking.
For the example above where we have that method to test, I guess I would want the shallow constructor to stub that child method so it's not called, preventing the test throwing an error.
I want to test the logic contained in that methodToTest method without the side effects of calling $refs.childComponent.childsMethod()

@ddykhoff
Copy link

I have a component Approvals.vue utilizing the vue-toast library. I don't need to call the library methods when testing my component.

<template>
    <div>
        {{ someContent }}
    </div>
    <vue-toast ref='toast'></vue-toast>
</template>

<script>
import VueToast from 'vue-toast';

export default {
    props: ['someContent'],
    components: {
        'vue-toast': VueToast
    },
    methods: {
      showToast() {
          const toast = this.$refs.toast;
          toast.showToast(this.someContent);
      },
  },
    mounted () {
        this.$refs.toast.setOptions({
            maxToasts: 3,
            position: 'bottom right'
        });
    }
}
</script>

And my test

describe('Approvals page', () => {
    const wrapper = shallow(Approvals);

    it('any test', () => {
        expect(wrapper).toBeDefined();
    });
});

This throws the following error and fails the test:

TypeError: Cannot read property 'setOptions' of undefined

Stubbing $refs would allow be to test this component, as is I cannot test anything in the Approvals component due to this issue.

@LinusBorg
Copy link
Member

Wouldn't that be solvable by stubbing the component that's being referenced?

messing with $refs means messing with the implementation of the component under test, which is not a good idea in my opinion.

@ddykhoff
Copy link

ddykhoff commented Jan 2, 2018

@LinusBorg shallow is already stubbing the referenced component so my example still stands. The issue appears to be that the $refs are not populated in testing like they are in production.

@eddyerburgh
Copy link
Member

@ddykhoff can you post a code example of $refs not being populated correctly in vue-test-utils?

@ddykhoff
Copy link

ddykhoff commented Jan 2, 2018

@eddyerburgh My original comment in the issue has the example I am facing the issue with (#271 (comment)). Let me know if there is more info I can provide.

@ddykhoff
Copy link

ddykhoff commented Jan 5, 2018

@eddyerburgh I was trying to create a JSFiddle to show the issue but I'm not sure its possible to transpile within JSFiddle, or at least I don't know how. Thus vue-test-utils cannot be loaded properly.

https://jsfiddle.net/7kgkfrbu/1/

Uncaught ReferenceError: require is not defined

Should I open another issue for this? Being able to provide JSFiddles is useful for bug reproduction.

@eddyerburgh
Copy link
Member

@ddykhoff There's an iife build that runs in browsers—https://github.com/vuejs/vue-test-utils/blob/dev/dist/vue-test-utils.iife.js. Once we've released we could add it to CDNJS. At the moment, your best bet is to copy the code from the iife into JSFiddle. You also need to include vueTemplateCompiler (https://cdn.jsdelivr.net/npm/vue-template-compiler@2.5.13/browser.js).

@ddykhoff
Copy link

ddykhoff commented Jan 5, 2018

@eddyerburgh I got it working in JSFiddle. You can use rawgit.com as a "CDN" for the iife build. Here is the bug repro you requested (child components methods not available in $refs).

https://jsfiddle.net/7kgkfrbu/6/

As you can see from the console output in the mounted callback, $refs is not populated.

@eddyerburgh
Copy link
Member

Hi @ddykhoff, thanks for the fiddle.

The reason you can't call the method on the ref is because shallow stubs all the child components. The $refs object is still populated, but the toast component doesn't have any methods (because it's been stubbed).

You can use mount instead of shallow if you want to interact with a child components methods. Alternatively, you could use shallow and pass a custom stub:

const VueToastStub = {
  render: () => {},
  methods: {
  	setOptions: () => {}
  }
}

const wrapper = shallow(Approvals, {
  stubs: {
    'vue-toast': VueToastStub
  }
})

@LinusBorg
Copy link
Member

So if that use case isn't one after all, I posit again: we don't have to mock refs, we can stub the components.

And we shouldn't either, because we create situations that are not possible in a real app.

@charliekassel
Copy link
Author

@eddyerburgh I can confirm that the above technique worked for me and allowed me to test the parent method without error.

@ddykhoff
Copy link

ddykhoff commented Jan 9, 2018

@eddyerburgh Is there an issue with referencing the $refs in the mounted callback? Using your suggestion I still see the same error

https://jsfiddle.net/7kgkfrbu/7/

@eddyerburgh
Copy link
Member

@ddykhoff That is an issue with using an HTML template instead of a template property. If you refactor your example to use a template property, it works as expected.

vue-test-utils doesn't support HTML templates at the moment, if you'd like to see it as a feautre, please make a new feature request issue.

@eddyerburgh
Copy link
Member

I'm going to close this issue as there are solutions to mock refs by stubbing components without a method that could potentially cause bugs.

@GarrettGeorge
Copy link

How can I mock $refs on the component I'm testing. If I have a computed function which calculates whether or not a div is "too tall" I need to do this.$refs.ref.clientHeight which is not accessible when testing.

Because of this I need a way to mock $refs.

@LinusBorg
Copy link
Member

LinusBorg commented Nov 6, 2018

Well, if clientHeight isn't a real thing in a unit test anyway, why bother to test it in the context of a real component instance?

If you want to test the computed property (which smells a lot like testing implementation details), just test that function on its own, providing a simple componentInstance mock with .call(mockObject):

// your computed property
function isTooTall() {
  return this.someData - this$refs.ref.clientHeight < 100
}
// your test
const mock = {
  someValue: 1000,
  $refs: {
    ref: {
      clientHeight: 910
    }
  }
}
const result YourComponent.computed.isTooTall.call(mock)

expect(result).toBe(true)

Generally, stuff involving dimensions should probably be tested in an end-to-end test where you can verify the correctness of the resulting behaviour (breakpoints, change of dimensions etc), not a unit test that tests implementation details. That computed prop could calculate correctly and a dozen other things only appearant in a real e2e test could make the behaviour relying on this property break.

@GarrettGeorge
Copy link

@LinusBorg I'm primarily trying to just get the component to complete the render cycle without throwing an error. I guess I could just check if this.$refs has the property ref.

@rehandialpad
Copy link

Im also having the same issue. I need to mock the clientWidth of an element that I'm using a $ref to retrieve. My functionality involves dynamically translating an object based on the width of the container. It's unfortunate that the $ref doesn't get updated when I modify the elements in the same way the event.target gets updated https://vue-test-utils.vuejs.org/api/wrapper/#trigger-eventtype-options

@xyyVee
Copy link

xyyVee commented May 21, 2019

same question about test $refs from element-ui form component:

submitFilter() {
    this.$refs.filter.validate((valid) => {
        if (valid) {
              this.$emit('submitFilter', this.formData)
          }
    })
}

Here is my part code of test:

it('submit data', () => {
    const wrapper = shallowMount(OrdersFilter, {
      localVue
    })
    wrapper.vm.formData = {
      value: 'form'
    }
    wrapper.vm.submitFilter()
    console.log(wrapper.emitted())
  })

I want to test if wrapper.emmitted, but jest throw an error about

this.$refs.filter.validate is not a function

still not familar with vue-test-utils, anyone can help? thanks in advance.

@krskibin
Copy link

krskibin commented Jul 3, 2019

@xiayuying I have the same problem with element-ui, any ideas?

@syzer
Copy link

syzer commented Jul 5, 2019

@krskibin I had same problem with element-ui..
I encourage you to use proper framework like:
vuiefy

Example workarounds might be:

  1. Just use e2e test with cypress, or
  2. in code of your component
if (!this.$refs.filter.validate) {
  return false
}

Option 2 is how bad testing practices can destroy codebase...
So I would choose Option1.

@ffxsam
Copy link

ffxsam commented Jul 25, 2019

Weird syntax, no? As this is creating an object, not a Vue component:

const VueToastStub = {
  render: () => {},
  methods: {
  	setOptions: () => {}
  }
}

const wrapper = shallow(Approvals, {
  stubs: {
    'vue-toast': VueToastStub
  }
})

Might work in standard JS, but TS is very unhappy with that. I had to do this in my code:

    // @ts-ignore
    const StubbedCropper = Vue.extend({
      methods: {
        getCroppedCanvas: () => 'placeholder',
      },
      render: () => ({}),
    });

@Hifounder
Copy link

Hifounder commented Mar 4, 2020

@xyyVee You can try this

import { shallowMount } from '@vue/test-utils';
import { Form } from 'element-ui';
import component from '.@/components/index';

describe('index.vue', () => {
    beforeEach(() => {
        wrapper = shallowMount(component, {
            stubs: {
                'el-form': Form,
            },
        });
    });
});

same question about test $refs from element-ui form component:

submitFilter() {
    this.$refs.filter.validate((valid) => {
        if (valid) {
              this.$emit('submitFilter', this.formData)
          }
    })
}

Here is my part code of test:

it('submit data', () => {
    const wrapper = shallowMount(OrdersFilter, {
      localVue
    })
    wrapper.vm.formData = {
      value: 'form'
    }
    wrapper.vm.submitFilter()
    console.log(wrapper.emitted())
  })

I want to test if wrapper.emmitted, but jest throw an error about

this.$refs.filter.validate is not a function

still not familar with vue-test-utils, anyone can help? thanks in advance.

@booomerang
Copy link

booomerang commented May 26, 2020

For me helped the way of changing shallowMount to mount, and mocking the ref method like this:

const mockedMethod = jest.fn()
wrapper.vm.$refs.childComponent.someMethod = mockedMethod

// link where parent method calls this.$refs.childComponent.someMethod
link.trigger('click')

await flushPromises()

expect(mockedMethod).toHaveBeenCalled()

Hope this may be useful for someone.

@LaraLeeChina
Copy link

@xyyVee You can try this

import { shallowMount } from '@vue/test-utils';
import { Form } from 'element-ui';
import component from '.@/components/index';

describe('index.vue', () => {
    beforeEach(() => {
        wrapper = shallowMount(component, {
            stubs: {
                'el-form': Form,
            },
        });
    });
});

same question about test $refs from element-ui form component:

submitFilter() {
    this.$refs.filter.validate((valid) => {
        if (valid) {
              this.$emit('submitFilter', this.formData)
          }
    })
}

Here is my part code of test:

it('submit data', () => {
    const wrapper = shallowMount(OrdersFilter, {
      localVue
    })
    wrapper.vm.formData = {
      value: 'form'
    }
    wrapper.vm.submitFilter()
    console.log(wrapper.emitted())
  })

I want to test if wrapper.emmitted, but jest throw an error about

this.$refs.filter.validate is not a function

still not familar with vue-test-utils, anyone can help? thanks in advance.

I have the same question about the element UI validate not a function error, do you fix this problem? waiting~~

@juniorgarcia
Copy link

juniorgarcia commented Jul 30, 2020

This is awkward. In my case, the tests pass when ran through WebStorm, but don't pass running on terminal.

Basically because something I'm doing with $refs which in here are just simple HTML elements, not components. Same problem:

 TypeError: Cannot read property 'contains' of undefined

      217 |     },
      218 |     clickedOutsideMainMenu(e) {
    > 219 |       if (this.$refs.mainMenuOpenBtn.contains(e.target)) return
          | ^
      220 |       this.isMainMenuOpen = this.$refs.mainMenu.contains(e.target)
      221 |     },
      222 |   },

this.$refs.mainMenuOpenBtn is just a simple <button>.

Switching from shallowMount to mount makes no difference.

Edit
I think I figured out why the tests work on WebStorm. Maybe because I ran them while debugging with breakpoints. If that's the reason, this might be happening due something that is not ready yet while I run them at the terminal. Anyway, didn't solve the problem, actually it got more weird, since I'm using an async/await methods with $nextTick but it still does not work at the terminal.

@fejj182
Copy link

fejj182 commented Aug 25, 2020

For me helped the way of changing shallowMount to mount, and mocking the ref method like this:

const mockedMethod = jest.fn()
wrapper.vm.$refs.childComponent.someMethod = mockedMethod

// link where parent method calls this.$refs.childComponent.someMethod
link.trigger('click')

await flushPromises()

expect(mockedMethod).toHaveBeenCalled()

Hope this may be useful for someone.

It was, thanks! I was trying to put the $refs object in mocks within the mount options but never thought of doing it like this :D

@fredrivett
Copy link

For those simply wanting to access a child method from a mounted component (similarly to how a parent component would use this.$refs.componentRef.childMethod()) you can simply call wrapper.vm.childMethod(), assuming you set up your mount as such:

const wrapper = mount(YourComponent, {
   ...
};

@taimoorsendoso
Copy link

taimoorsendoso commented May 11, 2021

In ParentComponent.vue file

<ChildComponentName
      ref="childReferenceName"
      :isPreviewed="isPreviewed"
    />

this.$refs.childReferenceName.childMethodName()

In Jest spec file

import ChildComponentName from "components/ChildComponentName";

it("test with child component method", async () => {
    let refChildComponent;
    //mount parent component instead of shallow
    wrapper = mount( ParentComponentName  );

    refChildComponent = wrapper.vm.$refs.childReferenceName = ChildComponentName;

    //use this if you want to call actual child component's method 
    refChildComponent.childMethodName = refChildComponent.methods.childMethodName;

   //But if you just want to mock response of child component's method, use this instead of above assignment
   refChildComponent.childMethod = jest.fn().mockReturnValue(false);
    
    //your code
});

@kiranparajuli589
Copy link

For me helped the way of changing shallowMount to mount, and mocking the ref method like this:

const mockedMethod = jest.fn()
wrapper.vm.$refs.childComponent.someMethod = mockedMethod

// link where parent method calls this.$refs.childComponent.someMethod
link.trigger('click')

await flushPromises()

expect(mockedMethod).toHaveBeenCalled()

Hope this may be useful for someone.

thank you! this solved my case.
we'd a method like:

closeHandler(sendFocusBackToButton) {
   this.isOpen = false
   if (sendFocusBackToButton) {
      this.$refs.button.focus()
   }
},

in a component like:

<div
      ref="dropdown"
      @click="closeHandler"
    />

test:

beforeEach(async () => {
  wrapper = await mount(MyComponent, {
    data() {
      return {
        isOpen: true
      }
    },
    stubs: {
      'test-button': TestButton
    }
  })
  wrapper.vm.$refs.button.focus = jest.fn()
})
it("should close the dropdown menu", async () => {
  await wrapper.find('.menu').trigger("click")
  expect(wrapper.find(".menu").attributes('hidden')).toBe("hidden")
})

@TangoPJ
Copy link

TangoPJ commented Jul 5, 2022

For me helped the way of changing shallowMount to mount, and mocking the ref method like this:

const mockedMethod = jest.fn()
wrapper.vm.$refs.childComponent.someMethod = mockedMethod

// link where parent method calls this.$refs.childComponent.someMethod
link.trigger('click')

await flushPromises()

expect(mockedMethod).toHaveBeenCalled()

Hope this may be useful for someone.

Thank you buddy, you saved my time!

@lakshay2711
Copy link

I am not sure about you but I got below error after doing this:

wrapper.vm.$refs.childComponent.someMethod = mockedMethod

Error:
Possible race condition: wrapper.vm.$refs.childReferenceName might be reassigned based on an outdated value of wrapper.vm.$refs.childReferenceName.

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