Search code examples
node.jsseleniumfirebasenightwatch.jsspawn

Nightwatch not terminating after browser.end()


I'm running Nightwatch after launching a child process that starts up my local servers. Nightwatch runs the tests, they complete successfully, and the browser windows all close, but the nightwatch process continues to run after printing the message "OK. 10 total assertions passed.".

I thought it may have something to do with how I'm watching events on the nightwatch process, but as far as I can tell I am watching all events that would indicate Nightwatch is exiting.

The method shutdown() in runner.js is never called. How do I get Nightwatch to terminate when the tests finish?

Update

If I remove the last test in sign-in.js then Nightwatch exits as expected.

runner.js

import spawn from 'cross-spawn'

// 1. start the dev server using production config
process.env.NODE_ENV = 'testing'

let servers

function shutdown (result) {
  console.log('HERE', result)
  try {
    // Passing a negative PID to kill will terminate all child processes, not just the parent
    if (servers) process.kill(-servers.pid)
  } catch (e) {
    console.error('Unable to shutdown servers, may need to be killed manually')
  }

  if (result) {
    console.error(result)
    process.exit(1)
  } else {
    process.exit(0)
  }
}

function watch (child) {
  child.on('close', shutdown)
  child.on('disconnect', shutdown)
  child.on('error', shutdown)
  child.on('exit', shutdown)
  child.on('uncaughtException', shutdown)
}

try {
  servers = spawn('yarn', ['run', 'dev-all'], { cwd: '..', stdio: 'inherit', detached: true })
  watch(servers)

  // 2. run the nightwatch test suite against it
  // to run in additional browsers:
  //    1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
  //    2. add it to the --env flag below
  // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
  // For more information on Nightwatch's config file, see
  // http://nightwatchjs.org/guide#settings-file
  var opts = process.argv.slice(2)
  if (opts.indexOf('--config') === -1) {
    opts = opts.concat(['--config', 'e2e/nightwatch.conf.js'])
  }
  if (opts.indexOf('--env') === -1) {
    opts = opts.concat(['--env', 'chrome'])
  }

  var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })
  watch(runner)
  watch(process)
} catch (error) {
  shutdown(error)
}

nightwatch.conf.js

require('babel-register')
var config = require('../../frontend/config')

// http://nightwatchjs.org/guide#settings-file
module.exports = {
  src_folders: ['e2e/specs'],
  output_folder: 'e2e/reports',
  custom_assertions_path: ['e2e/custom-assertions'],

  selenium: {
    start_process: true,
    server_path: 'node_modules/selenium-server/lib/runner/selenium-server-standalone-3.0.1.jar',
    host: '127.0.0.1',
    port: 4444,
    cli_args: {
      'webdriver.chrome.driver': require('chromedriver').path
    }
  },

  test_settings: {
    default: {
      selenium_port: 4444,
      selenium_host: 'localhost',
      silent: true,
      globals: {
        devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
      }
    },

    chrome: {
      desiredCapabilities: {
        browserName: 'chrome',
        javascriptEnabled: true,
        acceptSslCerts: true
      }
    },

    firefox: {
      desiredCapabilities: {
        browserName: 'firefox',
        javascriptEnabled: true,
        acceptSslCerts: true
      }
    }
  }
}

sign-in.js (one of the tests)

import firebase from 'firebase-admin'
import uuid from 'uuid'

import * as firebaseSettings from '../../../backend/src/firebase-settings'

const PASSWORD = 'toomanysecrets'

function createUser (user) {
  console.log('Creating user', user.uid)
  let db = firebase.database()
  return Promise.all([
    firebase.auth().createUser({
      uid: user.uid,
      email: user.email,
      emailVerified: true,
      displayName: user.fullName,
      password: PASSWORD
    }),
    db.ref('users').child(user.uid).set({
      email: user.email,
      fullName: user.fullName
    }),
    db.ref('roles').child(user.uid).set({
      instructor: false
    })
  ])
}

function destroyUser (user) {
  if (!user) return

  console.log('Removing user', user.uid)
  let db = firebase.database()
  try { db.ref('roles').child(user.uid).remove() } catch (e) {}
  try { db.ref('users').child(user.uid).remove() } catch (e) {}
  try { firebase.auth().deleteUser(user.uid) } catch (e) {}
}

module.exports = {
  'Sign In links exist': browser => {
    // automatically uses dev Server port from /config.index.js
    // default: http://localhost:8080
    // see nightwatch.conf.js
    const devServer = browser.globals.devServerURL

    browser
      .url(devServer)
      .waitForElementVisible('#container', 5000)

    browser.expect.element('.main-nav').to.be.present
    browser.expect.element('.main-nav a[href^=\'https://oauth.ais.msu.edu/oauth/authorize\']').to.be.present
    browser.expect.element('.main-nav a[href^=\'/email-sign-in\']').to.be.present
    browser.end()
  },
  'Successful Sign In with Email shows dashboard': browser => {
    const devServer = browser.globals.devServerURL

    firebase.initializeApp(firebaseSettings.appConfig)

    let userId = uuid.v4()
    let user = {
      uid: userId,
      email: `${userId}@test.com`,
      fullName: 'Test User'
    }

    createUser(user)

    browser.url(devServer)
      .waitForElementVisible('.main-nav a[href^=\'/email-sign-in\']', 5000)
      .click('.main-nav a[href^=\'/email-sign-in\']')
      .waitForElementVisible('button', 5000)
      .setValue('input[type=text]', user.email)
      .setValue('input[type=password]', PASSWORD)
      .click('button')
      .waitForElementVisible('.main-nav a[href^=\'/sign-out\']', 5000)
      .end(() => {
        destroyUser(user)
      })
  }
}

After the tests complete successfully, I see the following:

grimlock:backend egillespie$ ps -ef | grep nightwatch
501 13087 13085   0  1:51AM ttys000    0:02.18 node ./node_modules/.bin/nightwatch --presets es2015,stage-0 --config e2e/nightwatch.conf.js --env chrome

Solution

  • I was not explicitly closing the Firebase connection. This caused the last test to hang indefinitely.

    Here's how I am closing the connection after doing test cleanup:

    browser.end(() => {
      destroyUser(user).then(() => {
        firebase.app().delete()
      })
    })
    

    The destroyUser function now looks like this:

    function destroyUser (user) {
      if (!user) return Promise.resolve()
    
      let db = firebase.database()
      return Promise.all([
        db.ref('roles').child(user.uid).remove(),
        db.ref('users').child(user.uid).remove(),
        firebase.auth().deleteUser(user.uid)
      ])
    }