Skip to content

this.$t is not a function in a class component #505

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

Open
Mikilll94 opened this issue Jan 13, 2021 · 13 comments
Open

this.$t is not a function in a class component #505

Mikilll94 opened this issue Jan 13, 2021 · 13 comments

Comments

@Mikilll94
Copy link

Subject of the issue

I have the following component

<template>
  <div>{{someData}}</div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class App extends Vue {
  private someData = this.$t("message");
}
</script>

and the following unit test

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

describe('App.vue', () => {
  it('some unit test', () => {
    const wrapper = mount(App, {
      mocks: {
        $t: (key: string) => key
      }
    });
    expect(wrapper).toBeDefined();
  })
})

When running this unit test, it fails with the following error:

 FAIL  tests/unit/example.spec.ts
  HelloWorld.vue
    ✕ some unit test (22ms)

  ● HelloWorld.vue › some unit test

    TypeError: this.$t is not a function

       8 | @Component
       9 | export default class App extends Vue {
    > 10 |   private someData = this.$t("message");
         | ^
      11 | }
      12 | </script>
      13 | 

      at new App (src/App.vue:10:1)
      at collectDataFromConstructor (node_modules/vue-class-component/dist/vue-class-component.common.js:165:14)
      at VueComponent.data (node_modules/vue-class-component/dist/vue-class-component.common.js:227:14)
      at getData (node_modules/vue/dist/vue.runtime.common.dev.js:4732:17)
      at initData (node_modules/vue/dist/vue.runtime.common.dev.js:4689:7)
      at initState (node_modules/vue/dist/vue.runtime.common.dev.js:4628:5)
      at VueComponent.Vue._init (node_modules/vue/dist/vue.runtime.common.dev.js:4987:5)
      at new VueComponent (node_modules/vue/dist/vue.runtime.common.dev.js:5134:12)
      at createComponentInstanceForVnode (node_modules/vue/dist/vue.runtime.common.dev.js:3277:10)
      at init (node_modules/vue/dist/vue.runtime.common.dev.js:3108:45)
      at createComponent (node_modules/vue/dist/vue.runtime.common.dev.js:5958:9)
      at createElm (node_modules/vue/dist/vue.runtime.common.dev.js:5905:9)
      at VueComponent.patch [as __patch__] (node_modules/vue/dist/vue.runtime.common.dev.js:6455:7)
      at VueComponent.Vue._update (node_modules/vue/dist/vue.runtime.common.dev.js:3933:19)
      at VueComponent.updateComponent (node_modules/vue/dist/vue.runtime.common.dev.js:4054:10)
      at Watcher.get (node_modules/vue/dist/vue.runtime.common.dev.js:4465:25)
      at new Watcher (node_modules/vue/dist/vue.runtime.common.dev.js:4454:12)
      at mountComponent (node_modules/vue/dist/vue.runtime.common.dev.js:4061:3)
      at VueComponent.Object.<anonymous>.Vue.$mount (node_modules/vue/dist/vue.runtime.common.dev.js:8392:10)
      at mount (node_modules/@vue/test-utils/dist/vue-test-utils.js:13991:21)
      at Object.<anonymous> (tests/unit/example.spec.ts:6:21)

Steps to reproduce

Just run unit tests in this project:
i18n-test.zip

Expected behaviour

Error this.$t is not a function should not occur. The $t function is mocked.

Actual behaviour

An error this.$t is not a function is thrown.

Additional context

If I convert someData to computed property, like this:

