I have a production iPad application that I used to use Jest + @testing-library/react-native to perform e2e testing. I've recently decided to switch to Detox for a number of reasons.
After setting it up and migrating some tests to Detox syntax, I've noticed that actions are painfully slow. The demo on the site showed that each click/typing takes milliseconds, while it's taking way longer than that here, and I'm not sure what could be causing this. For context, I have around 200+ UI tests, and if each one is over a minute to execute, this just doesn't make sense for CI purposes. Below is a link for a demo as to what I'm referring to. I'll past my configs below, although I'm more looking for pointers in the right direction, since I'm not sure what could be causing this.
Example Screen: Login.tsx
import React, { useState, useCallback } from "react";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { View, ActivityIndicator } from "react-native";
import styles from "src/pages/Login/styles.stylesheet";
import {
PageTitle,
TextInput,
Button,
BasicModal,
BasicSafeAreaContainer,
} from "src/components";
import { LandingStackParamsList } from "src/routers/LandingStackNavigator";
import { useDispatch } from "react-redux";
import { palette } from "src/common/styles";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
type NavigationProps = NativeStackScreenProps<LandingStackParamsList, "Login">;
export const Login = ({ navigation }: NavigationProps) => {
const dispatch = useDispatch();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const validateLoginInputs = useCallback(async () => {
// Logic for logging in
}, [email, password]);
return (
<BasicSafeAreaContainer>
<View style={styles.container}>
<KeyboardAwareScrollView
extraScrollHeight={10}
showsVerticalScrollIndicator={false}
testID="login-scroll-view"
>
<View>
<PageTitle
text="Log In"
onBackPress={() => navigation.goBack()}
style={styles.pageTitle}
/>
<View style={styles.inputs}>
<TextInput
testID="login-email-input"
containerStyle={styles.eachInput}
label="Email Address"
value={email}
onChangeText={setEmail}
placeholder={"Enter your email address..."}
autoCorrect={false}
autoComplete="email"
autoCapitalize="none"
/>
<TextInput
testID="login-password-input"
containerStyle={styles.eachInput}
label="Password"
value={password}
secureTextEntry={true}
onChangeText={setPassword}
placeholder={"Enter your password..."}
autoCorrect={false}
autoCapitalize="none"
/>
</View>
</View>
{isLoading ? (
<ActivityIndicator
style={styles.createAccountButton}
size="large"
color={palette.blue}
/>
) : (
<Button
testID="login-button"
style={styles.createAccountButton}
onPress={async () => {
await validateLoginInputs();
}}
type={"primary"}
text={"Log In"}
/>
)}
<BasicModal
testID="login-error-modal"
isVisible={error !== undefined}
title={"Error"}
message={error}
onConfirm={() => setError(undefined)}
/>
</KeyboardAwareScrollView>
</View>
</BasicSafeAreaContainer>
);
};
Example Test: TestLoginFlow.test.ts
import { clearDatabase } from "src/test/server";
import { by, element, expect, device } from "detox";
afterAll(async () => {
await clearDatabase();
}, 10000);
beforeEach(async () => {
await clearDatabase();
await device.uninstallApp();
await device.installApp();
await device.launchApp({
newInstance: true,
permissions: {
notifications: "YES",
},
});
}, 50000);
test("Test signing in with valid credentials", async () => {
const restaurant = await mockCreatingARestaurant();
const loginButton = element(by.id("landing-login-button"));
await loginButton.tap();
const emailInput = element(by.id("login-email-input"));
const passwordInput = element(by.id("login-password-input"));
await emailInput.replaceText(restaurant.contactEmailAddress);
await passwordInput.replaceText("password");
await element(by.id("login-scroll-view")).tap({ x: 0, y: 0 });
const signInButton = element(by.id("login-button"));
await signInButton.tap();
const categoriesTab = element(by.id("dashboard-tab-empty-state"));
await expect(categoriesTab).toExist();
});
detoxrc.js
/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
args: {
'$0': 'jest',
config: './jest.config.js'
},
jest: {
setupTimeout: 120000
}
},
apps: {
'ios.debug': {
type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/Restaurant.app',
build: 'react-native start'
},
},
devices: {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPad (9th generation)'
}
},
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.debug'
},
}
};
jest.config.js
module.exports = {
clearMocks: true,
maxWorkers: 1,
moduleDirectories: ["node_modules", "<rootDir>"],
globals: {
__DEV__: true,
__BUNDLE_START_TIME__: 10101,
},
testMatch: ["<rootDir>/**/*.test.ts"],
setupFiles: ["./node_modules/react-native-gesture-handler/jestSetup.js"],
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/assetsTransformer.js",
"\\.(css|less)$": "<rootDir>/assetsTransformer.js",
uuid: require.resolve("uuid"),
},
testResultsProcessor: "jest-junit",
transformIgnorePatterns: [
"jest-runner",
"node_modules/(?!(jest-)?@?react-native|@react-native-community|@react-navigation|@rneui|@sentry/react-native)",
],
testTimeout: 120000,
globalSetup: "detox/runners/jest/globalSetup",
globalTeardown: "detox/runners/jest/globalTeardown",
reporters: ["detox/runners/jest/reporter"],
testEnvironment: "detox/runners/jest/testEnvironment",
verbose: true,
};