kazupon / vue-i18n

:globe_with_meridians: Internationalization plugin for Vue.js
https://kazupon.github.io/vue-i18n/
MIT License
7.28k stars 861 forks source link

How to stub vue-i18n within vue-test-utils #323

Closed Mrkisha closed 6 years ago

Mrkisha commented 6 years ago

I'm trying to write a test for page that uses vue-i18n plugin. @eddyerburgh has show how to test with mocked plugin. https://github.com/kazupon/vue-i18n/issues/198#issuecomment-316692326 This works fine.

However in some tests I do not care about translations. Instead mocking translations, I'd like to stub them. What ever I do i get [Vue warn]: Error in render: "TypeError: _vm.$t is not a function".

lmiller1990 commented 6 years ago

Hi @Mrkisha , I work on test utils a lot, and use i18n in my apps. You can just do

const wrapper = shallow(MyComponent, {
  mocks: {
    $t: () => {}
  }
})

and you should be fine, no warning.

If you want something to show you can do

const wrapper = shallow(MyComponent, {
  mocks: {
    $t: () => 'some specific text'
  }
})
Mrkisha commented 6 years ago

@lmiller1990 Thank you. It works.

lmiller1990 commented 6 years ago

@Mrkisha PS we added mocks to the global config recently in @vue/test-utils, which I assume you are using. So if you install the latest @vue/test-utils, you can do:

import VueTestUtils from '@vue/test-utils'

VueTestUtils.config["$t"] = () => {}

If you are using Jest, you would do this is setupFiles.

See here for the config options: https://vue-test-utils.vuejs.org/en/api/config.html

VictorCazanave commented 6 years ago

@lmiller1990 Thanks for the new tip! However I think you made a typo in your example, it should be:

VueTestUtils.config.mocks["$t"] = () => {}

Sometimes it's useful to check which wording is displayed (like for an error message), so in my projects I use:

VueTestUtils.config.mocks.$t = key => key
Laruxo commented 6 years ago

Thank you all. Additional note for those that use v-t directive. You can stub it like this:

import Vue from 'vue';
Vue.directive('t', () => {});
cesalberca commented 6 years ago

This works as long as it's shallow. If you mount the component and any subcomponent uses translations the tests will fail, even if you have a global mock. I wish there was something we could do to solve this issue, because we've had a lot of problems with [Vue warn]: Error in render: "TypeError: _vm.$t is not a function".

lmiller1990 commented 6 years ago

@Laruxo careful doing that. You will avoid polluting the global Vue instance. Consider using localVue:

import { createLocalVue } from "@vue/test-utils"

const localVue = createLocalVue()
localVue.directive("t", () => {})

const wrapper = shallowMount(Foo, {
  localVue
})

