21

Testing libs...always fun. I am using next-i18next within my NextJS project. We are using the useTranslation hook with namespaces.

When I run my test there is a warning:

console.warn react-i18next:: You will need to pass in an i18next instance by using initReactI18next

> 33 |   const { t } = useTranslation(['common', 'account']);
     |                 ^

I have tried the setup from the react-i18next test examples without success. I have tried this suggestion too.

as well as just trying to mock useTranslation without success.

Is there a more straightforward solution to avoid this warning? The test passes FWIW...

test('feature displays error', async () => {
    const { findByTestId, findByRole } = render(
      <I18nextProvider i18n={i18n}>
        <InviteCollectEmails onSubmit={jest.fn()} />
      </I18nextProvider>,
      {
        query: {
          orgId: 666,
        },
      }
    );

    const submitBtn = await findByRole('button', {
      name: 'account:organization.invite.copyLink',
    });

    fireEvent.click(submitBtn);

    await findByTestId('loader');

    const alert = await findByRole('alert');
    within(alert).getByText('failed attempt');
  });

Last, is there a way to have the translated plain text be the outcome, instead of the namespaced: account:account:organization.invite.copyLink?

Phil Lucks
  • 2,704
  • 6
  • 31
  • 57

5 Answers5

16

Use the following snippet before the describe block OR in beforeEach() to mock the needful.

jest.mock("react-i18next", () => ({
    useTranslation: () => ({ t: key => key }),
}));

Hope this helps. Peace.

Mayank Gangwal
  • 519
  • 3
  • 8
2

I figured out how to make the tests work with an instance of i18next using the renderHook function and the useTranslation hook from react-i18next based on the previous answers and some research.

This is the Home component I wanted to test:

import { useTranslation } from 'next-i18next';

const Home = () => {
  const { t } = useTranslation("");
  return (
    <main>
      <div>
        <h1> {t("welcome", {ns: 'home'})}</h1>
      </div>
    </main>
  )
};

export default Home;

First, we need to create a setup file for jest so we can start an i18n instance and import the translations to the configuration. test/setup.ts

import i18n from "i18next";
import { initReactI18next } from "react-i18next";

import homeES from '@/public/locales/es/home.json';
import homeEN from '@/public/locales/en/home.json';

i18n.use(initReactI18next).init({
  lng: "es",
  resources: {
    en: {
      home: homeEN,
    },
    es: {
      home: homeES,
    }
  },
  fallbackLng: "es",
  debug: false,
});

export default i18n;

Then we add the setup file to our jest.config.js:

setupFilesAfterEnv: ["<rootDir>/test/setup.ts"]

Now we can try our tests using the I18nextProvider and the useTranslation hook:

import '@testing-library/jest-dom/extend-expect';
import { cleanup, render, renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { I18nextProvider, useTranslation } from 'react-i18next';

import Home from '.';

describe("Index page", (): void => {
  afterEach(cleanup);

  it("should render properly in Spanish", (): void => {
    const t = renderHook(() => useTranslation());

    const component = render( 
      <I18nextProvider i18n={t.result.current.i18n}>
        <Home / >
      </I18nextProvider>
    );

    expect(component.getByText("Bienvenido a Pocky")).toBeInTheDocument();
  });

  it("should render properly in English", (): void => {
    const t = renderHook(() => useTranslation());
    act(() => {
      t.result.current.i18n.changeLanguage("en");
    });

    const component = render( 
      <I18nextProvider i18n={t.result.current.i18n}>
        <Home/>
      </I18nextProvider>
    );
    expect(component.getByText("Welcome to Pocky")).toBeInTheDocument();
  });

});

Here we used the I18nextProvider and send the i18n instance using the useTranslation hook. after that the translations were loaded without problems in the Home component.

We can also change the selected language running the changeLanguage() function and test the other translations.

1

use this for replace render function.


import { render, screen } from '@testing-library/react'
import DarkModeToggleBtn from '../../components/layout/DarkModeToggleBtn'
import { appWithTranslation } from 'next-i18next'
import { NextRouter } from 'next/router'


jest.mock('react-i18next', () => ({
    I18nextProvider: jest.fn(),
    __esmodule: true,
 }))

  
const createProps = (locale = 'en', router: Partial<NextRouter> = {}) => ({
    pageProps: {
        _nextI18Next: {
        initialLocale: locale,
        userConfig: {
            i18n: {
            defaultLocale: 'en',
            locales: ['en', 'fr'],
            },
        },
        },
    } as any,
    router: {
        locale: locale,
        route: '/',
        ...router,
    },
} as any)

const Component = appWithTranslation(() => <DarkModeToggleBtn />)

const defaultRenderProps = createProps()

const renderComponent = (props = defaultRenderProps) => render(
    <Component {...props} />
)


describe('', () => {
    it('', () => {

        renderComponent()
     
        expect(screen.getByRole("button")).toHaveTextContent("")

    })
})


  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 12 '22 at 00:22
0

I used a little bit more sophisticated approach than mocking to ensure all the functions work the same both in testing and production environment.

First, I create a testing environment:

// testing/env.ts
import i18next, { i18n } from "i18next";
import JSDomEnvironment from "jest-environment-jsdom";
import { initReactI18next } from "react-i18next";

declare global {
  var i18nInstance: i18n;
}

export default class extends JSDomEnvironment {
  async setup() {
    await super.setup();
    /* The important part start */
    const i18nInstance = i18next.createInstance();
    await i18nInstance.use(initReactI18next).init({
      lng: "cimode",
      resources: {},
    });
    this.global.i18nInstance = i18nInstance;
    /* The important part end */
  }
}

I add this environment in jest.config.ts:

// jest.config.ts
export default {
  // ...
  testEnvironment: "testing/env.ts",
};

Sample component:

// component.tsx
import { useTranslation } from "next-i18next";

export const Component = () => {
  const { t } = useTranslation();
  return <div>{t('foo')}</div>
}

And later on I use it in tests:

// component.test.tsx
import { setI18n } from "react-i18next";
import { create, act, ReactTestRenderer } from "react-test-renderer";
import { Component } from "./component";

it("renders Component", () => {
  /* The important part start */
  setI18n(global.i18nInstance);
  /* The important part end */
  let root: ReactTestRenderer;
  act(() => {
    root = create(<Component />);
  });
  expect(root.toJSON()).toMatchSnapshot();
});
Luixo
  • 176
  • 2
  • 11
0

For those who couldn't find a solution or any help in the library itself, this is what I ended up doing to actually load the translations and test them.

I'm using Next JS v12.x and next-i18next v12.1.0 along with jest and testing-library but could be applied to other environments.

  1. I added a testing utility file src/utils/testing.ts
/* eslint-disable import/no-extraneous-dependencies */
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

import { DEFAULT_LOCALE } from 'src/utils/constants';

/**
 * Initializes the i18n instance with the given namespaces.
 * @param {string[]} namespaces - An array of namespaces.
 * @param {string} locale - The locale to use.
 * @returns {i18n.i18n} The initialized i18n instance.
 */
const initializeI18n = async (
  namespaces: string[],
  locale = DEFAULT_LOCALE
) => {
  const resources: { [ns: string]: object } = {};

  // Load resources for the default language and given namespaces
  namespaces.forEach((ns) => {
    const filePath = `public/locales/${locale}/${ns}.json`;
    try {
      // eslint-disable-next-line @typescript-eslint/no-var-requires,global-require,import/no-dynamic-require
      const translations = require(`../../${filePath}`);
      resources[ns] = translations;
    } catch (error) {
      throw new Error(
        `Could not load translations for locale: ${locale}, namespace: ${ns}`
      );
    }
  });

  await i18n.use(initReactI18next).init({
    lng: locale,
    fallbackLng: locale,
    debug: false,
    ns: namespaces,
    defaultNS: namespaces[0],
    resources: { [locale]: resources },
    interpolation: { escapeValue: false },
  });

  return i18n;
};

export default initializeI18n;
  1. On my test file I asynchronously initialized the instance with the namespace I wanted to load, rendered my component and awaited for the screen to find the rendered translated text.
describe('when price is zero', () => {
  beforeEach(async () => {
    await initializeI18n(['common_areas']);

    render(<CommonAreaCard commonArea={mockCommonArea(0)} />);
  });

  it('should render the free price', async () => {
    expect(
      await screen.findByText('Sin costo de reservación')
    ).toBeInTheDocument();
  });
});

I hope it helps.