lib/helpers/screenshot.js
'use strict';
var logMethods = require('../log');
var type = require('../type');
var when = require('../when');
var screenshotScripts = require('../scripts/screenshot');
var PNGImage = require('pngjs-image');
var fs = require('fs');
module.exports = Screenshot;
/**
* Screenshot object
*
* @constructor
* @class Screenshot
* @module WebDriver
* @submodule Helpers
* @param {Driver} driver
*/
function Screenshot (driver) {
this._driver = driver;
}
/////////////////////
// Private Methods //
/////////////////////
/**
* Logs a method call by an event
*
* @param {object} event
* @method _logMethodCall
* @private
*/
Screenshot.prototype._logMethodCall = function (event) {
event.target = 'Screenshot';
this._driver._logMethodCall(event);
};
/**
* Performs a context dependent JSON request for the current session.
* The result is parsed for errors.
*
* @method _requestJSON
* @private
* @param {String} method
* @param {String} path
* @param {*} [body]
* @return {*}
*/
Screenshot.prototype._requestJSON = function (method, path, body) {
return this._driver._requestJSON(method, path, body);
};
////////////////////
// Public Methods //
////////////////////
/**
* Gets the driver object.
* Direct-access. No need to wait.
*
* @return {Driver}
*/
Screenshot.prototype.getDriver = function () {
return this._driver;
};
/**
* Takes a screenshot as-is from the webdriver protocol
*
* @method takeRawScreenshot
* @param {Object} [options]
* @return {Buffer}
*/
//Screenshot.prototype.id = 0; //TODO: Turn on for debugging
Screenshot.prototype.takeRawScreenshot = function (options) {
return when(this._driver._requestJSON('GET', '/screenshot', undefined, options), function (base64Data) {
var buffer = new Buffer(base64Data, 'base64');
//var browserName = this.getDriver().deviceName() != '' ? this.getDriver().deviceName() : this.getDriver().browserName();
//Screenshot.prototype.id++;
//fs.writeFileSync(__dirname + '/' + browserName + " " + this.getDriver().deviceOrientation() + " " + this.getDriver().browserVersion() + " " + this.getDriver().platform() + " " + Screenshot.prototype.id + '.png', buffer);
return buffer;
}.bind(this));
};
/**
* Takes a screenshot pre-processed and converted into an image
*
* @method takeProcessedScreenshot
* @param {Object} [options]
* @return {PNGImage}
*/
Screenshot.prototype.takeProcessedScreenshot = function (options) {
return when(this.takeRawScreenshot(options), function (buffer) {
return this._screenshotPrePrecessing(PNGImage.loadImageSync(buffer));
}.bind(this));
};
/**
* Gets the max. allowed resolution for one screenshot. If the document exceeds this resolution, then
* the screenshot will be stitched together from multiple smaller screenshots called sections.
*
* @method getMaxImageResolution
* @return {int}
*/
Screenshot.prototype.getMaxImageResolution = function () {
return this.getDriver().utils().resolve(this.getDriver().getValue('maxImageResolution'));
};
/**
* Takes a screenshot of the whole document
*
* @method documentScreenshot
* @param {object} [options]
* @param {int} [options.horizontalPadding=0] Padding of the document for adjustment
* @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
* @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
* @param {ActiveWindow|Element} [options.context] Context of screenshot
* @param {object[]|Element[]|string[]} [options.blockOuts] List of areas/elements that should be blocked-out
* @param {object} [options.blockOutColor=black] Color to be used for blocking-out areas {red, green, blue, alpha}
* @param {int} [options.wait=100] Wait in ms before each screenshot
* @return {Buffer}
*/
Screenshot.prototype.documentScreenshot = function (options) {
return this._takeScreenshot(function (initData) {
return {
x: 0,
y: 0,
width: initData.document.width,
height: initData.document.height
};
}, options);
};
/**
* Takes a screenshot of the current view-port
*
* @method viewPortScreenshot
* @param {object} [options]
* @param {int} [options.horizontalPadding=0] Padding of the document for adjustment
* @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
* @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
* @param {ActiveWindow|Element} [options.context] Context of screenshot
* @param {object[]|Element[]|string[]} [options.blockOuts] List of areas/elements that should be blocked-out
* @param {object} [options.blockOutColor=black] Color to be used for blocking-out areas {red, green, blue, alpha}
* @param {int} [options.wait=100] Wait in ms before each screenshot
* @return {Buffer}
*/
Screenshot.prototype.viewPortScreenshot = function (options) {
return this._takeScreenshot(function (initData) {
return {
x: initData.viewPort.x,
y: initData.viewPort.y,
width: initData.viewPort.width,
height: initData.viewPort.height
};
}, options);
};
/**
* Takes a screenshot of a specific area
*
* @method areaScreenshot
* @param {int} [x=0] X-coordinate for area
* @param {int} [y=0] Y-coordinate for area
* @param {int} [width=document.width-x] Width of area to be captured
* @param {int} [height=document.height-y] Height of area to be captured
* @param {object} [options]
* @param {int} [options.horizontalPadding=0] Padding of the document for adjustment
* @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
* @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
* @param {ActiveWindow|Element} [options.context] Context of screenshot
* @param {object[]|Element[]|string[]} [options.blockOuts] List of areas/elements that should be blocked-out
* @param {object} [options.blockOutColor=black] Color to be used for blocking-out areas {red, green, blue, alpha}
* @param {int} [options.wait=100] Wait in ms before each screenshot
* @return {Buffer}
*/
Screenshot.prototype.areaScreenshot = function (x, y, width, height, options) {
return this._takeScreenshot(function (initData) {
var localX = x || 0,
localY = y || 0,
localWidth = width || initData.document.width - localX,
localHeight = height || initData.document.height - localY;
return {
x: localX,
y: localY,
width: localWidth,
height: localHeight
};
}, options);
};
/**
* Determines the areas that should be blocked-out
*
* @method _determineBlockOuts
* @param {object} area Area that will be captured
* @param {object} context Context of the capture (ActiveWindow, Element)
* @param {object[]|Element[]|string[]} blockOuts List of block-out areas
* @param {object} [blockOutColor=black] Color to be used for blocking-out areas {red, green, blue, alpha}
* @return {object[]} List of rectangular areas {x, y, width, height}
* @private
*/
Screenshot.prototype._determineBlockOuts = function (area, context, blockOuts, blockOutColor) {
var defaultColor = blockOutColor || { red: 0, green: 0, blue: 0 };
return when(this.getDriver().utils().map(blockOuts, function (blockOutEl) {
if (typeof blockOutEl === 'string') {
return when(context.getElements(blockOutEl), function (elements) {
return elements.map(function (el) {
return el.getFrame();
});
});
} else if (typeof blockOutEl === 'object') {
if (blockOutEl instanceof require('../element')) {
return blockOutEl.getFrame();
} else {
return this.getDriver().utils().resolve(blockOutEl);
}
} else {
throw new Error('Unknown block-out type: ' + typeof blockOutEl);
}
}.bind(this)), function (blockOuts) {
var flattenList = [], reducedList;
// Make sure that we have a flat list
blockOuts.forEach(function (blockOut) {
if ((typeof blockOut === 'object') && (blockOut.length !== undefined)) { // Array
blockOut.forEach(function (item) {
flattenList.push(item);
});
} else {
flattenList.push(blockOut);
}
});
// Validate, reduce, and convert coordinates of each block-out area
flattenList = flattenList.map(function (blockOut) {
this._validateRectItem(blockOut);
this._reduceRectItemToArea(blockOut, area);
// Convert coordinates from root to captured area
blockOut.x -= area.x;
blockOut.y -= area.y;
// Use default block-out color if none specified
blockOut.color = blockOut.color || defaultColor;
return blockOut;
}.bind(this));
// Remove block-outs that doesn't need to be done
reducedList = flattenList.filter(function (blockOut) {
return ((blockOut.width !== 0) && (blockOut.height !== 0));
});
return reducedList;
}.bind(this));
};
/**
* Reduces a frame-item to a specified area
*
* @method _reduceRectItemToArea
* @param {object} item Item that should be reduced
* @param {object} area Area to reduce it to
* @private
*/
Screenshot.prototype._reduceRectItemToArea = function (item, area) {
if (item.x < area.x) {
item.width -= Math.abs(item.x - area.x);
item.x = area.x;
}
if (item.x > area.x + area.width) {
item.x = area.x + area.width;
item.width = 0;
}
if (item.y < area.y) {
item.height -= Math.abs(item.y - area.y);
item.y = area.y;
}
if (item.y > area.y + area.height) {
item.y = area.y + area.height;
item.height = 0;
}
if (item.width < 0) {
item.width = 0;
}
if (item.width > area.width) {
item.width = area.width;
}
if (item.height < 0) {
item.height = 0;
}
if (item.height > area.height) {
item.height = area.height;
}
};
/**
* Calidates if a rectangular item has all the required properties
*
* @method _validateRectItem
* @param {object} item
* @private
*/
Screenshot.prototype._validateRectItem = function (item) {
['x', 'y', 'width', 'height'].forEach(function (property) {
if (item[property] === undefined) {
throw new Error('The "' + property + '" property needs to be defined for item: ' + JSON.stringify(item));
}
if (typeof item[property] !== 'number') {
throw new Error('The "' + property + '" property needs to be numeric: ' + JSON.stringify(item));
}
});
};
/**
* Takes a screenshot of an area that will be specified through the return-value of the callback
*
* @method _takeScreenshot
* @param {function} fn Function to be called to get area to be captured. Supplies the data retrieved from the client as the first parameters.
* @param {object} [options]
* @param {int} [options.horizontalPadding=0] Padding of the document for adjustment
* @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
* @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
* @param {object} [options.padding] Padding for screenshots
* @param {object} [options.padding.viewPort] Padding for view-port screenshots, ignoring areas of the view-port screenshot
* @param {int} [options.padding.viewPort.top=0] Padding of view-port screenshot from the top
* @param {int} [options.padding.viewPort.bottom=0] Padding of view-port screenshot from the bottom
* @param {int} [options.padding.viewPort.left=0] Padding of view-port screenshot from the left
* @param {int} [options.padding.viewPort.right=0] Padding of view-port screenshot from the right
* @param {object} [options.padding.screenshot] Padding for screenshot, ignoring areas of the screenshot
* @param {int} [options.padding.screenshot.top=0] Padding of screenshot from the top
* @param {int} [options.padding.screenshot.bottom=0] Padding of screenshot from the bottom
* @param {int} [options.padding.screenshot.left=0] Padding of screenshot from the left
* @param {int} [options.padding.screenshot.right=0] Padding of screenshot from the right
* @param {object} [options.padding.document] Padding for document screenshots, ignoring areas of the document screenshot
* @param {int} [options.padding.document.top=0] Padding of document screenshot from the top
* @param {int} [options.padding.document.bottom=0] Padding of document screenshot from the bottom
* @param {int} [options.padding.document.left=0] Padding of document screenshot from the left
* @param {int} [options.padding.document.right=0] Padding of document screenshot from the right
* @param {ActiveWindow|Element} [options.context] Context of screenshot
* @param {object[]|Element[]|string[]} [options.blockOuts] List of areas/elements that should be blocked-out
* @param {object} [options.blockOutColor=black] Color to be used for blocking-out areas {red, green, blue, alpha}
* @param {int} [options.wait=100] Wait in ms before each screenshot
* @return {Buffer}
* @private
*/
Screenshot.prototype._takeScreenshot = function (fn, options) {
var DevicePixelRatio = require('./devicePixelRatio'),
devicePixelRatio = new DevicePixelRatio(this.getDriver()),
Stitching = require('./stitching'),
stitching = new Stitching(this.getDriver());
return when(devicePixelRatio.getDevicePixelRatio(options), function (devicePixelRatio) {
return when(stitching.doesNeedStitching(options), function (needsStitching) {
return when(this.getMaxImageResolution(), function (maxImageResolution) {
return when(this._execute(screenshotScripts.init, [needsStitching]), function (initData) {
initData = JSON.parse(initData);
var area, sections, padding;
// Make sure to account for the device-pixel-ratio
maxImageResolution *= (1 / devicePixelRatio);
// Make sure the document is not too wide; can fix this by increasing maxImageResolution
// We might need to capture only smaller areas, but we need to account also for correct behaving
// browsers like firefox which will always return the whole document.
if (initData.document.width > maxImageResolution) {
throw new Error('The maxImageResolution needs to be greater or equal to one time the document width.');
}
// Set defaults for padding (if none given) and validate
padding = this._browserPadding((options && options.padding) || {});
area = fn(initData);
// Add requested document padding
area.x = area.x + padding.document.left;
area.y = area.y + padding.document.top;
area.width = area.width - padding.document.left - padding.document.right;
area.height = area.height - padding.document.top - padding.document.bottom;
this._validateArea(area, padding, initData);
// Determine sections and view-ports to capture
sections = this._gatherSections(area, padding, initData, maxImageResolution, needsStitching);
return when(this._determineBlockOuts(area, options.context || this, options.blockOuts || [], options.blockOutColor), function (blockOuts) {
return when(this._takeSectionScreenshots(sections, initData, options), function () {
// Stitch images together
var image;
if (needsStitching) {
image = this._stitchImages(area, sections, devicePixelRatio);
} else {
image = sections[0].viewPorts[0].image;
image.clip(area.x, area.y, area.width, area.height);
}
return when(this._execute(screenshotScripts.revert, [initData]), function () {
// Apply block-outs (if available)
this._applyBlockOuts(image, blockOuts, devicePixelRatio);
return image.toBlobSync();
}.bind(this));
}.bind(this));
}.bind(this));
}.bind(this));
}.bind(this));
}.bind(this));
}.bind(this));
};
/**
* Applies all areas selected to be blocked-out
*
* @param {PNGImage} image Image to be blocked-out
* @param {object[]} blockOuts List of block-out areas
* @param {number} devicePixelRatio Device pixel ratio for browser
* @private
*/
Screenshot.prototype._applyBlockOuts = function (image, blockOuts, devicePixelRatio) {
blockOuts.forEach(function (blockOut) {
image.fillRect(
Math.floor(blockOut.x * devicePixelRatio),
Math.floor(blockOut.y * devicePixelRatio),
Math.floor(blockOut.width * devicePixelRatio),
Math.floor(blockOut.height * devicePixelRatio),
blockOut.color
);
});
};
/**
* Determines if the screenshots need additional padding
*
* Note:
* Overwrite this method if you want to implement standard browser padding.
*
* @method _browserPadding
* @param {object} padding
* @private
*/
Screenshot.prototype._browserPadding = function (padding) {
var paddingViewPortTop = 0, paddingViewPortBottom = 0,
paddingViewPortLeft = 0, paddingViewPortRight = 0,
paddingScreenshotTop = 0, paddingScreenshotBottom = 0,
paddingScreenshotLeft = 0, paddingScreenshotRight = 0,
paddingDocumentTop = 0, paddingDocumentBottom = 0,
paddingDocumentLeft = 0, paddingDocumentRight = 0,
browserName = this.getDriver().browserName().toLowerCase(),
browserVersion = this.getDriver().browserVersion(),
deviceName = this.getDriver().deviceName().toLowerCase(),
deviceOrientation = this.getDriver().deviceOrientation().toLowerCase();
// Exceptions
switch (browserName) {
case 'iphone':
paddingScreenshotTop = 65; // Browser address bar needs to be trimmed
paddingScreenshotBottom = 2; // Browser returns view-port that is one pixel smaller than reported
break;
case 'internet explorer':
// We expect for future versions to behave the same (black border on the right)
if (browserVersion >= 10) {
paddingDocumentRight = 4;
}
break;
}
padding.viewPort = padding.viewPort || {};
padding.viewPort.top = padding.viewPort.top || paddingViewPortTop;
padding.viewPort.bottom = padding.viewPort.bottom || paddingViewPortBottom;
padding.viewPort.left = padding.viewPort.left || paddingViewPortLeft;
padding.viewPort.right = padding.viewPort.right || paddingViewPortRight;
padding.screenshot = padding.screenshot || {};
padding.screenshot.top = padding.screenshot.top || paddingScreenshotTop;
padding.screenshot.bottom = padding.screenshot.bottom || paddingScreenshotBottom;
padding.screenshot.left = padding.screenshot.left || paddingScreenshotLeft;
padding.screenshot.right = padding.screenshot.right || paddingScreenshotRight;
padding.document = padding.document || {};
padding.document.top = padding.document.top || paddingDocumentTop;
padding.document.bottom = padding.document.bottom || paddingDocumentBottom;
padding.document.left = padding.document.left || paddingDocumentLeft;
padding.document.right = padding.document.right || paddingDocumentRight;
return padding;
};
/**
* Validates and corrects area that was given to capture
*
* @method _validateArea
* @param {object} area
* @param {object} padding Padding for screenshots
* @param {object} padding.viewPort Padding for view-port screenshots, ignoring areas of the view-port screenshot
* @param {int} padding.viewPort.top Padding of view-port screenshot from the top
* @param {int} padding.viewPort.bottom Padding of view-port screenshot from the bottom
* @param {int} padding.viewPort.left Padding of view-port screenshot from the left
* @param {int} padding.viewPort.right Padding of view-port screenshot from the right
* @param {object} padding.screenshot Padding for screenshots, ignoring areas of the screenshot
* @param {int} padding.screenshot.top Padding of screenshot from the top
* @param {int} padding.screenshot.bottom Padding of screenshot from the bottom
* @param {int} padding.screenshot.left Padding of screenshot from the left
* @param {int} padding.screenshot.right Padding of screenshot from the right
* @param {object} padding.document Padding for document screenshots, ignoring areas of the document screenshot
* @param {int} padding.document.top Padding of document screenshot from the top
* @param {int} padding.document.bottom Padding of document screenshot from the bottom
* @param {int} padding.document.left Padding of document screenshot from the left
* @param {int} padding.document.right Padding of document screenshot from the right
* @param {object} initData
* @private
*/
Screenshot.prototype._validateArea = function (area, padding, initData) {
if (area.width < 0) {
throw new Error('Width of area to capture cannot be negative.');
}
if (area.height < 0) {
throw new Error('Height of area to capture cannot be negative.');
}
if (area.x < 0) {
area.x = 0;
}
if (area.x >= initData.document.width) {
area.x = initData.document.width - 1;
}
if (area.y < 0) {
area.y = 0;
}
if (area.y >= initData.document.height) {
area.y = initData.document.height - 1;
}
if (area.x + area.width > initData.document.width) {
area.width = initData.document.width - area.x;
}
if (area.y + area.height > initData.document.height) {
area.height = initData.document.height - area.y;
}
};
/**
* Calculates the sections that need to be captured on their own to be able to capture the whole document.
*
* This method depends on the value given to "maxImageResolution".
*
* @method _gatherSections
* @param {object} area Area to capture
* @param {object} padding Padding for screenshots
* @param {object} padding.viewPort Padding for view-port screenshots, ignoring areas of the view-port screenshot
* @param {int} padding.viewPort.top Padding of view-port screenshot from the top
* @param {int} padding.viewPort.bottom Padding of view-port screenshot from the bottom
* @param {int} padding.viewPort.left Padding of view-port screenshot from the left
* @param {int} padding.viewPort.right Padding of view-port screenshot from the right
* @param {object} padding.screenshot Padding for screenshots, ignoring areas of the screenshot
* @param {int} padding.screenshot.top Padding of screenshot from the top
* @param {int} padding.screenshot.bottom Padding of screenshot from the bottom
* @param {int} padding.screenshot.left Padding of screenshot from the left
* @param {int} padding.screenshot.right Padding of screenshot from the right
* @param {object} initData Data that was initially gathered from the client
* @param {int} maxImageResolution Max. number of pixels allowed for a screenshot
* @param {boolean} needsStitching Does the browser need stitching?
* @return {object[]} List of sections
* @private
*/
Screenshot.prototype._gatherSections = function (area, padding, initData, maxImageResolution, needsStitching) {
var sections = [], section,
sectionCount, sectionHeight,
i, index = 0,
yOffset,
documentWidth,
viewPortHeight = initData.viewPort.height - padding.viewPort.top - padding.viewPort.bottom - padding.screenshot.bottom;
documentWidth = initData.document.width - area.x;
// Calculate max. section height considering the max resolution to capture
sectionHeight = Math.floor(maxImageResolution / documentWidth);
if (needsStitching) {
// When the screenshots are stitched together,
// we try here to reduce the amount of screenshots per section.
// We don't want to be wasteful so we make sure that the section border is on
// even viewPort height; we want to avoid taking a viewPort screenshot and only use
// the upper part of it, discarding it and requesting it for the next section.
sectionHeight = Math.floor(sectionHeight / viewPortHeight) * viewPortHeight;
}
sectionCount = Math.ceil(area.height / sectionHeight);
for (i = 0; i < sectionCount; i++) {
yOffset = (i * sectionHeight);
section = {
shift: (sectionCount !== 1),
x: area.x,
y: area.y + yOffset, // Vertical offset for sections
dstX: 0,
dstY: yOffset, // Vertical offset for sections
width: area.width,
height: Math.min(sectionHeight, area.height - yOffset),
viewPorts: undefined
};
if (needsStitching) {
section.viewPorts = this._gatherViewPortSections(section, padding, initData, index);
index += section.viewPorts.length;
} else {
section.viewPorts = [
{
srcX: padding.viewPort.left + padding.screenshot.left,
srcY: padding.viewPort.top + padding.screenshot.top,
x: 0,
y: 0,
width: section.width,
height: section.height,
image: undefined,
index: index
}
];
index++;
}
sections.push(section);
}
return sections;
};
/**
* Calculates the sections that need to be captured when the browser is not able to return pixels outside of the view-port.
*
* @method _gatherViewPortSections
* @param {object} section Section that should be captured
* @param {object} padding Padding for screenshots
* @param {object} padding.viewPort Padding for view-port screenshots, ignoring areas of the view-port screenshot
* @param {int} padding.viewPort.top Padding of view-port screenshot from the top
* @param {int} padding.viewPort.bottom Padding of view-port screenshot from the bottom
* @param {int} padding.viewPort.left Padding of view-port screenshot from the left
* @param {int} padding.viewPort.right Padding of view-port screenshot from the right
* @param {object} padding.screenshot Padding for screenshots, ignoring areas of the screenshot
* @param {int} padding.screenshot.top Padding of screenshot from the top
* @param {int} padding.screenshot.bottom Padding of screenshot from the bottom
* @param {int} padding.screenshot.left Padding of screenshot from the left
* @param {int} padding.screenshot.right Padding of screenshot from the right
* @param {object} initData Data that was initially gathered from the client
* @param {int} index Index of first view-port
* @return {object[]}
* @private
*/
Screenshot.prototype._gatherViewPortSections = function (section, padding, initData, index) {
var viewPorts = [],
sectionWidth = section.width,
sectionHeight = section.height,
viewPortWidth = initData.viewPort.width - padding.viewPort.left - padding.viewPort.right - padding.screenshot.right,
viewPortHeight = initData.viewPort.height - padding.viewPort.top - padding.viewPort.bottom - padding.screenshot.bottom,
columns = Math.ceil(sectionWidth / viewPortWidth),
rows = Math.ceil(sectionHeight / viewPortHeight),
offsetX, offsetY,
x, y;
for (y = 0; y < rows; y++) {
for (x = 0; x < columns; x++) {
offsetX = x * viewPortWidth;
offsetY = y * viewPortHeight;
viewPorts.push({
srcX: padding.viewPort.left + padding.screenshot.left,
srcY: padding.viewPort.top + padding.screenshot.top,
x: offsetX,
y: offsetY,
width: Math.min(viewPortWidth, sectionWidth - offsetX),
height: Math.min(viewPortHeight, sectionHeight - offsetY),
image: undefined,
index: index
});
index++;
}
}
return viewPorts;
};
/**
* Takes all the screenshots defined in section
*
* @method _takeSectionScreenshots
* @param {object[]} sections List of sections to capture
* @param {object} initData Data that was initially gathered from the client
* @param {object} [options]
* @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
* @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
* @param {int} [options.wait=100] Wait in ms before each screenshot
* @private
*/
Screenshot.prototype._takeSectionScreenshots = function (sections, initData, options) {
return when(this.getDriver().utils().map(sections, function (section) {
return this._takeViewPortScreenshots(section, section.viewPorts, initData, options);
}.bind(this)), function () {
return this._execute(options.completeFn);
}.bind(this));
};
/**
* Takes all the screenshots defined in the view-ports
*
* @method _takeViewPortScreenshots
* @param {object} section Section to capture
* @param {object[]} viewPorts List of view-ports to capture
* @param {object} initData Data that was initially gathered from the client
* @param {object} [options]
* @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
* @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
* @param {int} [options.wait=100] Wait in ms before each screenshot
* @private
*/
Screenshot.prototype._takeViewPortScreenshots = function (section, viewPorts, initData, options) {
return this.getDriver().utils().map(viewPorts, function (viewPort) {
return this._takeViewPortScreenshot(section, viewPort, initData, options);
}.bind(this));
};
/**
* Takes a screenshot of the given view-port in relation to the section
*
* @method _takeViewPortScreenshot
* @param {object} section Section to capture
* @param {object} viewPort View-ports to capture
* @param {object} initData Data that was initially gathered from the client
* @param {object} [options]
* @param {function} [options.eachFn] Will execute method on client before each screenshot is taken. First parameter is index of screenshot.
* @param {function} [options.completeFn] Will execute method on client after all screenshots are taken.
* @param {int} [options.wait=100] Wait in ms before each screenshot
* @private
*/
Screenshot.prototype._takeViewPortScreenshot = function (section, viewPort, initData, options) {
var offsetX = section.x + viewPort.x,
offsetY = section.y + viewPort.y,
viewPortIndex = viewPort.index,
sectionHeight = section.shift ? section.height : null,
wait = (options && options.wait) || 100;
return when(this._execute(screenshotScripts.documentOffset, [offsetX, offsetY, sectionHeight, initData]), function () {
// Just wait a little bit. Some browsers need time for that.
return when(this.getDriver().utils().sleep(wait), function () {
return when(this._execute(options.eachFn, [viewPortIndex]), function () {
// Take screenshot with current section and view-port
return when(this.takeProcessedScreenshot(), function (image) {
viewPort.image = image;
}.bind(this));
}.bind(this));
}.bind(this));
}.bind(this));
};
/**
* Processes the screenshots before it is used for stitching
*
* Note:
* Some browser need some pre-processing like rotating the screenshot.
*
* @method _screenshotPrePrecessing
* @param {PNGImage} image Image to process
* @return {PNGImage}
* @private
*/
Screenshot.prototype._screenshotPrePrecessing = function (image) {
var browserName = this.getDriver().browserName().toLowerCase(),
browserVersion = this.getDriver().browserVersion(),
deviceName = this.getDriver().deviceName().toLowerCase(),
deviceOrientation = this.getDriver().deviceOrientation().toLowerCase();
// Exceptions
switch (browserName) {
case 'iphone':
if (deviceOrientation === 'landscape') {
image = image.rotateCCW();
}
break;
}
return image;
};
/**
* Stitches all the sections and view-ports together to one final image
*
* @method _stitchImages
* @param {object} area Area that should have been captured on the page
* @param {object[]} sections List of sections that were captured
* @param {number} devicePixelRatio Device pixel ratio for browser
* @return {PNGImage}
* @private
*/
Screenshot.prototype._stitchImages = function (area, sections, devicePixelRatio) {
var image;
image = PNGImage.createImage(
Math.floor(area.width * devicePixelRatio),
Math.floor(area.height * devicePixelRatio)
);
sections.forEach(function (section) {
section.viewPorts.forEach(function (viewPort) {
var offsetX = Math.floor((section.dstX + viewPort.x) * devicePixelRatio),
offsetY = Math.floor((section.dstY + viewPort.y) * devicePixelRatio),
width = Math.floor(viewPort.width * devicePixelRatio),
height = Math.floor(viewPort.height * devicePixelRatio);
viewPort.image.getImage().bitblt(
image.getImage(),
Math.floor(viewPort.srcX * devicePixelRatio),
Math.floor(viewPort.srcY * devicePixelRatio),
Math.min(width - Math.floor(section.dstX * devicePixelRatio), viewPort.image.getWidth()),
Math.min(height - Math.floor(section.dstY * devicePixelRatio), viewPort.image.getHeight()),
offsetX,
offsetY
);
// Free-up some memory
viewPort.image = null;
});
});
// Force garbage-collection if available (can be turned on with option "--expose-gc" when running node)
if (global.gc) {
global.gc();
}
return image;
};
/**
* Executes a script in the browser and returns the result.
*
* This is a convenience method for accessing the execute method.
*
* @method _execute
* @param {String|Function} script
* @param {Array} [args]
* @return {*}
* @private
*/
Screenshot.prototype._execute = function (script, args) {
if (script) {
return this.getDriver().browser().activeWindow().execute(script, args);
} else { // Ignore script if there is nothing - might happen with screenshot requests
return this.getDriver().utils().resolve(undefined);
}
};
logMethods(Screenshot.prototype);