@cesalberca can you provide a minimal repro of the bug you described? If not I can try to do and post an issue in vue-test-utils. I contribute tovue-test-utils` and was you described sounds like a bug we should fix. Some problems related to child-child components will be fixed here https://github.com/vuejs/vue-test-utils/pull/840 , not sure if that will ficu your problem though.

cesalberca commented 6 years ago

@lmiller1990, ok, upon further investigation it seems this gets solved in 1.0.0-beta.21. However a new message appears in console:

[vue-test-utils]: an extended child component <SubComponent> has been modified to ensure it has the correct instance properties. This means it is not possible to find the component with a component selector. To find the component, you must stub it manually using the stubs mounting option.

This is the repo: https://github.com/cesalberca/i18n-typescript-tests-bug

Try updating @vue/test-utils from 1.0.0-beta.20 to 1.0.0-beta.21 to see the test pass and the message appear. So glad that at leas the error is solved, because it was a truly a pain to deal with. The only thing left to do is to know how to deal with that message.

eddyerburgh commented 6 years ago

The message is to warn you that you can't access the component by using the constructor as a selector. You can hide it by setting logModifiedComponents to false:

import { config } from '@vue/test-utils'

config.logModifiedComponents = false

We'll improve the message to make it clear what is happening.

The support for extended components (like TS components) is going to be improved in beta.23.

cesalberca commented 6 years ago

So glad to hear that @eddyerburgh! I just noticed that there is no export default anymore, which makes TS fail when importing from @vue/test-utils:

image

P.R incoming ;)

ffxsam commented 5 years ago

What about this?

import { shallowMount, createLocalVue } from '@vue/test-utils';
import Buefy from 'buefy';
import TheLogin from '@/components/TheLogin.vue';
import { setupI18n } from './i18n';

const localVue = createLocalVue();

localVue.use(Buefy);
const i18n = setupI18n(localVue);

describe('TheLogin.vue', () => {
  const wrapper = shallowMount(TheLogin, {
    localVue,
    i18n,
  });

  test('login button is initially disabled', () => {
    const loginButton = wrapper.find('.login-button');

    expect(loginButton.attributes().disabled).toBe('disabled');
  });
});

And the i18n.ts file:

import { VueConstructor } from 'vue';
import VueI18n, { LocaleMessages } from 'vue-i18n';

function loadLocaleMessages(): LocaleMessages {
  const messages = require('@/locales/en.json');

  return {
    en: messages,
  };
}

export function setupI18n(vueInstance: VueConstructor) {
  vueInstance.use(VueI18n);

  return new VueI18n({
    locale: process.env.VUE_APP_I18N_LOCALE || 'en',
    fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
    messages: loadLocaleMessages(),
  });
}

Otherwise, I'm totally stumped on this one. This seems to work, though. @eddyerburgh - thoughts?

lmiller1990 commented 5 years ago

@ffxsam what exactly is the problem? Do you get a warning/error?

ffxsam commented 5 years ago

No problem, I'm just suggesting a solution. (the above works great for me)

0xMMelvin commented 5 years ago
import { config, shallowMount } from '@vue/test-utils'
import BaseDialog from '@/components/BaseDialog/BaseDialog'

config.mocks.$t = key => key

describe('BaseDialog', () => {
  it('is called', () => {
    const wrapper = shallowMount(BaseDialog, {
      attachToDocument: true
    })
    expect(wrapper.name()).toBe('BaseDialog')
    expect(wrapper.isVueInstance()).toBeTruthy()
  })
})

And I get:

TypeError: Cannot read property '$t' of undefined

    at VueComponent.default (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/src/components/BaseDialog/BaseDialog.vue:2671:220)
    at getPropDefaultValue (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:1662:11)
    at validateProp (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:1619:13)
    at loop (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4612:17)
    at initProps (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4643:33)
    at initState (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4586:21)
    at VueComponent.Vue._init (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4948:5)
    at new VueComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:5095:12)
    at createComponentInstanceForVnode (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:3270:10)
    at init (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:3101:45)
    at createComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:5919:9)
    at createElm (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:5866:9)
    at VueComponent.patch [as __patch__] (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:6455:9)
    at VueComponent.Vue._update (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:3904:19)
    at VueComponent.updateComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4025:10)
    at Watcher.get (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4426:25)
    at new Watcher (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4415:12)
    at mountComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4032:3)
    at VueComponent.Object.<anonymous>.Vue.$mount (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:8350:10)
    at mount (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/@vue/test-utils/dist/vue-test-utils.js:8649:21)
    at shallowMount (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/@vue/test-utils/dist/vue-test-utils.js:8677:10)
    at Object.it (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/src/components/BaseDialog/__tests__/BaseDialog.spec.js:8:21)
    at Object.asyncJestTest (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/jasmine_async.js:108:37)
    at resolve (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/queue_runner.js:56:12)
    at new Promise (<anonymous>)
    at mapper (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/queue_runner.js:43:19)
    at promise.then (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/queue_runner.js:87:41)
    at process.internalTickCallback (internal/process/next_tick.js:77:7)

I've tried A LOT of stuff on the webs to get this to work and no matter what I get the same error.

lmiller1990 commented 5 years ago

@mmelvin0581 can you share BaseDialog?

What happens if you do

shallowMount(BaseDialog, {
  mocks: {
    $t: key => key
  }
})
0xMMelvin commented 5 years ago

Sorry for the alarm guys. Stupid mistake on my part. I was referencing ‘this’ inside of a prop validator...

Illyism commented 5 years ago

This is how I've solved it.

In a test file

import { mount } from '@vue/test-utils'
// Can be changed to
import { mount } from '../myVueTestUtils.js'

// And you can use mount or shallowMount as usual
const wrapper = mount(DocumentEditModal, { propsData: { document_id: '123' } })

In myVueTestUtils.js

import { mount as originalMount } from '@vue/test-utils'
import { i18n } from 'utils/i18n'

// wrap the @vue/test-utils mount method and extend it with i18n
export function mount(component, opts = {}) {
    return originalMount(component, { ...opts, i18n })
}

Then you can use your i18n as usual without mocking or doing a complicated localVue setup, or adding to the shallow/mount functions every time in every test file.

I've created a repository explaining it here: https://github.com/Illyism/vue-i18n-jest-example

DktRemiDoolaeghe commented 5 years ago

@Illyism What is this import you propose?

import { i18n } from 'utils/i18n'

The solution you propose looks great, but I can't make this import work.

lk77 commented 4 years ago

Hello,

im my case i do want to have real translations, so i'm importing my getI18n function (the same function that the real app is calling) :

[...]
export default function () {
    Vue.use(VueI18n);
    const i18n = new VueI18n({
        locale: defaultLocale,
        fallbackLocale: 'en',
        messages: messages
    });

    return i18n;
};

and then :

import {shallowMount, config} from '@vue/test-utils';
import getI18n from 'src/utils/get-i18n';

const i18n = getI18n();

config.mocks['i18n'] = i18n;

config.mocks["$t"] = (key: string) => {
  return i18n.t(key);
}

and then i'm using shallowMount to mount my component. You can also instantiate i18n inside your test direclty if you do not want to have a function.

Jeerjmin commented 4 years ago

Same error TypeError: _vm.$t is not a function Error from nested child of mount component Tried every solution in this topic

Illyism commented 4 years ago

@Jeerjmin @DktRemiDoolaeghe

Take a look here, I've created a simple boilerplate project to explain how it works: https://github.com/Illyism/vue-i18n-jest-example

You can see the unit test runs here: https://github.com/Illyism/vue-i18n-jest-example/runs/441929401#step:4:9

Baldrani commented 4 years ago

@Illyism I've got a quick glance of what you've done and decided to adapt it to my project. Definitely a good idea 👍

matthiassommer commented 4 years ago

Hello

I am using this.$t(...) in a sub-component AppModalDialog in the created() method.

console.error node_modules/vue/dist/vue.runtime.common.dev.js:621
[Vue warn]: Error in created hook: "TypeError: this.$t is not a function"
created() {
    const label = this.$t('app-modal.button').toString();
}

I am already mocking $t it with

const wrapper = shallowMount(UpperComp, {
    localVue,
    $t: () => 'Some label',
}

in UpperComp.

Also, I am wondering why it is necessary alltogether as i use shallowMount here. Can it be related to using the modal component lazy-loaded with AppModalDialog: () => import('@/components/shared/AppModalDialog.vue')?

Illyism commented 4 years ago

@matthiassommer It sounds like in the AppModalDialog you're using your own instance of Vue (for example Vue.extend()) that is causing that error and it's causing the modal to be mounted anyway. You could try mocking the AppModalDialog in your code to see what happens.

It depends a lot on your implementation. If you could share you code or make a minimal version somehow I could take a look.

matthiassommer commented 4 years ago

I am instantiating the component with

export default Vue.extend({
  name: 'app-modal-dialog',

Is there any benefit or need to use Vue.extend instead of just export default {} ?

I am mocking it with jest.mock('@/components/shared/AppModalDialog.vue', () => ({ name: 'app-modal-dialog' })); But now I get

console.error node_modules/vue/dist/vue.runtime.common.dev.js:621
      [Vue warn]: Failed to mount component: template or render function not defined.
      found in
      ---> <AppModalDialog>
jsborjesson commented 4 years ago

@matthiassommer I'm having the same issue _vm.$t is used in templates and mocked correctly by passing in mocks as mentioned above, but I'm also using this.$t in the code which is not solved by this. Did you find an elegant solution?

JRINC commented 3 years ago

I am testing a script which is a mixin. So this is what worked for me:

import myMixin from "../../../src/mixins/myMixin";
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import { ENGLISH_TRANSLATIONS } from '../../../src/translations/en';

Vue.use(VueI18n);
const TRANSLATIONS = {
  en: ENGLISH_TRANSLATIONS
};
const i18n = new VueI18n({
  locale: 'en',
  messages: TRANSLATIONS,
});

in the it scope:

it("testing stuff", () => {
      // arrange
      const wrapper = {
        $t: jest.fn((key) => i18n.t(key)),
      };
}
soboltatiana commented 3 years ago

@matthiassommer @jsborjesson How did you solve the problem with mocking the this.$t please? It would be very helpful if you shared.

jsborjesson commented 3 years ago

@tasha13 I should have not forgotten to reply that here last year, now I can't find it. I may go digging through commits but I do believe I solved it by passing in the real translations to Vue and not mocking that part, similar to above

MobideoNetser commented 3 years ago

Here is my solution (I used mocha instead of jest but it is not really matter): use global.mock.$t and create your own implementation

import { expect } from 'chai' //mocha.js
import { mount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'

const $t = () => {}

describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {

    const msg = 'new message'
    const wrapper = mount(HelloWorld, {
      props: { msg },
global: {
        mocks: {
          $t
        }
      }
    })
    expect(wrapper.text()).to.include(msg)
  })
})
art-kc commented 3 years ago

@soboltatiana @matthiassommer @jsborjesson Anyone solved issue with nested components and $t? I'm using Vue.extend({}) with nested component and the same error this.$t is undefined.

themeler commented 2 years ago

This problem made me pull out my hair far too many times. I couldn't test functionality without mounting few components at once, and I was left with testing each component separately. That can be really brittle and hard to maintain.

The problem is related to another issue I had a long time ago with unwanted console logs in tests.

Components that use Vue.extend() are importing Vue from the source. we are using localVue, but it seems to be used only for component that you are shallow mounting in test, every child component is still using Vue directly. This causes various issues, but it's 'hackable'. Maybe this is not a real solution, rather a hack, but it works. It requires a global Vue mock for Jest.

I wrote detailed explanation here, and it's not only about VueI18n, but any plugin that is used by child components that we want to mount in our tests.

JohnPAfr commented 2 years ago

Hello, I was wondering how do you handle component ?

Because when I run my tests i get : [Vue warn]: Failed to resolve component: i18n-t, If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.

luckylooke commented 2 years ago

Hi, my solution in actual Vue3/Vite/Vitest environment:

// vitest.config.ts
import { mergeConfig } from 'vite';
import { defineConfig } from 'vitest/config';
import viteConfig from './vite.config';

export default defineConfig(
    mergeConfig(viteConfig, { // extending app vite config
        test: {
            setupFiles: ['tests/unit.setup.ts'],
            environment: 'jsdom',
        }
    })
);
// tests/unit.setup.ts
import { config } from "@vue/test-utils"

config.global.mocks = {
  $t: tKey => tKey; // just return translation key
};
themeler commented 2 years ago

Hello, I was wondering how do you handle component ?

Because when I run my tests i get : [Vue warn]: Failed to resolve component: i18n-t, If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.

Never used VueI18n component, but in this case, the only option I see is to create a stub for it and register it in the same place where VueI18nStub is (from example provided in my previous comment).

dafcoe commented 2 years ago

Hi everyone, I faced this issue today and spent a lot of time on time trying all the solutions mentioned here. Unfortunately none worked :-( so I kept "digging" and managed to made it work by setting $t on beforeCreate hook:

   wrapper = shallowMount(MyComponent, {
      propsData: {
        ...
      },
      ...
      beforeCreate() {
        this.$t = jest.fn((key: string) => key);
      },
    });

Hope this is useful.

mj-watts commented 7 months ago

For Vue3 and Vite I got this working across all unit tests for anything using $t('whatever'):

// vitest.setup.ts
import { vi } from 'vitest'
import { config } from '@vue/test-utils'

config.global.mocks = {
  $t: vi.fn((key: string) => key)
}
// vite.config.ts
export default defineConfig(() => {
  return {
    //...etc
    test: {
      setupFiles: ['./vitest.setup.ts'],
    }
  }
})

Also if you're using the <i18n-t> component you can add the following to the vitest.setup.ts file:

import i18n from './src/i18n'

config.global.plugins = [i18n]

Which gets the components outputting what they're supposed to in the unit tests.

Hope it helps.