Sunday, May 18, 2014

Rerunning Protractor tests when files change and reusing the same browser instance

Running ng-scenario tests with karma had the nice feature of keeping browsers open and rerunning the tests when files change. I at least wrote my end-to-end tests very incrementally, having one test enabled, adding a pause() at the end and writing test code as I checked out what the browser looked like. So I was a bit annoyed with Protractor's explicit lack of a permanent runner. This hit me especially when running against node-webkit, which takes about 8s to start on my machine (vs. 1s for regular Chrome).

So I wrote a hacky script that patches enough of protractor to enable running it multiple times (you can see it on github too).


// keep_running.js
//
// _Example_ for using a single browser for rerunning protractor
// tests when files change (inspired by ng-scenario+karma's normal behaviour)
//
// By Mika Raento, Karhea Oy
// Put in the public domain, do whatever you wish with it
//
// Restrictions of the example:
// - only chromedriver
// - only jasmine
// - tests are assumed to live under acceptance/
// - watches current directory
// - simplistic assumption about process.kill
// - monkey-patches protractor, jasmine and node internals, will
//   easily break with new versions of any
// - there's a reason protractor doesn't support this: it's much easier to
//   get deterministic tests if you restart with a fresh browser
//
// node-requirements: protractor q minijasminenode chokidar
//
// run with:
// $ node keep_running.js   <protractor options go here>
//
var q = require('q');

// Monkey-patch the chrome provider to reuse the same browser instance
chrome = Object.getPrototypeOf(
  require('protractor/lib/driverProviders/chrome.dp.js')());
var orig_getDriver = chrome.getDriver;
var driver;
chrome.getDriver = function() {
  if (driver) {
    this.driver_ = driver;
  } else {
    driver = orig_getDriver.apply(this);
  }
  return driver;
};
chrome.teardownEnv = function() {
  // could we do something here to clean at least some things up?
  return q.fcall(function() {});
};
chrome.setupEnv = function() {
  return q.fcall(function() {});
};

var maybe_run;
var should_run = true;
process.exit = function() {
  is_running = false;
  maybe_run();
};
var is_running = false;
var pending;

require('minijasminenode');
maybe_run = function() {
  if (is_running) return;
  if (!should_run) {
    if (pending) return;
    pending = setTimeout(function() {
      pending = null;
      maybe_run();
    }, 300);
    return;
  }
  is_running = true;
  should_run = false;
  // protractor and (mini)jasmine use node's require() for side effects,
  // reset the modules here
  delete require.cache[require.resolve('protractor/lib/cli.js')];
  delete require.cache[require.resolve('protractor/lib/runner.js')];
  for (var k in require.cache) {
    // especially our test specs
    if (k.indexOf("/acceptance/") > 0) {
      // TODO: parse the command line and configuration file to
      // see what are out specs
      delete require.cache[k];
    }
  }
  // Don't reuse the same jasmine environment for new tests
  jasmine.currentEnv_ = undefined;

  // Kick the run off
  // Here we could call the underlying protractor/lib/launcher.js instead
  // and give it a list of changed files
  require('protractor/lib/cli.js');
};

require('chokidar').watch('.').on('all', function() {
  should_run = true;
});

process.on('exit', function() { if (driver) driver.quit(); });

maybe_run();

No comments: