vuetifyjs/vuetify

[Feature Request] Avoid importing globally Vuetify in all tests

Closed this issue ยท 26 comments

Versions and Environment

Vuetify: ^1.1.11
Vue: 2.5.16
Browsers: all
OS: all

Previously worked in:

Vuetify: ^1.1.10
Vue: 2.5.16

Steps to reproduce

Since Vuetify version 1.1.11, developers have to import globally Vuetify in all tests to avoid having 2 multiples Vue instances running (and see a warning referring to #4068 )

localVue.use(Vuetify)

has been replaced by

Vue.use(Vuetify)

Expected Behavior

Using localVue is better because it prevents polluting the original Vue copy during tests.
I expect Vuetify not to use its own instance of Vue.
So I can come back using only localVue.use(Vuetify) in all of my tests

Actual Behavior

I had to use Vue.use(Vuetify) in each of my test files

Reproduction Link

https://codesandbox.io/s/mz86zx5vnj

Watch the warnings generated by the test

Additional Comments:

We need to put together some testing documentation so that this is more clear.

Related to the same issue. When switching from

localVue.use(Vuetify)

to

Vue.use(Vuetify)

I have an issue when any watcher is triggered in my tests. A range Maximum callstack exceeded is triggered and the test fails. Has anyone else experienced this?

Also, should I open a separate issue for this? I can make a reproduction link if needed.

Please create a separate issue @AtofStryker

Actually looks like my issue is related to vuejs/vue-test-utils#647 for any who are interested. Will wait @johnleider for more testing documentation

I expect Vuetify not to use its own instance of Vue

We can't, see vuejs/vue#8278. It might be possible to hack something together with require.cache, but I haven't tried.

Vuetify 1.5.0
Vue 2.6.2

It is still an issue

This is super confusing, guys.
Any updates here?

Same here, I can't find a way to test my project correctly. @johnleider Do we have a documentation somewhere that can give us some exemples to test our projects properly?

Any idea if this will be addressed? And, if so, when? It's kind of a big deal to those of us who take testing seriously.

Thanks in advance for any feedback. And, more than that, thanks for giving us such a remarkable, feature-rich material implementation!

I found the reason of the error.

When you create a local Vue with function createLocalVue you create a VueComponent.
On the startup of Vuetify there is an if where it validates the Vue loaded (internally) with the Vue passed from the installation; here the source

if (OurVue !== Vue) {
      consoleError('Multiple instances of Vue detected\nSee https://github.com/vuetifyjs/vuetify/issues/4068\n\nIf you\'re seeing "$attrs is readonly", it\'s caused by this')
}

Whit debug I saw this (pseudocode):

if ([Function: Vue] !== [Function: VueComponent])

See also #4068

I do have a solution. (but it depends on what you are going to test)

import { shallowMount, createLocalVue } from "@vue/test-utils";
import VueRouter from "vue-router";

import web from "@/views/layouts/web/index.vue";
import sidebar from "@/views/layouts/web/components/SSidebar.vue";
import navbar from "@/views/layouts/web/components/SNavbar.vue";

describe("layouts/web/index.vue", () => {
  let wrapper;
  let localVue;
  beforeAll(() => {
    localVue = createLocalVue();
    localVue.use(VueRouter, {});
    wrapper = shallowMount(web, {
      localVue,
      stubs: {
        VApp: "<div></div>"
      }
    });
  });

  it("Renders Sidebar", () => {
    expect(wrapper.exists(sidebar)).toBe(true);
  });

  it("Renders Navbar", () => {
    expect(wrapper.exists(navbar)).toBe(true);
  });
});

what I just did was not to import Vuetify. instead, I stub <v-app>

stubs: {
   VApp: "<div></div>"
}

P.S. it depends on what you are going to test, I was only testing my methods, props and computed property.

Having same issue in rails
vue 2.6.7
vuetify 1.5.14

[Vuetify] Multiple instances of Vue detected

How do I test a component currently if I use vuetify in almost every one of them? Do I have to shadowMount it in every test with 'localVue.use(vuetify)' when wanting to pass custom properties? That doesn't sound so good.

An update on this issue. Currently with how we must extend the Vue object in order to have proper typescript support, you will have to bootstrap Vue in your test setup file:

// tests/setup.js

import Vue from 'vue'
import Vuetify from 'vuetify'

Vue.use(Vuetify)

I have dropped the ball on providing clear documentation on how to do this and for that I apologize. This is something that I will be adding to the documentation in the coming weeks for the next version.

In regards to a long term solution, I'm accepting any and all suggestions on how to accommodate typescript support while also enabling the bootstrapping of localVue.

iwko commented

Any progress on this?

Thanks @johnleider it worked fine ๐Ÿ‘

An update on this issue. Currently with how we must extend the Vue object in order to have proper typescript support, you will have to bootstrap Vue in your test setup file:

// tests/setup.js

import Vue from 'vue'
import Vuetify from 'vuetify'

Vue.use(Vuetify)

I have dropped the ball on providing clear documentation on how to do this and for that I apologize. This is something that I will be adding to the documentation in the coming weeks for the next version.

In regards to a long term solution, I'm accepting any and all suggestions on how to accommodate typescript support while also enabling the bootstrapping of localVue.

A great "thank you" from my side too - @johnleider , it works for me too :) Any progress on delivering a solution in the new version?

This will be resolved as of our v3 upgrade to Vue3.

For Vue 2, you can put this code in the jest setup file:

[Vuetify] Multiple instances of Vue detected

