devtime
devtime

Reputation: 93

Vue3 Testing Library - vue-i18n not loading text

I can't seem to get the following example to work with vue3 and testing library. https://github.com/testing-library/vue-testing-library/blob/main/src/tests/translations-vue-i18n.js

I've even tried to modify the example like so to get $t to be recognized by injecting messages into a mock but no luck.

Does anyone have an example that works with vue 3?

Here are the details ...

Translations.spec.js

import '@testing-library/jest-dom'
import {render, fireEvent} from '@testing-library/vue'
import Vuei18n from 'vue-i18n'
import Translations from '@/components/Translations'

const messages = {
  en: {
    Hello: 'Hello!',
    message: {
        hello: 'Hello!'
    }
  },
  ja: {
    Hello: 'こんにちは',
    message: {
        hello: 'こんにちは'
    }
  },
}

test('renders translations', async () => {
  const {queryByText, getByText} = render(Translations, {
    global: {
      mocks: {
        $t: (messages) => messages
      }
    }
  }, vue => {
    // Let's register and configure Vuei18n normally
    vue.use(Vuei18n)

    const i18n = new Vuei18n({
      locale: 'ja',
      fallbackLocale: 'ja',
      messages,
    })

    // Notice how we return an object from the callback function. It will be
    // merged as an additional option on the created Vue instance.
    return {
      i18n,
    }
  })

  //expect(getByText('Hello!')).toBeInTheDocument()

  //await fireEvent.click(getByText('Japanese'))

  expect(getByText('こんにちは')).toBeInTheDocument()

  //expect(queryByText('Hello!')).not.toBeInTheDocument()
})

Translations.vue

<template>
  <div>
    <h2>{{ $t("Hello") }}</h2>
    <h2>{{ $t("message.hello") }}</h2>
    <button @click="switchLocale('en')">English</button>
    <button @click="switchLocale('ja')">Japanese</button>
  </div>
</template>

<script>
export default {
  name: 'Translations',

  methods: {
    switchLocale(locale) {
      this.$i18n.locale = locale
    },
  },
}
</script>

package.json

{
  "name": "mc",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test:unit": "vue-cli-service test:unit"
  },
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.35",
    "@fortawesome/free-solid-svg-icons": "^5.15.3",
    "@fortawesome/vue-fontawesome": "^3.0.0-4",
    "@popperjs/core": "^2.9.2",
    "bootstrap": "^5.0.2",
    "core-js": "^3.6.5",
    "es6-promise": "^4.2.8",
    "vue": "^3.1.4",
    "vue-hotjar": "^1.4.0",
    "vue-i18n": "^9.1.6",
    "vue-loader": "^16.2.0",
    "vue-router": "^4.0.10",
    "vuex": "^4.0.2"
  },
  "devDependencies": {
    "@babel/core": "^7.14.8",
    "@babel/preset-env": "^7.14.8",
    "@testing-library/jest-dom": "^5.14.1",
    "@testing-library/vue": "^6.4.2",
    "@vue/cli-plugin-babel": "^4.5.13",
    "@vue/cli-plugin-eslint": "^4.5.13",
    "@vue/cli-plugin-router": "^4.5.13",
    "@vue/cli-plugin-unit-jest": "^4.5.13",
    "@vue/cli-plugin-vuex": "^4.5.13",
    "@vue/cli-service": "^4.5.13",
    "@vue/compiler-sfc": "^3.1.4",
    "@vue/eslint-config-prettier": "^6.0.0",
    "@vue/test-utils": "^2.0.0-rc.9",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-prettier": "^3.4.0",
    "eslint-plugin-vue": "^7.0.0",
    "flush-promises": "^1.0.2",
    "prettier": "^2.3.2",
    "typescript": "^4.3.5",
    "vue-jest": "^5.0.0-alpha.10"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/vue3-essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

Error

FAIL  tests/unit/Translations.spec.js        
  ● renders translations

    TestingLibraryElementError: Unable to find an element with the text: こんにちは. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

    <body>
      <div>
        <div>
          <h2>
            Hello
          </h2>
          <h2>
            message.hello
          </h2>
          <button>
            English
          </button>
          <button>
            Japanese
          </button>
        </div>
      </div>
    </body>

      47 |   //await fireEvent.click(getByText('Japanese'))
      48 |
    > 49 |   expect(getByText('こんにちは')).toBeInTheDocument()
         |          ^
      50 |
      51 |   //expect(queryByText('Hello!')).not.toBeInTheDocument()
      52 | })

Upvotes: 2

Views: 3935

Answers (2)

Robert Wloch
Robert Wloch

Reputation: 3

This answer is for everyone stumbling across that question when using Composition API where there's no global $t to mock.

I've solved it by exporting a function createConfiguredI18n in src/plugins/i18n.ts:

import { createI18n, I18nOptions } from 'vue-i18n'

import deDE from '@/locales/de-DE.json'
import enUS from '@/locales/en-US.json'

// Type-define 'de-DE' as the master schema for the resource
type MessageSchema = typeof deDE

export function createConfiguredI18n(locale: string, fallbackLocale: string) {
  return createI18n<I18nOptions, [MessageSchema], 'de-DE' | 'en-US'>({
    locale: locale || 'en-US',
    fallbackLocale: fallbackLocale || 'en-US',
    messages: {
      'de-DE': deDE,
      'en-US': enUS,
    },
  })
}

export const i18n = createConfiguredI18n('de-DE', 'en-US')

Then in the unit test you can do the following to initialize vue-i18n with your translations:

import {flushPromises, mount, VueWrapper} from '@vue/test-utils'
import {nextTick} from 'vue'
import {createConfiguredI18n} from '@/plugins/i18n'
...

describe('SubjectUnderTest', () => {
  it('should display translation "FooBar"', async () => {
    const locale = 'de-DE'
    const fallbackLocale = 'en-US'
    const wrapper = await createWrapper({locale, fallbackLocale})
    ...
  }

  async function createWrapper(options: {
    locale: string
    fallbackLocale: string
  }): Promise<VueWrapper> {
    const i18n = createConfiguredI18n(options.locale, options.fallbackLocale)

    const wrapper = mount(sut, {
      global: {
        plugins: [i18n],
      },
    })

    await nextTick()
    await flushPromises()

    return wrapper
  }
}

If you don't want the translations but instead mock them and check for the keys only, you can do the following in your unit test instead:

import {flushPromises, mount, VueWrapper} from '@vue/test-utils'
import {nextTick} from 'vue'
import {i18n} from '@/plugins/i18n'
...

i18n.global.t = (key) => key

describe('SubjectUnderTest', () => {
  it('should display translation for key "foo.bar"', async () => {
    const wrapper = await createWrapper()
    ...
  }

  async function createWrapper(): Promise<VueWrapper> {
    const wrapper = mount(sut, {
      global: {
        plugins: [i18n],
      },
    })

    await nextTick()
    await flushPromises()

    return wrapper
  }
}

Upvotes: 0

uke5tar
uke5tar

Reputation: 447

I had the same problem and solved it like this:

I am using the next version of @vue/test-utils and vue-jest ("@vue/test-utils": "^2.0.0-rc.16" + "vue-jest": "^5.0.0-alpha.10").

I created a file called jest.init.js (u can call it anything u like)

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


config.global.mocks = {
  $t: (msg) => translations[msg],
};

and then initiate it as setup file in jest.config.js

module.exports = {
...
  setupFiles: [
    './tests/unit/jest.init.js',
  ],
...
};

Upvotes: 2

Related Questions