2
votes

I am trying to set up end-to-end testing with Detox on a React-Native/Expo mobile app. The app and Jest are currently working fine, but Detox tests give ReferenceError's. I have followed the instructions in this link, https://blog.expo.io/testing-expo-apps-with-detox-and-react-native-testing-library-7fbdbb82ac87, as well as the Detox and Jest websites. I have run the tests through a jest script (yarn test) and detox test.

$ detox test --loglevel trace
detox[51199] INFO:  [test.js] configuration="ios.sim" loglevel="trace" artifactsLocation="artifacts/ios.sim.2019-04-26 12-31-53Z" recordLogs="none" takeScreenshots="manual" recordVideos="none" recordPerformance="none" node_modules/.bin/jest --config=e2e/config.json --maxWorkers=1 '--testNamePattern=^((?!:android:).)*$' "e2e"
● Validation Warning:

  Unknown option "setupFilesAfterEnv" with value ["./init.js"] was found.
  This is probably a typing mistake. Fixing it will remove this message.

  Configuration Documentation:
  https://jestjs.io/docs/configuration.html

 FAIL  e2e/features/login/index.spec.js
  App
    ✕ should have login screen (272ms)
    ✕ should show hello screen after tap (104ms)
    ✕ should show world screen after tap (105ms)

  ● App › should have login screen

    ReferenceError: device is not defined

      at reloadApp (../node_modules/detox-expo-helpers/index.js:68:3)

  ● App › should have login screen

    ReferenceError: element is not defined
...

setupFilesAfterEnv is a jest option.

The downloaded Expo IPA is in the directory bin/Exponent.app.

package.json

{
...
  "main": "node_modules/expo/AppEntry.js",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "test": "jest --watchAll --notify",
    "lint": "eslint .",
    "ci": "yarn lint && jest"
  },
  "dependencies": {
    "expo": "^32.0.0",
    "formik": "^1.5.1",
    "invariant": "^2.2.4",
    "prop-types": "^15.7.2",
    "react": "16.5.0",
    "react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz",
    "react-navigation": "^3.3.2",
    "react-redux": "^6.0.1",
    "redux": "^4.0.1",
    "redux-persist": "^5.10.0",
    "redux-thunk": "^2.3.0",
    "reselect": "^4.0.0"
  },
  "devDependencies": {
    "@babel/core": "^7.3.4",
    "babel-eslint": "^10.0.1",
    "babel-plugin-module-resolver": "^3.2.0",
    "babel-preset-expo": "^5.0.0",
    "detox": "^12.4.1",
    "detox-expo-helpers": "^0.6.0",
    "eslint": "^5.15.0",
    "eslint-config-prettier": "^4.1.0",
    "eslint-plugin-prettier": "^3.0.1",
    "eslint-plugin-react": "^7.12.4",
    "expo-detox-hook": "^1.0.10",
    "jest-expo": "^32.0.0",
    "prettier": "^1.16.4",
    "react-native-testing-library": "^1.7.0",
    "react-test-renderer": "^16.8.6",
    "redux-devtools-extension": "^2.13.8",
    "redux-mock-store": "^1.5.3"
  },
  "jest": {
    "preset": "jest-expo",
    "clearMocks": true
  },
  "detox": {
    "test-runner": "jest",
    "runner-config": "e2e/config.json",
    "configurations": {
      "ios.sim": {
        "binaryPath": "bin/Exponent.app",
        "type": "ios.simulator",
        "name": "iPhone XR"
      }
    }
  }
}

The e2e directory is straight from Detox with a few changes for Expo.

e2e/config.json

{
    "setupFilesAfterEnv": ["./init.js"],
    "testEnvironment": "node"
}

e2e/init.js

const detox = require('detox');
const config = require('../package.json').detox;
const adapter = require('detox/runners/jest/adapter');

jest.setTimeout(120000);
jasmine.getEnv().addReporter(adapter);

beforeAll(async () => {
    await detox.init(config);
});

beforeEach(async () => {
    await adapter.beforeEach();
});

afterAll(async () => {
    await adapter.afterAll();
    await detox.cleanup();
});

login.spec.js

import { reloadApp } from 'detox-expo-helpers';

describe('App', () => {
    beforeEach(async () => {
        await reloadApp();
    });

    it('should have login screen', async () => {
        await expect(element(by.id('login'))).toBeVisible();
    });

    it('should show hello screen after tap', async () => {
        await element(by.id('hello_button')).tap();
        await expect(element(by.text('Hello!!!'))).toBeVisible();
    });

    it('should show world screen after tap', async () => {
        await element(by.id('world_button')).tap();
        await expect(element(by.text('World!!!'))).toBeVisible();
    });
});

On a side note, I don't know why there is the jasmine reference in init.js. I guess it is because JestJS is based on Jasmine. Anyway, it does not seem to be connected with the errors.

1

1 Answers

4
votes

Summary

There is an open pull-request for this issue. I quote Yaron Malim

ReferenceError: device is not defined

  at Object.reloadApp (../node_modules/detox-expo-helpers/index.js:68:3)

When I added the following require:

const { device } = require('detox');

The issue was solved. In order to do so, I had to add detox as a dependency. This is the package.json

{
  "name": "detox-expo-helpers",
  "version": "0.6.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {},
  "dependencies": {
    "child-process-promise": "^2.2.1",
    "detox": "^12.2.0",
    "semver": "^5.6.0",
    "xdl": "^51.5.0"
  }
}

Details of the committed files in the pull request

Installing the Necessaries Dependencies

The detox-expo-helpers github repo includes an example app. Add expo-detox-hook to your package.json and run npm install

{
  "devDependencies": {
    "detox": "^9.0.6",
    "detox-expo-helpers": "^0.6.0",
    "expo-detox-hook": "^1.0.10",
    "mocha": "^3.5.0"
  },
  "detox": {
    "configurations": {
      "ios.sim": {
        "binaryPath": "bin/Exponent.app",
        "type": "ios.simulator",
        "name": "iPhone 7"
      }
    }
  }
}

As explained on the official detox-expo-helpers page you should follow every step to set up your detox project, except for the package.json configurations included in this step, which are different in expo.

The detox-expo-helpers includes the official configuration for your emulator at the step Download the Expo app to some directory in your project and configure in package.json.

It also includes an example of the Detox settings for your package.json

Your second test fails because Detox does not find the element on the page. You need to pass a testID prop to the component with value 'hello_button'.

it('should show hello screen after tap', async () => {
    await element(by.id('hello_button')).tap();
    await expect(element(by.text('Hello!!!'))).toBeVisible();
});

documentation on by.id matcher

by.id will match an id that is given to the view via testID prop.

Your component should look like

<TouchableOpacity testID={'hello_button'}>