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

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)

Related

How to write a jest test for opening of an URL in react native?

I'm trying to write a test case for testing URL in my react native app this is my mock
import { Linking } from "react-native";
jest.mock('react-native/Libraries/Linking/Linking', () => {
return {
openURL: jest.fn()
}
})
Linking.openURL.mockImplementation(() => true)
and this is my test
test('open google url',async ()=>{
expect(Linking.openURL()).toHaveBeenCalled('https://www.google.com/')
})
but I get this error what should I do?
Name the mock function in a constant and then test if that function has been called. Here's how you would set up:
import * as ReactNative from "react-native";
const mockOpenURL = jest.fn();
jest.spyOn(ReactNative, 'Linking').mockImplementation(() => {
return {
openURL: mockOpenURL,
}
});
and then you can test this way (my example uses react-testing-library, but you can use whatever). Note you should use toHaveBeenCalledWith(...) instead of toHaveBeenCalled(...)
test('open google url', async () => {
// I assume you're rendering the screen here and pressing the button in your test
// example code below
const { getByTestId } = render(<ScreenToTest />);
await act(async () => {
await fireEvent.press(getByTestId('TestButton'));
});
expect(mockOpenURL.toHaveBeenCalledWith('https://www.google.com/'));
});
If I understoof your question then you can use react-native-webview.
import WebView from 'react-native-webview';
export const WebView: React.FC<Props> = ({route}) => {
const {url} = route.params;
<WebView
source={{uri: url}}
/>
);
};
This is how I use my webview screen for any url I need to open (like terms and conditions, etc...)

How to hide element when device keyboard active using hooks?

I wanted to convert a hide element when keyboard active HOC I found to the newer react-native version using hooks (useEffect), the original solution using the older react lifecycle hooks looks like this - https://stackoverflow.com/a/60500043/1829251
So I created a useHideWhenKeyboardOpen function that wraps the child element and should hide that child if the device keyboard is active using useEffect. But on render the child element useHideWhenKeyboardOpen isn't displayed regardless of keyboard displayed.
When I've debugged the app I see the following error which I didn't fully understand,because the useHideWhenKeyboardOpen function does return a <BaseComponent>:
ExceptionsManager.js:179 Warning: Functions are not valid as a React
child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than
return it.
in RCTView (at View.js:34)
Question:
How can you attach keyboard displayed listener to a component in the render?
Example useHideWhenKeyboardOpen function:
import React, { useEffect, useState } from 'react';
import { Keyboard } from 'react-native';
// Wrapper component which hides child node when the device keyboard is open.
const useHideWhenKeyboardOpen = (BaseComponent: any) => (props: any) => {
// todo: finish refactoring.....
const [isKeyboadVisible, setIsKeyboadVisible] = useState(false);
const _keyboardDidShow = () => {
setIsKeyboadVisible(true);
};
const _keyboardDidHide = () => {
setIsKeyboadVisible(false);
};
/**
* Add callbacks to keyboard display events, cleanup in useeffect return.
*/
useEffect(() => {
console.log('isKeyboadVisible: ' + isKeyboadVisible);
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
return () => {
Keyboard.removeCurrentListener();
};
}, [_keyboardDidHide, _keyboardDidShow]);
return isKeyboadVisible ? null : <BaseComponent {...props}></BaseComponent>;
};
export default useHideWhenKeyboardOpen;
Example Usage:
return(
.
.
.
{useHideWhenKeyboardOpen(
<View style={[styles.buttonContainer]}>
<Button
icon={<Icon name="save" size={16} color="white" />}
title={strings.STOCKS_FEED.submit}
iconRight={true}
onPress={() => {
toggleSettings();
}}
style={styles.submitButton}
raised={true}
/>
</View>,
)}
)
Mindset shift will help: think of hooks as data source rather than JSX factory:
const isKeyboardShown = useKeyboardStatus();
...
{!isKeyboardShown && (...
Accordingly your hook will just return current status(your current version look rather as a HOC):
const useHideWhenKeyboardOpen = () => {
const [isKeyboadVisible, setIsKeyboadVisible] = useState(false);
const _keyboardDidShow = useCallback(() => {
setIsKeyboadVisible(true);
}, []);
const _keyboardDidHide = useCallback(() => {
setIsKeyboadVisible(false);
}, []);
useEffect(() => {
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
return () => {
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
};
}, [_keyboardDidHide, _keyboardDidShow]);
return isKeyboadVisible;
};
Note usage of useCallback. Without it your hook will unsubscribe from Keyboard and subscribe again on every render(since _keyboardDidHide would be referentially different each time and would trigger useEffect). And that's definitely redundant.

Check if screen is getting blurred or focus in React native?

i am using this
useEffect(() => {
const navFocusListener = navigation.addListener('didFocus', () => {
console.log('focus');
});
return () => {
navFocusListener.remove();
};
}, []);
I am using this code also tried other listeners. but there is no benefit, i am using react-native-immediate-call package for ussd dialing but as it doesn't have any callback. So i i call this function a dialer open for dialing for the USSD code. So now i want that when ussd dialing completes then comes back to screen and a api will call to get response. So how can i detect that USSD dialing is running running or completed so that i can make a request to the api.
For focus listener; you must change 'didFocus' to 'focus', If you are using react navigation v5+ and you should update like below:
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
// do something
});
return unsubscribe;
}, []);
You can examine its documentation from here.
in react-navigation 5 you can do this to check screen is focus or blur,
try this in react navigation 5 using usefocuseffect-hook
useEffect(
() => navigation.addListener('focus', () => {}),
[navigation]
);
useEffect(
() => navigation.addListener('blur', () => {}),
[navigation]
);
Try this thanks
import { NavigationEvents } from "react-navigation";
callback=()=>{
alert('I m always working when you come this Screen')
}
in return (
<Your Code>
<NavigationEvents onWillFocus={() => callback()} />
<Your Code/>
)
Actually, you need to detect app state if it is in foreground or background or needs to add callback function into react-native-immediate-call by writing native code of android or ios package like this
import React, { useRef, useState, useEffect } from "react";
import { AppState, StyleSheet, Text, View } from "react-native";
const AppStateExample = () => {
const appState = useRef(AppState.currentState);
const [appStateVisible, setAppStateVisible] = useState(appState.current);
useEffect(() => {
AppState.addEventListener("change", _handleAppStateChange);
return () => {
AppState.removeEventListener("change", _handleAppStateChange);
};
}, []);
const _handleAppStateChange = (nextAppState) => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === "active"
) {
console.log("App has come to the foreground!");
}
appState.current = nextAppState;
setAppStateVisible(appState.current);
console.log("AppState", appState.current);
};
return (
<View style={styles.container}>
<Text>Current state is: {appStateVisible}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
});
export default AppStateExample;

