i18n icon indicating copy to clipboard operation
i18n copied to clipboard

Error when testing with @nuxtjs/test-utils

Open obulat opened this issue 1 year ago • 8 comments

Environment

Nuxt project info:


  • Operating System: Linux
  • Node Version: v18.18.0
  • Nuxt Version: 3.8.0
  • CLI Version: 3.9.1
  • Nitro Version: 2.7.2
  • Package Manager: [email protected]
  • Builder: -
  • User Config: modules, i18n
  • Runtime Modules: @nuxtjs/[email protected]
  • Build Modules: -

Reproduction

https://stackblitz.com/edit/bobbiegoede-nuxt-i18n-starter-4pwdnd?file=test%2Findex.spec.ts

Describe the bug

Nuxt i18n throws errors when using it with vitest and @nuxtjs/test-utils.

Using $t in the template and i18n from useNuxtApp().$i18n work, but useI18n() and i18n-t components fails. useI18n throws a Syntax error: "SyntaxError: Need to install with app.use function" i18n-t component shows "Failed to resolve component":

[Vue Router warn]: No match found for location with path "/"
[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. 
  at <MountSuspendedComponent > 

In the reproduction I added failing tests for the component and the composable. The test that only uses $t in the template passes. Trying to test App with the LanguageSwitcher from the reproduction starter also throws an error in the setup.

Additional context

The issue was originally opened in @nuxtjs/test-utils. @danielroe wrote:

As far as I can tell the issue here is because the @nuxtjs/i18n module is calling vueApp.use on the CJS version of vue-i18n, but when we use useI18n in the test, it's the ESM version of the library, and so it thinks it's not registered.

This issue is currently the main obstacle for Openverse Nuxt 3 migration

Logs

Vitest caught 1 unhandled error during the test run.
This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected.

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
SyntaxError: Need to install with `app.use` function
 ❯ Module.createCompileError node_modules/@intlify/message-compiler/dist/message-compiler.mjs:78:19
     76|         : code;
     77|     const error = new SyntaxError(String(msg));
     78|     error.code = code;
       |                   ^
     79|     if (loc) {
     80|         error.location = loc;
 ❯ createI18nError node_modules/vue-i18n/dist/vue-i18n.mjs:105:34
 ❯ Module.useI18n node_modules/vue-i18n/dist/vue-i18n.mjs:2313:15
 ❯ setup components/LangSwitcher.vue:18:47
 ❯ callWithErrorHandling node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:156:18
 ❯ setupStatefulComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7190:25
 ❯ setupComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7151:36
 ❯ mountComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5555:7
 ❯ processComponent node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5521:9
 ❯ patch node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5007:11

obulat avatar Dec 26 '23 06:12 obulat

Thank you for your reporting! I'll start to find out the cause and fix. please just a wait.

kazupon avatar Dec 26 '23 12:12 kazupon

Hi!

When the component test was run, I noticed that the nuxt i18n plugin was not running 🤔

vue-i18n is a runtime side plugin of nuxt i18n that runs createI18n. useI18n requires the i18n instance created by createI18n to be installed by app.use.

BTW, When the Page component is tested, it seems to be running a working nuxt i18n plugin.

kazupon avatar Dec 26 '23 13:12 kazupon

workaround for component test.

To begin with, the components test case does not need to be tested in the nuxt layer. The mountSuspended contains options of @vue/test-utils. So you can test with the globals.plugins options provided by @vue/test-utils as follows

// ... 
import { createI18n } from 'vue-i18n'

// ... 

describe('Mounting hello', () => {
  // ...
  it('can mount using composable', async () => {
    const i18n = createI18n({
      // vue-i18n options here ...
    })

    const component = await mountSuspended(HelloComposable, { global: {
      plugins: [i18n]
    }})

    expect(component.vm.toBeTruthy())
    
    expect(component.text()).toMatchInlineSnapshot(
      `"Hi from $t template function: HelloHi from useI18n composable: Hello"`
    )
  })

  // ...
})

kazupon avatar Dec 26 '23 14:12 kazupon

import { createI18n } from 'vue-i18n'

Hi @kazupon thanks for the example, although I get this result from vitest:

- Expected
+ Received

- "Hi from $t template function: HelloHi from useI18n composable: Hello"
+ "pages.login.app-titlecomponents.login.email.labelcomponents.login.password.label components.login.sign-in-buttoncomponents.auth.login.forgot-passwordcomponents.auth.login.wrong-credentials"

My i18n.config.ts is:

import en from '~/locales/en.json'
import sv from '~/locales/sv.json'

export default defineI18nConfig(() => ({
  legacy: false,
  locale: 'sv',
  defaultLocale: 'sv',
  messages: { sv, en },
}))

Can you understand why it doesnt give me the actual translation?

Edit: Sorry, my mistake, I forgot to include locale: 'sv' and the actual messages in createi18n.

beejaz avatar Jan 06 '24 17:01 beejaz

Did anyone find a way to globally register the plugin? It's quite painful to manually setup the plugin on every mountSuspended suspended component.

bissolli avatar Feb 20 '24 11:02 bissolli

@kazupon is there any update on this issue? While the workaround "works" it would still be very beneficial if testing nuxt apps using i18n would "just work" out of the box.

markbrockhoff avatar Jul 12 '24 12:07 markbrockhoff

Did anyone find a way to globally register the plugin? It's quite painful to manually setup the plugin on every mountSuspended suspended component.

Something similar to this worked for me:

// vitestSetup.ts
import { config } from '@vue/test-utils';
import { createI18n } from 'vue-i18n';
import en from "@/config/i18n/en";

config.global.plugins.push(
  createI18n({
    legacy: false,
    locale: 'en',
    messages: { en },
  }),
);
// vitest.config.ts
import { defineVitestConfig } from "@nuxt/test-utils/config";
import { coverageConfigDefaults } from "vitest/config";

export default defineVitestConfig({
  test: {
    setupFiles: ['/tests/vitestSetup.ts'],
    environmentOptions: {
      nuxt: {
        domEnvironment: "jsdom",
      },
    },
  },
});

david-mears-2 avatar Jul 17 '24 15:07 david-mears-2

Hello ! In case anyone comes across this issue as well, I also encountered it and none of the solutions above worked for me. What did work was the following:

  • link to setup file in vitest.config.ts:

    setupFiles: [ './tests/mock/modules/vue-i18n/vue-i18n-setup.ts'],

  • vue-i18n-setup.ts:

import { defineNuxtModule } from '@nuxt/kit';

export default defineNuxtModule({ setup: (_options, nuxt) => { nuxt.hook('imports:extend', (imports) => { imports.push({ name: 'useI18n', from: './vue-i18n.ts' }); }); }, });

  • vue-i18n.ts:

import { vi } from 'vitest';

export const useI18n = () => ({ t: (key: string) => key, d: (key: string) => key, locale: 'en', setLocaleMessage: vi.fn(), mergeLocaleMessage: vi.fn(), availableLocales: ['en'], });

With this, all errors I had when trying to test some unit tests related to vue-i18n have gone away. Basically, you are overriding the default nuxt module with this mocked one. I don't know for sure why the other solutions did not work out for me.

Versions:

  • nuxt : 3.13.2;
  • @nuxtjs/i18n: 8.5.5;

georgeVasiliu avatar Oct 04 '24 15:10 georgeVasiliu

@kazupon is the workaround the expected solution for this, or is work still ongoing on it?

rigtigeEmil avatar Dec 01 '24 09:12 rigtigeEmil

For what it's worth, I tried diving a bit deeper into this, and I think there are multiple vue instances being initialized in the test environment causing this issue. See in this reproduction.

The instance we get from the nuxtApp.vueApp is different than the one we get from getCurrentInstance when we mount the environment in a unit test which is not the case if we run the app normally.

From my understanding, this module installs the i18n instance to the one from nuxtApp, but uses getCurrentInstance within the useI18n composable which causes this issue. If this is true, I guess it may also mean this is a @nuxt/test-utils issue? @danielroe @kazupon

gbyesiltas avatar Jan 27 '25 13:01 gbyesiltas

That sounds like it might be a @nuxt/test-utils issue indeed!

danielroe avatar Feb 04 '25 21:02 danielroe

@gbyesiltas investigation that there are multiple vue instances gave me the idea to try registering the i18n plugin in both:

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

beforeAll(() => {
  const nuxt = tryUseNuxtApp();
  if (nuxt) {
    const i18n = createI18n({
      ...
    });
    // register the i18n plugin in both nuxts vue app and the vue app used by vue/test-utils
    nuxt.vueApp.use(i18n);
    config.global.plugins.push(i18n);
  }
});

This seems to work, although I still get warnings like:

[Vue warn]: App already provides property with key "Symbol(vue-i18n)". It will be overwritten with the new value.
[Vue warn]: Component "i18n-t" has already been registered in target app.
etc...

maximilianmaihoefner avatar Mar 05 '25 10:03 maximilianmaihoefner

@maximilianmaihoefner

This seems to work, although I still get warnings like:

Yeah, I think it makes sense since we now probably register the plugin multiple times in some places in the app :D But the fact that this works kind of confirms the issue I guess

gbyesiltas avatar Mar 05 '25 11:03 gbyesiltas