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

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.

Related

Electron/react application shows extension server error

I've been using an electron/react boilerplate for my application, yet I find myself facing a dreaded empty white screen as the console spits the following error message at me repeatedly:
[31308:0413/104846.234:ERROR:CONSOLE(1)] "Extension server error: Operation failed: Permission denied", source: devtools://devtools/bundled/models/extensions/extensions.js (1)
This error suddenly popped up without much warning as it was working fine earlier, though I suspect it might have something to do with changes I made to the ipcRenderer.
Along with this, the following errors pop up in the console: https://gyazo.com/1c3e3f22f65fd6f7db0fd9549969581b
Here's my preload.ts:
contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
myPing() {
ipcRenderer.send('ipc-example', 'ping');
},
on(channel: string, func: (...args: unknown[]) => void) {
const validChannels = ['ipc-example'];
if (validChannels.includes(channel)) {
const subscription = (_event: IpcRendererEvent, ...args: unknown[]) =>
func(...args);
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, subscription);
return () => ipcRenderer.removeListener(channel, subscription);
}
return undefined;
},
once(channel: string, func: (...args: unknown[]) => void) {
const validChannels = ['ipc-example'];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.once(channel, (_event, ...args) => func(...args));
}
},
},
});
And here's my main.ts:
/**
* This module executes inside of electron's main process. You can start
* electron renderer process from here and communicate with the other processes
* through IPC.
*
* When running `npm run build` or `npm run build:main`, this file is compiled to
* `./src/main.js` using webpack. This gives us some performance wins.
*/
import path from 'path';
import { app, BrowserWindow, shell, ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
import MenuBuilder from './components/containers/menu';
import { resolveHtmlPath } from './main/util';
export default class AppUpdater {
constructor() {
log.transports.file.level = 'info';
autoUpdater.logger = log;
autoUpdater.checkForUpdatesAndNotify();
}
}
let mainWindow: BrowserWindow | null = null;
ipcMain.on('ipc-example', async (event, arg) => {
const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`;
console.log(msgTemplate(arg));
event.reply('ipc-example', msgTemplate('pong'));
});
if (process.env.NODE_ENV === 'production') {
const sourceMapSupport = require('source-map-support');
sourceMapSupport.install();
}
const isDevelopment =
process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
if (isDevelopment) {
require('electron-debug')();
}
const installExtensions = async () => {
const installer = require('electron-devtools-installer');
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
const extensions = ['REACT_DEVELOPER_TOOLS'];
return installer
.default(
extensions.map((name) => installer[name]),
forceDownload
)
.catch(console.log);
};
const createWindow = async () => {
if (isDevelopment) {
await installExtensions();
}
const RESOURCES_PATH = app.isPackaged
? path.join(process.resourcesPath, 'assets')
: path.join(__dirname, '../../assets');
const getAssetPath = (...paths: string[]): string => {
return path.join(RESOURCES_PATH, ...paths);
};
mainWindow = new BrowserWindow({
show: false,
width: 1024,
height: 728,
icon: getAssetPath('icon.png'),
webPreferences: {
contextIsolation: true,
preload: app.isPackaged
? path.join(__dirname, 'preload.js')
: path.join(__dirname, '../../.erb/dll/preload.js'),
},
});
mainWindow.loadURL(resolveHtmlPath('index.html'));
mainWindow.on('ready-to-show', () => {
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
if (process.env.START_MINIMIZED) {
mainWindow.minimize();
} else {
mainWindow.show();
}
});
mainWindow.on('closed', () => {
mainWindow = null;
});
const menuBuilder = new MenuBuilder(mainWindow);
menuBuilder.buildMenu();
// Open urls in the user's browser
mainWindow.webContents.setWindowOpenHandler((edata) => {
shell.openExternal(edata.url);
return { action: 'deny' };
});
// Remove this if your app does not use auto updates
// eslint-disable-next-line
new AppUpdater();
};
/**
* Add event listeners...
*/
app.on('window-all-closed', () => {
// Respect the OSX convention of having the application in memory even
// after all windows have been closed
if (process.platform !== 'darwin') {
app.quit();
}
});
app
.whenReady()
.then(() => {
createWindow();
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) createWindow();
});
})
.catch(console.log);
Does this error seem familiar to anyone? I've no clue how to go about solving this.

Ionic React: Implementing InAppPurchase 2 on React Hooks

I am trying to implement InAppPurchase 2 from https://github.com/j3k0/cordova-plugin-purchase in Ionic React.
It seems that the ready event is never been called.
Here is my code:
....
import { IAPProduct, InAppPurchase2 as iap } from "#ionic-native/in-app-purchase-2";
const App: React.FC = () => {
useEffect(() => {
const init = async () => {
await initInAppPurchase();
setInitialized(true);
}
init();
}, [])
....
}
export const initInAppPurchase = () => {
if (isPlatform('android') || isPlatform('ios')) {
iap.verbosity = iap.DEBUG;
iap.register({
id: "com.mysoftwares.posapp.test",
alias: "Test",
type: iap.NON_CONSUMABLE
});
iap.when("com.mysoftwares.posapp.test").updated((product: IAPProduct) => {
if (product.owned)
console.log('Product owned')
else
console.log('Product not owned')
});
iap.when("com.mysoftwares.posapp.test").approved(function (product: any) {
product.finish();
});
iap.ready(() => {
alert("Product ready!")
let product = iap.get('Test');
alert(product);
if (product.canPurchase) {
iap.order('Test');
}
})
iap.refresh();
}
}
Here is the log from debug verbose:
[store.js] DEBUG: state: com.mysoftwares.posapp.test -> registered
[store.js] DEBUG: store.trigger -> triggering action refreshed
InAppBilling[js]: setup ok
InAppBilling[js]: load ["com.mysoftwares.posapp.test"]
InAppBilling[js]: listener: {"type":"ready","data":{}}
[store.js] DEBUG: store.trigger -> triggering action refresh-finished
InAppBilling[js]: setup ok
InAppBilling[js]: load ["com.mysoftwares.posapp.test","com.mysoftwares.posapp.test"]
InAppBilling[js]: listener: {"type":"ready","data":{}}
InAppBilling[js]: setup ok
InAppBilling[js]: load ["com.mysoftwares.posapp.test","com.mysoftwares.posapp.test","com.mysoftwares.posapp.test"]
InAppBilling[js]: listener: {"type":"ready","data":{}}
InAppBilling[js]: setup ok
I am using React Hooks. I have published my app in Google Play Console and have added the test product in In-app Products section inside Google Play Console. Currently my app is on open testing phase.
I have created an app before coded in pure Apache Cordova without Ionic, it works fine, but this in React is not.
What is wrong with my code? Please help...
I am testing it on emulator which is dumb. On mobile devices, it works fine.
I had to do the following:
Install these in terminal
npm install #ionic-native/in-app-purchase-2
npm install cordova-plugin-purchase
This code in .tsx file
import React, { useState, useEffect } from 'react';
import { InAppPurchase2 as iap, IAPProduct } from "#ionic-native/in-app-purchase-2";
const Home: React.FC = () => {
//declare variables
const [productPrice, setPrice] = useState('')
const [product, setProduct] = useState([]) as any
//initiate initInAppPurchase function
useEffect(() => {
const init = async () => {
await initInAppPurchase();
}
init();
}, []);
//if on an ios or android device, then get product info
const initInAppPurchase = () => {
if ((isPlatform('ios')) || (isPlatform('android'))) {
iap.verbosity = iap.DEBUG;
iap.register({
id: "com.test.test",
alias: "Test",
type: iap.NON_CONSUMABLE
});
iap.ready(() => {
let product = iap.get('Test');
setPrice(product.price)
setProduct(product)
})
iap.refresh();
}
}
//if user clicks purchase button
const purchaseProduct = () => {
if (product.owned) {
alert('Product already owned, click restore button instead!')
} else {
iap.order('Test').then(() => {
iap.when("com.test.test").approved((p: IAPProduct) => {
//store product
p.verify();
p.finish();
});
})
iap.refresh();
}
}
//if user clicks retore or promo code button
const restore = () => {
iap.when("com.test.test").owned((p: IAPProduct) => {
if (product.owned) {
//store product
} else {
alert("You have not purchased this product before.")
}
});
iap.refresh();
}
return (
<IonPage>
<IonContent fullscreen>
<IonCard className="homeCards">
<IonButton onClick={purchaseProduct}>Buy for {productPrice}</IonButton>
<IonButton onClick={restore}>Restore</IonButton>
<IonButton onClick={restore}>Promo code</IonButton>
</IonCard>
</IonContent>
</IonPage>
);
};
export default Home;
To test apple in app purchase on device:
Had a developer account
Created in app purchase in apple connect
Added my email as a tester in apple connect
Uploaded app onto phone from Xcode and purchases worked
To test android in app purchase on device:
<uses-permission android:name="com.android.vending.BILLING" />
Added above line of code to AdnroidManifest.xml file
Had a developer account
Uploaded the .aab or .apk file to internal testing on google play console and added my email as a tester
Created in app purchase
Uploaded app onto phone from Android Studio and purchases worked

Problem with STUN/TURN servers in WEBRTC video app made in MERN stack

I have hosted a peer to peer meeting react app on netlify. I have used Peerjs for my video purpose. Everything is working as expected except the video. For some networks the video of the the remote person is working and for some others it is not working. I looked up and found out that it may be a STUN/TURN issue. I then implemented all the STUN/TURN servers in my code. However the video is still not getting setup in some cases. In some cases it is working fine, in others the video is not showing up. Herewith, I am attaching th code for the video and the link to the site.
import React,{useEffect,useState} from 'react';
import {io} from "socket.io-client";
import {useParams} from 'react-router-dom';
import {Grid} from "#material-ui/core";
import Peer from 'peerjs';
var connectionOptions = {
"force new connection" : true,
"reconnectionAttempts": "Infinity",
"timeout" : 10000,
"transports" : ["websocket"]
};
const Videobox = ({isVideoMute,isAudioMute}) => {
var myPeer = new Peer(
{
config: {'iceServers': [
{urls:'stun:stun01.sipphone.com'},
{urls:'stun:stun.ekiga.net'},
{urls:'stun:stun.fwdnet.net'},
{urls:'stun:stun.ideasip.com'},
{urls:'stun:stun.iptel.org'},
{urls:'stun:stun.rixtelecom.se'},
{urls:'stun:stun.schlund.de'},
{urls:'stun:stun.l.google.com:19302'},
{urls:'stun:stun1.l.google.com:19302'},
{urls:'stun:stun2.l.google.com:19302'},
{urls:'stun:stun3.l.google.com:19302'},
{urls:'stun:stun4.l.google.com:19302'},
{urls:'stun:stunserver.org'},
{urls:'stun:stun.softjoys.com'},
{urls:'stun:stun.voiparound.com'},
{urls:'stun:stun.voipbuster.com'},
{urls:'stun:stun.voipstunt.com'},
{urls:'stun:stun.voxgratia.org'},
{urls:'stun:stun.xten.com'},
{
urls: 'turn:numb.viagenie.ca',
credential: 'muazkh',
username: 'webrtc#live.com'
},
{
urls: 'turn:192.158.29.39:3478?transport=udp',
credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
username: '28224511:1379330808'
},
{
urls: 'turn:192.158.29.39:3478?transport=tcp',
credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
username: '28224511:1379330808'
}
]} /* Sample servers, please use appropriate ones */
}
);
const peers = {}
const [socket, setSocket] = useState()
const {id:videoId} = useParams();
const videoGrid = document.getElementById('video-grid')
useEffect(()=> {
const s=io("https://weconnectbackend.herokuapp.com",connectionOptions);
setSocket(s);
return () => {
s.disconnect();
}
},[])
// let myVideoStream;
const [myVideoStream, setmyVideoStream] = useState()
const muteUnmute = () => {
const enabled = myVideoStream.getAudioTracks()[0].enabled;
if (enabled) {
myVideoStream.getAudioTracks()[0].enabled = false;
//setUnmuteButton();
} else {
//setMuteButton();
myVideoStream.getAudioTracks()[0].enabled = true;
}
}
const playStop = () => {
//console.log('object')
let enabled = myVideoStream.getVideoTracks()[0].enabled;
if (enabled) {
myVideoStream.getVideoTracks()[0].enabled = false;
//setPlayVideo()
} else {
//setStopVideo()
myVideoStream.getVideoTracks()[0].enabled = true;
}
}
useEffect(() => {
if(myVideoStream)
playStop()
}, [isVideoMute])
useEffect(() => {
if(myVideoStream)
muteUnmute()
}, [isAudioMute])
useEffect(() => {
if(socket== null)
return;
myPeer.on('open',id=>{
socket.emit('join-room',videoId,id);
})
const myVideo = document.createElement('video')
myVideo.muted = true
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
}).then(stream => {
// myVideoStream = stream;
window.localStream=stream;
setmyVideoStream(stream);
console.log(myVideoStream,"myvideostream");
addVideoStream(myVideo, stream)
myPeer.on('call', call => {
call.answer(stream)
const video = document.createElement('video')
call.on('stream', userVideoStream => {
addVideoStream(video, userVideoStream)
})
})
socket.on('user-connected',userId =>{
connectToNewUser(userId, stream)
})
socket.on('user-disconnected', userId => {
if (peers[userId]) peers[userId].close()
})
})
}, [socket,videoId])
function addVideoStream(video, stream) {
video.srcObject = stream
video.addEventListener('loadedmetadata', () => {
video.play()
})
videoGrid.append(video)
}
function connectToNewUser(userId, stream) {
const call = myPeer.call(userId, stream)
const video = document.createElement('video')
call.on('stream', userVideoStream => {
addVideoStream(video, userVideoStream)
})
call.on('close', () => {
video.remove()
})
peers[userId] = call
}
return (
<div id="video-grid" className="videoStyleFromDiv">
{/* <Video srcObject={srcObject}/> */}
</div>
)
}
export default Videobox
Website Link
The TURN servers you are using have been out of commission for a couple of years in the case of the ones taken from https://www.html5rocks.com/en/tutorials/webrtc/infrastructure/
Copying credentials from random places is not how TURN works, you will need to run your own servers.

JSSIP and React audio issue

So I am using jssip 3.2.10 to make calls on a React project.
The server is setup on Asterisk and CentOS.
I can make calls where the call receiver hears me well, but I can't hear their audio, nor the waiting (traditional) beep noises it should make until the call is picked up.
It does work with some sipml5/asterisk udp online tests so I feel it's on my clients side issue. I tested it on Chrome and Firefox (both latest, with the same results).
My setup
I have a helper to connect called sip.js:
const JsSIP = require('jssip')
const GLOBAL = require('../globals')
function register(user, pass, cb) {
console.log('Registering to SIP')
JsSIP.debug.disable('JsSIP:*')
const address = GLOBAL.jssip_server + ':' + GLOBAL.jssip_port
let socket = new JsSIP.WebSocketInterface('ws://' + address + '/ws')
const configuration = {
sockets: [socket],
uri: 'sip:' + user + '#' + GLOBAL.jssip_server,
authorization_user: user,
password: pass,
connection_recovery_min_interval: 3,
register: true
}
let ua = new JsSIP.UA(configuration)
ua.start()
cb(ua)
}
export {
register
}
Then on my main component I do the following:
componentDidMount() {
if(GLOBAL.jssip) {
this.props.dispatch(connecting(true))
register('***', '***', (ua) => {
this.setState({ua: ua}, () => {
this.state.ua.on("registered", () => {
this.props.dispatch(connecting(false))
this.setState({critical: false})
})
this.state.ua.on("registrationFailed", () => {
this.props.dispatch(connecting(false))
this.setState({critical: true})
})
})
})
}
}
And when I try to make a call I do the following:
doCall(number) {
this.props.dispatch(placeCall(call))
if(GLOBAL.jssip) {
let eventHandlers = {
'connecting': (e) => {
console.log('call is in progress')
this.setState({sipStatus: "connecting"})
},
'progress': (e) => {
console.log('call is in progress')
this.setState({sipStatus: "progress"})
},
'failed': (e) => {
console.log('call failed with cause: ', e)
this.setState({sipStatus: "failed"})
},
'ended': (e) => {
console.log('call ended with cause: ', e)
this.setState({sipStatus: "ended"})
},
'confirmed': (e) => {
this.setState({sipStatus: "confirmed"})
}
}
let options = {
eventHandlers: eventHandlers,
mediaConstraints: { 'audio': true, 'video': false }
}
let session = this.state.ua.call('sip:'+number+'#'+GLOBAL.jssip_server, options)
}
}
Anyone has a clue on how to fix this?
Thanks to the answer here:
How to handle audio stream in JsSIP?
I found the solution, I needed to add to the file rendering the call:
<audio ref={(audio) => {this.audioElement = audio}} id="audio-element"></audio>
And changed doCall last bit to this:
this.setState({session: this.state.ua.call('sip:'+number+'#'+GLOBAL.jssip_server, options)}, () =>{
this.state.session.connection.addEventListener('addstream', (event: any) => {
this.audioElement.srcObject = event.stream
this.audioElement.play()
})
})

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

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');
});
});
});

Resources