How to mock react-native Platform.OS with Jest? - react-native

Jest runner use "ios" as default OS (Plaform.OS) for react-native tests.
How to do test in jest that have platform dependent code. An further more, how to mock static constant imported in a module.
Here is the code:
utils.ts:
import { Platform } from "react-native";
export const isIOS = Platform.OS === "ios"; // note constant, not function
items.ts:
import { isIOS } from "#my/utils";
export const items = [{
id: 1,
name: isIOS ? "Apple" : "Android"
}]
test:
import { items } from "./items";
// not working because Platform.OS is used statically during import statement
it("should be android", () => {
Platform.OS = "android"; // too late...
expect(items[0].name).toBe("Android"); // fail: Apple
}
it("should be ios", () => {
Platform.OS = "ios";
expect(items[0].name).toBe("Apple"); // works due to default platform value in Jest
}
I saw some workaround using jest.resetModules() & require inside test/it block (and change Plaform.OS before require) but does exist a more simple way to achieve that ?

In your test file
import { Platform } from 'react-native'
jest.doMock('react-native/Libraries/Utilities/Platform.android.js', () => ({
OS: 'android',
select: jest.fn(),
}))
jest.doMock('react-native/Libraries/Utilities/Platform.ios.js', () => ({
OS: 'android',
select: jest.fn(),
}))
describe('Platform', () => {
it('Should be android', () => {
expect(Platform.OS).toEqual('android')
})
it('Should be ios', () => {
Platform.OS = 'ios'
expect(Platform.OS).toEqual('ios')
})
it('Should be android', () => {
Platform.OS = 'android'
expect(Platform.OS).toEqual('android')
})
})

Related

How to mock react-native-copilot?

Trying to mock with jest react-native-copilot
This is the point in the screen where the module is imported.
import { walkthroughable, CopilotStep } from 'react-native-copilot';
...
const CopilotText = walkthroughable(Text);
...
<View>
<CopilotStep order={2} name='name'>
<CopilotText testID={'test-schedule-name'}>
{this.props.scheduleName}
</CopilotText>
</CopilotStep>
</View>
...
This is how it's mock it so far in the test:
jest.mock('react-native-copilot', () => ({
getCurrentStep: jest.fn(() => { }),
}));
This is the test:
it('displays name', () => {
const { getByTestId } = render(<MyComponent />);
const scheduleName = getByTestId('test-schedule-name'); // it doesn't get this... why?
...
});
This is the error:
Unable to find an element with testID: test-schedule-name
...
const CopilotText = walkthroughable(Text);
^
Any idea?

How to open app settings page in react native android?

At the end react native bridge is the only way.But is there any npm package which has been worked successfully for anyone?
You can open your app settings by using Linking library.
import {Linking} from 'react-native';
Linking.openSettings();
Offical document: https://reactnative.dev/docs/linking#opensettings
You can use #react-native-community/react-native-permissions library.
Here offical documantation: https://github.com/react-native-community/react-native-permissions#opensettings
Example:
import { openSettings } from 'react-native-permissions';
openSettings();
combining the existing answers: this works for me for both iOS and Android (without expo)
import { Linking, Platform } from 'react-native';
const handleOpenSettings = () => {
if (Platform.OS === 'ios') {
Linking.openURL('app-settings:');
} else {
Linking.openSettings();
}
};
// add a button that calls handleOpenSetttings()
Use
IntentLauncher.startActivity({
action: 'android.settings.SETTINGS',
})
instead of
IntentLauncher.startActivity({
action: 'android.settings.APPLICATION_DETAILS_SETTINGS',
data: 'package:' + pkg
})
Like
import IntentLauncher, { IntentConstant } from 'react-native-intent-launcher'
const openAppSettings = () => {
if (Platform.OS === 'ios') {
Linking.openURL('app-settings:')
} else {
IntentLauncher.startActivity({
action: 'android.settings.SETTINGS',
})
}
}
ANDROID
Wifi
IntentLauncher.startActivity({
action: 'android.settings.SETTINGS'
})
GPS
IntentLauncher.startActivity({
action: 'android.settings.LOCATION_SOURCE_SETTINGS'
})
IOS
WIFI
Linking.openURL("App-Prefs:root=WIFI");
GPS
Linking.openURL('App-Prefs:Privacy&path=LOCATION')
Linking API from react-native core provides the function to send custom intent action to Android.
NOTE - You can send any custom action that is acceptable by android moreover you can also pass data to the intent.
Here is an example to open settings :
import {Linking} from 'react-native';
// To open settings
Linking.sendIntent('android.settings.SETTINGS');
// To open GPS Location setting
Linking.sendIntent('android.settings.LOCATION_SOURCE_SETTINGS');
Hope this will help you or somebody else. Thanks!
Happy Coding :-)
This is a complete answer for both android & iOS.
Without Expo:
import DeviceInfo from 'react-native-device-info';
import IntentLauncher, { IntentConstant } from 'react-native-intent-launcher'
const pkg = DeviceInfo.getBundleId();
const openAppSettings = () => {
if (Platform.OS === 'ios') {
Linking.openURL('app-settings:')
} else {
IntentLauncher.startActivity({
action: 'android.settings.APPLICATION_DETAILS_SETTINGS',
data: 'package:' + pkg
})
}
}
With Expo:
import Constants from 'expo-constants'
import * as IntentLauncher from 'expo-intent-launcher'
const pkg = Constants.manifest.releaseChannel
? Constants.manifest.android.package
: 'host.exp.exponent'
const openAppSettings = () => {
if (Platform.OS === 'ios') {
Linking.openURL('app-settings:')
} else {
IntentLauncher.startActivityAsync(
IntentLauncher.ACTION_APPLICATION_DETAILS_SETTINGS,
{ data: 'package:' + pkg },
)
}
}
import React, { useCallback } from "react";
import { Button, Linking, StyleSheet, View } from "react-native";
const OpenSettingsButton = ({ children }) => {
const handlePress = useCallback(async () => {
// Open the custom settings if the app has one
await Linking.openSettings();
}, []);
return <Button title={children} onPress={handlePress} />;
};
const App = () => {
return (
<View style={styles.container}>
<OpenSettingsButton>Open Settings</OpenSettingsButton>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", alignItems: "center" },
});
import { Linking, Platform } from 'react-native';
const openSettingsAlert = () =>
Alert.alert('Please provide the require permission from settings', '', [
{
text: 'Cancel',
onPress: () => console.log('Cancel Pressed'),
style: 'cancel',
},
{ text: 'Go To Settings', onPress: () => openSettings() },
]);
const openSettings = () => {
if (Platform.OS === 'ios') {
Linking.openURL('app-settings:');
} else {
Linking.openSettings();
}
};
let isStoragePermitted = await requestExternalWritePermission();
if (isStoragePermitted === true) {
}else{
console.log('permission not granted');
openSettingsAlert();
}
const requestExternalWritePermission = async () => {
if (Platform.OS === 'android') {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
);
// If WRITE_EXTERNAL_STORAGE Permission is granted
return granted === PermissionsAndroid.RESULTS.GRANTED;
} catch (err) {
console.warn(err);
alert('Storage Access permission error:', err);
}
return false;
}
};

