How do I mock react-i18next and i18n.js in jest? - reactjs

package.json
"moduleNameMapper": {
"i18next": "<rootDir>/__mocks__/i18nextMock.js"
}
i18n.js
import i18n from 'i18next'
import XHR from 'i18next-xhr-backend'
// import Cache from 'i18next-localstorage-cache'
import LanguageDetector from 'i18next-browser-languagedetector'
i18n
.use(XHR)
// .use(Cache)
.use(LanguageDetector)
.init({
fallbackLng: 'en',
// wait: true, // globally set to wait for loaded translations in translate hoc
lowerCaseLng: true,
load: 'languageOnly',
// have a common namespace used around the full app
ns: ['common'],
defaultNS: 'common',
debug: true,
// cache: {
// enabled: true
// },
interpolation: {
escapeValue: false, // not needed for react!!
formatSeparator: ',',
format: function (value, format, lng) {
if (format === 'uppercase') return value.toUpperCase()
return value
}
}
})
export default i18n
i18nextMock.js
/* global jest */
const i18next = jest.genMockFromModule('react-i18next')
i18next.t = (i) => i
i18next.translate = (c) => (k) => k
module.exports = i18next
For some reason the jest unit tests are not getting a component.
Here is a unit test:
import React from 'react'
import { Provider } from 'react-redux'
import { MemoryRouter } from 'react-router-dom'
import { mount } from 'enzyme'
import { storeFake } from 'Base/core/storeFake'
import Container from '../container'
describe('MyContainer (Container) ', () => {
let Component;
beforeEach(() => {
const store = storeFake({})
const wrapper = mount(
<MemoryRouter>
<Provider store={store}>
<Container />
</Provider>
</MemoryRouter>
)
Component = wrapper.find(Container)
});
it('should render', () => {
// Component is undefined here
expect(Component.length).toBeTruthy()
})
})

You don't need to mock the t function, only the translate one is required. For the second one, your usage of the parameters are confusing, also, you need to return a Component.
I was able to make it work on my project. Here are my mock file and my Jest configuration
Jest configuration
"moduleNameMapper": {
"react-i18next": "<rootDir>/__mocks__/reacti18nextMock.js"
}
The source code to mock react-i18next
/* global jest */
import React from 'react'
const react_i18next = jest.genMockFromModule('react-i18next')
const translate = () => Component => props => <Component t={() => ''} {...props} />
react_i18next.translate = translate
module.exports = react_i18next

I used Atemu's anwser in my jest tests, but ended up with the following one line in the mock:
module.exports = {t: key => key};
Also modified jest config as well since I import 't' from 'i18next':
"moduleNameMapper": {
"i18next": "<rootDir>/__mocks__/reacti18nextMock.js"
}

In my case, using the useTranslation hook with TypeScript, it was as follows:
const reactI18Next: any = jest.createMockFromModule('react-i18next');
reactI18Next.useTranslation = () => {
return {
t: (str: string) => str,
i18n: {
changeLanguage: () => new Promise(() => {}),
},
};
};
module.exports = reactI18Next;
export default {};
The jest.config.ts:
const config: Config.InitialOptions = {
verbose: true,
moduleNameMapper: {
'react-i18next': '<rootDir>/__mocks__/react-i18next.ts',
},
};

As of 2023 and since this question has no accepted answer and I've had to modify slightly the examples provided by react-i18next I am posting the following hoping it will be of help to somebody. I am using jest and react-testing-library (RTL).
If you need different mocks in different tests the need for the mock is sparse, you can just mock the module at the beginning of each test (for example in one test you might just need to mock it and in another you want to spy on its use...). Nevertheless, if you are going to mock it, one way or another, in several test you'd better create the mocks separately as other answers recommend.
Just mock
If you just need to mock the module so the tests run seamlessly, react-i18next recommends you do the following:
jest.mock('react-i18next', () => ({
// this mock makes sure any components using the translate hook can use it without a warning being shown
useTranslation: () => {
return {
t: (str: string) => str,
i18n: {
changeLanguage: () => new Promise(() => {}),
// You can include here any property your component may use
},
}
},
}))
describe('Tests go here', () => {
it('Whatever', () => {})
})
Mock and spy
If you are using the useTranslation hook and need to spy on its use, that's another story. In the example provided by react-i18next they use enzyme which hasn't kept up with react and it doesn't run with RTL, here is the fix:
import { useTranslation } from 'react-i18next'
jest.mock('react-i18next', () => ({
useTranslation: jest.fn(),
}))
const tSpy = jest.fn((str) => str)
const changeLanguageSpy = jest.fn((lng: string) => new Promise(() => {}))
const useTranslationSpy = useTranslation as jest.Mock
beforeEach(() => {
jest.clearAllMocks()
useTranslationSpy.mockReturnValue({
t: tSpy,
i18n: {
changeLanguage: changeLanguageSpy,
language: 'en',
},
})
})
describe('Tests go here', () => {
it('Whatever', () => {})
})
The only change is that you have to set up the mock return value before each test otherwise what will reach the component is undefined, then you can assert the t function and the changeLanguage function have been called.

Returning the key "as is" isn't the best. We are using English text as the key and it would be nice to "evaluate" values that we pass in (i.e. t('{{timePeriod}} left') evaluated to: '5 days left' ). In this case I created a helper function to do this. Below are the jest and extra files needed:
Jest configuration (i.e. jest.config.js) :
moduleNameMapper: {
'react-i18next': '<rootDir>/src/tests/i18nextReactMocks.tsx',
'i18next': '<rootDir>/src/tests/i18nextMocks.ts',
// ...
},
i18nextMocks.ts:
function replaceBetween(startIndex: number, endIndex: number, original: string, insertion: string) {
const result = original.substring(0, startIndex) + insertion + original.substring(endIndex);
return result;
}
export function mockT(i18nKey: string, args?: any) {
let key = i18nKey;
while (key.includes('{{')) {
const startIndex = key.indexOf('{{');
const endIndex = key.indexOf('}}');
const currentArg = key.substring(startIndex + 2, endIndex);
const value = args[currentArg];
key = replaceBetween(startIndex, endIndex + 2, key, value);
}
return key;
}
const i18next: any = jest.createMockFromModule('i18next');
i18next.t = mockT;
i18next.language = 'en';
i18next.changeLanguage = (locale: string) => new Promise(() => {});
export default i18next;
i18nextReactMocks.tsx:
import React from 'react';
import * as i18nextMocks from './i18nextMocks';
export const useTranslation = () => {
return {
t: i18nextMocks.mockT,
i18n: {
changeLanguage: () => new Promise(() => {}),
},
};
};
export const Trans = ({ children }) => <React.Fragment>{children}</React.Fragment>;
And I'll throw in the mock's unit tests for free :)
import * as i18nextMocks from './i18nextMocks';
describe('i18nextMocks', () => {
describe('mockT', () => {
it('should return correctly with no arguments', async () => {
const testText = `The company's new IT initiative, code named Phoenix Project, is critical to the
future of Parts Unlimited, but the project is massively over budget and very late. The CEO wants
Bill to report directly to him and fix the mess in ninety days or else Bill's entire department
will be outsourced.`;
const translatedText = i18nextMocks.mockT(testText);
expect(translatedText).toBe(testText);
});
test.each`
testText | args | expectedText
${'{{fileName}} is invalid.'} | ${{ fileName: 'example_5.csv' }} | ${'example_5.csv is invalid.'}
${'{{fileName}} {is}.'} | ${{ fileName: ' ' }} | ${' {is}.'}
${'{{number}} of {{total}}'} | ${{ number: 0, total: 999 }} | ${'0 of 999'}
${'There was an error:\n{{error}}'} | ${{ error: 'Failed' }} | ${'There was an error:\nFailed'}
${'Click:{{li}}{{li2}}{{li_3}}'} | ${{ li: '', li2: 'https://', li_3: '!##$%' }} | ${'Click:https://!##$%'}
${'{{happy}}😏y✔{{sad}}{{laugh}}'} | ${{ happy: '😃', sad: '😢', laugh: '🤣' }} | ${'😃😏y✔😢🤣'}
`('should return correctly while handling arguments in different scenarios', ({ testText, args, expectedText }) => {
const translatedText = i18nextMocks.mockT(testText, args);
expect(translatedText).toBe(expectedText);
});
});
describe('language', () => {
it('should return language', async () => {
const language = i18nextMocks.default.language;
expect(language).toBe('en');
});
});
});

Related

Testing custom hook React

I created a hook
export function useRedirectStartParams() {
const scenarios: IScenario[] = useSelector(scenariosSelector);
const block: IBlockItem = useSelector(selectedBlockSelector);
const [redirects, setRedirects] = useState<IDropdownEl[]>([]);
useEffect(() => {
const newRedirects =
block?.block_data?.redirects?.map((block: any) => {
const scenarioName = scenarios.find(
(scenario) => scenario.id === block.scenario_id
)?.name;
return {
name: scenarioName,
val: {
scenarioId: block.scenario_id,
blockId: block.id,
},
};
}) || [];
setRedirects(newRedirects);
}, [block, scenarios]);
return { redirects };
}
use it in my component
const { redirects } = useRedirectStartParams();
and then try to test it with jest like this
import { useRedirectStartParams } from "#hooks/useRedirectStartParams";
jest.mock("#hooks/useRedirectStartParams");
beforeEach(() => {
useRedirectStartParams.mockReturnValueOnce({
redirects: [{ name: "ad", val: "123" }],
});
})
but got error that redirects are undefined.
I also tried to mock hook this way
jest.mock("#hooks/useRedirectStartParams", () => jest.fn());
And it didn't help me
I expect that redirects won't be undefined. Not renderHook. i want to mock value of hook for component

Property *** does not exist on type 'ExoticComponent<any>

I try to do code split in React / Next.js project, but facing with obstacles.
I am following the howto here: https://reactjs.org/docs/code-splitting.html
Importing here:
const { SHA512 } = React.lazy(() => import("crypto-js"));
and using in useEffect here:
useEffect(() => {
setTimeout(() => {
let window2: any = window;
if (window2.bp && process.env.NEXT_PUBLIC_BARION_ID) {
const { totalPriceInt, unitPriceInt } = calcUnitAndTotalPrice(
startPaymentIn,
buyTicketData
);
window2.bp("track", "initiateCheckout", {
contentType: "Product",
currency: "HUF",
id: startPaymentIn.eventId,
name: buyTicketData?.name,
quantity: startPaymentIn.quantity ?? 1.0,
unit: "db",
imageUrl: `https://ticket-t01.s3.eu-central-1.amazonaws.com/${buyTicketData?.imgId}_0.cover.jpg`,
list: "ProductPage",
});
window2.bp("track", "setUserProperties", {
userId: SHA512(getTempUserId(localStorage)).toString(), // <--- HERE
});
}
}, 4000);
}, []);
but get this error:
./components/LoginAndRegistration.tsx:25:9
Type error: Property 'SHA512' does not exist on type 'ExoticComponent<any> & { readonly _result: ComponentType<any>; }'.
23 | import { GoogleLogin } from "react-google-login";
24 | import axios from "axios";
> 25 | const { SHA512 } = React.lazy(() => import("crypto-js"));
| ^
26 |
27 | interface LoginAndRegistrationProps {
28 | isRegistration: boolean;
error Command failed with exit code 1.
Do you know maybe why it is wrong?
Before it worked without lazy loading:
import { SHA512 } from "crypto-js";
I also tryed Next.js workaround:
import dynamic from "next/dynamic";
const { SHA512 } = dynamic(() => import("crypto-js"));
but got this error:
Type error: 'await' expressions are only allowed within async functions and at the top levels of modules.
130 |
131 | useEffect(() => {
> 132 | const { SHA512 } = await import("crypto-js");
| ^
133 | setTimeout(() => {
134 | let window2: any = window;
135 | if (window2.bp && process.env.NEXT_PUBLIC_BARION_ID) {
As mentioned here,
React.lazy takes a function that must call a dynamic import(). This must return a Promise which resolves to a module with a default export containing a React component.
So you can't use React.lazy for the crypto-js package because it doesn't export a React component.
You can use dynamic import in NextJS. As such:
useEffect(() => {
import('crypto-js').then(({ SHA512 }) => {
// Use SHA512 here
});
}, []);
import("crypto-js") returns a promise, that once resolved, has the API of the module. It's not a React component, hence you can't use React.lazy() on it. (You could use React.lazy with a module that exports a component.)
If you need to lazily load crypto-js within a React component, and then do things, you can do
function MyComponent() {
const [cryptoJs, setCryptoJs] = React.useState(null);
React.useEffect(() => import("crypto-js").then(setCryptoJs), []);
React.useEffect(() => {
if(!cryptoJs) return; // not loaded yet...
const { SHA512 } = cryptoJs;
// ...
}, [cryptoJs]);
}
or
function MyComponent() {
React.useEffect(() => {
import("crypto-js").then(({ SHA512 }) => {
// ...
});
}, []);
}
However, depending on your bundler and bundler configuration it might be better to just use a regular import for the library, and instead lazily import (e.g. with React.lazy) the module that has this component that requires the library.

How to correctly wait for Translation with react-i18next

I'm using react-18next to load translations throughout my react app. I have a problem making my app waiting for translations. This breaks our tests in Jenkins as they are searching for translated keys in many cases.
i18n.tsx:
i18n
.use(initReactI18next)
.init({
resources, // set ressources to be used
lng: "en", // set default language
keySeparator: false,
interpolation: {
escapeValue: false
},
react: {
useSuspense: false,
}
});
Note: I tried both using Suspense as well as not using suspense, the useSuspense Flag was matching the attempt (true/default for Suspense).
Attempt with ready:
const SiteLabel: React.FunctionComponent<ISiteLabelProps> = (props) => {
const { t, ready } = useTranslation(undefined, {
useSuspense: false
});
const getTo = (): string => {
return "/" + props.module.toLowerCase();
}
const getLabel = (): string => {
return props.module.toLowerCase() + ":GEN_PAGETITLE";
}
if(!ready)
return (<div>Loading Content...</div>);
return (
<ConfirmLink
content={t(getLabel())}
dirty={props.dirty}
setDirty={props.setDirty}
className={Styles.label + " id-btn-riskshield"}
to={getTo()}
toState={null}
/>
);
}
export default SiteLabel;
Attempt with Suspense:
const SiteLabel: React.FunctionComponent<ISiteLabelProps> = (props) => {
const { t } = useTranslation();
const getTo = (): string => {
return "/" + props.module.toLowerCase();
}
const getLabel = (): string => {
return props.module.toLowerCase() + ":GEN_PAGETITLE";
}
return (
<Suspense fallback={<div>Loading...</div>}
<ConfirmLink
content={t(getLabel())}
dirty={props.dirty}
setDirty={props.setDirty}
className={Styles.label + " id-btn-riskshield"}
to={getTo()}
toState={null}
/>
</Suspense>
);
}
export default SiteLabel;
Neither version seems to work for me, I can see the translation key displayed for a brief moment. Do I need to go deeper until the point where the translation is actually written out instead of passed to the next prop or what is the Error I'm committing? I'm not using next.js for building/deployment. I would prefer a global solution right in app.tsx.
Maybe this could help you: https://www.i18next.com/overview/api#init
I use the callback to set a "all translations are really really loaded" state in my App component.
i18n
.use(initReactI18next)
.init(
interpolation: {
escapeValue: false
},
fallbackLng: "en",
react: {
useSuspense: false,
}, () => {
setTranslationsLoaded(true);
});
export const App = () => {
const [translationsLoaded, setTranslationsLoaded] = useState(false)
//...
return (
translationsLoaded ? <MyAppContent /> : <MySpinnerOrSomething />
)
}
try with wait option
react: { wait: true, useSuspense: false }

StyledComponents/Jest: Trying to insert a new style tag, but the given Node is unmounted

I have been looking around for answers, I find few ones very similar to mine, but still could not find a solution for the problem I had. I have also tried reinstalling my modules, running npm link after reinstalling my modules. It just has not really worked out for me.
This is the complete error:
Trying to insert a new style tag, but the given Node is unmounted!
- Are you using a custom target that isn't mounted?
- Does your document not have a valid head element?
- Have you accidentally removed a style tag manually?
18 | } from '../visual-elements/responsive-button'
19 |
> 20 | const StyledSelect = styled(Select)`
| ^
21 | width: 100%;
22 | `
23 |
As for my testing File, I am using Jest/MSW/React Testing Library + it is TypeScript.
import React from 'react'
import { useFlag } from '#boxine/tonies-ui'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import * as utils from '../../mocks/utils'
import { render, screen } from '../../utils/test-utils'
import { useAudioLibraryFlag } from '../../hooks/useAudioLibraryFlag'
import Home from '.'
jest.mock('../../hooks/useAudioLibraryFlag')
const requiredHomeProps = {
location: {
search: '',
},
}
const households = [
utils.generateHousehold(),
utils.generateHousehold(),
utils.generateHousehold(),
]
const server = setupServer(
rest.post('https://api.tonie.cloud/v2/graphql/', (req, res, ctx) => {
return res(
ctx.json({
data: {
households,
},
})
)
})
)
describe('Home component', () => {
beforeEach(() => {
;(useAudioLibraryFlag as jest.Mock).mockImplementation(() => true)
;(useFlag as jest.Mock).mockImplementation(() => true)
})
// Enable API mocking before tests.
beforeAll(() => server.listen())
// Reset any runtime request handlers we may add during the tests.
afterEach(() => server.resetHandlers())
afterAll(() => {
;(useAudioLibraryFlag as jest.Mock).mockRestore()
;(useFlag as jest.Mock).mockRestore()
// Disable API mocking after the tests are done.
server.close()
})
test('renders correctly', () => {
render(<Home {...requiredHomeProps} />)
expect(
screen.getByText(/Ready to add some new Tonies to your collection/)
).toBeInTheDocument()
expect(screen.getByText('Go to the shop')).toBeInTheDocument()
expect(screen.getByText('to download')).toBeInTheDocument()
})
})```

React Native - How to initialize OneSignal in a stateless function component?

I've read OneSignal installation here: https://documentation.onesignal.com/docs/react-native-sdk-setup#step-5---initialize-the-onesignal-sdk. The documentation is written in a component class style.
How to add the OneSignal in the stateless function component on React Native app?
I've tried using useEffect but OneSignal still can't detect my app.
Thanks.
Enjoy
import OneSignal from 'react-native-onesignal';
const SplashScreen = () => {
useEffect(() => {
OneSignal.setLogLevel(6, 0);
OneSignal.init('Your-id-app', {
kOSSettingsKeyAutoPrompt: false,
kOSSettingsKeyInAppLaunchURL: false,
kOSSettingsKeyInFocusDisplayOption: 2,
});
OneSignal.inFocusDisplaying(2);
OneSignal.addEventListener('received', onReceived);
OneSignal.addEventListener('opened', onOpened);
OneSignal.addEventListener('ids', onIds);
return () => {
OneSignal.removeEventListener('received', onReceived);
OneSignal.removeEventListener('opened', onOpened);
OneSignal.removeEventListener('ids', onIds);
};
}, []);
const onReceived = (notification) => {
console.log('Notification received: ', notification);
};
const onOpened = (openResult) => {
console.log('Message: ', openResult.notification.payload.body);
console.log('Data: ', openResult.notification.payload.additionalData);
console.log('isActive: ', openResult.notification.isAppInFocus);
console.log('openResult: ', openResult);
};
const onIds = (device) => {
console.log('Device info: ', device);
};
return (
...
);
};
I got it worked by added these lines on App.js:
import OneSignal from 'react-native-onesignal';
...
...
const App = () => {
...
onIds = (device) => {
if (state.playerId) {
OneSignal.removeEventListener('ids', onIds);
return;
} else {
setState({ ...state, playerId: device.userId });
console.log('Device info: ', state.playerId);
}
};
OneSignal.addEventListener('ids', onIds);
...
}
And in index.js:
import OneSignal from 'react-native-onesignal'; // Import package from node modules
OneSignal.setLogLevel(6, 0);
// Replace 'YOUR_ONESIGNAL_APP_ID' with your OneSignal App ID.
OneSignal.init(YOUR_ONESIGNAL_APP_ID, {
kOSSettingsKeyAutoPrompt: false,
kOSSettingsKeyInAppLaunchURL: false,
kOSSettingsKeyInFocusDisplayOption: 2
});
OneSignal.inFocusDisplaying(2); // Controls what should happen if a notification is received while the app is open. 2 means that the notification will go directly to the device's notification center.

Resources