QuiiBz / next-international

Type-safe internationalization (i18n) for Next.js
https://next-international.vercel.app
MIT License
1.31k stars 61 forks source link

Error when testing `TypeError: (0 , import_react2.cache) is not a function` #178

Open sshmaxime opened 1 year ago

sshmaxime commented 1 year ago

Hello,

I'm getting an TypeError: (0 , import_react2.cache) is not a function error while setting up the tests with next-international. Have you already see those types of error ?

QuiiBz commented 1 year ago

It seems related to https://github.com/vercel/next.js/discussions/49304, can you try the examples?

sshmaxime commented 1 year ago

I tried what's suggested but it doesn't work. And ideally, I don't wanna use canaries releases as we will use this in production.

QuiiBz commented 1 year ago

Can you share a minimal reproduction? We could maybe do the same as suggested here, but it feels a bit hacky: https://github.com/vercel/next.js/discussions/49304#discussioncomment-6910332

Please note that the Next.js App Router uses a canary version of React too.

sshmaxime commented 1 year ago

I've been trying to get a minimal reproduction on Codesandbox since this morning but I'm getting additional weird errors, could you try on that repo and branch directly https://github.com/LedgerHQ/cex-deposit-live-app/tree/support/i18n ? You should only have to do yarn and yarn dev. And then run yarn test.

QuiiBz commented 1 year ago

Thanks for the reproduction repo. Using jsdom seems to be wrong for testing server components, but using the node environment triggers this error:

Screenshot 2023-09-18 at 21 04 15

I've also tried to remove completely the usage of cache() but Next.js breaks in useCurrentLocale() when using useParams() from next/navigation - I have no idea why.

Installing react@canary gives the same error, whereas react@rc gives our first error with cache is not a function. We'll probably have to ask the Next.js team at some point.

sshmaxime commented 1 year ago

Hey @QuiiBz, thanks for taking the time to check out our code. It means a lot.

While this issue is getting fixed I changed our components as Clients but I'm now getting Error: Uncaught [TypeError: Cannot read properties of null (reading 'locale')]. Everything is mocked properly as per the docs, did i miss anything ?

Is this what you meant when saying Next.js breaks in useCurrentLocale() when using useParams() from next/navigation ? Because it seems to be related to that after following the stack trace:

Line 214-215 in next-international/dist/app/client

const params = (0, import_navigation2.useParams)();
const segment = params[(_a = config.segmentName) != null ? _a : DEFAULT_SEGMENT_NAME];
QuiiBz commented 1 year ago

Is this what you meant when saying Next.js breaks in useCurrentLocale() when using useParams() from next/navigation

Yeah, exactly. I've been able to go further by adding the following mocks:

jest.mock('next/navigation', () => ({
  ...jest.requireActual('next/navigation'),
  useParams: jest.fn().mockReturnValue({
    locale: 'en',
  }),
}))

jest.mock('next/headers', () => ({
  headers: jest.fn().mockReturnValue(new Headers({
    'x-next-locale': 'en'
  })),
}))

jest.mock('react', () => ({
  ...jest.requireActual('react'),
  cache: jest.fn().mockImplementation(fn => fn)
}))

...but I'm now getting the following render error:

Screenshot 2023-09-19 at 13 25 00

...which makes me believe that testing Server Components isn't really ready (no documentation about this on the Next.js docs, even though there is one for the Pages Router): https://github.com/testing-library/react-testing-library/issues/1209

https://github.com/vercel/next.js/issues/53065 suggests using testEnvironment: "node", which only needs the React's cache() mock, but we get again the same error as my first comment (https://github.com/QuiiBz/next-international/issues/178#issuecomment-1724216504)

There seems to be a fix on canary Next.js, but we might want to wait for an official release: https://github.com/vercel/next.js/issues/47448#issuecomment-1705573492

sshmaxime commented 1 year ago

@QuiiBz The weird thing is that the issue comes from useCurrentLocale which is actually executed in a Client component. I don't quite follow why it has anything to do with Server Components ? Is it because it's embedded in one ?

ps: I tried the Next.js canary version mentionned in the issue and it didn't work. Even the latest one v13.4.20-canary.40.

QuiiBz commented 1 year ago

Is it because it's embedded in one

That's my assumption. Either way, Next.js doesn't have any documentation yet for testing Server Components or Client Components, and next-international doesn't seem to be the issue here (you could use cache(), useParams()... in application code too, and you'll get the same errors even if you're not using next-international).

I guess we'll have to wait for examples and documentation from Next.js, there is sadly nothing much that I can do.

khRasikh commented 1 year ago

I am getting the same error while running Jest for Server Components: image

I am using Next version 13.5.1. Any idea how to solve this problem?

QuiiBz commented 1 year ago

Please read the above discussion for an answer. We don't know yet how to properly test new Next.js features.

wforte4 commented 1 year ago

For anyone out there struggling with testing for this I was able to get this working with all the following mocks

import { screen } from '@testing-library/react';
import { renderWithStoreI18n } from '@/utils/testing/testingLibraryHelper';
import Home from './page';

const testCache = (func) => func;

jest.mock('react', () => {
  const originalModule = jest.requireActual('react');
  return {
    ...originalModule,
    cache: testCache
  };
});

jest.mock('next/router', () => ({
  useRouter: jest.fn().mockImplementation(() => ({
    locale: 'en',
    defaultLocale: 'en',
    locales: ['en', 'ca', 'gb']
  }))
}));

jest.mock('next/navigation', () => ({
  ...jest.requireActual('next/navigation'),
  useParams: jest.fn().mockReturnValue({
    locale: 'en'
  })
}));

jest.mock('next/headers', () => ({
  headers: jest.fn().mockReturnValue(new Headers({
    'x-next-locale': 'en'
  }))
}));

jest.mock('../../i18n/server', () => ({
  getI18n: jest.fn().mockImplementation(() => jest.fn())
}));

jest.mock('../../i18n/client', () => ({
  useI18n: jest.fn().mockImplementation(() => jest.fn())
}));

describe('Page', () => {
  test('Page renders', async () => {
    const Result = await Home();
    renderWithStoreI18n(Result);
    const homeEl = screen.getByTestId('home-test');
    expect(homeEl).toBeDefined();
  });
});
CostierLucas commented 12 months ago

wforte4 can I see your component, I can get my component but the translation doesn't work, the example is far too limited in the documentation.

GasparAdragna commented 11 months ago

@QuiiBz Still no way to test with app router?

wforte4 commented 11 months ago

Yes this works with app router and no the translations themselves don't work. But it's a way at least for now to be able to do testing and have translations in your app. At my company we also do testing to use the application which is where we would test translations so it didn't matter for us. I also moved these mocks to the setup file for jest so that you don't have to put that on each test. Hope that helps!

kpaccess commented 11 months ago

I am using this library for client components and I have customRender below

import { render } from '@testing-library/react'; import { I18nProviderClient } from '@/locales/client';

jest.mock('next/router', () => require('next-router-mock'));

jest.mock('next/router', () => ({ useRouter: jest.fn().mockImplementation(() => ({ locale: 'en', defaultLocale: 'en', locales: ['en', 'fr'], })), }));

const Providers = ({ children }: { children: React.ReactElement }) => { return {children}; };

const customRender = (ui: React.ReactElement, options = {}) => render(ui, { wrapper: Providers, ...options, });

export * from '@testing-library/react'; export { default as userEvent } from '@testing-library/user-event'; export { customRender as render };

But when i try to run the test I get these error

Error: Uncaught [TypeError: (0 , import_react.use) is not a function]

18 | const customRender = (ui: React.ReactElement, options = {}) =>
> 19 |     render(ui, {
     |           ^
  20 |         wrapper: Providers,
  21 |         ...options,
  22 |     });

  I have added all the mocks mentioned by [wforte4] but nothing is working. Would really appreciate some help.
mateusfg7 commented 10 months ago

I am with the same problem as @kpaccess. The solution of @wforte4 doesn't work for me.

image

kpaccess commented 10 months ago

customRender didn't work for me. I just started mocking for whatever files that I was using.

like jest.mock('./src/locales/client', () => ({ useI18n: jest.fn(), useScopedI18n: jest.fn(), useChangeLocale: jest.fn(), useCurrentLocale: jest.fn(() => 'en'), }));

QuiiBz commented 10 months ago

Are you using next/jest.js inside your Jest configuration?

kpaccess commented 10 months ago

yes const nextJest = require('next/jest');

murphcfa commented 9 months ago

customRender didn't work for me. I just started mocking for whatever files that I was using.

like jest.mock('./src/locales/client', () => ({ useI18n: jest.fn(), useScopedI18n: jest.fn(), useChangeLocale: jest.fn(), useCurrentLocale: jest.fn(() => 'en'), }));

Are you mocking the client I18NProvider for your tests? Could I see an example of this? I am running into the same issue

vpanichkin commented 7 months ago

Hey, is there any news? I tried to set the same mocking

jest.mock('../../locales/client', () => ({
  useI18n: jest.fn().mockImplementation(() => jest.fn())
}));

This mock causes more issues, namely

console.error
    Warning: React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports. Check the render method of 'wrapper'.
vpanichkin commented 7 months ago

I found one way to test. Here is my test version:

Test.tsx.ts

import { cultures } from '@autoscout24/culture'

import { I18nProviderClient, useScopedI18n } from '../../locales/client'
import { renderWithI18N, screen } from '../../tests/utils/renderWithI18N'

import PriceEstimationStart from './PriceEstimationStart'

jest.mock('../../locales/client')

describe('Test translation in all countries', () => {
  let mockedUseScopedI18n: jest.MockWithArgs<typeof useScopedI18n>
  beforeEach(() => {
    mockedUseScopedI18n = jest.mocked(useScopedI18n)
    const mockedI18nProviderClient = jest.mocked(I18nProviderClient)
    mockedI18nProviderClient.mockImplementation(({ children }) => (
      <>{children}</>
    ))
  })
  test.each([
    [cultures.de_DE.iso, 'Hallo Welt!'],
    [cultures.de_AT.iso, 'Hallo Welt!'],
    [cultures.en_GB.iso, 'Hello World!'],
    [cultures.fr_BE.iso, 'Bonjour le monde!'],
    [cultures.fr_FR.iso, 'Bonjour le monde!'],
    [cultures.it_IT.iso, 'Ciao mondo!'],
    [cultures.nl_BE.iso, 'Hallo wereld!'],
    [cultures.nl_NL.iso, 'Hallo wereld!'],
  ])('should resolve %s locale with text: %s', async (locale, expectedText) => {
    mockedUseScopedI18n.mockImplementation(() => () => {
      const translations: Record<string, string> = {
        [cultures.de_AT.iso]: 'Hallo Welt!',
        [cultures.de_DE.iso]: 'Hallo Welt!',
        [cultures.en_GB.iso]: 'Hello World!',
        [cultures.fr_BE.iso]: 'Bonjour le monde!',
        [cultures.fr_FR.iso]: 'Bonjour le monde!',
        [cultures.it_IT.iso]: 'Ciao mondo!',
        [cultures.nl_BE.iso]: 'Hallo wereld!',
        [cultures.nl_NL.iso]: 'Hallo wereld!',
      }
      return translations[locale] ?? 'Unknown locale'
    })

    renderWithI18N(<PriceEstimationStart />, { locale })

    expect(screen.queryByText(expectedText)).toBeInTheDocument()
  })
})

Custom renderer

import React, { ReactElement } from 'react'

import { cleanup, render } from '@testing-library/react'

import { I18nProviderClient } from '../../locales/client'

afterEach(() => {
  cleanup()
})

const renderWithI18N = (ui: ReactElement, options: { locale: string }) =>
  render(ui, {
    // wrap provider(s) here if needed
    wrapper: ({ children }) => (
      <I18nProviderClient locale={options.locale}>
        {children}
      </I18nProviderClient>
    ),
    ...options,
  })

export * from '@testing-library/react'
export { default as userEvent } from '@testing-library/user-event'
export { renderWithI18N }