diffux/diffux-core

View on GitHub
lib/diffux_core/script/take-snapshot.js

Summary

Maintainability
A
1 hr
Test Coverage
var page = require('webpage').create(),
    opts = JSON.parse(require('system').args[1]);

page.viewportSize = opts.viewportSize;
if (opts.userAgent) {
  page.settings.userAgent = opts.userAgent;
} else {
  page.settings.userAgent = page.settings.userAgent + ' Diffux';
}

/**
 * Configure timeouts
 */
page.waitTimeouts = {
  initial: 1000,
  afterResourceDone: 300,
  fallback: 60000
};
// Don't wait more than 5 seconds on resources
page.settings.resourceTimeout = 5000;

/**
 * Saves a log string. The full log will be added to the console.log in the end,
 * so that consumers of this script can use that information.
 */
page.allLogs = [];
page.log = function(string) {
  page.allLogs.push(new Date().getTime() + ': ' + string);
};

/**
 * By preventing animations from happening when we are taking the snapshots, we
 * avoid timing issues that cause unwanted differences.
 */
page.preventAnimations = function() {
  // CSS Transitions
  var css   = document.createElement('style');
  css.type  = 'text/css';
  document.head.appendChild(css);
  var sheet = css.sheet;
  sheet.addRule('*', '-webkit-transition: none !important;');
  sheet.addRule('*', 'transition: none !important;');
  sheet.addRule('*', '-webkit-animation-duration: 0 !important;');
  sheet.addRule('*', 'animation-duration: 0 !important;');

  // jQuery
  if (window.jQuery) {
    jQuery.fx.off = true;
    jQuery('*').stop(true, true);
  }

  // Prevent things like blinking cursors by un-focusing any focused
  // elements
  document.activeElement.blur();
};


/**
 * Waits until the page is ready and then fires a callback.
 *
 * This method will keep track of all resources requested (css, javascript, ajax
 * requests, etc). As soon as we have no outstanding requests active, we start a
 * short timer which fires the callback. If a new resource is requested in that
 * short timeframe, we cancel the timer and wait for the new resource.
 *
 * In case something goes wrong, there's a 10 second fallback timer running in
 * the background.
 */
page.waitUntilReady = function(callback) {
  var fireCallback = function() {
    page.log('Done - page is ready.');
    clearTimeout(page.resourceWaitTimer);
    clearTimeout(page.fallbackWaitTimer);
    callback();
  };

  page.resourcesActive = [];

  page.onResourceRequested = function(request) {
    page.log('Ready: Request started - [' + request.id + '] ' + request.url);
    page.log('Active requests - ' + page.resourcesActive);
    if (page.resourceWaitTimer) {
      page.log('Clearing timeout.');
      clearTimeout(page.resourceWaitTimer);
      page.resourceWaitTimer = null;
    }
    page.resourcesActive.push(request.id);
  };

  function onResourceEnded(response) {
    page.log('Ready: Resource received - [' + response.id + '] '
        + response.url);
    page.log('Active requests - ' + page.resourcesActive);

    page.resourcesActive.splice(page.resourcesActive.indexOf(response.id), 1);

    if (page.resourcesActive.length === 0) {
      page.log('Potentially done, firing after short timeout.');
      page.resourceWaitTimer = setTimeout(fireCallback,
          page.waitTimeouts.afterResourceDone);
    }
  }


  page.onResourceError = onResourceEnded;
  page.onResourceTimout = onResourceEnded;
  page.onResourceReceived = function(response) {
    if (response.stage === 'end') {
      onResourceEnded(response);
    }
  };

  page.log('Starting default timeouts. ' + JSON.stringify(page.waitTimeouts));
  page.resourceWaitTimer = setTimeout(fireCallback, page.waitTimeouts.initial);
  page.fallbackWaitTimer = setTimeout(fireCallback, page.waitTimeouts.fallback);
};

/**
* Gets the top, left, width, and height of an element to crop and sets that to
* `page.clipRect` (a PhantomJS thing that will force `render` to only render
* that rectangle).
*/
page.cropOutElement = function() {
  page.log('Cropping out ' + opts.cropSelector);

  page.clipRect = page.evaluate(function(selector) {
    var cropElement = document.querySelector(selector);
    if (!cropElement) {
      return;
    }
    return cropElement.getBoundingClientRect();
  }, opts.cropSelector);
  page.log('Crop set to ' + JSON.stringify(page.clipRect));
};

/**
 * Main place for taking the screenshot. Will exit the script when done.
 */
page.takeDiffuxSnapshot = function() {
  // Try to prevent animations from running, to reduce variation in
  // snapshots.
  page.evaluate(page.preventAnimations);

  // Check if the snapshot image should be cropped.
  if (opts.cropSelector) {
    page.cropOutElement();
  }
  // Save a PNG of the rendered page
  page.render(opts.outfile);

  // Capture metadata
  var response = page.evaluate(function() {
    return { title: document.title };
  });

  response.opts   = opts;
  response.log    = page.allLogs.join('\n');
  response.status = status;

  // The phantomjs gem can read what is written to STDOUT which includes
  // console.log, so we can use that to pass information from phantomjs back
  // to the app.
  console.log(JSON.stringify(response));

  phantom.exit();
};

page.open(opts.address, function(status) {
  page.waitUntilReady(page.takeDiffuxSnapshot);
});