How to test Platform react-native with Jest?

I want to show apple button if platform is ios. The button show properly in ios emulator. But I am confused to test the platfrom.
I have try to mock the platform but the platform will be ios by default in the first time (you can see the image).
This is my component.
import React, { useState, useEffect } from 'react'
import { ScrollView, StatusBar, Platform, View, Text, Linking, SafeAreaView } from 'react-native'
import Header from './components/Header'
import Form from './components/Form'
import ButtonFull from '../../../__global__/button/buttonFull'
import styles from './styles/StyleLogin'
import color from '../../../__global__/styles/themes/colorThemes'
const LoginScreen = () => {
const showAppleButton = () => {
console.log('Platform ', Platform.OS)
if (Platform.OS === 'ios') {
console.log('Platform OS ', Platform.OS)
console.log('Platform Version ', Platform.Version)
const version = Platform.Version ? Platform.Version.split('.')[0] : 0
if (version >= 13) {
return (
<View style={styles.containerButton}>
<ButtonFull
accessibilityLabel={'appleButton'}
isDisabled={false}
buttonColor={color.black}
onPress={() => loginWithApple()}
title={'Apple ID'}
/>
</View>
)
} else {
return false
}
} else {
return false
}
}
return (
<ScrollView>
<StatusBar
translucent
backgroundColor="transparent"
barStyle="light-content"
/>
<Header />
<Form
phoneNumber={phoneNumber}
changePhoneNumber={(value) => changePhoneNumber(value)}
focus={focus}
setFocus={(value) => setFocus(value)}
loginSubmit={() => loginSubmit()}
error={error}
fullFilled={fullFilled}
submited={submited}
setSubmited={(value) => setSubmited(value)}
sendOtp={() => sendOtp()} />
{showAppleButton()}
</ScrollView>
)
}
export default LoginScreen
Test File
import React from 'react'
import { configure, shallow } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import LoginScreen from '../index'
import renderer from 'react-test-renderer'
import { fireEvent, render, waitFor } from 'react-native-testing-library'
import '#testing-library/jest-native/extend-expect'
import { Provider } from 'react-redux'
import reducers from '../../../../redux/store'
import { createStore } from 'redux'
import { Platform } from 'react-native'
jest.mock('#react-navigation/native', () => ({
useNavigation: component => component,
}))
describe('Login screen', () => {
const mockPlatform = (OS, Version) => {
jest.resetModules()
jest.doMock('react-native/Libraries/Utilities/Platform', () => ({
OS,
select: config => config[OS],
Version,
}))
};
const store = createStore(reducers)
configure({ adapter: new Adapter() })
const wrapper = shallow(<Provider store={store}><LoginScreen /></Provider>)
const rendered = renderer.create(<Provider store={store}><LoginScreen /></Provider>)
it('renders correctly', () => {
expect(rendered.toJSON()).toBeTruthy()
})
it('should render the header component', () => {
expect(wrapper.find('Header').exists())
})
it('should render the form component', () => {
expect(wrapper.find('Form').exists())
})
it('should render the button social media', () => {
mockPlatform('android', '15.0.1')
console.log('Apple Button ', wrapper.find('[accessibilityLabel="appleButton"]').exists())
})
})
In this image, platform will be ios in the first time. I dont know why.
It's not safe to rely on module internals like react-native/Libraries/Utilities/Platform. Platform is imported from react-native and should be preferably mocked on this module.
jest.doMock doesn't affect modules that were imported on top level. In order for a mock to take effect, the whole hierarchy that depends on mocked module needs to re-imported locally with require.
In this case this isn't needed because Platform is referred as an object, so its properties can be mocked instead. The mock needs to be aware which properties can be mocked as functions:
let originalOS;
beforeEach(() => {
originalOS = Platform.OS;
});
afterEach(() => {
Platform.OS = originalOS;
jest.restoreAllMocks();
});
it('should mock Platform', () => {
jest.spyOn(Platform, 'select').mockReturnValue('android');
jest.spyOn(Platform, 'Version', 'get').mockReturnValue('15.0.1')
Platform.OS = 'android';
...
});

What is the best practice for unit testing my functional component with hooks in React Native?

I've written a simple wrapper component around ScrollView that enables/disables scrolling based on the available height it's given:
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Keyboard, ScrollView} from 'react-native';
import {deviceHeight} from '../../../platform';
export default function ScrollingForm({
availableHeight = deviceHeight,
children,
}) {
const [scrollEnabled, setScrollEnabled] = useState(true);
const [formHeight, setFormHeight] = useState(0);
const scrollingForm = useRef(null);
const checkScrollViewEnabled = useCallback(() => {
setScrollEnabled(formHeight > availableHeight);
}, [availableHeight, formHeight]);
const onFormLayout = async event => {
await setFormHeight(event.nativeEvent.layout.height);
checkScrollViewEnabled();
};
useEffect(() => {
Keyboard.addListener('keyboardDidHide', checkScrollViewEnabled);
// cleanup function
return () => {
Keyboard.removeListener('keyboardDidHide', checkScrollViewEnabled);
};
}, [checkScrollViewEnabled]);
return (
<ScrollView
ref={scrollingForm}
testID="scrollingForm"
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
scrollEnabled={scrollEnabled}
onLayout={onFormLayout}
onKeyboardDidShow={() => setScrollEnabled(true)}>
{children}
</ScrollView>
);
}
I need to write unit tests for this component. So far I have:
import React from 'react';
import {Keyboard} from 'react-native';
import {render} from 'react-native-testing-library';
import {Text} from '../..';
import ScrollingForm from './ScrollingForm';
describe('ScrollingForm', () => {
Keyboard.addListener = jest.fn();
Keyboard.removeListener = jest.fn();
let renderer;
describe('keyboard listener', () => {
it('renders text with default props', () => {
renderer = render(
<ScrollingForm>
<Text>Hello World!</Text>
</ScrollingForm>,
);
expect(Keyboard.addListener).toHaveBeenCalled();
});
it('should call listener.remove on unmount', () => {
renderer = render(
<ScrollingForm>
<Text>Goodbye World!</Text>
</ScrollingForm>,
);
renderer.unmount();
expect(Keyboard.removeListener).toHaveBeenCalled();
});
});
});
I want to also confirm that on layout if the available height is greater than the form height, that scrollEnabled is correctly being set to false. I've tried something like this:
it('sets scrollEnabled to false onFormLayout width 1000 height', () => {
mockHeight = 1000;
const {getByTestId} = render(
<ScrollingForm availableHeight={500}>
<Text>Hello World!</Text>
</ScrollingForm>,
);
const scrollingForm = getByTestId('scrollingForm');
fireEvent(scrollingForm, 'onLayout', {
nativeEvent: {layout: {height: mockHeight}},
});
expect(scrollingForm.props.scrollEnabled).toBe(false);
});