<template>
  <div>{{someData}}</div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class App extends Vue {
  private get someData() {
    return this.$t("message");
  }
</script>

the error is gone.

@Mikilll94
Copy link
Author

@kaorun343
vuejs/vue-test-utils#1767 (comment)

As you can see this is rather a bug in vue-class-components than in vue-test-utils.

@lmiller1990
Copy link
Member

Does this work in a browser with the real $t? The problem is only when mocking $t?

@Mikilll94
Copy link
Author

@lmiller1990
In browser it is working. You can check this in my repro project. Only unit tests are failing.

@lmiller1990
Copy link
Member

I am guessing VCC does some transform first, then VTU applies the mock $t, at which point it is too late.

I would recommend using data or computed as a work-around in the meantime.

Are you interested in investigating this bug/making a PR?

@Mikilll94
Copy link
Author

@lmiller1990
In the meantime I was debugging this for a while. It seems that vue-class-component overrides the class constructor and transforms property initializers into data in Vue component. This is happening in this function:

export function collectDataFromConstructor (vm: Vue, Component: VueClass<Vue>) {

If you debug vm variable you can see that when running unit tests, it is missing _i18n property. In the browser this property is present.

BTW. accessing this stuff in property initializers is a valid functionality of VCC. You check this here:
#432
#434

@lmiller1990
Copy link
Member

lmiller1990 commented Jan 22, 2021

Interesting. I suppose this makes sense. Class Component -> VCC transform -> VTU $mocks. By the time $mocks happens, it is too late and the component is using whatever this.$i18n was at the time of the transform. It expects $t to exist when VCC does it's transform, since it doesn't, boom.

I do not see any way we can fix this in VTU - whatever we do, it's going to be too late to get the mocked value in. Seems VTU v2 and the next version of class component exhibit the same bug for the same reason.

An alternative would be using a createLocalVue, where you install a mocked $i18n. A bit more boilerplate, but you could make a function. Does that make sense?

@Mikilll94
Copy link
Author

@lmiller1990

Interesting. I suppose this makes sense. Class Component -> VCC transform -> VTU $mocks. By the time $mocks happens, it is too late and the component is using whatever this.$i18n was at the time of the transform. It expects $t to exist when VCC does it's transform, since it doesn't, boom.

When the mocking exactly happens in vue-test-utils? (you can show this in the source code) Does mocking occur after creating a component? If yes, then this is problematic for class components, because this Component.prototype._init function is executed before creating a component. This function transforms the property initializers to the data object.

Could you check the VCC source code to see if there is any solution to this? Maybe this can be fixed in VCC source code?

An alternative would be using a createLocalVue, where you install a mocked $i18n. A bit more boilerplate, but you could make a function. Does that make sense?

Could you show an example?

@lmiller1990
Copy link
Member

lmiller1990 commented Jan 24, 2021

The mocking happens here. createInstance is ultimately called when mount is called. The class component transform is applied far before this - it's all compiled to a regular object before it's even passed into mount.

I don't see a way to fix this by patching VCC.

Could you show an example

Looks like my idea doesn't work. I thought I could do something like this:

class MockPlugin {
  static install(Vue: any) {
    Vue.$t = (key: string) => key
  }
}

const localVue = createLocalVue()
localVue.use(MockPlugin)

describe('App.vue', () => {
  it('some unit test', () => {
    const wrapper = mount(App, {
      localVue
    });
    expect(wrapper).toBeDefined();
  })
})

But the result is the same. Hm. I think something else is at work here... I cannot even get this to work with the real i18n plugin.

@lmiller1990
Copy link
Member

lmiller1990 commented Jan 24, 2021

Ok... this worked (webpack + mocha combo).

import { expect } from 'chai'
import Vue from 'vue'
import { mount, createLocalVue } from '@vue/test-utils'
import App from '@/App.vue'

class i18n {
  static install(Vue: any) {
    Vue.prototype.$t = (key: string) => key
  }
}

Vue.use(i18n)

describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    const wrapper = mount(App)
    console.log(wrapper.html())
  })
})

But this does not:

import { expect } from 'chai'
import Vue from 'vue'
import { mount, createLocalVue } from '@vue/test-utils'
import App from '@/App.vue'

class i18n {
  static install(Vue: any) {
    Vue.prototype.$t = (key: string) => key
  }
}

const localVue = createLocalVue()
localVue.use(i18n)

describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    const wrapper = mount(App, {
      localVue
    })
    console.log(wrapper.html())
  })
})

I guess when VCC does it's thing, Vue already has $t. It does not work with localVue - VCC does not know about localVue. I don't see any way to "tell" VCC which Vue instance to use, so this is likely out of the question.

If you dont't mind using a mutated global Vue for all your tests, you could do something like I've shown here (making a "fake" i18n plugin).

I am still not sure on how we can make mocks work with VCC. Let's experiment a bit more.

@ktsn
Copy link
Member

ktsn commented Jan 24, 2021

A repro excluded the dep to vue-test-utils would be:

    @Component
    class Base extends Vue {
      test() {
        return 'base'
      }
    }

    @Component
    class Test extends Base {
      result = this.test()
    }

    const vm = new Test()
    expect(vm.result).to.equal('base')

    const Replaced = Vue.extend()
    Replaced.prototype.test = () => 'replaced'

    const replaced: Test = new (Replaced.extend({
      ...(Test as any).options,
    }))()
    expect(replaced.result).to.equal('replaced')

We could replace the prototype of the original constructor with the one of localVue in collectDataFromConstructor but I'm not very sure about it and if it is really a good idea. I'll need more time to take a look...

@lmiller1990
Copy link
Member

lmiller1990 commented Jan 25, 2021

I thought about this too, but doesn't feel like a great solution.

We should also think about how this works in vue-class-component-next and vue-test-utils-next. vue-test-utils-next has the exact same problem... but does not use the localVue. We should think of a solution that will work there, too.

@ygj6
Copy link
Member

ygj6 commented Jan 25, 2021

hope it helps you.

@Component
export default class App extends Vue {
  private someData();
  created() {
     this.someData = this.$t("message");
   }
}
 

@Mikilll94
Copy link
Author

@ktsn
Please take a look at these comments:
vuejs/vue-test-utils#1767 (comment)
vuejs/vue-test-utils#1767 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants