eyes.selenium.v68patch
Version:
Applitools Eyes SDK For Selenium JavaScript WebDriver
1,019 lines (945 loc) • 46.5 kB
JavaScript
/*
---
name: EyesSeleniumUtils
description: Handles browser related functionality.
---
*/
(function () {
"use strict";
var EyesSDK = require('eyes.sdk'),
EyesUtils = require('eyes.utils');
var MutableImage = EyesSDK.MutableImage,
CoordinatesType = EyesSDK.CoordinatesType,
GeneralUtils = EyesUtils.GeneralUtils,
GeometryUtils = EyesUtils.GeometryUtils,
ImageUtils = EyesUtils.ImageUtils;
var EyesSeleniumUtils = {};
/**
* @private
* @type {string}
*/
var JS_GET_VIEWPORT_SIZE =
"var height = undefined; " +
"var width = undefined; " +
"if (window.innerHeight) { height = window.innerHeight; } " +
"else if (document.documentElement && document.documentElement.clientHeight) { height = document.documentElement.clientHeight; } " +
"else { var b = document.getElementsByTagName('body')[0]; if (b.clientHeight) {height = b.clientHeight;} }; " +
"if (window.innerWidth) { width = window.innerWidth; } " +
"else if (document.documentElement && document.documentElement.clientWidth) { width = document.documentElement.clientWidth; } " +
"else { var b = document.getElementsByTagName('body')[0]; if (b.clientWidth) { width = b.clientWidth;} }; " +
"return [width, height];";
/**
* @private
* @type {string}
*/
var JS_GET_CURRENT_SCROLL_POSITION =
"var doc = document.documentElement; " +
"var x = window.scrollX || ((window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0)); " +
"var y = window.scrollY || ((window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)); " +
"return [x, y];";
/**
* @private
* @type {string}
*/
var JS_GET_CONTENT_ENTIRE_SIZE =
"var scrollWidth = document.documentElement.scrollWidth; " +
"var bodyScrollWidth = document.body.scrollWidth; " +
"var totalWidth = Math.max(scrollWidth, bodyScrollWidth); " +
"var clientHeight = document.documentElement.clientHeight; " +
"var bodyClientHeight = document.body.clientHeight; " +
"var scrollHeight = document.documentElement.scrollHeight; " +
"var bodyScrollHeight = document.body.scrollHeight; " +
"var maxDocElementHeight = Math.max(clientHeight, scrollHeight); " +
"var maxBodyHeight = Math.max(bodyClientHeight, bodyScrollHeight); " +
"var totalHeight = Math.max(maxDocElementHeight, maxBodyHeight); " +
"return [totalWidth, totalHeight];";
/**
* @return {string}
*/
var JS_GET_COMPUTED_STYLE_FORMATTED_STR = function (propStyle) {
return "var elem = arguments[0]; var styleProp = '" + propStyle + "'; " +
"if (window.getComputedStyle) { " +
"return window.getComputedStyle(elem, null).getPropertyValue(styleProp);" +
"} else if (elem.currentStyle) { " +
"return elem.currentStyle[styleProp];" +
"} else { " +
"return null;" +
"}";
};
/**
* @private
* @type {string[]}
*/
var JS_TRANSFORM_KEYS = ["transform", "-webkit-transform"];
/**
* Executes a script using the browser's executeScript function - and optionally waits a timeout.
*
* @param {WebDriver} browser The driver using which to execute the script.
* @param {string} script The code to execute on the given driver.
* @param {PromiseFactory} promiseFactory
* @param {number|undefined} [stabilizationTimeMs] The amount of time to wait after script execution to
* let the browser a chance to stabilize (e.g., finish rendering).
* @return {Promise<void>} A promise which resolves to the result of the script's execution on the tab.
*/
EyesSeleniumUtils.executeScript = function executeScript(browser, script, promiseFactory, stabilizationTimeMs) {
return browser.executeScript(script).then(function (result) {
if (stabilizationTimeMs) {
return GeneralUtils.sleep(stabilizationTimeMs, promiseFactory)
.then(function () {
return result;
});
}
return result;
});
};
/**
* Returns the computed value of the style property for the current element.
*
* @param {WebDriver} browser The driver which will execute the script to get computed style.
* @param {WebElement} element
* @param {string} propStyle The style property which value we would like to extract.
* @return {Promise<string>} The value of the style property of the element, or {@code null}.
*/
EyesSeleniumUtils.getComputedStyle = function (browser, element, propStyle) {
var scriptToExec = JS_GET_COMPUTED_STYLE_FORMATTED_STR(propStyle);
return browser.executeScript(scriptToExec, element).then(function(computedStyle) {
return computedStyle;
});
};
/**
* Returns a location based on the given location.
*
* @param {Logger} logger The logger to use.
* @param {WebElement|EyesRemoteWebElement} element The element for which we want to find the content's location.
* @param {{x: number, y: number}} location The location of the element.
* @param {PromiseFactory} promiseFactory
* @return {Promise<{x: number, y: number}>} The location of the content of the element.
*/
EyesSeleniumUtils.getLocationWithBordersAddition = function (logger, element, location, promiseFactory) {
logger.verbose("BordersAdditionFrameLocationProvider(logger, element, [" + location.x + "," + location.y + "])");
if (element.getRemoteWebElement !== undefined) {
element = element.getRemoteWebElement();
}
var leftBorderWidth, topBorderWidth;
return _getLeftBorderWidth(logger, promiseFactory, element).then(function (val) {
leftBorderWidth = val;
return _getTopBorderWidth(logger, promiseFactory, element);
}).then(function (val) {
topBorderWidth = val;
logger.verbose("Done!");
// Frame borders also have effect on the frame's location.
return GeometryUtils.locationOffset(location, {x: leftBorderWidth, y: topBorderWidth});
});
};
/**
* Return width of left border
*
* @private
* @param {Logger} logger
* @param {PromiseFactory} promiseFactory
* @param {WebElement} element
* @return {Promise<number>}
*/
function _getLeftBorderWidth(logger, promiseFactory, element) {
return promiseFactory.makePromise(function (resolve) {
logger.verbose("Get element border left width...");
return EyesSeleniumUtils.getComputedStyle(element.getDriver(), element, "border-left-width").then(function (styleValue) {
return styleValue;
}, function (err) {
logger.verbose("Using getComputedStyle failed: ", err);
logger.verbose("Using getCssValue...");
return element.getCssValue("border-left-width");
}).then(function (propValue) {
logger.verbose("Done!");
var leftBorderWidth = Math.round(parseFloat(propValue.trim().replace("px", "")));
logger.verbose("border-left-width: ", leftBorderWidth);
resolve(leftBorderWidth);
}, function (err) {
logger.verbose("Couldn't get the element's border-left-width: ", err, ". Falling back to default");
resolve(0);
});
});
}
/**
* Return width of top border
*
* @private
* @param {Logger} logger
* @param {PromiseFactory} promiseFactory
* @param {WebElement} element
* @return {Promise<number>}
*/
function _getTopBorderWidth(logger, promiseFactory, element) {
return promiseFactory.makePromise(function (resolve) {
logger.verbose("Get element's border top width...");
return EyesSeleniumUtils.getComputedStyle(element.getDriver(), element, "border-top-width").then(function (styleValue) {
return styleValue;
}, function (err) {
logger.verbose("Using getComputedStyle failed: ", err);
logger.verbose("Using getCssValue...");
return element.getCssValue("border-top-width");
}).then(function (propValue) {
logger.verbose("Done!");
var topBorderWidth = Math.round(parseFloat(propValue.trim().replace("px", "")));
logger.verbose("border-top-width: ", topBorderWidth);
resolve(topBorderWidth);
}, function (err) {
logger.verbose("Couldn't get the element's border-top-width: ", err, ". Falling back to default");
resolve(0);
});
});
}
/**
* Gets the device pixel ratio.
*
* @param {WebDriver} browser The driver which will execute the script to get the ratio.
* @param {PromiseFactory} promiseFactory
* @return {Promise<number>} A promise which resolves to the device pixel ratio (float type).
*/
EyesSeleniumUtils.getDevicePixelRatio = function getDevicePixelRatio(browser, promiseFactory) {
return EyesSeleniumUtils.executeScript(browser, 'return window.devicePixelRatio', promiseFactory, undefined).then(function (results) {
return parseFloat(results);
});
};
/**
* Get the current transform of page.
*
* @param {WebDriver} browser The driver which will execute the script to get the scroll position.
* @param {PromiseFactory} promiseFactory
* @return {Promise<object.<string, string>>} A promise which resolves to the current transform value.
*/
EyesSeleniumUtils.getCurrentTransform = function getCurrentTransform(browser, promiseFactory) {
var script = "return { ";
for (var i = 0, l = JS_TRANSFORM_KEYS.length; i < l; i++) {
script += "'" + JS_TRANSFORM_KEYS[i] + "': document.documentElement.style['" + JS_TRANSFORM_KEYS[i] + "'],";
}
script += " }";
return EyesSeleniumUtils.executeScript(browser, script, promiseFactory, undefined);
};
/**
* Sets transforms for document.documentElement according to the given map of style keys and values.
*
* @param {WebDriver} browser The browser to use.
* @param {object.<string, string>} transforms The transforms to set. Keys are used as style keys and values are the values for those styles.
* @param {PromiseFactory} promiseFactory
* @returns {Promise<void>}
*/
EyesSeleniumUtils.setTransforms = function (browser, transforms, promiseFactory) {
var script = "";
for (var key in transforms) {
if (transforms.hasOwnProperty(key)) {
script += "document.documentElement.style['" + key + "'] = '" + transforms[key] + "';";
}
}
return EyesSeleniumUtils.executeScript(browser, script, promiseFactory, 250);
};
/**
* Set the given transform to document.documentElement for all style keys defined in {@link JS_TRANSFORM_KEYS}
*
* @param {WebDriver} browser The driver which will execute the script to set the transform.
* @param {string} transformToSet The transform to set.
* @param {PromiseFactory} promiseFactory
* @return {Promise<void>} A promise which resolves to the previous transform once the updated transform is set.
*/
EyesSeleniumUtils.setTransform = function setTransform(browser, transformToSet, promiseFactory) {
var transforms = {};
if (!transformToSet) {
transformToSet = '';
}
for (var i = 0, l = JS_TRANSFORM_KEYS.length; i < l; i++) {
transforms[JS_TRANSFORM_KEYS[i]] = transformToSet;
}
return EyesSeleniumUtils.setTransforms(browser, transforms, promiseFactory);
};
/**
* CSS translate the document to a given location.
*
* @param {WebDriver} browser The driver which will execute the script to set the transform.
* @param {{x: number, y: number}} point
* @param {PromiseFactory} promiseFactory
* @return {Promise<void>} A promise which resolves to the previous transform when the scroll is executed.
*/
EyesSeleniumUtils.translateTo = function translateTo(browser, point, promiseFactory) {
return EyesSeleniumUtils.setTransform(browser, 'translate(-' + point.x + 'px, -' + point.y + 'px)', promiseFactory);
};
/**
* Scroll to the specified position.
*
* @param {WebDriver} browser - The driver which will execute the script to set the scroll position.
* @param {{x: number, y: number}} point Point to scroll to
* @param {PromiseFactory} promiseFactory
* @return {Promise<void>} A promise which resolves after the action is performed and timeout passed.
*/
EyesSeleniumUtils.scrollTo = function scrollTo(browser, point, promiseFactory) {
return EyesSeleniumUtils.executeScript(browser,
'window.scrollTo(' + parseInt(point.x, 10) + ', ' + parseInt(point.y, 10) + ');',
promiseFactory, 250);
};
/**
* Gets the current scroll position.
*
* @param {WebDriver} browser The driver which will execute the script to get the scroll position.
* @param {PromiseFactory} promiseFactory
* @return {Promise<{x: number, y: number}>} A promise which resolves to the current scroll position.
*/
EyesSeleniumUtils.getCurrentScrollPosition = function getCurrentScrollPosition(browser, promiseFactory) {
return EyesSeleniumUtils.executeScript(browser, JS_GET_CURRENT_SCROLL_POSITION, promiseFactory, undefined).then(function (results) {
// If we can't find the current scroll position, we use 0 as default.
var x = parseInt(results[0], 10) || 0;
var y = parseInt(results[1], 10) || 0;
return {x: x, y: y};
});
};
/**
* Get the entire page size.
*
* @param {WebDriver} browser The driver used to query the web page.
* @param {PromiseFactory} promiseFactory
* @return {Promise<{width: number, height: number}>} A promise which resolves to an object containing the width/height of the page.
*/
EyesSeleniumUtils.getEntirePageSize = function getEntirePageSize(browser, promiseFactory) {
// IMPORTANT: Notice there's a major difference between scrollWidth
// and scrollHeight. While scrollWidth is the maximum between an
// element's width and its content width, scrollHeight might be
// smaller (!) than the clientHeight, which is why we take the
// maximum between them.
return EyesSeleniumUtils.executeScript(browser, JS_GET_CONTENT_ENTIRE_SIZE, promiseFactory).then(function (results) {
var totalWidth = parseInt(results[0], 10) || 0;
var totalHeight = parseInt(results[1], 10) || 0;
return {width: totalWidth, height: totalHeight};
});
};
/**
* Updates the document's documentElement "overflow" value (mainly used to remove/allow scrollbars).
*
* @param {WebDriver} browser The driver used to update the web page.
* @param {string} overflowValue The values of the overflow to set.
* @param {PromiseFactory} promiseFactory
* @return {Promise<string>} A promise which resolves to the original overflow of the document.
*/
EyesSeleniumUtils.setOverflow = function setOverflow(browser, overflowValue, promiseFactory) {
var script;
if (overflowValue === null) {
script =
"var origOverflow = document.documentElement.style.overflow; " +
"document.documentElement.style.overflow = undefined; " +
"return origOverflow";
} else {
script =
"var origOverflow = document.documentElement.style.overflow; " +
"document.documentElement.style.overflow = \"" + overflowValue + "\"; " +
"return origOverflow";
}
return EyesSeleniumUtils.executeScript(browser, script, promiseFactory, 100);
};
/**
* Hides the scrollbars of the current context's document element.
*
* @param {WebDriver} browser The browser to use for hiding the scrollbars.
* @param {PromiseFactory} promiseFactory
* @return {Promise<string>} The previous value of the overflow property (could be {@code null}).
*/
EyesSeleniumUtils.hideScrollbars = function (browser, promiseFactory) {
return EyesSeleniumUtils.setOverflow(browser, "hidden", promiseFactory);
};
/**
* Tries to get the viewport size using Javascript. If fails, gets the entire browser window size!
*
* @param {WebDriver} browser The browser to use.
* @param {PromiseFactory} promiseFactory
* @return {Promise<{width: number, height: number}>} The viewport size.
*/
EyesSeleniumUtils.getViewportSize = function (browser, promiseFactory) {
return promiseFactory.makePromise(function (resolve, reject) {
return EyesSeleniumUtils.executeScript(browser, JS_GET_VIEWPORT_SIZE, promiseFactory, undefined).then(function (results) {
if (isNaN(results[0]) || isNaN(results[1])) {
reject("Can't parse values.");
} else {
resolve({
width: parseInt(results[0], 10) || 0,
height: parseInt(results[1], 10) || 0
});
}
}, function (err) {
reject(err);
});
});
};
/**
* @param {Logger} logger
* @param {WebDriver} browser The browser to use.
* @param {PromiseFactory} promiseFactory
* @return {Promise<{width: number, height: number}>} The viewport size of the current context, or the display size if the viewport size cannot be retrieved.
*/
EyesSeleniumUtils.getViewportSizeOrDisplaySize = function (logger, browser, promiseFactory) {
logger.verbose("getViewportSizeOrDisplaySize()");
return EyesSeleniumUtils.getViewportSize(browser, promiseFactory).catch(function (err) {
logger.verbose("Failed to extract viewport size using Javascript:", err);
// If we failed to extract the viewport size using JS, will use the window size instead.
logger.verbose("Using window size as viewport size.");
return browser.manage().window().getSize().then(function (size) {
logger.verbose(String.format("Done! Size is", size));
return size;
});
});
};
/**
* @param {Logger} logger
* @param {WebDriver} browser The browser to use.
* @param {{width: number, height: number}} requiredSize
* @param {PromiseFactory} promiseFactory
* @return {Promise<boolean>}
*/
EyesSeleniumUtils.setBrowserSize = function (logger, browser, requiredSize, promiseFactory) {
return _setBrowserSize(logger, browser, requiredSize, 3, promiseFactory).then(function () {
return true;
}, function () {
return false;
});
};
function _setBrowserSize(logger, browser, requiredSize, retries, promiseFactory) {
return promiseFactory.makePromise(function (resolve, reject) {
logger.verbose("Trying to set browser size to:", requiredSize);
return browser.manage().window().setSize(requiredSize.width, requiredSize.height).then(function () {
return GeneralUtils.sleep(1000, promiseFactory);
}).then(function () {
return browser.manage().window().getSize();
}).then(function (currentSize) {
logger.log("Current browser size:", currentSize);
if (currentSize.width === requiredSize.width && currentSize.height === requiredSize.height) {
resolve();
return;
}
if (retries === 0) {
reject("Failed to set browser size: retries is out.");
return;
}
_setBrowserSize(logger, browser, requiredSize, retries - 1, promiseFactory).then(function () {
resolve();
}, function (err) {
reject(err);
});
});
});
}
/**
* @param {Logger} logger
* @param {WebDriver} browser The browser to use.
* @param {{width: number, height: number}} actualViewportSize
* @param {{width: number, height: number}} requiredViewportSize
* @param {PromiseFactory} promiseFactory
* @return {Promise<boolean>}
*/
EyesSeleniumUtils.setBrowserSizeByViewportSize = function (logger, browser, actualViewportSize, requiredViewportSize, promiseFactory) {
return browser.manage().window().getSize().then(function (browserSize) {
logger.verbose("Current browser size:", browserSize);
var requiredBrowserSize = {
width: browserSize.width + (requiredViewportSize.width - actualViewportSize.width),
height: browserSize.height + (requiredViewportSize.height - actualViewportSize.height)
};
return EyesSeleniumUtils.setBrowserSize(logger, browser, requiredBrowserSize, promiseFactory);
});
};
/**
* Tries to set the viewport
*
* @param {Logger} logger
* @param {WebDriver} browser The browser to use.
* @param {{width: number, height: number}} requiredSize The viewport size.
* @param {PromiseFactory} promiseFactory
* @returns {Promise<void>}
*/
EyesSeleniumUtils.setViewportSize = function (logger, browser, requiredSize, promiseFactory) {
// First we will set the window size to the required size.
// Then we'll check the viewport size and increase the window size accordingly.
logger.verbose("setViewportSize(", requiredSize, ")");
return promiseFactory.makePromise(function (resolve, reject) {
try {
var actualViewportSize;
EyesSeleniumUtils.getViewportSize(browser, promiseFactory).then(function (viewportSize) {
actualViewportSize = viewportSize;
logger.verbose("Initial viewport size:", actualViewportSize);
// If the viewport size is already the required size
if (actualViewportSize.width === requiredSize.width && actualViewportSize.height === requiredSize.height) {
resolve();
return;
}
// We move the window to (0,0) to have the best chance to be able to
// set the viewport size as requested.
browser.manage().window().setPosition(0, 0).catch(function () {
logger.verbose("Warning: Failed to move the browser window to (0,0)");
}).then(function () {
return EyesSeleniumUtils.setBrowserSizeByViewportSize(logger, browser, actualViewportSize, requiredSize, promiseFactory);
}).then(function () {
return EyesSeleniumUtils.getViewportSize(browser, promiseFactory);
}).then(function (actualViewportSize) {
if (actualViewportSize.width === requiredSize.width && actualViewportSize.height === requiredSize.height) {
resolve();
return;
}
// Additional attempt. This Solves the "maximized browser" bug
// (border size for maximized browser sometimes different than non-maximized, so the original browser size calculation is wrong).
logger.verbose("Trying workaround for maximization...");
return EyesSeleniumUtils.setBrowserSizeByViewportSize(logger, browser, actualViewportSize, requiredSize, promiseFactory).then(function () {
return EyesSeleniumUtils.getViewportSize(browser, promiseFactory);
}).then(function (viewportSize) {
actualViewportSize = viewportSize;
logger.verbose("Current viewport size:", actualViewportSize);
if (actualViewportSize.width === requiredSize.width && actualViewportSize.height === requiredSize.height) {
resolve();
return;
}
return browser.manage().window().getSize().then(function (browserSize) {
var MAX_DIFF = 3;
var widthDiff = actualViewportSize.width - requiredSize.width;
var widthStep = widthDiff > 0 ? -1 : 1; // -1 for smaller size, 1 for larger
var heightDiff = actualViewportSize.height - requiredSize.height;
var heightStep = heightDiff > 0 ? -1 : 1;
var currWidthChange = 0;
var currHeightChange = 0;
// We try the zoom workaround only if size difference is reasonable.
if (Math.abs(widthDiff) <= MAX_DIFF && Math.abs(heightDiff) <= MAX_DIFF) {
logger.verbose("Trying workaround for zoom...");
var retriesLeft = Math.abs((widthDiff === 0 ? 1 : widthDiff) * (heightDiff === 0 ? 1 : heightDiff)) * 2;
var lastRequiredBrowserSize = null;
return _setWindowSize(logger, browser, requiredSize, actualViewportSize, browserSize,
widthDiff, widthStep, heightDiff, heightStep, currWidthChange, currHeightChange,
retriesLeft, lastRequiredBrowserSize, promiseFactory).then(function () {
resolve();
}, function () {
reject("Failed to set viewport size: zoom workaround failed.");
});
}
reject("Failed to set viewport size!");
});
});
});
}).catch(function (err) {
reject(err);
});
} catch (err) {
reject(new Error(err));
}
});
};
/**
* @private
* @param {Logger} logger
* @param {WebDriver} browser
* @param {{width: number, height: number}} requiredSize
* @param actualViewportSize
* @param browserSize
* @param widthDiff
* @param widthStep
* @param heightDiff
* @param heightStep
* @param currWidthChange
* @param currHeightChange
* @param retriesLeft
* @param lastRequiredBrowserSize
* @param {PromiseFactory} promiseFactory
* @return {Promise<void>}
*/
function _setWindowSize(
logger,
browser,
requiredSize,
actualViewportSize,
browserSize,
widthDiff,
widthStep,
heightDiff,
heightStep,
currWidthChange,
currHeightChange,
retriesLeft,
lastRequiredBrowserSize,
promiseFactory
) {
return promiseFactory.makePromise(function (resolve, reject) {
logger.verbose("Retries left: " + retriesLeft);
// We specifically use "<=" (and not "<"), so to give an extra resize attempt
// in addition to reaching the diff, due to floating point issues.
if (Math.abs(currWidthChange) <= Math.abs(widthDiff) && actualViewportSize.width !== requiredSize.width) {
currWidthChange += widthStep;
}
if (Math.abs(currHeightChange) <= Math.abs(heightDiff) && actualViewportSize.height !== requiredSize.height) {
currHeightChange += heightStep;
}
var requiredBrowserSize = {
width: browserSize.width + currWidthChange,
height: browserSize.height + currHeightChange
};
if (lastRequiredBrowserSize && requiredBrowserSize.width === lastRequiredBrowserSize.width && requiredBrowserSize.height === lastRequiredBrowserSize.height) {
logger.verbose("Browser size is as required but viewport size does not match!");
logger.verbose("Browser size: " + requiredBrowserSize + " , Viewport size: " + actualViewportSize);
logger.verbose("Stopping viewport size attempts.");
resolve();
return;
}
return EyesSeleniumUtils.setBrowserSize(logger, browser, requiredBrowserSize, promiseFactory).then(function () {
lastRequiredBrowserSize = requiredBrowserSize;
return EyesSeleniumUtils.getViewportSize(browser, promiseFactory);
}).then(function (actualViewportSize) {
logger.verbose("Current viewport size:", actualViewportSize);
if (actualViewportSize.width === requiredSize.width && actualViewportSize.height === requiredSize.height) {
resolve();
return;
}
if ((Math.abs(currWidthChange) <= Math.abs(widthDiff) || Math.abs(currHeightChange) <= Math.abs(heightDiff)) && (--retriesLeft > 0)) {
return _setWindowSize(logger, browser, requiredSize, actualViewportSize, browserSize,
widthDiff, widthStep, heightDiff, heightStep, currWidthChange, currHeightChange,
retriesLeft, lastRequiredBrowserSize, promiseFactory).then(function () {
resolve();
}, function (err) {
reject(err);
});
}
reject("Failed to set window size!");
});
});
}
/**
* @private
* @param {{left: number, top: number, width: number, height: number}} part
* @param {Array<{position: {x: number, y: number}, size: {width: number, height: number}, image: Buffer}>} parts
* @param {{imageBuffer: Buffer, width: number, height: number}} imageObj
* @param {WebDriver} browser
* @param {Promise<void>} promise
* @param {PromiseFactory} promiseFactory
* @param {{width: number, height: number}} viewportSize
* @param {PositionProvider} positionProvider
* @param {ScaleProviderFactory} scaleProviderFactory
* @param {CutProvider} cutProvider
* @param {{width: number, height: number}} entirePageSize
* @param {number} pixelRatio
* @param {number} rotationDegrees
* @param {boolean} automaticRotation
* @param {number} automaticRotationDegrees
* @param {boolean} isLandscape
* @param {int} waitBeforeScreenshots
* @param {{left: number, top: number, width: number, height: number}} regionInScreenshot
* @param {boolean} [saveDebugScreenshots=false]
* @param {string} [debugScreenshotsPath=null]
* @return {Promise<void>}
*/
var _processPart = function (
part,
parts,
imageObj,
browser,
promise,
promiseFactory,
viewportSize,
positionProvider,
scaleProviderFactory,
cutProvider,
entirePageSize,
pixelRatio,
rotationDegrees,
automaticRotation,
automaticRotationDegrees,
isLandscape,
waitBeforeScreenshots,
regionInScreenshot,
saveDebugScreenshots,
debugScreenshotsPath
) {
return promise.then(function () {
return promiseFactory.makePromise(function (resolve) {
// Skip 0,0 as we already got the screenshot
if (part.left === 0 && part.top === 0) {
parts.push({
image: imageObj.imageBuffer,
size: {width: imageObj.width, height: imageObj.height},
position: {x: 0, y: 0}
});
resolve();
return;
}
var partPosition = {x: part.left, y: part.top};
return positionProvider.setPosition(partPosition).then(function () {
return positionProvider.getCurrentPosition();
}).then(function (currentPosition) {
return _captureViewport(browser, promiseFactory, viewportSize, scaleProviderFactory, cutProvider, entirePageSize,
pixelRatio, rotationDegrees, automaticRotation, automaticRotationDegrees, isLandscape,
waitBeforeScreenshots, regionInScreenshot, saveDebugScreenshots, debugScreenshotsPath).then(function (partImage) {
return partImage.asObject();
}).then(function (partObj) {
parts.push({
image: partObj.imageBuffer,
size: {width: partObj.width, height: partObj.height},
position: {x: currentPosition.x, y: currentPosition.y}
});
resolve();
});
});
});
});
};
/**
* @private
* @param {WebDriver} browser
* @param {PromiseFactory} promiseFactory
* @param {{width: number, height: number}} viewportSize
* @param {ScaleProviderFactory} scaleProviderFactory
* @param {CutProvider} cutProvider
* @param {{width: number, height: number}} entirePageSize
* @param {number} pixelRatio
* @param {number} rotationDegrees
* @param {boolean} automaticRotation
* @param {number} automaticRotationDegrees
* @param {boolean} isLandscape
* @param {int} waitBeforeScreenshots
* @param {{left: number, top: number, width: number, height: number}} [regionInScreenshot]
* @param {boolean} [saveDebugScreenshots=false]
* @param {string} [debugScreenshotsPath=null]
* @return {Promise<MutableImage>}
*/
var _captureViewport = function (
browser,
promiseFactory,
viewportSize,
scaleProviderFactory,
cutProvider,
entirePageSize,
pixelRatio,
rotationDegrees,
automaticRotation,
automaticRotationDegrees,
isLandscape,
waitBeforeScreenshots,
regionInScreenshot,
saveDebugScreenshots,
debugScreenshotsPath
) {
var mutableImage, scaleRatio = 1;
return GeneralUtils.sleep(waitBeforeScreenshots, promiseFactory).then(function () {
return browser.takeScreenshot().then(function (screenshot64) {
return new MutableImage(new Buffer(screenshot64, 'base64'), promiseFactory);
}).then(function (image) {
mutableImage = image;
if (saveDebugScreenshots) {
var filename = "screenshot " + (new Date()).getTime()+ " original.png";
return mutableImage.saveImage(debugScreenshotsPath + filename.replace(/ /g, '_'));
}
}).then(function () {
return mutableImage.getSize();
}).then(function (imageSize) {
if (isLandscape && automaticRotation && imageSize.height > imageSize.width) {
rotationDegrees = automaticRotationDegrees;
}
if (scaleProviderFactory) {
var scaleProvider = scaleProviderFactory.getScaleProvider(imageSize.width);
scaleRatio = scaleProvider.getScaleRatio();
}
if (cutProvider) {
cutProvider = cutProvider.scale(1 / scaleRatio);
return cutProvider.cut(mutableImage, promiseFactory).then(function (image) {
mutableImage = image;
});
}
}).then(function () {
if (regionInScreenshot) {
var scaledRegion = GeometryUtils.scaleRegion(regionInScreenshot, 1 / scaleRatio);
return mutableImage.cropImage(scaledRegion);
}
}).then(function () {
if (saveDebugScreenshots) {
var filename = "screenshot " + (new Date()).getTime() + " cropped.png";
return mutableImage.saveImage(debugScreenshotsPath + filename.replace(/ /g, '_'));
}
}).then(function () {
if (scaleRatio !== 1) {
return mutableImage.scaleImage(scaleRatio);
}
}).then(function () {
if (saveDebugScreenshots) {
var filename = "screenshot " + (new Date()).getTime() + " scaled.png";
return mutableImage.saveImage(debugScreenshotsPath + filename.replace(/ /g, '_'));
}
}).then(function () {
if (rotationDegrees !== 0) {
return mutableImage.rotateImage(rotationDegrees);
}
}).then(function () {
return mutableImage.getSize();
}).then(function (imageSize) {
// If the image is a viewport screenshot, we want to save the current scroll position (we'll need it for check region).
if (imageSize.width <= viewportSize.width && imageSize.height <= viewportSize.height) {
return EyesSeleniumUtils.getCurrentScrollPosition(browser, promiseFactory).then(function (scrollPosition) {
return mutableImage.setCoordinates(scrollPosition);
}, function () {
// Failed to get Scroll position, setting coordinates to default.
return mutableImage.setCoordinates({x: 0, y: 0});
});
}
}).then(function () {
return mutableImage;
});
});
};
/**
* Capture screenshot from given driver
*
* @param {WebDriver} browser
* @param {PromiseFactory} promiseFactory
* @param {{width: number, height: number}} viewportSize
* @param {PositionProvider} positionProvider
* @param {ScaleProviderFactory} scaleProviderFactory
* @param {CutProvider} cutProvider
* @param {boolean} fullPage
* @param {boolean} hideScrollbars
* @param {boolean} useCssTransition
* @param {number} rotationDegrees
* @param {boolean} automaticRotation
* @param {number} automaticRotationDegrees
* @param {boolean} isLandscape
* @param {int} waitBeforeScreenshots
* @param {boolean} checkFrameOrElement
* @param {RegionProvider} [regionProvider]
* @param {boolean} [saveDebugScreenshots=false]
* @param {string} [debugScreenshotsPath=null]
* @returns {Promise<MutableImage>}
*/
EyesSeleniumUtils.getScreenshot = function getScreenshot(
browser,
promiseFactory,
viewportSize,
positionProvider,
scaleProviderFactory,
cutProvider,
fullPage,
hideScrollbars,
useCssTransition,
rotationDegrees,
automaticRotation,
automaticRotationDegrees,
isLandscape,
waitBeforeScreenshots,
checkFrameOrElement,
regionProvider,
saveDebugScreenshots,
debugScreenshotsPath
) {
var MIN_SCREENSHOT_PART_HEIGHT = 10,
MAX_SCROLLBAR_SIZE = 50;
var originalPosition,
originalOverflow,
entirePageSize,
regionInScreenshot,
pixelRatio,
imageObject,
screenshot;
hideScrollbars = hideScrollbars === null ? useCssTransition : hideScrollbars;
// step #1 - get entire page size for future use (scaling and stitching)
return positionProvider.getEntireSize().then(function (pageSize) {
entirePageSize = pageSize;
}, function () {
// Couldn't get entire page size, using viewport size as default.
entirePageSize = viewportSize;
}).then(function () {
// step #2 - get the device pixel ratio (scaling)
return EyesSeleniumUtils.getDevicePixelRatio(browser, promiseFactory).then(function (ratio) {
pixelRatio = ratio;
}, function () {
// Couldn't get pixel ratio, using 1 as default.
pixelRatio = 1;
});
}).then(function () {
// step #3 - hide the scrollbars if instructed
if (hideScrollbars) {
return EyesSeleniumUtils.setOverflow(browser, "hidden", promiseFactory).then(function (originalVal) {
originalOverflow = originalVal;
});
}
}).then(function () {
// step #4 - if this is a full page screenshot we need to scroll to position 0,0 before taking the first
if (fullPage) {
return positionProvider.getState().then(function (state) {
originalPosition = state;
return positionProvider.setPosition({x: 0, y: 0});
}).then(function () {
return positionProvider.getCurrentPosition();
}).then(function (location) {
if (location.x !== 0 || location.y !== 0) {
throw new Error("Could not scroll to the x/y corner of the screen");
}
});
}
}).then(function () {
if (regionProvider) {
return _captureViewport(browser, promiseFactory, viewportSize, scaleProviderFactory, cutProvider, entirePageSize, pixelRatio,
rotationDegrees, automaticRotation, automaticRotationDegrees, isLandscape, waitBeforeScreenshots).then(function (image) {
return regionProvider.getRegionInLocation(image, CoordinatesType.SCREENSHOT_AS_IS, promiseFactory);
}).then(function (region) {
regionInScreenshot = region;
});
}
}).then(function () {
// step #5 - Take screenshot of the 0,0 tile / current viewport
return _captureViewport(browser, promiseFactory, viewportSize, scaleProviderFactory, cutProvider, entirePageSize, pixelRatio, rotationDegrees,
automaticRotation, automaticRotationDegrees, isLandscape, waitBeforeScreenshots,
checkFrameOrElement ? regionInScreenshot : null, saveDebugScreenshots, debugScreenshotsPath)
.then(function (image) {
screenshot = image;
return screenshot.asObject();
}).then(function (imageObj) {
imageObject = imageObj;
});
}).then(function () {
return promiseFactory.makePromise(function (resolve) {
if (!fullPage && !checkFrameOrElement) {
resolve();
return;
}
// IMPORTANT This is required! Since when calculating the screenshot parts for full size,
// we use a screenshot size which is a bit smaller (see comment below).
if (imageObject.width >= entirePageSize.width && imageObject.height >= entirePageSize.height) {
resolve();
return;
}
// We use a smaller size than the actual screenshot size in order to eliminate duplication
// of bottom scroll bars, as well as footer-like elements with fixed position.
var screenshotPartSize = {
width: imageObject.width,
height: Math.max(imageObject.height - MAX_SCROLLBAR_SIZE, MIN_SCREENSHOT_PART_HEIGHT)
};
var screenshotParts = GeometryUtils.getSubRegions({
left: 0, top: 0, width: entirePageSize.width,
height: entirePageSize.height
}, screenshotPartSize, false);
var parts = [];
var promise = promiseFactory.makePromise(function (resolve) {
resolve();
});
screenshotParts.forEach(function (part) {
promise = _processPart(part, parts, imageObject, browser, promise, promiseFactory,
viewportSize, positionProvider, scaleProviderFactory, cutProvider, entirePageSize, pixelRatio, rotationDegrees, automaticRotation,
automaticRotationDegrees, isLandscape, waitBeforeScreenshots, checkFrameOrElement ? regionInScreenshot : null, saveDebugScreenshots, debugScreenshotsPath);
});
promise.then(function () {
return ImageUtils.stitchImage(entirePageSize, parts, promiseFactory).then(function (stitchedBuffer) {
screenshot = new MutableImage(stitchedBuffer, promiseFactory);
resolve();
});
});
});
}).then(function () {
if (hideScrollbars) {
return EyesSeleniumUtils.setOverflow(browser, originalOverflow, promiseFactory);
}
}).then(function () {
if (fullPage) {
return positionProvider.restoreState(originalPosition);
}
}).then(function () {
if (!checkFrameOrElement && regionInScreenshot) {
return screenshot.cropImage(regionInScreenshot);
}
}).then(function () {
return screenshot;
});
};
module.exports = EyesSeleniumUtils;
}());