Which technique is recommended to use when making a React App International?
I am thinking of coding the following:
Create a React Context "Languages"
Create different modules exporting a map with all strings for each language "pt", "en", "fr", "sp", "it"...
Try to load the default language from AsyncStorage in my Splash Screen, using a method provided by the Languages Context Provider.
If not found, get the user language based on his location.
For me this has sense, but there might be other easier ways to achieve the same, feeling more profesional.
// This is my Languages Context
import React, { createContext, useState } from "react";
import * as Localization from "expo-localization";
import { VALID_LANGUAGES } from "../../utils/languages/constants"; // "English", "Spanish", "Portuguese"...
import languages from "../../languages"; // en, sp, pt, ... modules with all texts (key, value)
import AsyncStorage from "../../lib/async-storage/AsyncStorage";
const LanguagesContext = createContext(null);
export default LanguagesContext;
export function LanguagesProvider({ children }) {
const [language, setLanguage] = useState(undefined);
const loadLanguage = async () => {
// Get the default language from async storage
const language = await AsyncStorage.getData("language");
// TODO - if undefined -> get user location and use the corresponding lang
// TODO - if not supported, use english
if (!language) {
setLanguage(VALID_LANGUAGES[0]);
}
};
const changeLanguage = async (language) => {
// Update state
setLanguage(language);
// Save language in async storage
await AsyncStorage.storeData("language", language);
};
return (
<LanguagesContext.Provider
value={{ language, loadLanguage, changeLanguage }}
>
{children}
</LanguagesContext.Provider>
);
}
export { LanguagesProvider };
What I am doing is using the method "loadLanguage" in my Splash Screen Component, something that ensures the app to be ready for use before rendering any content. What do you think about this technique?
How can I use the texts in the app? I thought to make another method getAppTexts() in the Context provider, just to return the correct map from my languages modules ("en", "it", "pt", ...)
Any library and an example?
Thanks you.
First you just init a new react-native project
$ npx react-native init rn_example_translation
i would like to prefer to create a src folder to put all our JS code so we modify the index.js at the project root dir below like this:
import {AppRegistry} from 'react-native';
import App from './src/App';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => App);
Manage translations
We will translate the app using 'i18n-js' module so we install it with:
$ npm install --save i18n-js
Then create a file 'i18n.js' with:
import {I18nManager} from 'react-native';
import i18n from 'i18n-js';
import memoize from 'lodash.memoize';
export const DEFAULT_LANGUAGE = 'en';
export const translationGetters = {
// lazy requires (metro bundler does not support symlinks)
en: () => require('./assets/locales/en/translations.json'),
fr: () => require('./assets/locales/fr/translations.json'),
};
export const translate = memoize(
(key, config) => i18n.t(key, config),
(key, config) => (config ? key + JSON.stringify(config) : key),
);
export const t = translate;
export const setI18nConfig = (codeLang = null) => {
// fallback if no available language fits
const fallback = {languageTag: DEFAULT_LANGUAGE, isRTL: false};
const lang = codeLang ? {languageTag: codeLang, isRTL: false} : null;
const {languageTag, isRTL} = lang ? lang : fallback;
// clear translation cache
translate.cache.clear();
// update layout direction
I18nManager.forceRTL(isRTL);
// set i18n-js config
i18n.translations = {[languageTag]: translationGetters[languageTag]()};
i18n.locale = languageTag;
return languageTag;
};
Then Create Empty Translations files like below:
'./src/assets/locales/en/translations.json' and './src/assets/locales/fr/translations.json'
now we can translate app JS string in french and english just like below:
i18n.t('Hello world!')
Switch locale in app
Now we'll setup a react context to keep the current user language and a switch to give the option to the user to change the language. It is cool to translate the strings but the translated strings have to match with user language.
To keep current user language along the app with a react context, we need to create a file 'LocalisationContext.js' in context folder with:
import React from 'react';
const LocalizationContext = React.createContext();
export default LocalizationContext;
Now in your App.js
import React, {useEffect, useCallback} from 'react';
import {StyleSheet} from 'react-native';
import * as i18n from './i18n';
import LocalizationContext from './context/LocalizationContext';
import HomeScreen from './HomeScreen';
const App: () => React$Node = () => {
const [locale, setLocale] = React.useState(i18n.DEFAULT_LANGUAGE);
const localizationContext = React.useMemo(
() => ({
t: (scope, options) => i18n.t(scope, {locale, ...options}),
locale,
setLocale,
}),
[locale],
);
return (
<>
<LocalizationContext.Provider value={localizationContext}>
<HomeScreen localizationChange={handleLocalizationChange} />
</LocalizationContext.Provider>
</>
);
};
and create the 'HomeScreen.js' file:
import React, {useContext} from 'react';
import {StyleSheet, SafeAreaView, Text, Button} from 'react-native';
import LocalizationContext from './context/LocalizationContext';
function HomeScreen(props) {
const {localizationChange} = props;
const {t, locale, setLocale} = useContext(LocalizationContext);
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>React-Native example translation</Text>
<Text style={styles.subtitle}>{t('Home screen')}</Text>
<Text style={styles.paragraph}>Locale: {locale}</Text>
{locale === 'en' ? (
<Button title="FR" onPress={() => localizationChange('fr')} />
) : (
<Button title="EN" onPress={() => localizationChange('en')} />
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
title: {
textAlign: 'center',
fontSize: 22,
marginBottom: 40,
},
subtitle: {
textAlign: 'center',
fontSize: 18,
marginBottom: 10,
},
paragraph: {
fontSize: 14,
marginBottom: 10,
},
langButton: {
flex: 1,
},
});
export default HomeScreen;
here we can translate the strings in js.
Handle localization system change
now we have to install localization module now:
$ npm install --save react-native-localize
then modify the app.js to this
import React, {useEffect, useCallback} from 'react';
import {StyleSheet} from 'react-native';
import * as RNLocalize from 'react-native-localize';
import * as i18n from './i18n';
import LocalizationContext from './context/LocalizationContext';
import HomeScreen from './HomeScreen';
const App: () => React$Node = () => {
const [locale, setLocale] = React.useState(i18n.DEFAULT_LANGUAGE);
const localizationContext = React.useMemo(
() => ({
t: (scope, options) => i18n.t(scope, {locale, ...options}),
locale,
setLocale,
}),
[locale],
);
const handleLocalizationChange = useCallback(
(newLocale) => {
const newSetLocale = i18n.setI18nConfig(newLocale);
setLocale(newSetLocale);
},
[locale],
);
useEffect(() => {
handleLocalizationChange();
RNLocalize.addEventListener('change', handleLocalizationChange);
return () => {
RNLocalize.removeEventListener('change', handleLocalizationChange);
};
}, []);
return (
<>
<LocalizationContext.Provider value={localizationContext}>
<HomeScreen localizationChange={handleLocalizationChange} />
</LocalizationContext.Provider>
</>
);
};
then 'i18n.js' file like this:
import {I18nManager} from 'react-native';
import * as RNLocalize from 'react-native-localize';
import i18n from 'i18n-js';
import memoize from 'lodash.memoize';
export const DEFAULT_LANGUAGE = 'en';
export const translationGetters = {
// lazy requires (metro bundler does not support symlinks)
en: () => require('./assets/locales/en/translations.json'),
fr: () => require('./assets/locales/fr/translations.json'),
};
export const translate = memoize(
(key, config) => i18n.t(key, config),
(key, config) => (config ? key + JSON.stringify(config) : key),
);
export const t = translate;
export const setI18nConfig = (codeLang = null) => {
// fallback if no available language fits
const fallback = {languageTag: DEFAULT_LANGUAGE, isRTL: false};
const lang = codeLang ? {languageTag: codeLang, isRTL: false} : null;
# Use RNLocalize to detect the user system language
const {languageTag, isRTL} = lang
? lang
: RNLocalize.findBestAvailableLanguage(Object.keys(translationGetters)) ||
fallback;
// clear translation cache
translate.cache.clear();
// update layout direction
I18nManager.forceRTL(isRTL);
// set i18n-js config
i18n.translations = {[languageTag]: translationGetters[languageTag]()};
i18n.locale = languageTag;
return languageTag;
};
Generate translations
in order to generate language files you can use i18next-scanner.
we need to install it globally
npm install -g i18next-scanner
create a 'i18next-scanner.config.js' file at your project dir root with:
const fs = require('fs');
const chalk = require('chalk');
module.exports = {
input: [
'src/**/*.{js,jsx}',
// Use ! to filter out files or directories
'!app/**/*.spec.{js,jsx}',
'!app/i18n/**',
'!**/node_modules/**',
],
output: './',
options: {
debug: false,
removeUnusedKeys: true,
func: {
list: ['i18next.t', 'i18n.t', 't'],
extensions: ['.js', '.jsx'],
},
trans: {
component: 'Trans',
i18nKey: 'i18nKey',
defaultsKey: 'defaults',
extensions: [],
fallbackKey: function (ns, value) {
return value;
},
acorn: {
ecmaVersion: 10, // defaults to 10
sourceType: 'module', // defaults to 'module'
// Check out https://github.com/acornjs/acorn/tree/master/acorn#interface for additional options
},
},
lngs: ['en', 'fr'],
ns: ['translations'],
defaultLng: 'en',
defaultNs: 'translations',
defaultValue: '__STRING_NOT_TRANSLATED__',
resource: {
loadPath: 'src/assets/locales/{{lng}}/{{ns}}.json',
savePath: 'src/assets/locales/{{lng}}/{{ns}}.json',
jsonIndent: 2,
lineEnding: '\n',
},
nsSeparator: false, // namespace separator
keySeparator: false, // key separator
interpolation: {
prefix: '{{',
suffix: '}}',
},
},
transform: function customTransform(file, enc, done) {
'use strict';
const parser = this.parser;
const options = {
presets: ['#babel/preset-flow'],
plugins: [
'#babel/plugin-syntax-jsx',
'#babel/plugin-proposal-class-properties',
],
configFile: false,
};
const content = fs.readFileSync(file.path, enc);
let count = 0;
const code = require('#babel/core').transform(content, options);
parser.parseFuncFromString(
code.code,
{list: ['i18next._', 'i18next.__']},
(key, options) => {
parser.set(
key,
Object.assign({}, options, {
nsSeparator: false,
keySeparator: false,
}),
);
++count;
},
);
if (count > 0) {
console.log(
`i18next-scanner: count=${chalk.cyan(count)}, file=${chalk.yellow(
JSON.stringify(file.relative),
)}`,
);
}
done();
},
};
Here we can use the command :
$ i18next-scanner
now it will generate prefill translation files
'./src/assets/locales/en/translations.json' and './src/assets/locales/fr/translations.json'.
we just need to change in these files by right translation
now run the App;
npx react-native run-android
It will run succesfully.
if you want to switch language then This is the top Solution. try this one.
i18n.locale = "ar";
i18n.reset();
Related
I'm fairly new to React testing library and am using a function within a useEffect to decode a user token from keycloak when they sign up, to determine what type of user they are and render different Menu's based on that. This new Jwt function I created though, is causing a lot of my previously working tests within other files to fail as it throws an error like below:
I'm sure how to deal with this error on the testing side, should I be mocking this decoder function within the test file?
This is my main file:
import React, {useEffect, useState, useMemo, useCallback} from "react";
import {withTranslation} from "react-i18next";
import "./index.scss";
import {historyObject} from "historyObject";
import {Heading} from "#xriba/ui";
import { keycloak } from "utils/keycloak";
export const Menu = (props) => {
const {t} = props;
const [userHasCommunities, setUserHasCommunities] = useState(false);
useEffect(() => {
function parseJwt (token) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
if (JSON.parse(jsonPayload).communities?.length > 0){
setUserHasCommunities(true)
}
return JSON.parse(jsonPayload);
};
parseJwt(keycloak.token);
}, [userHasCommunities])
const buildItem = useCallback((item) => {
const isCurrent = historyObject.location.pathname.includes(item.path);
return <li key={item.path} className={isCurrent ? "selected" : ""} onClick={() => historyObject.push(item.path)}>
<Heading level={"4"}>{t(item.title)}</Heading>
</li>
}, [t])
const renderMenu = useMemo(() => {
let menuItems = []
if (userHasCommunities){
menuItems = [
{
path: "/portfolio",
title: "pages.portfolio.menu_entry"
},
{
path: "/transactions",
title: "pages.transactions.menu_entry"
},
{
path: "/community",
title: "pages.community.menu_entry"
},
{
path: "/reports",
title: "pages.reports.menu_entry"
}
]
} else {
menuItems = [
{
path: "/portfolio",
title: "pages.portfolio.menu_entry"
},
{
path: "/transactions",
title: "pages.transactions.menu_entry"
},
{
path: "/reports",
title: "pages.reports.menu_entry"
}
]
}
return menuItems.map(i => buildItem(i))
}, [userHasCommunities, buildItem])
return <div className={"menu"}>
<ul>
{renderMenu}
</ul>
</div>
}
export default withTranslation()(Menu)
And my current test file:
import React from "react";
import {shallow} from "enzyme";
import {Menu} from "features/layouts/logged-in/menu/index";
import {historyObject} from "historyObject";
import { keycloak } from "utils/keycloak";
jest.mock("utils/keycloak");
jest.mock("historyObject")
describe("Menu test", () => {
const props = {
t: jest.fn(),
};
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(React, "useEffect").mockImplementationOnce(x => x()).mockImplementationOnce(x => x()).mockImplementationOnce(x => x()).mockImplementationOnce(x => x());
});
it('should render Menu', () => {
const wrapper = shallow(<Menu {...props} />);
expect(wrapper).toBeDefined();
wrapper.find('li').at(0).prop('onClick')();
expect(historyObject.push).toHaveBeenCalled()
});
it('should render Menu with an object as current', () => {
historyObject.location.pathname = "/portfolio"
const wrapper = shallow(<Menu {...props} />);
expect(wrapper).toBeDefined();
expect(wrapper.find('.selected')).toHaveLength(1);
});
});
Thanks in advance for any advice!
I'm trying to implement the Adyen dropin payment UI using NextJS but I'm having trouble initializing the Adyen dropin component.
I'm need to dynamically import Adyen web or I get the error window is not defined however, after reading through the NextJS docs, dynamic import creates a component which I can't figure out how to use as a constructor.
I tried the code below but receive the error TypeError: AdyenCheckout is not a constructor
I'm new to NextJS and am at a total loss as to how I should import and initialize Adyen.
Can anyone point me in the right direction?
import Head from 'next/head';
import { useRef, useEffect, useState } from 'react';
import {callServer, handleSubmission} from '../util/serverHelpers';
//dynamic import below. Imports as a component
//import dynamic from 'next/dynamic';
//const AdyenCheckout = dynamic(() => import('#adyen/adyen-web'), {ssr: false});
import '#adyen/adyen-web/dist/adyen.css';
export default function Dropin(){
const dropinContainer = useRef(null);
const [paymentMethods, setPaymentMethods] = useState();
//const [dropinHolder, setDropinHolder] = useState();
//Get payment methods after page render
useEffect( async () => {
const response = await callServer(`${process.env.BASE_URL}/api/getPaymentMethods`);
setPaymentMethods(prev => prev = response);
},[]);
//Adyen config object to be passed to AdyenCheckout
const configuration = {
paymentMethodsResponse: paymentMethods,
clientKey: process.env.CLIENT_KEY,
locale: "en_AU",
environment: "test",
paymentMethodsConfiguration: {
card: {
showPayButton: true,
hasHolderName: true,
holderNameRequired: true,
name: "Credit or debit card",
amount: {
value: 2000,
currency: "AUD"
}
}
},
onSubmit: (state, component) => {
if (state.isValid) {
handleSubmission(state, component, "/api/initiatePayment");
}
},
onAdditionalDetails: (state, component) => {
handleSubmission(state, component, "/api/submitAdditionalDetails");
},
};
//const checkout = new AdyenCheckout(configuration);
const AdyenCheckout = import('#adyen/adyen-web').default;
const adyenCheckout = new AdyenCheckout(configuration);
const dropin = adyenCheckout.create('dropin').mount(dropinContainer.current);
return (
<div>
<Head>
<title>Dropin</title>
</Head>
<div ref={dropin}></div>
</div>
)
}
I was able to resolve the issue by importing the module using the default value inside an async function nested in the useEffect function.
import Head from 'next/head';
import { useRef, useEffect, useState } from 'react';
import {callServer, handleSubmission} from '../util/serverHelpers';
import '#adyen/adyen-web/dist/adyen.css';
export default function Dropin(){
const dropinContainer = useRef();
const [paymentMethods, setPaymentMethods] = useState({});
useEffect(() => {
const init = async () => {
const response = await callServer(`${process.env.BASE_URL}/api/getPaymentMethods`)
.then(setPaymentMethods(response));
console.log(paymentMethods);
const configuration = {
paymentMethodsResponse: paymentMethods,
clientKey: process.env.CLIENT_KEY,
locale: "en_AU",
environment: "test",
paymentMethodsConfiguration: {
card: {
showPayButton: true,
hasHolderName: true,
holderNameRequired: true,
name: "Credit or debit card",
amount: {
value: 2000,
currency: "AUD"
}
}
},
onSubmit: (state, component) => {
if (state.isValid) {
handleSubmission(state, component, "/api/initiatePayment");
}
},
onAdditionalDetails: (state, component) => {
handleSubmission(state, component, "/api/submitAdditionalDetails");
},
};
console.log(configuration.paymentMethodsResponse);
const AdyenCheckout = (await import('#adyen/adyen-web')).default;
const checkout = new AdyenCheckout(configuration);
checkout.create('dropin').mount(dropinContainer.current);
}
init();
},[]);
return (
<div>
<Head>
<title>Dropin</title>
</Head>
<div ref={dropinContainer}></div>
</div>
)
}
I am trying to make a simple React Native app. For the start there is one default language, there is a button, by clicking on it changing the language and reloads the app with the new language. The idea is to store that language in the AsyncStorage (#react-native-async-storage/async-storage).
Here is my cache.js:
import AsyncStorage from '#react-native-async-storage/async-storage';
import moment from 'moment';
const prefix = 'cache';
const expiryInMinutes = 5;
const store = async (key, value) => {
try {
const item = {
value,
timestamp: Date.now()
}
await AsyncStorage.setItem(prefix + key, JSON.stringify(item));
} catch (error) {
// console.log(error);
}
}
const get = async (key) => {
try {
const value = await AsyncStorage.getItem(prefix + key);
const item = JSON.parse(value);
if(!item) return null;
if(isExpired(item)) {
await AsyncStorage.removeItem(prefix + key);
return null;
}
return item.value;
} catch(error) {
// console.log(error);
}
}
const isExpired = (item) => {
const now = moment(Date.now());
const storedTime = item.timestamp;
return now.diff(storedTime, 'minutes') > expiryInMinutes;
}
export default { store, get };
and here is my App.js:
import React from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import * as Updates from 'expo-updates';
import i18n from './app/locales/locales';
import cache from './app/utility/cache';
export default function App() {
// When a value is missing from a language it'll fallback to another language with the key present.
i18n.fallbacks = true;
i18n.locale = 'de';
cache.get('lang').then(function(result) {
if(result) {
i18n.locale = result;
// console.log("New lang " + result);
}
});
const handleRestart = async () => {
// console.log("Storing lang: ");
// console.log("Reseting lang: ");
cache.store("lang", 'en');
// console.log(cache.get('lang'));
// console.log("Hmm");
// console.log(i18n.locale);
await Updates.reloadAsync();
}
return (
<View style={styles.container}>
<Text>{i18n.t('hello')}</Text>
<Button title="Restart" onPress={handleRestart} />
</View>
);
}
It starts in German, when I press the button it stores the English, but it doesn't change the application language. What am I doing wrong?
I'm think UI is not getting updated, because react re-renders only if props or state gets changed. Since non of those changed, you are not going to see any updated on UI.
try something like this
const [lang, setLang] = React.useState(langValueFromStorage);
const handleRestart = () => {
cache.get('lang').then(function(result) {
if(result) {
i18n.locale = result;
setLang(result)
}
});
}
I have found a solution though it looks a bit hacky. i18n.js:
import AsyncStorage from '#react-native-async-storage/async-storage';
import i18n from 'i18next';
import {initReactI18next} from 'react-i18next';
import en from './src/localization/en/en.json';
import uz from './src/localization/uz/uz.json';
const resources = {
en: {
translation: en,
},
uz: {
translation: uz,
},
};
const getDefaultLang = async () => {
const storedLang = await AsyncStorage.getItem('lang');
return i18n
.use(initReactI18next)
.init({
resources,
lng: storedLang ? storedLang : 'uz',
interpolation: {
escapeValue: false,
},
fallbackLng: ['uz', 'en'],
});
};
export default getDefaultLang();
There is also a plugin used for detecting lang from asyncstorage which I have just found
I am using i18n-js within my expo project to translate my app.
This is how I configure it:
import React from 'react';
import * as Localization from 'expo-localization';
import i18n from 'i18n-js';
export default function configureI18n(translations) {
i18n.fallbacks = true;
i18n.translations = translations;
i18n.locale = Localization.locale;
const [locale, setLocale] = React.useState(Localization.locale);
const localizationContext = React.useMemo(() => ({
t: (scope, options) => i18n.t(scope, { locale, ...options }),
locale,
setLocale,
}), [locale]);
return localizationContext;
}
I pass this to my AppContext and try to use setLocale within my view:
function HomeView(props) {
const { locale, setLocale } = useContext(AppContext);
return (
<View>
<Button
style={{ marginTop: 4 }}
icon="translate"
mode="contained"
title="toggle navigation"
onPress={() => setLocale(locale.includes('en') ? 'fr' : 'en')}
>
{locale.includes('en') ? 'FR' : 'EN'}
</Button>
</View>
);
}
The function is called, but the text is still in english, what am I doing wrong ?
You need to setup the translation in your top level component, like App.js. Then, you have to create 2 json files: fr.json and en.json in /src/locales/.
Finally, in any screen, you have to import i18n and use the t() function to translate strings.
In App.js
import React, { useEffect, useState } from 'react'
import { loadLocale } from './locales/i18n'
export default function App() {
const [theme, setTheme] = useState(null)
useEffect(() => {
init()
}, [])
const init = async () => {
await loadLocale()
}
return (
<AppContainer />
)
}
In i18n.js
import * as Localization from 'expo-localization'
import i18n from 'i18n-js'
i18n.defaultLocale = 'fr'
i18n.locale = 'fr'
i18n.fallbacks = true
export const loadLocale = async () => {
for (const locale of Localization.locales) {
if (i18n.translations[locale.languageCode] !== null) {
i18n.locale = locale.languageCode
switch (locale.languageCode) {
case 'en':
import('./en.json').then(en => {
i18n.translations = { en }
})
break
default:
case 'fr':
import('./fr.json').then(fr => {
i18n.translations = { fr }
})
break
}
break
}
}
}
export default i18n
In HomeView.js
import React from 'react'
import i18n from '../locales/i18n'
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1 }}>
<Text>{i18n.t('home.welcome')}</Text>
<Text>{i18n.t('home.content')}</Text>
</View>
)
}
export default HomeView
In fr.json
{
"home": {
"welcome": "Bienvenue",
"content": "Du contenu ici"
}
}
In order to change between languages and avoid getting [missing "X.string" translation] error you can add a function like this "changeLanguage" function below:
// Imagine you have spanish and english languages support
import es from './locales/es';
import en from './locales/en';
const availableTranslations = {
es,
en
};
/* This function is useful to load spanish or english language translations and set the corresponding locale */
const changeLanguage = (languageCode) => {
I18n.translations = {
[languageCode]: availableTranslations[languageCode]
};
I18n.locale = languageCode;
};
I'm trying to test a connected TSX component. I have tested connected components before and I exactly know how to implement it, but seems like there is some issue in the way that jest and typescript interact.
What I have tried ?
I have exported an unconnected component for testing purposes
I have created a mock store and wrapper the component around a provider in the test file
I have modified jest.config.js as suggest by the error
I keep getting the same error!
Cannot find module 'react' from 'Provider.js'
However, Jest was able to find:
'components/Provider.js'
You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['web.js', 'js', 'web.ts', 'ts', 'web.tsx', 'tsx', 'json', 'web.jsx', 'jsx', 'node'].
See https://jestjs.io/docs/en/configuration#modulefileextensions-array-string
However, Jest was able to find:
'./App.test.tsx'
'./App.tsx'
You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['web.js', 'js', 'web.ts', 'ts', 'web.tsx', 'tsx', 'json', 'web.jsx', 'jsx', 'node'].
See https://jestjs.io/docs/en/configuration#modulefileextensions-array-string
at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:259:17)
at Object.<anonymous> (../node_modules/react-redux/lib/components/Provider.js:10:38)
My component is as below (App.tsx):
import React from "react";
import { connect } from "react-redux";
import { Album, Photo, fetchAlbums, fetchPhotos } from "../actions";
import { StoreState } from "../reducers";
// *Notice: in this file we have used React.UseEffect and React.UseState instead of importing
// hooks directly from React. That's for the reasons of testing and how Enzyme has not yet adopted
// very well with hooks.
// the type of your action creators has been intentionally set to "any", as typescript does not play well with redux-thunk
interface AppProps {
albums?: Album[];
photos?: Photo[];
fetchAlbums?(): any;
fetchPhotos?(id: number): any;
}
export const _App = ({
albums,
photos,
fetchAlbums,
fetchPhotos
}: AppProps) => {
// setting the initial state of the loader and thmbnail
const [fetching, setFetching] = React.useState(false);
const [image, setImage] = React.useState();
// setting the state back to false once our data updates
React.useEffect(() => {
setFetching(false);
}, [albums, photos]);
// click evnet handler
const ClickHandler = (): void => {
fetchAlbums();
setFetching(true);
};
// album entry event handler
const AlbumClickHandler = (id: number): void => {
fetchPhotos(id);
};
const display = (id: number): JSX.Element[] => {
const relevantThumbs = photos.filter(photo => photo.albumId === id);
return relevantThumbs.map((thumb, idx) => {
return (
<img
onClick={() => setImage(thumb.id)}
key={idx}
alt={thumb.title}
src={image === thumb.id ? thumb.url : thumb.thumbnailUrl}
></img>
);
});
};
// helper function to render jsx elements
const renderList = (): JSX.Element[] =>
albums.map(album => (
<div className="albums" key={album.id}>
<h2 onClick={() => AlbumClickHandler(album.id)}>{album.title}</h2>
{display(album.id).length !== 0 ? (
<div className="albums__thumbnails">{display(album.id)}</div>
) : null}
</div>
));
return (
<section className="container">
<button className="container__button" onClick={() => ClickHandler()}>
Fetch Albums
</button>
{/* conditionally rendering the loader */}
{fetching ? "loading" : null}
{renderList()}
</section>
);
};
const mapStateToProps = ({
albums,
photos
}: StoreState): { albums: Album[]; photos: Photo[] } => {
return { albums, photos };
};
export default connect(mapStateToProps, { fetchAlbums, fetchPhotos })(_App);
and here is my test file (App.test.tsx):
import React from "react";
import Enzyme, { mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { findByClass } from "../test/Utils";
import App from "./App";
Enzyme.configure({ adapter: new Adapter() });
// setting our initial mount, we use mount here bcause of the hooks
const setup = () => mount(<App />);
describe("app", () => {
it("renders succesfully", () => {
// Arrange
const wrapper = setup();
const component = findByClass(wrapper, "container");
// Assert & Act
expect(component.length).toBe(1);
});
});
What am I missing ?