I'm attempting to create an automated UI test suite for my React Native app with Expo. I have looked everywhere for good tutorials but when I get to the actual test writing portion, my tests never even run because of environment issues such as "Unexpected Identifier/Token" on import Icon from...
or other stupid issues that I cannot find any tutorials on how to fix them. I literally have spent a week trying to resolve these issues.
I am new to React Native and new to Jest/Detox/Expo
Here's my package.json
{
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"test": "node_modules/.bin/jest test/**/*.spec.js",
"eject": "expo eject"
},
"jest": {
"verbose": true,
"preset": "jest-expo"
},
"dependencies": {
"apsl-react-native-button": "^3.1.1",
"react": "16.5.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz",
"react-native-camera": "git+https://[email protected]/react-native-community/react-native-camera.git",
"react-native-camera-roll-picker": "^1.2.3",
"react-native-elements": "^1.0.0",
"react-native-fontawesome": "^6.0.1",
"react-native-is-iphonex": "^1.0.1",
"react-native-vector-icons": "^6.2.0",
"react-navigation": "^3.1.5"
},
"devDependencies": {
"babel-preset-expo": "^5.0.0",
"bunyan-debug-stream": "^2.0.0",
"detox": "^10.0.9",
"detox-expo-helpers": "^0.6.0",
"expo-detox-hook": "^1.0.10",
"jest-expo": "^32.0.0",
"react-native-testing-library": "^1.5.0",
"react-test-renderer": "^16.8.2",
"babel-jest": "^24.1.0",
"enzyme": "^3.9.0",
"@babel/core": "^7.3.3",
"@expo/vector-icons": "^9.0.0",
"expo": "^32.0.0",
"jest": "^24.1.0"
},
"private": true,
"detox": {
"test-runner": "jest",
"configurations": {
"ios.sim": {
"binaryPath": "bin/Exponent.app",
"type": "ios.simulator",
"name": "iPhone X"
}
}
}
}
Here are the errors I'm getting
ip-10-101-32-118:KitchenProject bob.dole$ detox test --loglevel trace
configuration="ios.sim" loglevel="trace" artifactsLocation="artifacts/ios.sim.2019-02-21 21-54-14Z" node_modules/.bin/jest "e2e" --config=e2e/config.json --maxWorkers=1 '--testNamePattern=^((?!:android:).)*$'
● Deprecation Warning:
Option "setupTestFrameworkScriptFile" was replaced by configuration "setupFilesAfterEnv", which supports multiple paths.
Please update your configuration.
Configuration Documentation:
https://jestjs.io/docs/configuration.html
FAIL e2e/RoomLayout.spec.js
● Test suite failed to run
/Users/bob.dole/KitchenDetail/KitchenProject/node_modules/@expo/vector-icons/FontAwesome.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import glyphMap from './vendor/react-native-vector-icons/glyphmaps/FontAwesome.json';
^^^^^^^^
SyntaxError: Unexpected identifier
> 1 | import FontAwesomeI from 'react-native-vector-icons/FontAwesome'
| ^
2 | import React from 'react'
3 |
4 | export const FontAwesome = props => (
at ScriptTransformer._transformAndBuildScript (../node_modules/jest/node_modules/jest-runtime/build/ScriptTransformer.js:440:17)
at Object.<anonymous> (../Components/icons.js:1:1)
FAIL e2e/tests/components/RoomLayoutDetox.spec.js
● Test suite failed to run
/Users/bob.dole/KitchenDetail/KitchenProject/node_modules/@expo/vector-icons/FontAwesome.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import glyphMap from './vendor/react-native-vector-icons/glyphmaps/FontAwesome.json';
^^^^^^^^
SyntaxError: Unexpected identifier
> 1 | import FontAwesomeI from 'react-native-vector-icons/FontAwesome'
| ^
2 | import React from 'react'
3 |
4 | export const FontAwesome = props => (
at ScriptTransformer._transformAndBuildScript (../node_modules/jest/node_modules/jest-runtime/build/ScriptTransformer.js:440:17)
at Object.<anonymous> (../Components/icons.js:1:1)
Test Suites: 2 failed, 2 total
Tests: 0 total
Snapshots: 0 total
Time: 0.827s
Ran all test suites matching /e2e/i with tests matching "^((?!:android:).)*$".
child_process.js:677
throw err;
^
Error: Command failed: node_modules/.bin/jest "e2e" --config=e2e/config.json --maxWorkers=1 '--testNamePattern=^((?!:android:).)*$'
at checkExecSyncError (child_process.js:637:11)
at Object.execSync (child_process.js:674:13)
at runJest (/Users/bob.dole/KitchenDetail/KitchenProject/node_modules/detox/local-cli/detox-test.js:166:6)
at run (/Users/bob.dole/KitchenDetail/KitchenProject/node_modules/detox/local-cli/detox-test.js:86:7)
at Object.<anonymous> (/Users/bob.dole/KitchenDetail/KitchenProject/node_modules/detox/local-cli/detox-test.js:229:1)
at Module._compile (internal/modules/cjs/loader.js:738:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:749:10)
at Module.load (internal/modules/cjs/loader.js:630:32)
at tryModuleLoad (internal/modules/cjs/loader.js:570:12)
at Function.Module._load (internal/modules/cjs/loader.js:562:3)
Here's my component file RoomLayout.js
import React, { Component } from 'react';
import { StyleSheet, View, Text, Button } from 'react-native';
import { LayoutButtons } from './LayoutButtons';
import { CameraLauncher } from './CameraLauncher';
import { CommentsLauncher } from './CommentsLauncher';
export class RoomLayout extends Component {
render() {
return (
<View>
<Text testID='roomLayoutText' style={styles.room}>
Room Layout{"\n"}
</Text>
<Text testID='infoText' style={styles.infoText}>
Take photos from opposite corners of the room{"\n"}
</Text>
<LayoutButtons />
</View>
);
}
}
const styles = StyleSheet.create({
view: {
marginTop: 80,
textAlign: 'center',
alignItems: 'center',
justifyContent: 'center'
},
infoText: {
marginTop: -10,
fontWeight: 'normal',
textAlign: 'center',
fontSize: 12,
justifyContent: 'center',
alignSelf: 'center',
color: 'grey'
},
room: {
marginTop: 15,
fontWeight: 'bold',
textAlign: 'center',
lineHeight: 14,
fontSize: 15
}
});
Here's my RoomLayout.spec.js file
import React from 'react';
// import { RoomLayout } from '../Components/RoomLayout';
import { render } from 'react-native-testing-library';
describe('RoomLayout', () => {
// *** EDIT - I have removed this code ***
// beforeEach(async () => {
// const tree = render(<RoomLayout />);
//
// });
test('should have header and info text', async () => {
await element(by.text('Room Layout'));
await element(by.id('infoText'));
await element(by.id('infoText').and(by.text(' Take photos from opposite corners of the room')));
await expect(element(by.id('layoutButtonsReference'))).toBeVisible();
});
});
We have no specific support in Detox for Expo applications (ejected or otherwise).
To do so, run yarn add jest-expo --dev or npm i jest-expo --save-dev depending on which package manager you prefer. Then, install the test renderer library: yarn add react-test-renderer --dev or npm i react-test-renderer --save-dev .
Cypress supports almost all modern architecture development frameworks such as Angular, React Native, Vu, as well as MVC-type frameworks.
Setting up Detox with an Expo app. You're probably best placed to start with a clean app that you haven't done anything with yet. You’ll want to make sure you have followed the basic setup (step 1) for getting detox to work on your machine
#Install the following devDependencies
npm i -D detox detox-expo-helpers expo-detox-hook jest
#Update the package.json
Add the following to your package.json
file, this configures detox. You can choose the type of iPhone that you want.
"detox": {
"configurations": {
"ios.sim": {
"binaryPath": "bin/Exponent.app",
"type": "ios.simulator",
"name": "iPhone X"
}
},
"test-runner": "jest"
}
In the scripts section add the following:
"scripts": {
"e2e": "detox test --configuration ios.sim"
}
This will allow us to run the detox test but using npm run e2e
#Setup your first test
Run the following to set up your first test
detox init -r jest
This will add a folder called e2e
in your project. You will find three files inside it
config.json
firstTest.spec.js
init.js
firstTest.spec.js
is a sample test. You will need to make the following changes to it.
const { reloadApp } = require('detox-expo-helpers');
You also need to change the following line
await device.reloadReactNative();
to
await reloadApp();
#Add the Expo Client to your project
Exponent.app
. It'll have a file icon but will still be a folder.bin
folder and put Exponent.app
inside so it matches the binaryPath set above.Or alternatively you could use the following script, create a file and name it setup.sh
in your project root directory, copy the contents and then run it (you will probably need to give it permission to run which you can do by running chmod +x setup.sh
first, then you can run it using ./setup.sh
).
#!/bin/bash -e
# query expo.io to find most recent ipaUrl
IPA_URL=`curl https://expo.io/--/api/v2/versions | python -c 'import sys, json; print json.load(sys.stdin)["iosUrl"]'`
# download tar.gz
TMP_PATH=/tmp/exponent.tar.gz
wget -O $TMP_PATH $IPA_URL
# recursively make app dir
APP_PATH=bin/Exponent.app
mkdir -p $APP_PATH
# unzip tar.gz into APP_PATH
tar -C $APP_PATH -xzf $TMP_PATH
This script does the same as the above steps.
#Run your first test
Start the packager with expo start -c
Launch the simulator that you plan to use for your test (so if you picked an iPhone X, launch the iPhone X etc).
Then in your terminal you can run npm run e2e
if you added the script or you can run detox test
.
What you will find is that your test will fail. This is ok and to be expected.
Now if you want to make your test pass you need to implement all of the test cases that exist in the firstTest.spec.js
or you can scrape those and write your own.
#Links
#Make your tests pass
If you want to make your tests pass you can update the following files and you should get 3 passing tests.
firstTest.spec.js
const { reloadApp } = require('detox-expo-helpers');
describe('Example', () => {
beforeEach(async () => {
await reloadApp();
});
it('should have welcome screen', async () => {
await expect(element(by.id('welcome'))).toBeVisible();
});
it('should show hello screen after tap', async () => {
await element(by.id('hello_button')).tap();
await expect(element(by.label('Hello!!!'))).toBeVisible();
});
it('should show world screen after tap', async () => {
await element(by.id('world_button')).tap();
await expect(element(by.label('World!!!'))).toBeVisible();
});
});
App.js
import React, { Component } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
export default class App extends Component {
state = {
greeting: undefined
};
render () {
if (this.state.greeting) return this.renderAfterButton();
return (
<View
testID="welcome"
style={{
flex: 1,
paddingTop: 20,
justifyContent: 'center',
alignItems: 'center'
}}>
<Text style={{ fontSize: 25, marginBottom: 30 }}>Welcome</Text>
<TouchableOpacity
testID="hello_button"
onPress={this.onButtonPress.bind(this, 'Hello')}>
<Text style={{ color: 'blue', marginBottom: 20 }}>Say Hello</Text>
</TouchableOpacity>
<TouchableOpacity
testID="world_button"
onPress={this.onButtonPress.bind(this, 'World')}>
<Text style={{ color: 'blue', marginBottom: 20 }}>Say World</Text>
</TouchableOpacity>
</View>
);
}
renderAfterButton () {
return (
<View
style={{
flex: 1,
paddingTop: 20,
justifyContent: 'center',
alignItems: 'center'
}}>
<Text style={{ fontSize: 25 }}>{this.state.greeting}!!!</Text>
</View>
);
}
onButtonPress (greeting) {
this.setState({
greeting: greeting
});
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center'
}
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With