// Add a decorator to the console.error function, to suppress displaying error messages that are useless.
const consoleErrorDecorator = function(func) {
const decorator = function(args) {
if (arguments.length === 1 && arguments[0] === "[Vuetify] Multiple instances of Vue detected\n" +
"See https://github.com/vuetifyjs/vuetify/issues/4068\n" +
"\n" +
"If you're seeing "$attrs is readonly", it's caused by this") {
return;
}
func.apply(console, [args].concat([].slice.call(arguments)));
}
return decorator;
}
console.error = consoleErrorDecorator(console.error);

An update on this issue. Currently with how we must extend the Vue object in order to have proper typescript support, you will have to bootstrap Vue in your test setup file:

// tests/setup.js

import Vue from 'vue'
import Vuetify from 'vuetify'

Vue.use(Vuetify)

I have dropped the ball on providing clear documentation on how to do this and for that I apologize. This is something that I will be adding to the documentation in the coming weeks for the next version.

In regards to a long term solution, I'm accepting any and all suggestions on how to accommodate typescript support while also enabling the bootstrapping of localVue.

Thanks @johnleider It worked.

I use following workaround:

describe('test', () => {
    let localVue = null;

    beforeEach(() => {
        localVue = require('vue');
        localVue.use(Vuex);
        localVue.use(Vuetify);
    });

    afterEach(() => {
        localVue = null;
    });

    it('hello world', () => {
        const wrapper = shallowMount(HelloWorld, {
            localVue,
            vuetify: new Vuetify({})
        });

        expect(wrapper.text()).toMatch('Hello World!');
    });
});

So I can use the global Vue instance but because I dynamically require it in beforeEach, I retrieve always a new one.

Nodejs caches modules, if you aren't clearing the module cache that's the same as just doing it once at the top of the file.

Yeah, bad first draft. If you use jest (vue test utils use it anyways) and based on this: https://stackoverflow.com/questions/60735880/reset-vue-for-every-jest-test. You can do this:

Utils:

export const addVuetify = context => {
  context.vuetify = require('vuetify');
  context.vue.use(context.vuetify);
  context.vuetifyInstance = new context.vuetify();
};

export const addVuex = context => {
  context.vuex = require('vuex');
  context.vue.use(context.vuex);
};

export const addI18n = options => {
  return context => {
    context.i18n = require('vue-i18n');
    context.vue.use(context.i18n);
    context.i18nInstance = new context.i18n(options);
  };
};

export const addFilter = (name, lambda) => {
  return context => context.vue.filter(name, lambda);
};

export const compositeConfiguration = (...configs) => {
  return context => configs.forEach(config => config(context));
};

export const bootstrapVueContext = configureContext => {
  const context = {};
  const teardownVueContext = () => {
    jest.unmock('vue');
    Object.keys(context).forEach(key => delete context[key]);
    jest.resetModules();
  };

  jest.isolateModules(() => {
    context.vueTestUtils = require('@vue/test-utils');
    context.vue = context.vueTestUtils.createLocalVue();

    jest.doMock('vue', () => context.vue);

    configureContext && configureContext(context);
  });

  return {
    teardownVueContext: teardownVueContext,
    ...context
  };
};

Usage:

describe('test', () => {
  let vueContext = null;

  beforeEach(() => {
    vueContext = bootstrapVueContext(
      compositeConfiguration(addVuex, addVuetify)
    );
  });

  afterEach(() => {
    vueContext.teardownVueContext();
  });

  it('hello world', () => {
    const wrapper = vueContext.vueTestUtils.shallowMount(HelloWorld, {
      localVue: vueContext.vue,
      vuetify: vueContext.vuetifyInstance
    });

    expect(wrapper.text()).toMatch('Hello World!');
  });
});

I already had this set in the test setup and it works fine for the current versions https://gitlab.com/gadelkareem/skeleton/-/blob/master/src/frontend/test/_setup.js#L6

import Vue from 'vue'
import Vuetify from 'vuetify'
import Vuex from 'vuex'
import VueRouter from 'vue-router'

Vue.use(Vuetify)

However, if I upgrade I get TypeError: Cannot read property 'component' of undefined errors
example failing test: https://gitlab.com/gadelkareem/skeleton/-/blob/master/src/frontend/src/components/base/__tests__/Alert.spec.js#L4

import { mount } from '@vue/test-utils'
import Alert from '../Alert'

describe('Alert.vue', () => {
  it('should have a custom error and match snapshot', () => {
    const w = mount(Alert, {
      propsData: {
        errors: [{ title: 'test error' }]
      }
    })

    expect(w.html()).toMatchSnapshot()

    const title = w.find('.v-alert__content > div')

    expect(title.text()).toBe('test error')
  })

  it('should have a custom success message', () => {
    const w = mount(Alert, {
      propsData: {
        successText: 'test success',
        success: true
      }
    })

    expect(w.find('.v-alert__content > div').text()).toBe('test success')
  })
})

The only way to fix it, is to reimport vuetify on the test as following:

import { mount } from '@vue/test-utils'
import Vuetify from 'vuetify'
import Alert from '../Alert'

const vuetify = new Vuetify()

describe('Alert.vue', () => {
  it('should have a custom error and match snapshot', () => {
    const w = mount(Alert, {
      propsData: {
        errors: [{ title: 'test error' }]
      },
      vuetify
    })

    expect(w.html()).toMatchSnapshot()

    const title = w.find('.v-alert__content > div')

    expect(title.text()).toBe('test error')
  })

  it('should have a custom success message', () => {
    const w = mount(Alert, {
      propsData: {
        successText: 'test success',
        success: true
      },
      vuetify
    })

    expect(w.find('.v-alert__content > div').text()).toBe('test success')
  })
})

Any idea where are these errors coming from?

Seems like upgrading Vuetify to 2.2.12 causes this problem