Cannot read property 'Direction' of undefined, tests only

I just added TouchableOpacity to a component and the app is working fine, but my tests, using react-native-testing-library, fail to run:
● Test suite failed to run
TypeError: Cannot read property 'Direction' of undefined
at Object.Direction (node_modules/react-native-gesture-handler/Directions.js:3:39)
at Object.<anonymous> (node_modules/react-native-gesture-handler/GestureHandler.js:2:1)
I just removed and re-added react-native-gesture-handler with yarn, and ran pod install. Again, the app is working, but the tests fail to run.
I actually get the same error when using <Text onPress={() => onOptionPress(opt)} /> rather than TouchableOpacity.
component:
const SelectOptions = ({ field, dismissOverlay, onOptionPress }) => {
return (
<Overlay
isVisible
overlayStyle={styles.overlay}
height={"auto"}
onBackdropPress={dismissOverlay}
>
<View>
{field.options.map((opt, i) => (
<TouchableOpacity
style={styles.option}
key={i}
onPress={() => onOptionPress(opt)}
>
<Text>{opt}</Text>
</TouchableOpacity>
))}
</View>
</Overlay>
);
};
test:
describe("CardFormView", () => {
let wrapper, birthdayField;
beforeEach(() => {
wrapper = render(
<React.Fragment>
<CardFormView form={form} />
</React.Fragment>
);
birthdayField = wrapper.getByText("Add a Birthday Gift Card");
});
const message1 =
"...";
const message2 =
"...";
it("shows the options for a birthday card when clicked", () => {
fireEvent.press(birthdayField);
expect(wrapper.getByText(message1)).toBeDefined();
});
it("sets an option when clicked", () => {
fireEvent.press(birthdayField);
const firstOption = wrapper.getByText(message1);
fireEvent.press(firstOption);
expect(wrapper.queryByText(message2)).toBeNull();
expect(wrapper.getByText(message1)).toBeDefined();
});
});
This is because you are not mocking the react-navigation-gesture-handler
To use mock of react-navigation-gesture-handler you should add jestSetup.js from node_modules in jest.config.json or jest.config.js
setupFiles: [
"./node_modules/react-native-gesture-handler/jestSetup.js"
]
I found a reference from the following link and It's working for me.
https://github.com/software-mansion/react-native-gesture-handler/issues/344#issuecomment-489547513
For me just adding the setupFiles didn't work. I added setupFiles and transformIgnorePatterns at "jest" in package.json
Here the code to make the gestureHandler work, but I tested it with AsyncStorage and the storage stopped work. If you aren't using AsyncStorage I presume this code will work very well!
"setupFiles": [
"./node_modules/react-native-gesture-handler/jestSetup.js"
],
"transformIgnorePatterns": [
"/node_modules/(?!native-base)/"
]
My reference:
https://github.com/software-mansion/react-native-gesture-handler/issues/344
Updating package.json and reinstalling npm package worked for me.
"jest": {
"preset": "react-native",
"transformIgnorePatterns": ["node_modules/(?!(jest-)?react-native|#?react-navigation)"],
"setupFiles": ["./node_modules/react-native-gesture-handler/jestSetup.js"]
}
This is happening because you have to mock the NativeModules module from react-native. It can happen with several modules but it was happening to me specifically with the ImagePicker, Linking and #react-navigation/native. This is what I did to mock the native modules.
/src/testSetup.ts
import {NativeModules} from 'react-native';
NativeModules.RNGestureHandlerModule= {
attachGestureHandler: jest.fn(),
createGestureHandler: jest.fn(),
dropGestureHandler: jest.fn(),
updateGestureHandler: jest.fn(),
State: {},
Directions: {},
},
NativeModules.ImagePickerManager = {
showImagePicker: jest.fn(),
}
NativeModules.Linking = {
canOpenUrl: jest.fn().mockResolvedValue(true),
openUrl: jest.fn().mockResolvedValue(true)
}
NativeModules.Platform = {
OS: 'iOS'
}
jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper');
jest.mock('react-native/Libraries/Animated/src/animations/TimingAnimation');
const mockNavigation = () => {
const mockedNavigate = jest.fn();
const mockedAddListener = jest.fn();
jest.mock('#react-navigation/native', () => ({ // #ts-ignore
...(jest.requireActual('#react-navigation/native')),
useNavigation: () => ({
navigate: mockedNavigate,
addListener: mockedAddListener
})
}));
return {mockedNavigate, mockedAddListener}
}
in your tests
import { fireEvent, act, render } = '#testing-library/react-native'
const {mockedNavigate, mockedAddListener} = mockNavigation()
test('Should navigate', () => {
const { queryByText } = render(<Component />)
fireEvent.press(getByText('View Page Button'))
expect(mockedNavigate).toHaveBeenCalledWith('Your Page Name')
expect(mockedAddListener).toHaveBeenCalled()
})
In my case, I was using react-native-cli when encountered this problem. I removed it and installed #react-native-community/cli instead. It fixed everything!

