3

I'm trying to unit test a simple component that uses translations with the vue-i18n module. Here are the files used:

src/i18n/index.ts

import { createI18n } from 'vue-i18n';
    
export function loadLanguages() {
  const context = import.meta.globEager('./languages/*.ts');

  const languages: Record<string, any> = {};

  const langs = Object.keys(context);
  for (const key of langs) {
    if (key === './index.ts') return;
    const { lang } = context[key];
    const name = key.replace(/(\.\/languages\/|\.ts)/g, '');
    languages[name] = lang;
  }

  return languages;
}

export const i18n = createI18n({
  legacy: false,
  locale: 'es',
  fallbackLocale: 'es',
  messages: loadLanguages(),
  missingWarn: false,
  fallbackWarn: false,
});

export const i18nGlobal = i18n.global;

export function setLanguage(locale: string) {
  i18n.global.locale.value = locale;
}

src/i18n/hooks/helper.ts

import { useI18n } from 'vue-i18n';
import { watch } from 'vue';
import { useGetters } from '@store-common/hooks/helpers';

export const useI18nGlobal = () => useI18n({ useScope: 'global' });

export const useI18nLocal = () => {
  const { locale } = useI18nGlobal();

  const local = useI18n({
    locale: locale.value as string,
    inheritLocale: true,
    useScope: 'local',
  });

  const { getLocale } = useGetters();

  watch(getLocale, (loc: string) => {
    local.locale.value = loc;
  });

  return local;
};

src/components/Example.vue

<template>
  <div>
    {{ greeting }}
    {{ t('common.btn.send') }}
    {{ translate }}
  </div>
</template>

<script setup lang="ts">
import { useI18nLocal } from '@i18n/hooks/helper';

const { t } = useI18nLocal();

const greeting = 'Vue and TDD';
const translate = t('common.btn.send');
</script>

src/components/tests/Example.spec.ts

import { shallowMount } from '@vue/test-utils';
import { describe, expect, it } from 'vitest';
import Example from '../Example.vue';

describe('Example.vue', () => {
  it('Traducciones i18n', () => {
    const wrapper = shallowMount(Example);
    expect(wrapper.text()).toMatch('Vue and TDD');
    expect(wrapper.vm.translate).toMatch('Enviar');
  });
});

package.json

{
  "name": "PROJECT_NAME",
  "version": "1.0.0",
  "scripts": {
    ...
    "test:unit": "vitest --environment jsdom --dir src/ --coverage",
    ...
  },
}

When I launch the yarn test:unit command, declared in the package.json, the console gives me the following error:

cmd> yarn test:unit

yarn run v1.22.11
warning package.json: No license field
warning ..\..\package.json: No license field
$ vitest --environment jsdom --dir src/ --coverage Example

 DEV  v0.25.5 C:/Users/jgomezle/projects/HISVAR_FRONT
      Coverage enabled with c8

 ❯ src/shared/components/__tests__/Example.spec.ts (1)
   ❯ Example.vue (1)
     × Traducciones i18n

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
 FAIL  src/shared/components/__tests__/Example.spec.ts > Example.vue > Traducciones i18n
TypeError: $setup.t is not a function
 ❯ Proxy._sfc_render src/shared/components/Example.vue:20:207

 ❯ renderComponentRoot node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:891:44
 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5570:57
 ❯ ReactiveEffect.run node_modules/@vue/runtime-core/node_modules/@vue/reactivity/dist/reactivity.cjs.js:191:25
 ❯ instance.update node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5684:56
 ❯ setupRenderEffect node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5698:9
 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5480:9
 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5438:17
 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5042:21
 ❯ ReactiveEffect.componentUpdateFn [as fn] node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5577:21



⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
 Test Files  1 failed (1)
      Tests  1 failed (1)
   Start at  11:13:05
   Duration  3.60s (transform 1.27s, setup 1ms, collect 956ms, tests 29ms)

As we can see in the error TypeError: $setup.t is not a function it seems that it cannot find the t function of the i18n module to perform the translations.

I have tried mocking the t function in multiple ways when doing shallowMount, but none of them have worked for me and the error remains the same. These are all the ways I've tried:

const wrapper = shallowMount(Example, {
      mocks: {
        t: (str) => str,
        setup: {
          t: (str) => str,
        },
        $setup: {
          t: (str) => str,
        },
      },
      global: {
        mocks: {
          useI18n: {
            t: (msg) => msg,
            $t: (msg) => msg,
          },
          $setup: {
            t: (msg) => msg,
            $t: (msg) => msg,
          },
          setup: {
            t: (msg) => msg,
            $t: (msg) => msg,
          },
          t: (msg) => msg,
          $t: (msg) => msg,
        },
        // plugins: [i18n],
      },
    });

I have also tried these configurations, but the result is still the same:

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

