Search code examples
javascriptreact-nativetestingpouchdbdetox

Detox testing + React Native + PouchDB app: our login test keep failing with timeout because of some PouchDB syncing?


tldr

It seems that initializing an instance of the PouchDB client (calling new PouchDB(...)) causes some queue worker or background process to spawn that periodically sends a network request to its CouchDB server and in doing so prevents our Detox test suite from letting our React Native app + iOS simulator go idle and move on to the next assertion, causing our tests to fail with either App has not responded to the network requests below or DetoxRuntimeError: Test Failed: No elements found for “MATCHER(identifier == “foo”)”.

We've tried calling device.disableSynchronization/device.enableSyncronization or setting the blacklist with either launchArgs: { detoxURLBlacklistRegex: '.*' } or device.setURLBlacklist(['.*']) but none of it seems to make it work.

Is there any way to get Detox to ignore the PouchDB network requests, or perhaps to manually pause PouchDB, so that we can reach the next assertions we want to make?

Overview

My team's trying to use Detox to write a login test for an iOS app running in the simulator built with React Native. The app uses PouchDB for its networking/data layer so it can connect to a remote CouchDB server.

The problem is that Detox always seems to fail / freeze / hang and timeout past a certain point, which is basically whenever PouchDB gets initialized (by calling new PouchDB(...)).

In our test, this happens as a side effect of tapping the login button with valid credentials:

await element(by.id('auth.login.button')).tap();

All valid assertions prior to that line will work fine (eg, try clicking the forgot passsword button, seeing that next component load, etc). Any assertion after that line will never succeed,

await expect(element(by.id('auth.linkedin.skip'))).toBeVisible();

even though that the exact component that we reference (auth.linkedin.skip) is plainly visible in the simulator before the timer has expired, and this generally results in a failure in one of two ways.

Scenario 1 - Test fails with timeout

The test runs for a long time until it fails with an error messsage saying Timeout exceeded, or more precisely:

App has not responded to the network requests below:
  (id = 8) invoke: {"type":"expectation","predicate":{"type":"id","value":"auth.linkedin.skip"},"expectation":"toBeVisible"}

That might be the reason why the test "Login test allow existing the existing test user to sign in" has timed out.

In verbose mode, we'll also see this over and over until it fails:

detox[35499] INFO:  [actions.js] The system is busy with the following tasks:

Dispatch Queue
⏱ Queue: “Main Queue (<OS_dispatch_queue_main: com.apple.main-thread>)” with 2 work items

Run Loop
⏱ “Main Run Loop”

One-time Events
⏱ “Network Request” with object: “URL: “https://our.couch.endpoint/userdb-foo/bar/baz””
⏱ “Network Request” with object: “URL: “https://our.couch.endpoint/userdb-foo/bar/baz””

Scenario 2 - Test fails prematurely

The test runs for a shorter time and fails with an error message saying DetoxRuntimeError: Test Failed: No elements found for “MATCHER(identifier == “auth.linkedin.skip”)” and prints the stack of native components loaded into the simulator that correspond to the previous login screen (and not the next screen, which has the linkedin button, and has already been loaded into the simulator by the time it fails).

What's additionally confusing here is that these two scenarios will occur roughly 67% / 33% of the time, meaning the failure with the timeout will happen roughly twice for every single failure without the timeout, all with the exact same test command, test, and configuration between executions.

The Detox test

describe('Login test', () => {

  beforeAll(async () => {
    await device.launchApp({
      newInstance: true,
      launchArgs: {
        // detoxURLBlacklistRegex: '.*',
      }
    });
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('allow existing the existing test user to sign in', async () => {
    // see login screen
    await expect(element(by.id('auth.login.keyboardView'))).toBeVisible();
    // run login
    await element(by.id('auth.login.emailInput')).replaceText('[email protected]');
    await element(by.id('auth.login.passwordInput')).replaceText('pass123');    
    await element(by.id('auth.login.button')).tap();

    // the test fails on the line below (it times out while waiting for some pouchdb thing...even though the skip button is visible in the simulator)
    await expect(element(by.id('auth.linkedin.skip'))).toBeVisible();
  });
});

Versions

node: 12.18.3

xcode: 12.0.1

package.json:

"@craftzdog/pouchdb-core-react-native": "^7.0.0",
"@craftzdog/pouchdb-replication-react-native": "^7.0.0",
"detox": "^18.9.0",
"jest": "^25.1.0",
"jest-circus": "^26.6.3",
"pouchdb-adapter-http": "^7.2.1",
"pouchdb-adapter-react-native-sqlite": "^2.0.0",
"pouchdb-authentication": "^1.1.3",
"pouchdb-debug": "^7.2.1",
"pouchdb-find": "^7.2.1",
"pouchdb-upsert": "^2.2.0",
"pouchdb-upsert-bulk": "^1.0.2",
"pouchdb-validation": "^4.2.0",
"react": "16.11.0",
"react-native": "0.62.2",

Test command

`detox test -c ios -l verbose --record-logs all`

Configuration

.detoxrc.json

{
  "testRunner": "jest",
  "runnerConfig": "e2e/config.json",
  "apps": {
    "ios": {
      "type": "ios.app",
      "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/hooli.app",
      "build": "xcodebuild -workspace appName.xcworkspace/ -scheme appScheme -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build"
    },
    "android": {
      "type": "android.apk",
      "binaryPath": "SPECIFY_PATH_TO_YOUR_APP_BINARY"
    }
  },
  "devices": {
    "simulator": {
      "type": "ios.simulator",
      "device": {
        "type": "iPhone 11"
      }
    },
    "emulator": {
      "type": "android.emulator",
      "device": {
        "avdName": "Pixel_3a_API_30_x86"
      }
    }
  },
  "configurations": {
    "ios": {
      "device": "simulator",
      "app": "ios"
    },
    "android": {
      "device": "emulator",
      "app": "android"
    }
  }
}

e2e/config.json

{
    "testEnvironment": "./environment",
    "testRunner": "jest-circus/runner",
    "testTimeout": 35000,
    "testRegex": "\\.e2e\\.js$",
    "reporters": ["detox/runners/jest/streamlineReporter"],
    "verbose": true
}

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;

Last notes

We've tried calling

device.disableSynchronization();
...
device.enableSynchronization();

And setting the blacklist

// this
device.launchApp({ 
  launchArgs: { detoxURLBlacklistRegex: '.*' },
});
// or this
device.setURLBlacklist(['.*']);

But none of it seems to make it work.


Solution

  • Is there any way to get Detox to ignore the PouchDB network requests, or perhaps to manually pause PouchDB, so that we can reach the next assertions we want to make?

    Author here, I was able to manually pause PouchDB's live synchronization by toggling this configuration value to false whenever we're in the test env. Source: PouchDB docs on sync.

    // localDB, authDB have been initialized with new PouchDB(...)
    // isTestEnv=true
    localDB.sync(authDB, {
      live: (isTestEnv)?false:true,
      // rest of options
    });
    

    After this change, the Detox tests were able to move past the login step and make more assertions inside the app! Feels hacky but at least it's working again.