React-Native + Enzyme + Jest: How to test platform specific behaviour?

TLDR: How can I tell my Enzyme / Jest test it should run the tests as if it was running on iOS? I want to test platform specific behaviour.
I'm building a custom status bar component that adds 20 pixels of height, if it runs on iOS to prevent my content from overlapping with the status bar. (Yes, I know React-Navigation has a SafeAreaView, but this only works for iPhone X, not for e.g. iPad.)
Here is my component:
import React from "react";
import { StatusBar as ReactNativeStatusBar, View } from "react-native";
import styles from "./styles";
const StatusBar = ({ props }) => (
<View style={styles.container}>
<ReactNativeStatusBar {...props} />
</View>
);
export default StatusBar;
Here is the styles.js file:
import { StyleSheet, Platform } from "react-native";
const height = Platform.OS === "ios" ? 20 : 0;
const styles = StyleSheet.create({
container: {
height: height
}
});
export default styles;
And here are the tests so far:
import React from "react";
import { shallow } from "enzyme";
import { View } from "react-native";
import StatusBar from "./StatusBar";
const createTestProps = props => ({
...props
});
describe("StatusBar", () => {
describe("rendering", () => {
let wrapper;
let props;
beforeEach(() => {
props = createTestProps();
wrapper = shallow(<StatusBar {...props} />);
});
it("should render a <View />", () => {
expect(wrapper.find(View)).toHaveLength(1);
});
it("should give the <View /> the container style", () => {
expect(wrapper.find(View)).toHaveLength(1);
});
it("should render a <StatusBar />", () => {
expect(wrapper.find("StatusBar")).toHaveLength(1);
});
});
});
Now what I would like to do is add two more describe areas that explicitly test for the height to be either 20 on iOS or 0 or Android. The problem is I couldn't find how to emulate the platform with Enzyme / Jest tests.
So how do I tell my test suite that it should run the code for the respective platform?
You can override the RN Platform object and perform different tests for each platform. Here's an example for how a test file would like like:
describe('tests', () => {
let Platform;
beforeEach(() => {
Platform = require('react-native').Platform;
});
describe('ios tests', () => {
beforeEach(() => {
Platform.OS = 'ios';
});
it('should test something on iOS', () => {
});
});
describe('android tests', () => {
beforeEach(() => {
Platform.OS = 'android';
});
it('should test something on Android', () => {
});
});
});
By the way, regardless of the question about testing, setting the status-bar height to 20 on iOS is wrong since it can have different sizes on different devices (iPhone X for example)

