vuetifyjs / vuetify

🐉 Vue Component Framework
https://vuetifyjs.com
MIT License
39.9k stars 6.97k forks source link

[Feature Request] Avoid importing globally Vuetify in all tests #4964

Closed trollepierre closed 3 years ago

trollepierre commented 6 years ago

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:

johnleider commented 6 years ago

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

AtofStryker commented 6 years ago

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.

johnleider commented 6 years ago

Please create a separate issue @AtofStryker

AtofStryker commented 6 years ago

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

KaelWD commented 5 years ago

I expect Vuetify not to use its own instance of Vue

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

m4ss1m0g commented 5 years ago

Vuetify 1.5.0 Vue 2.6.2

It is still an issue

stas-kh commented 5 years ago

This is super confusing, guys. Any updates here?

boutils commented 5 years ago

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?

eddie-craftsmanshipcounts commented 5 years ago

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!

m4ss1m0g commented 5 years ago

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

yob-yob commented 5 years ago

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.

Mei152 commented 5 years ago

Having same issue in rails vue 2.6.7 vuetify 1.5.14

[Vuetify] Multiple instances of Vue detected

slushnys commented 5 years ago

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.

johnleider commented 5 years ago

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 5 years ago

Any progress on this?

JohanSantamaria17 commented 4 years ago

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.

WaldemarEnns commented 4 years ago

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

johnleider commented 4 years ago

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

kimtuck commented 4 years ago

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);

jatin-maropost commented 3 years ago

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.

bennysalewski commented 3 years ago

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.

KaelWD commented 3 years ago

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.

bennysalewski commented 3 years ago

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!');
  });
});
gadelkareem commented 3 years ago

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?

gadelkareem commented 3 years ago

Seems like upgrading Vuetify to 2.2.12 causes this problem

KaelWD commented 3 years ago

Our testing documentation now shows this: https://vuetifyjs.com/en/getting-started/unit-testing/#spec-tests