config.global.mocks = {
  $t: (msg) => msg,
  t: (msg) => msg,
  $setup: {
    $t: (msg) => msg,
    t: (msg) => msg,
  },
  setup: {
    $t: (msg) => msg,
    t: (msg) => msg,
  },
};
AlbertGmzL
  • 31
  • 1
  • 3
  • I found a solution to mock *vue-i18n* and described it [here](https://stackoverflow.com/a/75485837/21234581). – Diogo Dias Feb 17 '23 at 15:53

3 Answers3

1

Lol, found your question when was looking for solution :)

Luckily, this stuff worked for me. Remembered i saw somebody with the same problem and found you again :)

The key concept you and I were missing is that when you mount component in tests, it has nothing in relation with your application. It's just an isolated, incapsulated piece of code. So, if you want to support translations (and not just mock them), you have to manually initialize i18n plugin on each test run (or globally for all tests, like in the answer in my link).

  • Hello @theillarionov! Thanks for your comment! I have tried to add this lines in the test to use i18n module as a plugins: `import { createI18n } from 'vue-i18n';` `const i18n = createI18n();` `config.global.plugins = [i18n];` However, I am getting the same error: `TypeError: $setup.t is not a function`. How did you do to initialize i18n module in your code? – AlbertGmzL Dec 20 '22 at 08:12
  • @AlbertGmzL exactly like in the link answer. Have you wrote "setupFiles" in vite? Have you restarted a server to pick new config? – theillarionov Dec 21 '22 at 09:05
  • Yes, of course. I have created the _vitest.setup.ts_ file and added `test: { environment: 'jsdom', setupFiles: 'vitest.setup.ts' }` to the _vite.config.ts_ file, but I still get the same error. I can't find what's wrong. – AlbertGmzL Dec 21 '22 at 10:28
  • @AlbertGmzL doesn't Vite complains on the setupFile location? In my case it did, so i had to rewrite path to: `setupFiles: fileURLToPath(new URL("./vitest.setup.ts", import.meta.url)),`. BTW, i also have `test: { globals: true }`, but that shouldn't be a matter (but maybe it is). – theillarionov Dec 22 '22 at 03:55
  • no, Vite doesn't complains on the setup file location. I have tried with fileURLToPath, and I have the same error: `TypeError: $setup.t is not a function` – AlbertGmzL Dec 22 '22 at 13:09
  • @AlbertGmzL i'm afraid i can't help then ( have you tried with globals: true? – theillarionov Dec 25 '22 at 14:38
  • yes, i have tried – AlbertGmzL Dec 27 '22 at 08:20
  • exact same issue here. did you find a solution @AlbertGmzL – noel293 Feb 15 '23 at 08:36
  • No sorry, I haven't found any solution yet @theillarionov . I think I have a peculiar i18n config, try with Diogo Dias comment, maybe it will work for you – AlbertGmzL Feb 23 '23 at 07:54
1

This error is because you are calling t() in your template. I don't know why, but... You can avoid this error calling t() method inside a computed property and use this computed in your template. Ex:

// in your <script setup>
const sendText = computed(() => t('common.btn.send'));

// in your <template>
{{ sendText }}

If anyone finds out why, let me know! but for now I'm doing it this way.

Yoji Kojio
  • 11
  • 3
  • Also bumped in that problem after upgrading to Vue 3.3, in my case while running `storybook` with `vite`. It works properly while running just `vite` though – Jojko May 18 '23 at 09:39
0

Stackoverflow posted my answer as comment because it was too simple. So, let's try again...

I found a solution to mock vue-i18n and described it here.

In short, I did it in my test:

import { useI18n } from "vue-i18n";
// ... other imports

vi.mock("vue-i18n");

useI18n.mockReturnValue({
  t: (tKey) => tKey,
});

config.global.mocks = {
  $t: (tKey) => tKey,
};

// tests

Someday I will try to do it globally instead of on each test file.

Diogo Dias
  • 69
  • 3
  • 1
    Thanks for your comment @diogo-dias :D. I have tried with your code, but I have some errors in the i18n config file when I run a test. I have a i18n config file like this: `export const i18n = createI18n({` `legacy: false,` `locale: 'es',` `fallbackLocale: 'es',` `messages: loadLanguages(),` `missingWarn: false,` `fallbackWarn: false,` `});` `export const i18nGlobal = i18n.global;` And I have next error: `TypeError: Cannot read properties of undefined (reading 'global')` `export const i18nGlobal = i18n.global;` I dont know if my i18n module is well configured – AlbertGmzL Feb 23 '23 at 07:47
  • =] @AlbertGmzL maybe you need to mock `createI18n`. Other alternative is to remove the `const i18nGlobal`. But I'm just guessing, your configuration is way more "sophisticated" than mine and I can't try this right now and I'm not working on my vue app. – Diogo Dias Mar 29 '23 at 12:20