UNPKG

@applitools/eyes.selenium

Version:

Applitools Eyes SDK for Selenium WebDriver

344 lines (303 loc) 12.9 kB
'use strict'; const { RectangleSize, ArgumentGuard, EyesJsBrowserUtils } = require('@applitools/eyes.sdk.core'); const { EyesDriverOperationError } = require('./errors/EyesDriverOperationError'); const { ImageOrientationHandler } = require('./ImageOrientationHandler'); const { JavascriptHandler } = require('./JavascriptHandler'); let imageOrientationHandler = new class ImageOrientationHandlerImpl extends ImageOrientationHandler { /** @override */ isLandscapeOrientation(driver) { // noinspection JSValidateTypes return driver.getCapabilities() .then(capabilities => EyesSeleniumUtils.isLandscapeOrientationFromCaps(capabilities)) .catch(err => { throw new EyesDriverOperationError('Failed to get orientation!', err); }); } /** @override */ tryAutomaticRotation(logger, driver, image) { // noinspection JSValidateTypes return driver.controlFlow().execute(() => 0); } }(); let javascriptHandler = new class JavascriptHandlerImpl extends JavascriptHandler { /** @override */ handle(script, ...args) { throw new Error('You should init javascriptHandler before, using setJavascriptHandler method.'); } }(); /** * @param {Logger} logger * @param {IWebDriver} driver * @param {RectangleSize} requiredSize * @param {number} sleep * @param {number} retriesLeft * @return {Promise<boolean>} */ const setBrowserSizeLoop = (logger, driver, requiredSize, sleep, retriesLeft) => { logger.verbose(`Trying to set browser size to: ${requiredSize}`); return driver.manage().window().setSize(requiredSize.getWidth(), requiredSize.getHeight()) .then(() => driver.sleep(sleep)) .then(() => driver.manage().window().getSize()) .then(/** {width: number, height: number} */ result => { const currentSize = new RectangleSize(result.width, result.height); logger.verbose(`Current browser size: ${currentSize}`); if (currentSize.equals(requiredSize)) { return true; } if (retriesLeft <= 1) { logger.verbose('Failed to set browser size: retries is out.'); return false; } return setBrowserSizeLoop(logger, driver, requiredSize, sleep, retriesLeft - 1); }); }; // noinspection OverlyComplexFunctionJS /** * @param logger * @param driver * @param requiredSize * @param actualVSize * @param browserSize * @param widthDiff * @param widthStep * @param heightDiff * @param heightStep * @param currWidthChange * @param currHeightChange * @param retriesLeft * @param lastRequiredBrowserSize * @return {Promise<boolean>} */ const setViewportSizeLoop = ( logger, driver, requiredSize, actualVSize, browserSize, widthDiff, widthStep, heightDiff, heightStep, currWidthChange, currHeightChange, retriesLeft, lastRequiredBrowserSize ) => { 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) && actualVSize.getWidth() !== requiredSize.getWidth()) { currWidthChange += widthStep; } if (Math.abs(currHeightChange) <= Math.abs(heightDiff) && actualVSize.getHeight() !== requiredSize.getHeight()) { currHeightChange += heightStep; } const requiredBrowserSize = new RectangleSize( browserSize.getWidth() + currWidthChange, browserSize.getHeight() + currHeightChange ); if (requiredBrowserSize.equals(lastRequiredBrowserSize)) { logger.verbose('Browser size is as required but viewport size does not match!'); logger.verbose(`Browser size: ${requiredBrowserSize}, Viewport size: ${actualVSize}`); logger.verbose('Stopping viewport size attempts.'); return driver.controlFlow().promise(resolve => resolve(true)); } return EyesSeleniumUtils.setBrowserSize(logger, driver, requiredBrowserSize) .then(() => { lastRequiredBrowserSize = requiredBrowserSize; return EyesSeleniumUtils.getViewportSize(driver); }) .then(/** RectangleSize */ finalViewportSize => { logger.verbose(`Current viewport size: ${finalViewportSize}`); if (finalViewportSize.equals(requiredSize)) { return true; } if ((Math.abs(currWidthChange) <= Math.abs(widthDiff) || Math.abs(currHeightChange) <= Math.abs(heightDiff)) && (retriesLeft > 1)) { return setViewportSizeLoop( logger, driver, requiredSize, finalViewportSize, browserSize, widthDiff, widthStep, heightDiff, heightStep, currWidthChange, currHeightChange, retriesLeft - 1, lastRequiredBrowserSize ); } throw new Error('EyesError: failed to set window size! Zoom workaround failed.'); }); }; /** * Handles browser related functionality. */ class EyesSeleniumUtils extends EyesJsBrowserUtils { /** * @param {ImageOrientationHandler} value */ static setimageOrientationHandler(value) { imageOrientationHandler = value; } /** * @param {IWebDriver} driver The driver for which to check the orientation. * @return {Promise<boolean>} {@code true} if this is a mobile device and is in landscape orientation. {@code * false} otherwise. */ static isLandscapeOrientation(driver) { return imageOrientationHandler.isLandscapeOrientation(driver); } /** * @param {Capabilities} capabilities The driver's capabilities. * @return {boolean} {@code true} if this is a mobile device and is in landscape orientation. {@code false} otherwise. */ static isLandscapeOrientationFromCaps(capabilities) { const capsOrientation = capabilities.get('orientation') || capabilities.get('deviceOrientation'); return capsOrientation === 'LANDSCAPE'; } /** * @param {Logger} logger * @param {IWebDriver} driver * @param {MutableImage} image * @return {Promise<number>} */ static tryAutomaticRotation(logger, driver, image) { return imageOrientationHandler.tryAutomaticRotation(logger, driver, image); } /** * @param {JavascriptHandler} handler */ static setJavascriptHandler(handler) { javascriptHandler = handler; } /** * @param {string} script * @param {object...} args */ static handleSpecialCommands(script, ...args) { return javascriptHandler.handle(script, ...args); } /** * @param {Logger} logger The logger to use. * @param {IWebDriver} driver The web driver to use. * @return {Promise<RectangleSize>} The viewport size of the current context, or the display size if the viewport * size cannot be retrieved. */ static getViewportSizeOrDisplaySize(logger, driver) { logger.verbose('getViewportSizeOrDisplaySize()'); return EyesSeleniumUtils.getViewportSize(driver).catch(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 driver.manage().window().getSize().then(/** {width:number, height:number} */result => { let { width, height } = result; return EyesSeleniumUtils.isLandscapeOrientation(driver) .then(isLandscape => { if (isLandscape && height > width) { const temp = width; // noinspection JSSuspiciousNameCombination width = height; height = temp; } }) .catch(ignore => { // Not every IWebDriver supports querying for orientation. }) .then(() => { logger.verbose(`Done! Size ${width} x ${height}`); return new RectangleSize(width, height); }); }); }); } /** * @param {Logger} logger The logger to use. * @param {IWebDriver} driver The web driver to use. * @param {RectangleSize} requiredSize The size to set * @return {Promise<boolean>} */ static setBrowserSize(logger, driver, requiredSize) { // noinspection MagicNumberJS const SLEEP = 1000; const RETRIES = 3; return setBrowserSizeLoop(logger, driver, requiredSize, SLEEP, RETRIES); } /** * @param {Logger} logger The logger to use. * @param {IWebDriver} driver The web driver to use. * @param {RectangleSize} actualViewportSize * @param {RectangleSize} requiredViewportSize * @return {Promise<boolean>} */ static setBrowserSizeByViewportSize(logger, driver, actualViewportSize, requiredViewportSize) { return driver.manage().window().getSize().then(/** {width: number, height: number} */browserSize => { const currentSize = new RectangleSize(browserSize); logger.verbose(`Current browser size: ${currentSize}`); const requiredBrowserSize = new RectangleSize( currentSize.getWidth() + (requiredViewportSize.getWidth() - actualViewportSize.getWidth()), currentSize.getHeight() + (requiredViewportSize.getHeight() - actualViewportSize.getHeight()) ); return EyesSeleniumUtils.setBrowserSize(logger, driver, requiredBrowserSize); }); } /** * Tries to set the viewport size * * @param {Logger} logger The logger to use. * @param {IWebDriver} driver The web driver to use. * @param {RectangleSize} requiredSize The viewport size. * @return {Promise<void>} */ static setViewportSize(logger, driver, requiredSize) { ArgumentGuard.notNull(requiredSize, 'requiredSize'); // 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 EyesSeleniumUtils.getViewportSize(driver).then(initViewportSize => { logger.verbose(`Initial viewport size: ${initViewportSize}`); // If the viewport size is already the required size if (initViewportSize.equals(requiredSize)) { logger.verbose('Required size already set.'); return; } // We move the window to (0,0) to have the best chance to be able to set the viewport size as requested. return driver.manage().window().setPosition(0, 0) .catch(ignore => { logger.verbose('Warning: Failed to move the browser window to (0,0)'); }) .then(() => EyesSeleniumUtils.setBrowserSizeByViewportSize(logger, driver, initViewportSize, requiredSize)) .then(() => EyesSeleniumUtils.getViewportSize(driver)) .then(actualViewportSize => { if (actualViewportSize.equals(requiredSize)) { return true; } // 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, driver, actualViewportSize, requiredSize) .then(() => EyesSeleniumUtils.getViewportSize(driver)) .then(/** RectangleSize */finalViewportSize => { logger.verbose(`Current viewport size: ${finalViewportSize}`); if (finalViewportSize.equals(requiredSize)) { return true; } const MAX_DIFF = 3; const widthDiff = finalViewportSize.getWidth() - requiredSize.getWidth(); const widthStep = widthDiff > 0 ? -1 : 1; // -1 for smaller size, 1 for larger const heightDiff = finalViewportSize.getHeight() - requiredSize.getHeight(); const heightStep = heightDiff > 0 ? -1 : 1; return driver.manage().window().getSize().then(/** {width:number, height:number} */result => { const browserSize = new RectangleSize(result.width, result.height); const currWidthChange = 0; const 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...'); const retriesLeft = Math.abs((widthDiff === 0 ? 1 : widthDiff) * (heightDiff === 0 ? 1 : heightDiff)) * 2; const lastRequiredBrowserSize = null; return setViewportSizeLoop( logger, driver, requiredSize, finalViewportSize, browserSize, widthDiff, widthStep, heightDiff, heightStep, currWidthChange, currHeightChange, retriesLeft, lastRequiredBrowserSize ); } throw new Error('EyesError: failed to set window size!'); }); }); }); }); } } exports.EyesSeleniumUtils = EyesSeleniumUtils;