Mocking platform detection in Jest and React Native

Some of the code I am trying to test detects the platform, using, e.g.:
import { Platform } from 'react-native';
...
if (Platform.OS === 'android') {
...
} else {
...
}
Is there a sensible way to mock this with Jest and/or something else, so I can test both branches in one test run?
Or is the smart way to decouple it and put the platform into, e.g., a context variable? Although it always feels restructuring code to make it easier to test is something of a cheat.
This worked for me (Jest 21.2.1, Enzyme 3.2.0):
jest.mock('Platform', () => {
const Platform = require.requireActual('Platform');
Platform.OS = 'android';
return Platform;
});
Put it either at the top of your test, or in a beforeAll for example.
For everyone looking for this, what it helped me was the following:
jest.mock('react-native/Libraries/Utilities/Platform', () => ({
OS: 'android', // or 'ios'
select: () => null
}));
The way that I achieved mocking setting the platform was just set it directly in the tests:
it('should only run for Android', () => {
Platform.OS = 'android'; // or 'ios'
// For my use case this module was failing on iOS
NativeModules.MyAndroidOnlyModule = {
fetch: jest.fn(
(url, event) => Promise.resolve(JSON.stringify(event.body))
),
};
return myParentFunction().then(() => {
expect(NativeModules.MyAndroidOnlyModule.fetch.mock.calls.length).toBe(1);
expect(fetch.mock.calls.length).toBe(0);
});
});
This would setup the platform to only run on Android during tests to make sure that my function was calling only specific functions. My function that was wrapped in platform dependent compilation looked like:
export default function myParentFunction() {
if (Platform.OS === 'ios') {
return fetch();
}
return NativeModules.MyAndroidOnlyModule.fetch();
}
I would suggest just creating two different tests one with the platform set to iOS and the other to Android since ideally a function should only have one responsibility. However, I'm sure you can use this to run the first test, dynamically set the platform and run test number two all in one function.
I implemented a small mock that allows you to change Platform during tests in the same test file.
Add this to your jest setup file
jest.mock('react-native/Libraries/Utilities/Platform', () => {
let platform = {
OS: 'ios',
}
const select = jest.fn().mockImplementation((obj) => {
const value = obj[platform.OS]
return !value ? obj.default : value
})
platform.select = select
return platform
});
Then you can easily change Platform in your test. If you are using Platform.select it will also work as expected!
import { Platform } from 'react-native'
describe('When Android', () => {
it('should ...', () => {
Platform.OS = 'android'
...
})
})
describe('When iOS', () => {
it('should ...', () => {
Platform.OS = 'ios'
...
})
})
React Native 0.61 update
Though the accepted solution works for versions of React Native 0.60 and below, React Native 0.61 has dropped Haste support and this gives an error.
I was able to mock platform detection following the implementation described in this blog post.
Practically, according to the React team, we now have to mock the react-native interface. So, you can create a react-native.js file inside the tests/__mocks__ folder and add this code to mock Platform:
import * as ReactNative from "react-native";
export const Platform = {
...ReactNative.Platform,
OS: "ios",
Version: 123,
isTesting: true,
select: objs => objs[Platform.OS]
};
export default Object.setPrototypeOf(
{
Platform
},
ReactNative
);
With this implementation, we can now simply overwrite the OS before running the test like:
Platform.OS = 'android'
Since the other answers will not work if you want to mock different OSs in the same test suite and in one test run, here's another way. Instead of using Platform.OS directly in your code, define a helper function somewhere and use that to get references to the OS in your components:
in 'helpers.js':
export function getOS() {
return Platform.OS;
}
in your component:
import * as helpers from './helpers';
render() {
if (helpers.getOS() === 'android') {// do something}
}
This function can then be mocked it in your tests, e.g.
import * as helpers from './helpers';
// ...
it('does something on Android', () => {
jest.spyOn(helpers, 'getOS').mockImplementation(() => 'android');
// ...
}
it('does something else on iOS', () => {
jest.spyOn(helpers, 'getOS').mockImplementation(() => 'ios');
// ...
}
Credit for the idea goes to this GitHub issue comment.
This works for me...
jest.mock('react-native/Libraries/Utilities/Platform', () => {
const Platform = require.requireActual(
'react-native/Libraries/Utilities/Platform'
)
Platform.OS = 'android'
return Platform
})
this is the mock you need:
const mockPlatform = OS => {
jest.resetModules();
jest.doMock("Platform", () => ({ OS, select: objs => objs[OS] }));
};
with it you can do the following:
it("my test on Android", () => {
mockPlatform("android");
});
it("my test on iOS", () => {
mockPlatform("ios");
});
That way you can have tests for both platforms
I'm using the solution from this github issue https://github.com/facebook/jest/issues/1370#issuecomment-352597475
I moved the jest config from package.json to separate files.
So far everything seems to work great, including:
a) the right file is imported according to the platform. For example on ios: .ios.tsx, then .native.tsx then .tsx
b) PLATFORM.IOS returns true when running test-ios, no need to mock anything
// package.json
"scripts": {
"test": "cross-env NODE_ENV=test jest --config config/jest.desktop.json",
"test-ios": "cross-env NODE_ENV=test jest --config config/jest.ios.json",
"test-android": "cross-env NODE_ENV=test jest --config config/jest.android.json"
}
// config/jest.web.json
{
...
}
// config/jest.ios.json
{
...
"preset": "react-native",
"haste": {
"defaultPlatform": "ios",
"platforms": [
"android",
"ios",
"native"
],
"providesModuleNodeModules": [
"react-native"
]
},
}
// config/jest.android.json
{
...
"preset": "react-native",
"haste": {
"defaultPlatform": "android",
"platforms": [
"android",
"ios",
"native"
],
"providesModuleNodeModules": [
"react-native"
]
},
}
use jest.doMock and jest.resetModules
jest.resetModules()
jest.doMock('react-native', () => ({ Platform: { OS: 'android' }}))
Maybe the problem in the "import" method, check this:
const isAndroid = require('app/helpers/is_android');
//import isAndroid from 'app/helpers/is_android'
with "import" this will not work, need to use "require".
beforeEach(() => {
jest.resetModules();
});
it("should be true when Android", () => {
jest.mock('Platform', () => {
return { OS: 'android' };
});
expect(isAndroid).toBe(true);
});
import React from "react";
import renderer from "react-test-renderer";
import SmartText from "../SmartText";
describe("markdown smart text component", () => {
beforeEach(() => {
jest.resetModules();
});
it("renders with props on ios", () => {
jest.mock("Platform", () => {
return { OS: "ios" };
});
expect(
renderer.create(<SmartText title="code ios" code />).toJSON()
).toMatchSnapshot();
});
it("renders with props on android", () => {
jest.mock("Platform", () => {
return { OS: "android" };
});
expect(
renderer.create(<SmartText title="code android" code />).toJSON()
).toMatchSnapshot();
});
});
for newer version
"react-native": "0.62.2"
"enzyme": "^3.11.0"
"jest": "24.5.0"
Put it at the top of your test
Object.defineProperty(Platform, 'OS', { get: jest.fn(() => 'ios') })
jest: ^26.5.3
See bottom of this article
import { Platform } from 'react-native';
describe('Android', () => {
it('renders Element if Android', () => {
Platform.OS = 'android';
renderIfAndroid();
expect(wrapper.find(Element)).exists()).toBe(true);
});
});
describe('IOS', () => {
it('renders Element if IOS', () => {
Platform.OS = 'ios';
renderIfIOS();
expect(wrapper.find(Element)).exists()).toBe(true);
});
});
To change Platform only for a specific test, the following can be used:
test('Platform should be Android', () => {
jest.doMock('react-native/Libraries/Utilities/Platform', () => ({
OS: 'android',
}));
expect(Platform.OS).toBe('android');
// restore the previous value 'ios' for Platform.OS
jest.dontMock('react-native/Libraries/Utilities/Platform');
});
You can mock whatever you want from React-Native like this:
describe('notifications actions tests', () => {
let Platform;
beforeEach(() => {
jest.mock('react-native', () => ({
Platform: {
...
}));
Platform = require('react-native').Platform; // incase u would like to refer to Platform in your tests
});
If anyone is looking out to mock Platform.select. The below code can fix your issue.
const mockedData = 'MOCKED-DATA'
jest.mock('react-native', () => ({
Platform: {
select: jest.fn(() => {
return { mockedData } // Your Mocked Value
}),
}
}));
And To mock both OS and Platform. Please refer below code.
jest.mock('Platform', () => ({
OS: 'android', // or 'ios'
select: () => 'mocked-value'
}));
You have to mock the module and import it into your test. Then you can use mockImplementation to set the it to either android or ios
import reactNative from 'react-native';
jest.mock('react-native', () = > jest.fn();
it('is android', () => {
reactNative.mockImplementation(()=>({Platform:{OS: 'android'}}))
//test the android case
})
it('is android', ()=>{
reactNative.mockImplementation(()=>({Platform: { OS: 'io' }}))
//test the ios case
})
OS can be set directly for each test
test('android', () => {
Platform.OS = 'android'
const component = renderer.create(<Component />).toJSON()
expect(component).toMatchSnapshot()
})
test('ios', () => {
Platform.OS = 'ios'
const component = renderer.create(<Component />).toJSON()
expect(component).toMatchSnapshot()
})