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';
...
});
Related
I have an app that is currently live in the store. After clicking a button 3 times in the application, an advertisement is shown. There has been a serious decrease in advertising revenues in the last two days. The reason for this was that on some devices, it was necessary to press more than 3 times for the ad to appear. On my friend's phone, normally it was necessary to click 3 times for the ad to appear, but now after 13 clicks, the ad appeared. How can I solve this issue?
Splash screen
import { View } from 'react-native'
import React, { useEffect } from 'react'
import mobileAds from 'react-native-google-mobile-ads';
export default Splash = ({ navigation }) => {
useEffect(() => {
mobileAds().initialize()
.then(e => {
navigation.navigate("MainScreen")
})
}, [])
return (
<View />
)
}
MainScreen:
import { Button } from 'react-native'
import React, { useState, useEffect } from 'react'
import { InterstitialAd, TestIds, AdEventType } from 'react-native-google-mobile-ads';
const adUnitId = __DEV__ ? TestIds.INTERSTITIAL : 'ca-app-pub-XXXX';
const interstitialAd = InterstitialAd.createForAdRequest(adUnitId);
export default MainScreen = ({ navigation }) => {
const [request, setRequest] = useState(0)
useEffect(() => {
interstitialAd.addAdEventsListener(({ type }) => {
if (type === AdEventType.LOADED) {
interstitialAd.show();
}
});
}, [])
useEffect(() => {
if (countOfRequests % 3 == 0) {
interstitialAd.load();
}
}, [request])
return (
<Button
title='press'
onPress={() => setRequest(prev => prev + 1)}
/>
)
}
I want to check If the user has a secure Token in a useEffect but I get this error Message.
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application.
This happens when I use the useEffect. If I remove it, then I get no error message but I need to check if the user has the token.
import React, { useEffect } from 'react';
import { View, Text } from 'react-native';
import getSecureKey from '../utilies/getSecureKey';
const Stack = createStackNavigator();
const AppStack = ({ navigation }) => {
useEffect(() => {
getSecureKey().then(res => console.log(res)).catch(e => console.log(e));
}, []);
return (
<Stack.Navigator showIcon={true} initialRouteName="AppTabs">
<Stack.Screen name="AppTabs" component={AppTabs} options={{headerTitle: () => <Header />, headerStyle: {
backgroundColor: '#fff'
}}} />
.....
getSecureToken:
import * as SecureStore from 'expo-secure-store';
const getSecureKey = async () => {
const key = await SecureStore.getItemAsync('jwt');
return key;
};
export default getSecureKey;
App.js
import React, { useState, useEffect } from 'react';
import * as Font from 'expo-font';
import { NavigationContainer } from '#react-navigation/native';
import AppLoading from 'expo-app-loading';
import { Provider } from 'react-redux';
import store from './src/redux/store/index';
import AppStack from './src/navigation/stack';
const getFonts = async () => {
await Font.loadAsync({
"nunito-regular": require("./assets/fonts/Nunito-Regular.ttf"),
"nunito-bold": require("./assets/fonts/Nunito-Bold.ttf"),
});
};
const App = () => {
const [fontsLoaded, setFontsLoaded] = useState(false);
if(fontsLoaded) {
return (
<Provider store={store}>
<NavigationContainer><AppStack /></NavigationContainer>
</Provider>)
} else {
return (<AppLoading startAsync={getFonts} onFinish={() => setFontsLoaded(true)} onError={() => {}} />)
}
};
export default App;
Don't restore token in the navigator. Instead do this -
Firstly, install expo-app-loading from here
Then, create a folder called navigation where your App.js is located. Then inside it create a File called AppNavigator.js.
Inside AppNavigator.js, paste this
import React, { useEffect } from 'react';
import { View, Text } from 'react-native';
import { createStackNavigator } from '#react-navigation/stack';
import getSecureKey from '../utilities/getSecureKey';
const Stack = createStackNavigator();
const AppNavigator = () => {
// Remove these Lines --
// useEffect(() => {
// getSecureKey()
// .then((res) => console.log(res))
// .catch((e) => console.log(e));
// }, []);
return (
<Stack.Navigator showIcon={true} initialRouteName="AppTabs">
<Stack.Screen
name="AppTabs"
component={AppTabs}
options={{
headerTitle: () => <Header />,
headerStyle: {
backgroundColor: '#fff',
},
}}
/>
</Stack.Navigator>
);
};
export default AppNavigator;
For your fonts create a folder called hooks where your App.js is located and inside that create a file useFonts.js
In useFonts.js write like this -
import * as Font from "expo-font";
export default useFonts = async () => {
await Font.loadAsync({
"nunito-regular": require("./assets/fonts/Nunito-Regular.ttf"),
"nunito-bold": require("./assets/fonts/Nunito-Bold.ttf"),
});
};
In your App.js
import React, { useState } from 'react';
import { Text, View, StyleSheet } from 'react-native';
import Constants from 'expo-constants';
import { NavigationContainer } from '#react-navigation/native';
import AppLoading from 'expo-app-loading';
import useFonts from "./hooks/useFonts";
import getSecureKey from './utilities/getSecureKey';
import AppNavigator from './navigation/AppNavigator';
export default function App() {
const [IsReady, SetIsReady] = useState(false);
// Always perform Token Restoration in App.js just to keep code clear.
const FontAndTokenRestoration = async () => {
await useFonts(); // Font is being loaded here
const token = await getSecureKey();
if (token) {
console.log(token);
}
};
if (!IsReady) {
return (
<AppLoading
startAsync={FontAndTokenRestoration}
onFinish={() => SetIsReady(true)}
onError={() => {}}
/>
);
}
return (
<NavigationContainer>
<AppNavigator />
</NavigationContainer>
);
}
I have a custom react hook 'useSample' which uses useNavigation and useNavigationParam
import { useContext } from 'react'
import { useNavigation, useNavigationParam } from 'react-navigation-hooks'
import sampleContext from '../sampleContext'
import LoadingStateContext from '../LoadingState/Context'
const useSample = () => {
const sample = useContext(sampleContext)
const loading = useContext(LoadingStateContext)
const navigation = useNavigation()
const Mode = !!useNavigationParam('Mode')
const getSample = () => {
if (Mode) {
return sample.selectors.getSample(SAMPLE_ID)
}
const id = useNavigationParam('sample')
sample.selectors.getSample(id)
navigation.navigate(SAMPLE_MODE_ROUTE, { ...navigation.state.params}) // using navigation hook here
}
return { getSample }
}
export default useSample
I need to write unit tests for the above hook using jest and I tried the following
import { renderHook } from '#testing-library/react-hooks'
import sampleContext from '../../sampleContext'
import useSample from '../useSample'
describe('useSample', () => {
it('return sample data', () => {
const getSample = jest.fn()
const sampleContextValue = ({
selectors: {
getSample
}
})
const wrapper = ({ children }) => (
<sampleContext.Provider value={sampleContextValue}>
{children}
</sampleContext.Provider>
)
renderHook(() => useSample(), { wrapper })
})
})
I got the error
'react-navigation hooks require a navigation context but it couldn't be found. Make sure you didn't forget to create and render the react-navigation app container. If you need to access an optional navigation object, you can useContext(NavigationContext), which may return'
Any help would be appreciated!
versions I am using
"react-navigation-hooks": "^1.1.0"
"#testing-library/react-hooks":"^3.4.1"
"react": "^16.11.0"
You have to mock the react-navigation-hooks module.
In your test:
import { useNavigation, useNavigationParam } from 'react-navigation-hooks';
jest.mock('react-navigation-hooks');
And it's up to you to add a custom implementation to the mock. If you want to do that you can check how to mock functions on jest documentation.
for me, soved it by usingenter code here useRoute():
For functional component:
import * as React from 'react';
import { Button } from 'react-native';
import { useNavigation } from '#react-navigation/native';
function MyBackButton() {
const navigation = useNavigation();
return (
<Button
title="Back"
onPress={() => {
navigation.goBack();
}}
/>
);
}
For class component:
class MyText extends React.Component {
render() {
// Get it from props
const { route } = this.props;
}
}
// Wrap and export
export default function(props) {
const route = useRoute();
return <MyText {...props} route={route} />;
}
I am trying to snapshot test with Jest, Expo, React Navigation and my whole app uses hooks only. I'd like to eventually make these into e2e tests where Jest clicks through and snapshot tests everything but I can't even get react navigation to render. My snapshot after the expo loader always shows "null." I followed the basic example from the tabs starter that comes with expo init but the way it outlines how to setup the mocks simply doesn't work for my app. I've tried all sorts of things but nothing works.
App.tsx
import { Ionicons } from '#expo/vector-icons';
import { AppLoading } from 'expo';
import { Asset } from 'expo-asset';
import * as Font from 'expo-font';
import React, { useState } from 'react';
import { YellowBox } from 'react-native';
import { Provider as PaperProvider } from 'react-native-paper';
import { useScreens } from 'react-native-screens';
import { Provider as RxProvider } from 'reactive-react-redux';
import { applyMiddleware, compose, createStore } from 'redux';
import thunk from 'redux-thunk';
import SnackBar from './components/UI/Snackbar';
import { socketMiddleware } from './lib/socketMiddleware';
import SwitchNavigator from './navigation/AppNavigator';
import rootReducer from './reducers/rootReducer';
import { theme } from './Theme';
const middleware = [thunk, socketMiddleware()];
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export const store = createStore<iAppState, any, any, any>(
rootReducer,
composeEnhancers(applyMiddleware(...middleware))
);
// Must be called prior to navigation stack rendering
useScreens();
YellowBox.ignoreWarnings(['Require cycle:']);
const App = (props) => {
const [isLoadingComplete, setLoadingComplete] = useState(false);
if (!isLoadingComplete && !props.skipLoadingScreen) {
return (
<AppLoading
startAsync={loadResourcesAsync}
onError={handleLoadingError}
onFinish={() => handleFinishLoading(setLoadingComplete)}
/>
);
} else {
return (
<RxProvider store={store}>
<PaperProvider theme={theme}>
<SwitchNavigator />
<SnackBar />
</PaperProvider>
</RxProvider>
);
}
};
const loadResourcesAsync = async () => {
await Promise.all([
Asset.loadAsync([
//nothing
]),
Font.loadAsync({
...Ionicons.font,
TitilliumText250: require('./assets/fonts/TitilliumText22L-250wt.otf'),
TitilliumText800: require('./assets/fonts/TitilliumText22L-800wt.otf')
})
]);
};
const handleLoadingError = (error: Error) => {
console.warn(error);
};
const handleFinishLoading = (setLoadingComplete) => {
setLoadingComplete(true);
};
export default App;
App.test.tsx:
import React from 'react';
import { Provider as PaperProvider } from 'react-native-paper';
import NavigationTestUtils from 'react-navigation/NavigationTestUtils';
import renderer from 'react-test-renderer';
import App from './App';
import { theme } from './Theme';
jest.mock('expo', () => ({
AppLoading: 'AppLoading'
}));
jest.mock('react-native-screens');
jest.mock('react-native-paper');
jest.mock('redux');
jest.mock('reactive-react-redux');
jest.mock('./navigation/AppNavigator', () => 'SwitchNavigator');
describe('App', () => {
jest.useFakeTimers();
beforeEach(() => {
NavigationTestUtils.resetInternalState();
});
// success
it(`renders the loading screen`, () => {
const tree = renderer.create(<App />).toJSON();
expect(tree).toMatchSnapshot();
});
// this snapshot is always null
it(`renders the root without loading screen`, () => {
const tree = renderer
.create(
<PaperProvider theme={theme}>
<App skipLoadingScreen></App>
</PaperProvider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
});
/navigation/AppNavigator.tsx:
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
import LoginStack from './LoginStack';
import TabStack from './TabStack';
/** The most root navigator which allocates to the others. */
const SwitchNavigator = createAppContainer(
createSwitchNavigator(
{
LoginStack: LoginStack,
TabStack: TabStack
},
{
initialRouteName: 'LoginStack'
}
)
);
export default SwitchNavigator;
I was having a similar issue and found a fix via: https://github.com/expo/expo/issues/7155#issuecomment-592681861
Seems like the act() worked magically for me to stop it from returning null (not sure how)
Update your test to use it like this:
import { act, create } from 'react-test-renderer';
it('renders the root without loading screen', () => {
let tree;
act(() => {
tree = create(
<PaperProvider theme={theme}>
<App skipLoadingScreen></App>
</PaperProvider>
);
});
expect(tree.toJSON()).toMatchSnapshot();
});
Note: When I changed how I was using the imports from 'react-test-renderer' my tests couldn't find act() as a function, a simple re-install of npm packages solved this problem!
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)