Intégrer vue-i18n dans les tests Jest d’un projet Vue.js

La mise en place de vue-i18n dans une application Vue.js dont les tests sont écrits avec Jest est semée d’embûches. Voyons lesquelles et comment les surmonter.

Avant de commencer, si vous n’avez pas de préférence pour écrire les tests, il est beaucoup plus simple de faire fonctionner vue-i18n avec Mocha + Chai qu’avec Jest.

Pour illustrer les problèmes rencontrés, je crée une nouvelle application, atm-frontend, qui pourrait servir d’interface utilisateur pour un guichet automatique bancaire.

Dans un terminal, avec Vue CLI (en v4.5.9 au moment de rédiger cet article) déjà installé sur ma machine :

vue create atm-frontend

Je choisis l’option Manually select features. Je rajoute la feature Unit Testing. Je conserve la version 2.x de Vue.js. Je choisis ESLint + Airbnb config, Lint on save, Jest, In dedicated config files. Je ne sauvegarde pas mes choix en tant que preset pour de futurs projets.

J’ajoute vue-i18n :

vue add i18n

Je choisis d’activer les messages localisés dans les single file components. Je conserve les propositions de l’assistant pour les autres questions.

Pour expérimenter, je crée un composant AtmAccountBalance localisé :

<template>
  <p>{{ $t('account-balance') }} {{ $n(amount, 'currency') }}</p>
</template>

<script>
export default {
  name: 'AtmAccountBalance',
  props: {
    amount: Number,
  },
};
</script>

<i18n>
{
  "en": {
    "account-balance": "Account Balance"
  },
  "fr": {
    "account-balance": "Solde"
  }
}
</i18n>

$t et $n sont des fonctions de vue-i18n, qui permettent d’obtenir une représentation localisée d’un texte pour la première, d’un nombre pour la seconde.

Pour que $n fonctionne, je modifie le fichier i18n.js, qui a été généré par Vue CLI, quand j’ai ajouté le support de l’internationalisation.

const numberFormats = {
  en: {
    currency: {
      style: 'currency', currency: 'USD',
    },
  },
  fr: {
    currency: {
      style: 'currency', currency: 'EUR',
    },
  },
};

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

Pour vérifier que le composant AtmAccountBalance se comporte comme je le voudrais, je l’utilise dans App.vue. Je démarre le serveur et vérifie dans le navigateur que l’affichage est conforme à ce que j’imaginais.

Jusqu’ici tout va bien. Passons aux tests.

import { shallowMount } from '@vue/test-utils';
import AtmAccountBalance from '@/components/AtmAccountBalance.vue';

describe('AtmAccountBalance.vue', () => {
  it('should render Account Balance $1,000.00.', () => {
    const wrapper = shallowMount(AtmAccountBalance, {
      propsData: { amount: 1000 },
    });
    expect(wrapper.text()).toBe('Account Balance $1,000.00');
  });
});

Ma première tentative se solde par un échec :

TypeError: _vm.$t is not a function

Une première solution à ce problème consiste à mocker $t et $n.

import { shallowMount } from '@vue/test-utils';
import AtmAccountBalance from '@/components/AtmAccountBalance.vue';

describe('AtmAccountBalance.vue', () => {
  it('should render account-balance 1000.', () => {
    const $t = (key) => key;
    const $n = (value) => value.toString();
    const wrapper = shallowMount(AtmAccountBalance, {
      propsData: { amount: 1000 },
      mocks: { $t, $n },
    });
    expect(wrapper.text()).toBe('account-balance 1000');
  });
});

Bien que l’erreur ait disparu, quelle valeur a ce test ? En particulier, il ne vérifie pas que l’intégration de vue-i18n dans le composant est correcte et je veux absolument découvrir ce type de problèmes par des tests automatisés.

Idéalement, je voudrais en profiter pour tester ma configuration de vue-i18n. Cependant, ajouter import i18n from '@/i18n'; dans AtmAccountBalance.spec.js provoque une erreur :

FAIL  tests/unit/components/AtmAccountBalance.spec.js
 ● Test suite failed to run

   TypeError: require.context is not a function

      5 |
      6 | function loadLocaleMessages() {
   >  7 |   const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i);
        |                           ^
      8 |   const messages = {};
      9 |   locales.keys().forEach((key) => {
     10 |     const matched = key.match(/([A-Za-z0-9-_]+)\./i);

     at loadLocaleMessages (src/i18n.js:7:27)
     at Object.<anonymous> (src/i18n.js:35:13)
     at Object.<anonymous> (tests/unit/components/AtmAccountBalance.spec.js:2:1)

La configuration générée par vue-cli-plugin-i18n utilise require.context, une fonction de Webpack, que les développeurs de Jest ont choisi de ne pas supporter. Plusieurs solutions ont été proposées sur StackOverflow pour mocker require.context. J’ai retenu celle décrite dans le readme de Storyshots, qui me conduit à installer babel-plugin-require-context-hook en tant que dépendance :

npm i babel-plugin-require-context-hook

Je crée ensuite un fichier .babelrc à la racine du projet :

{
  "env": {
    "test": {
      "plugins": ["require-context-hook"]
    }
  }
}

Puis, dans le fichier i18n.js, juste après les imports, j’ajoute require('babel-plugin-require-context-hook/register')();.

J’ajoute un nouveau test qui vérifie l’intégration de vue-i18n :

it('should render Account Balance $1,000.00.', () => {
  const wrapper = shallowMount(AtmAccountBalance, {
    i18n,
    propsData: { amount: 1000 },
  });
  expect(wrapper.text()).toBe('Account Balance $1,000.00');
});

Mais il ne fonctionne pas encore : les traductions dans les single file components ne sont pas exploitées, car vue-jest ne reconnaît pas les blocs <i18n>.

Pour corriger le problème, j’installe vue-i18n-jest :

npm i vue-i18n-jest -D

J’édite jest.config.js pour remplacer l’utilisation de vue-jest (cf. node_modules/@vue/cli-plugin-unit-jest/presets/default/jest-preset.js) par vue-i18n-jest :

module.exports = {
  preset: '@vue/cli-plugin-unit-jest',
  transform: {
    '^.+\\.vue$': require.resolve('vue-i18n-jest')
  }
};

Tout fonctionne désormais. En fonction de ce que je veux tester, je peux mocker vue-i18n ou l’intégrer. Le code source du projet est disponible sur GitHub.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *