Skip to main content
Version: 20.x

Migration Guide

We are improving Detox API as we go along, sometimes these changes require us to break the API in order for it to make more sense. These migration guides refer to breaking changes. If a newer version has no entries in this document, it means it does not require special migration steps. Refer to the release notes of the latter builds to learn about their improvements and changes.

20.0

No Mocha support

If you were using Mocha, now you have two options:

Jest 27.2.5 and higher

You have to upgrade your Jest version to at least 27.2.5 or higher. The recommended choice is 28.x or 29.x.

All-in-one configs

If you see an error like this:

DetoxConfigError: Configuration "legacy" uses a deprecated all-in-one schema,
which is not supported by Detox.

You have to extract device and app configs from your configuration, as shown in the example below:

Legacy schemaModern schema
{
"configurations": {
"ios.sim.debug": {
"type": "ios.simulator",
"device": "iPhone 12",
"binaryPath": "/some/path/ios.app",
"build": "..."
},
"android.emu.debug": {
"type": "android.emulator",
"device": "Pixel_30_API",
"binaryPath": "/some/path/android.apk",
"build": "...",
"launchArgs": {}
}
}
}
{
"apps": {
"ios.debug": {
"type": "ios.app",
"binaryPath": "/some/path/ios.app",
"build": "..."
},
"android.debug": {
"type": "android.apk",
"binaryPath": "/some/path/android.apk",
"build": "...",
"launchArgs": {}
}
},
"devices": {
"ios.simulator": {
"type": "ios.simulator",
"device": { "type": "iPhone 12" }
},
"android.emulator": {
"type": "android.apk",
"device": { "avdName": "Pixel_30_API" }
}
},
"configurations": {
"ios.sim.debug": {
"device": "ios.simulator",
"app": "ios.debug"
},
"android.emu.debug": {
"device": "android.emulator",
"app": "android.debug"
}
}
}

For more details refer to the Config > Devices and Config > Apps sections.

testRunner config section

Three top-level string properties (testRunner, runnerConfig, specs) have been unified into a complex testRunner section which strives to be as agnostic as possible:

.detoxrc.js
-  testRunner: 'jest',
- runnerConfig: 'e2e/jest.config.js',
- specs: 'e2e',
- skipLegacyWorkersInjection: true,
+ testRunner: {
+ $0: 'jest',
+ args: {
+ config: 'e2e/jest.config.js',
+ _: ['e2e']
+ },
+ },

Also, skipLegacyWorkersInjection migration option no longer has meaning, so you can remove it safely.

If you didn’t have runnerConfig previously, use e2e/config.json (because that was an implicit default in Detox 19 and earlier):

  testRunner: {
$0: 'jest',
args: {
+ config: 'e2e/config.json',
},
},

Custom Jest environment

You no longer need to have an autogenerated environment file if you haven’t been customizing it, and if it looks like below:

e2e/environment.js
const {
DetoxCircusEnvironment,
SpecReporter,
WorkerAssignReporter,
} = require('detox/runners/jest-circus');

class CustomDetoxEnvironment extends DetoxCircusEnvironment {
constructor(config, context) {
super(config, context);

// Can be safely removed, if you are content with the default value (=300000ms)
this.initTimeout = 300000;

// This takes care of generating status logs on a per-spec basis. By default, Jest only reports at file-level.
// This is strictly optional.
this.registerListeners({
SpecReporter,
WorkerAssignReporter,
});
}
}

module.exports = CustomDetoxEnvironment;

If you want, for example:

  • to reduce the init timeout to 120 seconds,
  • remove SpecReporter output,
  • remove WorkerAssignReporter output,

then you can express this intent via config:

.detoxrc.js
/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
$0: 'jest',
args: {
config: 'e2e/jest.config.js',
_: ['e2e']
},
jest: {
setupTimeout: 120000,
reportSpecs: false,
reportWorkerAssign: false,
},
},
// …
};

If you have some customizations beyond that, make sure to remove the boilerplate code:

e2e/environment.js
const { DetoxCircusEnvironment } = require('detox/runners/jest');

class CustomDetoxEnvironment extends DetoxCircusEnvironment {
constructor(config, context) {
super(config, context);

// leave your custom code
}
}

module.exports = CustomDetoxEnvironment;

Pay attention that the import has been changed to detox/runners/jest (previously it was detox/runners/jest-circus), and the reporters (SpecReporter, WorkerAssignReporter) are no longer exported. You can continue using this.initTimeout if you rename it there to this.setupTimeout, but you can also delegate that to Detox config like shown earlier.

New Jest config

Let's examine the old configs first to see what needs to be removed or changed:

e2e/jest.config.js
module.exports = {
maxWorkers: 1,
testEnvironment: './environment',
testRunner: 'jest-circus/runner',
testTimeout: 120000,
testRegex: '\\.e2e\\.js$',
reporters: ['detox/runners/jest/streamlineReporter'],
verbose: true
};

See comments line by line:

1. As a recommendation, switch to CommonJS (module.exports = {}) instead of JSON, if you haven’t done that already.

2. If you don’t have maxWorkers: 1 configured, please add it.

3. If you removed your test environment in the previous step, remove the testEnvironment property.

4. Remove testRunner since it is already jest-circus by default in Jest 27.x and higher.

5. You need a long testTimeout value, because usually end-to-end tests cannot complete in 5 seconds, which is Jest's default timeout value. We suggest 120 seconds by default, but you can fine-tine it.

6. We recommend to use rootDir and testMatch instead of testRegex, like this (assuming your Jest config is in e2e folder of your project, together with the Detox test files):

-  testRegex: '',
+ rootDir: '..',
+ testMatch: ['<rootDir>/e2e/**/*.test.js'],

7. Remove that streamlineReporter. You’ll be adding a new reporter in a second.

8. You need to set verbose: true to disable log batching. In other words, Jest will be printing logs with a significant delay, which is just inconvenient and annoying when you need to know what exactly is happening right now.

Now, let's take a look at the new config and the required additions:

e2e/jest.config.js
module.exports = {
maxWorkers: 1,
testTimeout: 120000,
verbose: true,
reporters: ['detox/runners/jest/reporter'],
globalSetup: 'detox/runners/jest/globalSetup',
globalTeardown: 'detox/runners/jest/globalTeardown',
testEnvironment: 'detox/runners/jest/testEnvironment',
};

Let's focus on lines 5-8:

  1. Add the new Detox reporter as the first one to your existing reporters: [...] array or create that property like we show. It is a reserved reporter for Detox core logic, so it does not affect your logs or any other output – just make sure you have it.

  2. Register globalSetup shipped with Detox. If you have your own globalSetup, then call both in a wrapper function:

    module.exports = async () => {
    await require('detox/runners/jest/index').globalSetup();
    await yourGlobalSetupFunction();
    };
  3. Register globalTeardown shipped with Detox. If you have your own globalTeardown, then you should wrap it with try-finally and call the teardown of Detox too:

    module.exports = async () => {
    try {
    await yourGlobalTeardownFunction();
    } finally {
    await require('detox/runners/jest/index').globalTeardown();
    }
    };
  4. If you already have removed your custom test environment, please switch to the default test environment shipped with Detox like shown in the example. Otherwise, stay with your custom test environment but make sure to migrate it as was described earlier.

Stop using timeline artifacts

If you have been using --record-timeline all CLI option or had timeline artifact configured in your config,

 {
artifacts: {
plugins: {
logs: 'all',
screenshot: 'failing',
- timeline: 'all',
}
}
}

please remove it from your configs and scripts. Timeline artifacts have been merged with log artifacts, so if you need to have detox.trace.json – just don’t turn off your logs, and you’ll get it.

Updating command-line scripts

Check your Detox scripts whether they had command-line arguments such as:

detox test … -o path/to/jest.config …
detox test … --runner-config path/to/jest.config …
detox test … --workers 3

Detox became agnostic about third-party test runner arguments, and now you need to forward those arguments directly to your test runner (i.e. Jest):

detox test … --config path/to/jest.config …
detox test … --maxWorkers 3

You’ll be able to tell that you’ve run into a situation like this if you see error messages like:

● Unrecognized CLI Parameter:

Unrecognized option "workers".

CLI Options Documentation:
https://jestjs.io/docs/cli

Besides, verify you don’t have a deprecated --device-launch-args parameter in your scripts. It has been superseded by --device-boot-args:

detox test ... --device-boot-args="arg1 arg2"

Revisit environment variables

If you’re relying somewhere in your code on things like process.env.DETOX_CONFIGURATION or any other that starts from DETOX_, as a matter of a workaround, you can turn on testRunner.forwardEnv in your Detox config:

.detoxrc.js
/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
args: {
$0: 'jest',
},
forwardEnv: true,
},
devices: { /* ... */ },
apps: { /* ... */ },
configurations: { /* ... */ },
};

That should solve temporarily the issue with missing environment variables, but this is not the best solution overall. The better fix for that would be switch to using Detox Internals API.

For example, you were determining the number of workers depending on your configuration:

e2e/jest.config.js
module.exports = {
maxWorkers: process.env.CI
? (`${process.env.DETOX_CONFIGURATION}`.startsWith('ios.') ? 3 : 2)
: 1,
globalSetup: '...',
globalTeardown: '...',
// ... and so on ...
};

That would translate to a cleaner code in Detox 20:

e2e/jest.config.js
const { resolveConfig } = require('detox/internals');

module.exports = async () => {
const { device } = await resolveConfig();

return {
maxWorkers: process.env.CI ? (device.type === 'ios.simulator' ? 3 : 2) : 1,
globalSetup: '...',
globalTeardown: '...',
// ... and so on ...
};
};

Miscellaneous

Detox 20 drops ios.none driver due to the low demand and legacy code issues. If you have been using it, please follow the new Debugging > Native application code tutorial which explains how to use launchApp: 'manual' as a replacement for ios.none.

If you have been using { permanent: true } option for device.appLaunch.args.modify API, please note that it has been removed in PR #3360, in favor of appLaunchArgs.shared:

// device.appLaunchArgs.modify({ /* ... */ }, { permanent: true });
device.appLaunchArgs.shared.modify({ /* ... */ });

The hard part is over now, congratulations on finishing the migration! Stay tuned for the upcoming minor releases leveraging the recent architectural changes in Detox.

19.2

The release has a developer experience improvement – Detect pending in-flight requests (#3003, @jonathanmos). The feature adds extra logic that prevents forgotten await statements on asynchronous Detox APIs. That’s why you might see a new error like this:

FAILED
DetoxRuntimeError: The pending request \#246 ("invoke") has been rejected due to the following error:

Detox has detected multiple interactions taking place simultaneously. Have you forgotten to apply an await over one of the Detox actions in your test code?

That should help you find forgotten awaits in your code that are a potential reason for flakiness in E2E tests. You’ll need to find those places and apply trivial fixes like shown below:

   await screenDriver.performSomeAction();
- expect(screenDriver.get.myElement()).toBeNotVisible();
+ await expect(screenDriver.get.myElement()).toBeNotVisible();

19.0

Version 19 is not really a breaking change!

We decided to bump Detox into a major version release, nonetheless, because it is breaking for projects that sport custom Detox drivers, such as detox-puppeteer.

If you are a maintainer of such a project, and you wish to upgrade your Detox dependency to 19 (kudos! 👏), follow this step-by-step migration guide; You can refer to this pull-request, which does that for the detox-puppeteer project.

Migrating Custom Drivers

The core of the change is that Detox' drivers framework is no longer a single monolith, responsible for everything platform-specific. Rather, it’s been broken down to these subresponsibilies:

  • Allocation: The process of launching / selecting a device over which the tests would run in the current execution.
  • Validation: Execution environment checkups.
  • Artifacts: Platform-based selection of build-artifacts implementation (e.g. screenshots).
  • Runtime

You can find a visual explanation, here.

In addition, the runtime driver is no longer state-less – basically, allowing implementation to hold any state that is required in identifying and managing the associated device.

How to migrate

Everything here will be based on the changes made in the detox-puppeteer example - names included (please don’t use them as-is in your own implementation!).

Allocation:

  • Create a new class, called PuppeteerDeviceAllocation (change name to something that would make sense in your project).
  • Move everything currently in PuppeteerDriver.acquireFreeDevice() and .shutdown() onto PuppeteerDeviceAllocation.allocate() and .free(), respectively.
  • Create a POJO class called PuppeteerAllocCookie. This class should hold anything that would later be required in order to specify the specifically associated device (example: UDID for iOS simulators, adb names for Android devices).
  • Make .allocate() return an instance of your cookie class. Puppeteer example: here.
  • Delete PuppeteerDriver.acquireFreeDevice() and PuppeteerDriver.shutdown().

For a precise class c'tor and method signatures, see here.

Add the new allocation class to the module.exports list, under the name: DeviceAllocationDriverClass.

Validation:

  • If you have any validations implemented in PuppeteerDriver.prepare(), create a class called PuppeteerEnvironmentValidator.
  • Move anything inside PuppeteerDriver.prepare() to PuppeteerEnvironmentValidator.validate().
  • Delete PupeteerDriver.prepare().

For a precise class c'tor and method signatures, see here.

Add the new (optional) class to the module.exports list, under the name: EnvironmentValidatorClass.

Artifacts:

  • Move your implementation of PuppeteerDriver.declareArtifactPlugins() to the same method in a new class, called PuppeteerArtifactPluginsProvider.declareArtifactPlugins() (change name to something that would make sense in your project).

There are no changes in method signature in this case.

Add the new class to the module.exports list, under the name: ArtifactPluginsProviderClass.

Runtime:

  • Optionally rename your class from PuppeteerDriver to PuppeteerRuntimeDriver.
  • In the methods remaining in the class accepting the deviceId arg: remove the deviceId arg entirely. This might break your implementation - don’t worry, continue reading.
  • If applicable, change the signature of the class' c'tor to accept the cookie as its 2nd argument (instance previously allocated in PuppeteerAllocationDriver.allocate()). Save data from the cookie as part of the driver’s state, in order to unbreak your implementation, following the previous step.
  • Add two methods: getExternalId() and getDeviceName(). Implement them such that they would comply with the device.id and device.name API contracts, respectively.

Export the runtime driver class in the module.exports list as RuntimeDriverClass, instead of DriverClass.

Troubleshooting

For issue related to these migrations, approach us by submitting an issue on GitHub. Please apply the Detox19 label.

18.6.0

Detox has normalized the configuration format, so that along with the combined configurations object you now can define your devices and apps separately. Please refer to the configuration doc to obtain more details. This change is backward-compatible, although the new format is now the recommended option.

18.0

Detox now uses a custom synchronization system on iOS, developed in-house; this is the second step in phasing out our Earl Grey usage. We have tested this system extensively internally, and are confident that it should work as expected. There are no known limitations with the new system.

If you are seeing issues with the new sync system, please open an issue.

Breaking:

  • iOS. Detox now requires iOS 13.0 and above iOS simulator runtimes, and iOS 12.x and below are no longer supported. This does not require that you drop support for iOS 12.x in your apps, just that tests will no longer work on iOS 12 and below. Please make sure your tests are running on iOS 13 or above
  • JS. ⚠️ Detox no longer launches the app automatically (even if asked to do so in configuration) — you have to launch your app explicitly:
+  beforeAll(async () => {
+ await device.launchApp();
+ });
  • JS (jest-circus). The DetoxCircusEnvironment provided from detox/runners/jest-circus package now requires two arguments in its constructor, so you have to update your descendant class signature:
class CustomDetoxEnvironment extends DetoxCircusEnvironment {
- constructor(config) {
- super(config);
+ constructor(config, context) {
+ super(config, context);
  • JS (iOS). device.launchApp({ launchArgs: { ... }) argument escaping has been improved. If you use complex launch args such as regular expressions, make sure you remove manual escaping from now on to avoid erroneous double escaping, e.g.:
 await device.launchApp({
launchArgs: {
- detoxURLBlacklistRegex: '(\\".*example.com/some-url/.*\\")' }`,
+ detoxURLBlacklistRegex: '(".*example.com/some-url/.*")' }`,
},
});
  • JS (internal). There is a breaking change for people writing custom Detox integrations. Environment variable naming schema has changed – now Detox uses prefix to distinguish its own environment variables (usually passed from detox test CLI), e.g.: recordLogs=all becomes DETOX_RECORD_LOGS=all, loglevel=trace becomes DETOX_LOGLEVEL=trace, and so on.

17.5.2

Fixes the issue from 17.4.7 (see below) - now the migration guide for 17.4.7 can be safely ignored.

17.4.7

This release was not meant to be breaking in any sense, but unfortunately there are two minor caveats that leaked in.

jest-cli

From now on, Detox explicitly depends on jest-cli package (marked as a peer dependency), that’s why if you see an error like the one below:

Cannot find module 'jest-cli/build/cli/args'

You should add jest-cli to your package.json’s devDependencies and rerun npm install, e.g.:

UPD: since detox@17.5.2 you can ignore this advice. The problem should go away without these edits:

 "devDependencies": {
"jest": "26.x.x",
+ "jest-cli": "26.x.x",

detox-cli

If you were using detox-cli global package, make sure to upgrade it before proceeding to detox@17.4.7.

npm install detox-cli --global

If you have an older version of detox-cli, then you might see the following error on an attempt to run detox test <...args>:

'jest' is not recognized as an internal or external command,
operable program or batch file.
detox[43764] ERROR: [cli.js] Error: Command failed: jest --config e2e/config.json --testNamePattern "^((?!:android:).)*$" --maxWorkers 1 e2e

17.3.0

In the context of introducing the element screenshots feature (#2012), we decided to slightly change the contract between Detox and externally-implemented drivers. These should be modified according to the follow diff-snippet:

class Expect {
- constructor(invocationManager) {
+ constructor({ invocationManager }) {
this._invocationManager = invocationManager;
}
}

class PluginDriver {
constructor() {
- this.matchers = new Expect(new invocationManager());
}
}

-module.exports = PluginDriver;
+module.exports = {
+ DriverClass: PluginDriver,
+ ExpectClass: Expect,
+}

17.0.0

Detox for iOS now uses an entirely new, custom-built matcher, action and expectation infrastructure. This is the first step in our roadmap of removing Earl Grey as a dependency.

While the new system has been designed to be as compatible as possible with the existing system, some changes we made to existing APIs that may or may not require your attention.

New API

  • pinch()—new API for pinching elements, replacing the deprecated pinchWithAngle() (iOS)
  • getAttributes()—new API for obtaining element properties (iOS)
  • not—new API for inverting expectation logic (iOS, Android)

Modified API (Potentially Breaking Changes)

The following APIs have changed and require attention

  • by.text()—matching elements by text actually uses the element’s text value instead of using the accessibility label (iOS)
  • by.traits()—the supported trait values have changed (iOS)
  • atIndex()—matched elements are now sorted by x and y axes to allow for stability between tests; indices will most likely change after upgrading to this version of Detox (iOS)
  • tap()—this method now accepts an optional point to tap (iOS, Android)
  • setColumnToValue()—this method no longer supports date pickers; use setDatePickerDate() to change picker dates (iOS)
  • setDatePickerDate()—in addition to normal date formats, a new special case is introduced for ISO 8601 formatted date strings: "ISO8601" (iOS)

Deprecated API

The following APIs have been deprecated, but is still available

  • tapAtPoint()—the API has been consolidated with tap(point) (iOS, Android)
  • pinchWithAngle()—this API has been replaced with pinch() (iOS)
  • toBeNotVisible()—deprecated in favor of not.toBeVisible() (iOS, Android)
  • toNotExist()—deprecated in favor of not.toExist() (iOS, Android)

Make sure to read the API reference for matchers, actions and expectations.

If you see unexpected results, make sure to open an issue.

16.0.0

Detox now comes as a prebuilt framework on iOS, thus lowering npm install times and saving some build issues that happen due to unexpected Xcode setups.

To support this, Detox needs Swift 5 support, so the iOS requirements have changed slightly:

  • Xcode: 10.2 or higher
    • iOS Simulator Runtime: iOS 12.2 or higher

This does not require that your app require iOS 12.2, only that you build and run your app on Xcode 10.2 or above, and use an iOS 12.2 or above simulator.