text: /* globals jasmineRequire, phantom */
  // Verify arguments
  var system = require('system');
  var args;
  
  if(phantom.args) {
      args = phantom.args;
  } else {
      args = system.args.slice(1);//use system args for phantom 2.0+
  }
  
  if (args.length === 0) {
      console.log("Simple JasmineBDD test runner for phantom.js");
      console.log("Usage: phantomjs-testrunner.js url_to_runner.html");
      console.log("Accepts http:// and file:// urls");
      console.log("");
      console.log("NOTE: This script depends on jasmine.HtmlReporter being used\non the page, for the DOM elements it creates.\n");
      phantom.exit(2);
  }
  else {
      var fs = require("fs"),
          pages = [],
          page, address, resultsKey, i, l;
  
  
      var setupPageFn = function(p, k) {
          return function() {
              setupWriteFileFunction(p, k, fs.separator);
          };
      };
  
      for (i = 0, l = args.length; i < l; i++) {
          address = args[i];
          console.log("Loading " + address);
  
          // if provided a url without a protocol, try to use file://
          address = address.indexOf("://") === -1 ? "file://" + address : address;
  
          // create a WebPage object to work with
          page = require("webpage").create();
          page.url = address;
  
          // When initialized, inject the reporting functions before the page is loaded
          // (and thus before it will try to utilize the functions)
          resultsKey = "__jr" + Math.ceil(Math.random() * 1000000);
          page.onInitialized = setupPageFn(page, resultsKey);
          page.open(address, processPage(null, page, resultsKey));
          pages.push(page);
  
          page.onConsoleMessage = logAndWorkAroundDefaultLineBreaking;
      }
  
      // bail when all pages have been processed
      setInterval(function(){
          var exit_code = 0;
          for (i = 0, l = pages.length; i < l; i++) {
              page = pages[i];
              if (page.__exit_code === null) {
                  // wait until later
                  return;
              }
              exit_code |= page.__exit_code;
          }
          phantom.exit(exit_code);
      }, 100);
  }
  
  // Thanks to hoisting, these helpers are still available when needed above
  /**
   * Logs a message. Does not add a line-break for single characters '.' and 'F' or lines ending in ' ...'
   *
   * @param msg
   */
  function logAndWorkAroundDefaultLineBreaking(msg) {
      var interpretAsWithoutNewline = /(^(\u001b\[\d+m)*[\.F](\u001b\[\d+m)*$)|( \.\.\.$)/;
      if (navigator.userAgent.indexOf("Windows") < 0 && interpretAsWithoutNewline.test(msg)) {
          try {
              system.stdout.write(msg);
          } catch (e) {
              var fs = require('fs');
              fs.write('/dev/stdout', msg, 'w');
          }
      } else {
          console.log(msg);
      }
  }
  
  /**
   * Stringifies the function, replacing any %placeholders% with mapped values.
   *
   * @param {function} fn The function to replace occurrences within.
   * @param {object} replacements Key => Value object of string replacements.
   */
  function replaceFunctionPlaceholders(fn, replacements) {
      if (replacements && typeof replacements === "object") {
          fn = fn.toString();
          for (var p in replacements) {
              if (replacements.hasOwnProperty(p)) {
                  var match = new RegExp("%" + p + "%", "g");
                  do {
                      fn = fn.replace(match, replacements[p]);
                  } while(fn.indexOf(match) !== -1);
              }
          }
      }
      return fn;
  }
  
  /**
   * Custom "evaluate" method which we can easily do substitution with.
   *
   * @param {phantomjs.WebPage} page The WebPage object to overload
   * @param {function} fn The function to replace occurrences within.
   * @param {object} replacements Key => Value object of string replacements.
   */
  function evaluate(page, fn, replacements) {
      return page.evaluate(replaceFunctionPlaceholders(fn, replacements));
  }
  
  /** Stubs a fake writeFile function into the test runner.
   *
   * @param {phantomjs.WebPage} page The WebPage object to inject functions into.
   * @param {string} key The name of the global object in which file data should
   *                     be stored for later retrieval.
   */
  // TODO: not bothering with error checking for now (closed environment)
  function setupWriteFileFunction(page, key, path_separator) {
      evaluate(page, function(){
          window["%resultsObj%"] = {};
          window.fs_path_separator = "%fs_path_separator%";
          window.__phantom_writeFile = function(filename, text) {
              window["%resultsObj%"][filename] = text;
          };
      }, {resultsObj: key, fs_path_separator: path_separator.replace("\\", "\\\\")});
  }
  
  /**
   * Returns the loaded page's filename => output object.
   *
   * @param {phantomjs.WebPage} page The WebPage object to retrieve data from.
   * @param {string} key The name of the global object to be returned. Should
   *                     be the same key provided to setupWriteFileFunction.
   */
  function getXmlResults(page, key) {
      return evaluate(page, function(){
          return window["%resultsObj%"] || {};
      }, {resultsObj: key});
  }
  
  /**
   * Processes a page.
   *
   * @param {string} status The status from opening the page via WebPage#open.
   * @param {phantomjs.WebPage} page The WebPage to be processed.
   */
  function processPage(status, page, resultsKey) {
      if (status === null && page) {
          page.__exit_code = null;
          return function(stat){
              processPage(stat, page, resultsKey);
          };
      }
      if (status !== "success") {
          console.error("Unable to load resource: " + address);
          page.__exit_code = 2;
      }
      else {
          var isFinished = function() {
              return evaluate(page, function(){
                  // if there's a JUnitXmlReporter, return a boolean indicating if it is finished
                  if (window.jasmineReporters && window.jasmineReporters.startTime) {
                      return !!window.jasmineReporters.endTime;
                  }
                  // otherwise, scrape the DOM for the HtmlReporter "finished in ..." output
                  var durElem = document.querySelector(".html-reporter .duration");
                  if (!durElem) {
                      durElem = document.querySelector(".jasmine_html-reporter .duration");
                  }
                  return durElem && durElem.textContent && durElem.textContent.toLowerCase().indexOf("finished in") === 0;
              });
          };
          var getResultsFromHtmlRunner = function() {
              return evaluate(page, function(){
                  var resultElem = document.querySelector(".html-reporter .alert .bar");
                  if (!resultElem) {
                      resultElem = document.querySelector(".jasmine_html-reporter .alert .bar");
                  }
                  return resultElem && resultElem.textContent &&
                      resultElem.textContent.match(/(\d+) spec.* (\d+) failure.*/) ||
                     ["Unable to determine success or failure."];
              });
          };
          var timeout = 60000;
          var loopInterval = 100;
          var ival = setInterval(function(){
              if (isFinished()) {
                  // get the results that need to be written to disk
                  var fs = require("fs"),
                      xml_results = getXmlResults(page, resultsKey),
                      output;
                  for (var filename in xml_results) {
                      if (xml_results.hasOwnProperty(filename) && (output = xml_results[filename]) && typeof(output) === "string") {
                          fs.write(filename, output, "w");
                      }
                  }
  
                  // print out a success / failure message of the results
                  var results = getResultsFromHtmlRunner();
                  var failures = Number(results[2]);
                  if (failures > 0) {
                      page.__exit_code = 1;
                      clearInterval(ival);
                  }
                  else {
                      page.__exit_code = 0;
                      clearInterval(ival);
                  }
              }
              else {
                  timeout -= loopInterval;
                  if (timeout <= 0) {
                      console.log('Page has timed out; aborting.');
                      page.__exit_code = 2;
                      clearInterval(ival);
                  }
              }
          }, loopInterval);
      }
  }
  