UNPKG

@applitools/eyes.selenium

Version:

Applitools Eyes SDK for Selenium WebDriver

1,445 lines (1,295 loc) 52.4 kB
'use strict'; const { EyesBase, FixedScaleProviderFactory, NullScaleProvider, RegionProvider, NullRegionProvider, ContextBasedScaleProviderFactory, ScaleProviderIdentityFactory, ArgumentGuard, SimplePropertyHandler, Logger, CoordinatesType, TestFailedError, NullCutProvider, UserAgent, ReadOnlyPropertyHandler, Region, Location, RectangleSize, FailureReports, } = require('@applitools/eyes.sdk.core'); const { ImageProviderFactory } = require('./capture/ImageProviderFactory'); const { EyesWebDriverScreenshotFactory } = require('./capture/EyesWebDriverScreenshotFactory'); const { FullPageCaptureAlgorithm } = require('./capture/FullPageCaptureAlgorithm'); const { FrameChain } = require('./frames/FrameChain'); const { EyesWebDriver } = require('./wrappers/EyesWebDriver'); const { EyesSeleniumUtils } = require('./EyesSeleniumUtils'); const { EyesWebElement } = require('./wrappers/EyesWebElement'); const { EyesWebDriverScreenshot } = require('./capture/EyesWebDriverScreenshot'); const { RegionPositionCompensationFactory } = require('./positioning/RegionPositionCompensationFactory'); const { ImageRotation } = require('./positioning/ImageRotation'); const { ScrollPositionProvider } = require('./positioning/ScrollPositionProvider'); const { CssTranslatePositionProvider } = require('./positioning/CssTranslatePositionProvider'); const { ElementPositionProvider } = require('./positioning/ElementPositionProvider'); const { StitchMode } = require('./positioning/StitchMode'); const { MoveToRegionVisibilityStrategy } = require('./regionVisibility/MoveToRegionVisibilityStrategy'); const { NopRegionVisibilityStrategy } = require('./regionVisibility/NopRegionVisibilityStrategy'); const { SeleniumJavaScriptExecutor } = require('./SeleniumJavaScriptExecutor'); const { JavascriptHandler } = require('./JavascriptHandler'); const { Target } = require('./fluent/Target'); const VERSION = require('../package.json').version; const DEFAULT_STITCHING_OVERLAP = 50; // px const DEFAULT_WAIT_BEFORE_SCREENSHOTS = 100; // Milliseconds const DEFAULT_WAIT_SCROLL_STABILIZATION = 200; // Milliseconds /** * @param positionProvider * @param frameChain * @param switchTo * @param promiseFactory * @return {Promise<void>} */ const ensureFrameVisibleLoop = (positionProvider, frameChain, switchTo, promiseFactory) => promiseFactory.resolve() .then(() => { if (frameChain.size() > 0) { return switchTo.parentFrame() .then(() => { const frame = frameChain.pop(); return positionProvider.setPosition(frame.getLocation()); }) .then(() => ensureFrameVisibleLoop(positionProvider, frameChain, switchTo, promiseFactory)); } }); /** * The main API gateway for the SDK. */ class Eyes extends EyesBase { /** * Creates a new (possibly disabled) Eyes instance that interacts with the Eyes Server at the specified url. * * @param {string} [serverUrl=EyesBase.getDefaultServerUrl()] The Eyes server URL. * @param {boolean} [isDisabled=false] Set to true to disable Applitools Eyes and use the webdriver directly. * @param {PromiseFactory} [promiseFactory] If not specified will be created using `Promise` object */ constructor(serverUrl, isDisabled, promiseFactory) { super(serverUrl, isDisabled, promiseFactory); /** @type {EyesWebDriver} */ this._driver = undefined; /** @type {boolean} */ this._dontGetTitle = false; /** @type {boolean} */ this._forceFullPageScreenshot = false; /** @type {boolean} */ this._checkFrameOrElement = false; /** @type {string} */ this._originalDefaultContentOverflow = false; /** @type {string} */ this._originalFrameOverflow = false; /** @type {Region} */ this._regionToCheck = null; /** @type {boolean} */ this._hideScrollbars = false; /** @type {WebElement} */ this._scrollRootElement = undefined; /** @type {ImageRotation} */ this._rotation = undefined; /** @type {number} */ this._devicePixelRatio = Eyes.UNKNOWN_DEVICE_PIXEL_RATIO; /** @type {StitchMode} */ this._stitchMode = StitchMode.SCROLL; /** @type {number} */ this._waitBeforeScreenshots = DEFAULT_WAIT_BEFORE_SCREENSHOTS; /** @type {RegionVisibilityStrategy} */ this._regionVisibilityStrategy = new MoveToRegionVisibilityStrategy(this._logger, this.getPromiseFactory()); /** @type {ElementPositionProvider} */ this._elementPositionProvider = undefined; /** @type {SeleniumJavaScriptExecutor} */ this._jsExecutor = undefined; /** @type {UserAgent} */ this._userAgent = undefined; /** @type {ImageProvider} */ this._imageProvider = undefined; /** @type {RegionPositionCompensation} */ this._regionPositionCompensation = undefined; /** @type {EyesWebElement} */ this._targetElement = null; /** @type {boolean} */ this._stitchContent = false; /** @type {number} */ this._stitchingOverlap = DEFAULT_STITCHING_OVERLAP; this._init(); } /** * @private */ _init() { EyesSeleniumUtils.setJavascriptHandler(new JavascriptHandler(this.getPromiseFactory())); } // noinspection JSMethodCanBeStatic, JSUnusedGlobalSymbols /** * @override */ getBaseAgentId() { return `eyes.selenium/${VERSION}`; } // noinspection JSUnusedGlobalSymbols /** * @return {Region} */ getRegionToCheck() { return this._regionToCheck; } // noinspection JSUnusedGlobalSymbols /** * @param {Region} regionToCheck */ setRegionToCheck(regionToCheck) { this._regionToCheck = regionToCheck; } // noinspection JSUnusedGlobalSymbols /** * @return {boolean} */ shouldStitchContent() { return this._stitchContent; } /** * @return {?EyesWebDriver} */ getDriver() { return this._driver; } // noinspection JSUnusedGlobalSymbols /** * Forces a full page screenshot (by scrolling and stitching) if the browser only supports viewport screenshots). * * @param {boolean} shouldForce Whether to force a full page screenshot or not. */ setForceFullPageScreenshot(shouldForce) { this._forceFullPageScreenshot = shouldForce; } // noinspection JSUnusedGlobalSymbols /** * @return {boolean} Whether Eyes should force a full page screenshot. */ getForceFullPageScreenshot() { return this._forceFullPageScreenshot; } // noinspection JSUnusedGlobalSymbols /** * Sets the time to wait just before taking a screenshot (e.g., to allow positioning to stabilize when performing a * full page stitching). * * @param {number} waitBeforeScreenshots The time to wait (Milliseconds). Values smaller or equal to 0, will cause the * default value to be used. */ setWaitBeforeScreenshots(waitBeforeScreenshots) { if (waitBeforeScreenshots <= 0) { this._waitBeforeScreenshots = DEFAULT_WAIT_BEFORE_SCREENSHOTS; } else { this._waitBeforeScreenshots = waitBeforeScreenshots; } } // noinspection JSUnusedGlobalSymbols /** * @return {number} The time to wait just before taking a screenshot. */ getWaitBeforeScreenshots() { return this._waitBeforeScreenshots; } // noinspection JSUnusedGlobalSymbols /** * Turns on/off the automatic scrolling to a region being checked by {@code checkRegion}. * * @param {boolean} shouldScroll Whether to automatically scroll to a region being validated. */ setScrollToRegion(shouldScroll) { if (shouldScroll) { this._regionVisibilityStrategy = new MoveToRegionVisibilityStrategy(this._logger, this.getPromiseFactory()); } else { this._regionVisibilityStrategy = new NopRegionVisibilityStrategy(this._logger, this.getPromiseFactory()); } } // noinspection JSUnusedGlobalSymbols /** * @return {boolean} Whether to automatically scroll to a region being validated. */ getScrollToRegion() { return !(this._regionVisibilityStrategy instanceof NopRegionVisibilityStrategy); } // noinspection JSUnusedGlobalSymbols /** * Set the type of stitching used for full page screenshots. When the page includes fixed position header/sidebar, * use {@link StitchMode#CSS}. Default is {@link StitchMode#SCROLL}. * * @param {StitchMode} mode The stitch mode to set. */ setStitchMode(mode) { this._logger.verbose(`setting stitch mode to ${mode}`); this._stitchMode = mode; if (this._driver) { this._initPositionProvider(); } } // noinspection JSUnusedGlobalSymbols /** * @return {StitchMode} The current stitch mode settings. */ getStitchMode() { return this._stitchMode; } // noinspection JSUnusedGlobalSymbols /** * Sets the stitching overlap in pixels. * * @param {number} pixels The width (in pixels) of the overlap. */ setStitchOverlap(pixels) { this._stitchingOverlap = pixels; } // noinspection JSUnusedGlobalSymbols /** * @return {number} Returns the stitching overlap in pixels. */ getStitchOverlap() { return this._stitchingOverlap; } // noinspection JSUnusedGlobalSymbols /** * Hide the scrollbars when taking screenshots. * * @param {boolean} shouldHide Whether to hide the scrollbars or not. */ setHideScrollbars(shouldHide) { this._hideScrollbars = shouldHide; } // noinspection JSUnusedGlobalSymbols /** * @return {boolean} Whether or not scrollbars are hidden when taking screenshots. */ getHideScrollbars() { return this._hideScrollbars; } // noinspection JSUnusedGlobalSymbols /** * @param {WebElement|By} element */ setScrollRootElement(element) { this._scrollRootElement = this._driver.findElement(element); } /** * @return {WebElement} */ getScrollRootElement() { return this._scrollRootElement; } // noinspection JSUnusedGlobalSymbols /** * @param {ImageRotation} rotation The image rotation data. */ setRotation(rotation) { this._rotation = rotation; if (this._driver) { this._driver.setRotation(rotation); } } // noinspection JSUnusedGlobalSymbols /** * @return {ImageRotation} The image rotation data. */ getRotation() { return this._rotation; } // noinspection JSUnusedGlobalSymbols /** * @return {number} The device pixel ratio, or {@link #UNKNOWN_DEVICE_PIXEL_RATIO} if the DPR is not known yet or if * it wasn't possible to extract it. */ getDevicePixelRatio() { return this._devicePixelRatio; } // noinspection JSUnusedGlobalSymbols /** * Starts a test. * * @param {WebDriver} driver The web driver that controls the browser hosting the application under test. * @param {string} appName The name of the application under test. * @param {string} testName The test name. * @param {RectangleSize|{width: number, height: number}} [viewportSize=null] The required browser's viewport size * (i.e., the visible part of the document's body) or to use the current window's viewport. * @param {SessionType} [sessionType=null] The type of test (e.g., standard test / visual performance test). * @return {Promise<EyesWebDriver>} A wrapped WebDriver which enables Eyes trigger recording and frame handling. */ open(driver, appName, testName, viewportSize = null, sessionType = null) { ArgumentGuard.notNull(driver, 'driver'); const that = this; that._flow = driver.controlFlow(); // Set PromiseFactory to work with the protractor control flow and promises const promiseFn = that.getPromiseFactory().getFactoryMethod(); that.getPromiseFactory().setFactoryMethod(asyncAction => that._flow.execute(() => promiseFn(asyncAction))); if (this.getIsDisabled()) { this._logger.verbose('Ignored'); return this.getPromiseFactory().resolve(driver); } that._initDriver(driver); return that._driver.getUserAgent() .then(uaString => { if (uaString) { that._userAgent = UserAgent.parseUserAgentString(uaString, true); } that._imageProvider = ImageProviderFactory.getImageProvider(that._userAgent, that, that._logger, that._driver); that._regionPositionCompensation = RegionPositionCompensationFactory.getRegionPositionCompensation(that._userAgent, that, that._logger); return super.openBase(appName, testName, viewportSize, sessionType); }) .then(() => { that._devicePixelRatio = Eyes.UNKNOWN_DEVICE_PIXEL_RATIO; that._jsExecutor = new SeleniumJavaScriptExecutor(that._driver); that._initPositionProvider(); that._driver.setRotation(this._rotation); return that._driver; }); } /** @private */ _initDriver(driver) { if (driver instanceof EyesWebDriver) { // noinspection JSValidateTypes this._driver = driver; } else { this._driver = new EyesWebDriver(this._logger, this, driver); } } /** @private */ _initPositionProvider() { // Setting the correct position provider. const stitchMode = this.getStitchMode(); this._logger.verbose(`initializing position provider. stitchMode: ${stitchMode}`); switch (stitchMode) { case StitchMode.CSS: this._positionProviderHandler.set(new CssTranslatePositionProvider(this._logger, this._jsExecutor)); break; default: this._positionProviderHandler.set(new ScrollPositionProvider(this._logger, this._jsExecutor)); } } // noinspection JSUnusedGlobalSymbols /** * Preform visual validation * * @param {string} name A name to be associated with the match * @param {SeleniumCheckSettings} checkSettings Target instance which describes whether we want a window/region/frame * @return {Promise<MatchResult>} A promise which is resolved when the validation is finished. */ check(name, checkSettings) { ArgumentGuard.notNull(checkSettings, 'checkSettings'); let matchResult; const that = this; return that.getPromiseFactory().resolve() .then(() => { that._logger.verbose(`check("${name}", checkSettings) - begin`); that._stitchContent = checkSettings.getStitchContent(); const targetRegion = checkSettings.getTargetRegion(); let switchedToFrameCount; return this._switchToFrame(checkSettings) .then(switchedToFrameCount_ => { that._regionToCheck = null; switchedToFrameCount = switchedToFrameCount_; if (targetRegion) { return super.checkWindowBase(new RegionProvider(targetRegion, that.getPromiseFactory()), name, false, checkSettings); } if (checkSettings) { const targetSelector = checkSettings.getTargetSelector(); let targetElement = checkSettings.getTargetElement(); if (!targetElement && targetSelector) { targetElement = that._driver.findElement(targetSelector); } if (targetElement) { that._targetElement = targetElement instanceof EyesWebElement ? targetElement : new EyesWebElement(that._logger, that._driver, targetElement); if (that._stitchContent) { return that._checkElement(name, checkSettings); } return that._checkRegion(name, checkSettings); } if (checkSettings.getFrameChain().length > 0) { if (that._stitchContent) { return that._checkFullFrameOrElement(name, checkSettings); } return that._checkFrameFluent(name, checkSettings); } return super.checkWindowBase(new NullRegionProvider(that.getPromiseFactory()), name, false, checkSettings); } }) .then(newMatchResult => { matchResult = newMatchResult; that._targetElement = null; return that._switchToParentFrame(switchedToFrameCount); }) .then(() => { that._stitchContent = false; that._logger.verbose('check - done!'); return matchResult; }); }); } /** * @private * @return {Promise<void>} */ _checkFrameFluent(name, checkSettings) { const frameChain = new FrameChain(this._logger, this._driver.getFrameChain()); const targetFrame = frameChain.pop(); this._targetElement = targetFrame.getReference(); const that = this; return this._driver.switchTo() .framesDoScroll(frameChain) .then(() => this._checkRegion(name, checkSettings)) .then(() => { that._targetElement = null; }); } /** * @private * @return {Promise<number>} */ _switchToParentFrame(switchedToFrameCount) { if (switchedToFrameCount > 0) { const that = this; return that._driver.switchTo() .parentFrame() .then(() => that._switchToParentFrame(switchedToFrameCount - 1)); } return this.getPromiseFactory().resolve(); } /** * @private * @return {Promise<number>} */ _switchToFrame(checkSettings) { if (!checkSettings) { return this.getPromiseFactory().resolve(0); } const that = this; const frameChain = checkSettings.getFrameChain(); let switchedToFrameCount = 0; return frameChain.reduce((promise, frameLocator) => promise.then(() => that._switchToFrameLocator(frameLocator)) .then(isSuccess => { if (isSuccess) { switchedToFrameCount += 1; } return switchedToFrameCount; }), this.getPromiseFactory().resolve()); } /** * @private * @return {Promise<boolean>} */ _switchToFrameLocator(frameLocator) { const switchTo = this._driver.switchTo(); if (frameLocator.getFrameIndex()) { return switchTo.frame(frameLocator.getFrameIndex()).then(() => true); } if (frameLocator.getFrameNameOrId()) { return switchTo.frame(frameLocator.getFrameNameOrId()).then(() => true); } if (frameLocator.getFrameElement()) { const frameElement = frameLocator.getFrameElement(); if (frameElement) { return switchTo.frame(frameElement).then(() => true); } } if (frameLocator.getFrameSelector()) { const frameElement = this._driver.findElement(frameLocator.getFrameSelector()); if (frameElement) { return switchTo.frame(frameElement).then(() => true); } } return this.getPromiseFactory().resolve(false); } /** * @private * @return {Promise<void>} */ _checkFullFrameOrElement(name, checkSettings) { this._checkFrameOrElement = true; const that = this; this._logger.verbose('checkFullFrameOrElement()'); const RegionProviderImpl = class RegionProviderImpl extends RegionProvider { // noinspection JSUnusedGlobalSymbols /** @override */ getRegion() { if (that._checkFrameOrElement) { // FIXME - Scaling should be handled in a single place instead return that._ensureFrameVisible().then(fc => that._updateScalingParams().then(scaleProviderFactory => { let screenshotImage; return that._imageProvider.getImage() .then(screenshotImage_ => { screenshotImage = screenshotImage_; return that._debugScreenshotsProvider.save(screenshotImage_, 'checkFullFrameOrElement'); }) .then(() => { const scaleProvider = scaleProviderFactory.getScaleProvider(screenshotImage.getWidth()); // TODO: do we need to scale the image? We don't do it in Java return screenshotImage.scale(scaleProvider.getScaleRatio()); }) .then(screenshotImage_ => { screenshotImage = screenshotImage_; const switchTo = that._driver.switchTo(); return switchTo.frames(fc); }) .then(() => { const screenshot = new EyesWebDriverScreenshot( that._logger, that._driver, screenshotImage, that.getPromiseFactory() ); return screenshot.init(); }) .then(/** EyesWebDriverScreenshot */ screenshot => { that._logger.verbose('replacing regionToCheck'); that.setRegionToCheck(screenshot.getFrameWindow()); return that.getPromiseFactory().resolve(Region.EMPTY); }); })); } return that.getPromiseFactory().resolve(Region.EMPTY); } }; return super.checkWindowBase(new RegionProviderImpl(), name, false, checkSettings).then(() => { that._checkFrameOrElement = false; }); } /** * @private * @return {Promise<FrameChain>} */ _ensureFrameVisible() { const that = this; const originalFC = new FrameChain(this._logger, this._driver.getFrameChain()); const fc = new FrameChain(this._logger, this._driver.getFrameChain()); // noinspection JSValidateTypes return ensureFrameVisibleLoop(this._positionProviderHandler.get(), fc, this._driver.switchTo(), this.getPromiseFactory()) .then(() => that._driver.switchTo().frames(originalFC)) .then(() => originalFC); } /** * @private * @param {WebElement} element * @return {Promise<void>} */ _ensureElementVisible(element) { if (!element) { // No element? we must be checking the window. return this.getPromiseFactory().resolve(); } const originalFC = new FrameChain(this._logger, this._driver.getFrameChain()); const switchTo = this._driver.switchTo(); const that = this; let elementBounds; const eyesRemoteWebElement = new EyesWebElement(this._logger, this._driver, element); return eyesRemoteWebElement.getBounds() .then(bounds => { const currentFrameOffset = originalFC.getCurrentFrameOffset(); elementBounds = bounds.offset(currentFrameOffset.getX(), currentFrameOffset.getY()); return that._getViewportScrollBounds(); }) .then(viewportBounds => { if (!viewportBounds.contains(elementBounds)) { let elementLocation; return that._ensureFrameVisible() .then(() => element.getLocation()) .then(p => { elementLocation = new Location(p.x, p.y); if (originalFC.size() > 0 && !EyesWebElement.equals(element, originalFC.peek().getReference())) { return switchTo.frames(originalFC); } }) .then(() => that._positionProviderHandler.get().setPosition(elementLocation)); } }); } /** * @private * @return {Promise<Region>} */ _getViewportScrollBounds() { const that = this; const originalFrameChain = new FrameChain(this._logger, this._driver.getFrameChain()); const switchTo = this._driver.switchTo(); return switchTo.defaultContent().then(() => { const spp = new ScrollPositionProvider(that._logger, that._jsExecutor); return spp.getCurrentPosition() .then(location => that.getViewportSize() .then(size => { const viewportBounds = new Region(location, size); return switchTo.frames(originalFrameChain) .then(() => viewportBounds); })); }); } /** * @private * @return {Promise<void>} */ _checkRegion(name, checkSettings) { const that = this; const RegionProviderImpl = class RegionProviderImpl extends RegionProvider { // noinspection JSUnusedGlobalSymbols /** @override */ getRegion() { return that._targetElement.getLocation() .then(point => that._targetElement.getSize() .then(dimension => new Region( Math.ceil(point.x), Math.ceil(point.y), dimension.width, dimension.height, CoordinatesType.CONTEXT_RELATIVE ))); } }; return super.checkWindowBase(new RegionProviderImpl(), name, false, checkSettings) .then(() => { that._logger.verbose('Done! trying to scroll back to original position..'); }); } /** * @private * @return {Promise<void>} */ _checkElement(name, checkSettings) { const eyesElement = this._targetElement; const scrollPositionProvider = new ScrollPositionProvider(this._logger, this._jsExecutor); const that = this; let originalScrollPosition, originalOverflow, error, originalPositionMemento; return this._positionProviderHandler.get().getState() .then(positionMemento => { originalPositionMemento = positionMemento; return scrollPositionProvider.getCurrentPosition(); }) .then(newScrollPosition => { originalScrollPosition = newScrollPosition; return eyesElement.getLocation(); }) .then(point1 => { that._checkFrameOrElement = true; let elementLocation, elementSize; return eyesElement.getComputedStyle('display') .then(displayStyle => { if (displayStyle !== 'inline') { that._elementPositionProvider = new ElementPositionProvider(that._logger, that._driver, eyesElement); } if (that._hideScrollbars) { return eyesElement.getOverflow().then(newOverflow => { originalOverflow = newOverflow; return eyesElement.setOverflow('hidden'); }); } }) .then(() => eyesElement.getClientWidth() .then(elementWidth => eyesElement.getClientHeight() .then(elementHeight => { elementSize = new RectangleSize(elementWidth, elementHeight); }))) .then(() => eyesElement.getComputedStyleInteger('border-left-width') .then(borderLeftWidth => eyesElement.getComputedStyleInteger('border-top-width') .then(borderTopWidth => { elementLocation = new Location(point1.x + borderLeftWidth, point1.y + borderTopWidth); }))) .then(() => { const elementRegion = new Region(elementLocation, elementSize, CoordinatesType.CONTEXT_RELATIVE); that._logger.verbose(`Element region: ${elementRegion}`); that._logger.verbose('replacing regionToCheck'); that._regionToCheck = elementRegion; return super.checkWindowBase(new NullRegionProvider(this.getPromiseFactory()), name, false, checkSettings); }); }) .catch(error_ => { error = error_; }) .then(() => { if (originalOverflow) { return eyesElement.setOverflow(originalOverflow); } }) .then(() => this._positionProviderHandler.get().restoreState(originalPositionMemento)) .then(() => { that._checkFrameOrElement = false; that._regionToCheck = null; that._elementPositionProvider = null; return scrollPositionProvider.setPosition(originalScrollPosition); }) .then(() => { if (error) { throw error; } }); } /** * Updates the state of scaling related parameters. * * @protected * @return {Promise<ScaleProviderFactory>} */ _updateScalingParams() { // Update the scaling params only if we haven't done so yet, and the user hasn't set anything else manually. if ( this._devicePixelRatio === Eyes.UNKNOWN_DEVICE_PIXEL_RATIO && this._scaleProviderHandler.get() instanceof NullScaleProvider ) { this._logger.verbose('Trying to extract device pixel ratio...'); const that = this; return EyesSeleniumUtils.getDevicePixelRatio(that._jsExecutor) .then(ratio => { that._devicePixelRatio = ratio; }) .catch(err => { that._logger.verbose('Failed to extract device pixel ratio! Using default.', err); that._devicePixelRatio = Eyes.DEFAULT_DEVICE_PIXEL_RATIO; }) .then(() => { that._logger.verbose(`Device pixel ratio: ${that._devicePixelRatio}`); that._logger.verbose('Setting scale provider...'); return that._getScaleProviderFactory(); }) .catch(err => { that._logger.verbose('Failed to set ContextBasedScaleProvider.', err); that._logger.verbose('Using FixedScaleProvider instead...'); return new FixedScaleProviderFactory(1 / that._devicePixelRatio, that._scaleProviderHandler); }) .then(factory => { that._logger.verbose('Done!'); return factory; }); } // If we already have a scale provider set, we'll just use it, and pass a mock as provider handler. const nullProvider = new SimplePropertyHandler(); return this.getPromiseFactory() .resolve(new ScaleProviderIdentityFactory(this._scaleProviderHandler.get(), nullProvider)); } /** * @private * @return {Promise<ScaleProviderFactory>} */ _getScaleProviderFactory() { const that = this; return this._positionProviderHandler.get().getEntireSize() .then(entireSize => new ContextBasedScaleProviderFactory( that._logger, entireSize, that._viewportSizeHandler.get(), that._devicePixelRatio, false, that._scaleProviderHandler )); } // noinspection JSUnusedGlobalSymbols /** * Takes a snapshot of the application under test and matches it with the expected output. * * @param {string} tag An optional tag to be associated with the snapshot. * @param {number} matchTimeout The amount of time to retry matching (Milliseconds). * @return {Promise<MatchResult>} A promise which is resolved when the validation is finished. */ checkWindow(tag, matchTimeout) { return this.check(tag, Target.window().timeout(matchTimeout)); } // noinspection JSUnusedGlobalSymbols /** * Matches the frame given as parameter, by switching into the frame and using stitching to get an image of the * frame. * * @param {EyesWebElement} element The element which is the frame to switch to. (as would be used in a call to * driver.switchTo().frame() ). * @param {number} matchTimeout The amount of time to retry matching (milliseconds). * @param {string} tag An optional tag to be associated with the match. * @return {Promise<MatchResult>} A promise which is resolved when the validation is finished. */ checkFrame(element, matchTimeout, tag) { return this.check(tag, Target.frame(element).timeout(matchTimeout)); } // noinspection JSUnusedGlobalSymbols /** * Takes a snapshot of the application under test and matches a specific element with the expected region output. * * @param {WebElement|EyesWebElement} element The element to check. * @param {?number} matchTimeout The amount of time to retry matching (milliseconds). * @param {string} tag An optional tag to be associated with the match. * @return {Promise<MatchResult>} A promise which is resolved when the validation is finished. */ checkElement(element, matchTimeout, tag) { return this.check(tag, Target.region(element).timeout(matchTimeout).fully()); } // noinspection JSUnusedGlobalSymbols /** * Takes a snapshot of the application under test and matches a specific element with the expected region output. * * @param {By} locator The element to check. * @param {?number} matchTimeout The amount of time to retry matching (milliseconds). * @param {string} tag An optional tag to be associated with the match. * @return {Promise<MatchResult>} A promise which is resolved when the validation is finished. */ checkElementBy(locator, matchTimeout, tag) { return this.check(tag, Target.region(locator).timeout(matchTimeout).fully()); } // noinspection JSUnusedGlobalSymbols /** * Visually validates a region in the screenshot. * * @param {Region} region The region to validate (in screenshot coordinates). * @param {string} tag An optional tag to be associated with the screenshot. * @param {number} matchTimeout The amount of time to retry matching. * @return {Promise<MatchResult>} A promise which is resolved when the validation is finished. */ checkRegion(region, tag, matchTimeout) { return this.check(tag, Target.region(region).timeout(matchTimeout)); } // noinspection JSUnusedGlobalSymbols /** * Visually validates a region in the screenshot. * * @param {WebElement|EyesWebElement} element The element defining the region to validate. * @param {string} tag An optional tag to be associated with the screenshot. * @param {number} matchTimeout The amount of time to retry matching. * @return {Promise<MatchResult>} A promise which is resolved when the validation is finished. */ checkRegionByElement(element, tag, matchTimeout) { return this.check(tag, Target.region(element).timeout(matchTimeout)); } // noinspection JSUnusedGlobalSymbols /** * Visually validates a region in the screenshot. * * @param {By} by The WebDriver selector used for finding the region to validate. * @param {string} tag An optional tag to be associated with the screenshot. * @param {number} matchTimeout The amount of time to retry matching. * @return {Promise<MatchResult>} A promise which is resolved when the validation is finished. */ checkRegionBy(by, tag, matchTimeout) { return this.check(tag, Target.region(by).timeout(matchTimeout)); } // noinspection JSUnusedGlobalSymbols /** * Switches into the given frame, takes a snapshot of the application under test and matches a region specified by * the given selector. * * @param {string} frameNameOrId The name or id of the frame to switch to. (as would be used in a call to * driver.switchTo().frame()). * @param {By} locator A Selector specifying the region to check. * @param {?number} matchTimeout The amount of time to retry matching. (Milliseconds) * @param {string} tag An optional tag to be associated with the snapshot. * @param {boolean} stitchContent If {@code true}, stitch the internal content of the region (i.e., perform * {@link #checkElement(By, number, string)} on the region. * @return {Promise<MatchResult>} A promise which is resolved when the validation is finished. */ checkRegionInFrame(frameNameOrId, locator, matchTimeout, tag, stitchContent) { return this.check(tag, Target.region(locator, frameNameOrId).timeout(matchTimeout).stitchContent(stitchContent)); } /** * Adds a mouse trigger. * * @param {MouseTrigger.MouseAction} action Mouse action. * @param {Region} control The control on which the trigger is activated (context relative coordinates). * @param {Location} cursor The cursor's position relative to the control. */ addMouseTrigger(action, control, cursor) { if (this.getIsDisabled()) { this._logger.verbose(`Ignoring ${action} (disabled)`); return; } // Triggers are actually performed on the previous window. if (!this._lastScreenshot) { this._logger.verbose(`Ignoring ${action} (no screenshot)`); return; } if (!FrameChain.isSameFrameChain(this._driver.getFrameChain(), this._lastScreenshot.getFrameChain())) { this._logger.verbose(`Ignoring ${action} (different frame)`); return; } super.addMouseTriggerBase(action, control, cursor); } // noinspection JSUnusedGlobalSymbols /** * Adds a mouse trigger. * * @param {MouseTrigger.MouseAction} action Mouse action. * @param {WebElement} element The WebElement on which the click was called. * @return {Promise<void>} */ addMouseTriggerForElement(action, element) { if (this.getIsDisabled()) { this._logger.verbose(`Ignoring ${action} (disabled)`); return this.getPromiseFactory().resolve(); } // Triggers are actually performed on the previous window. if (!this._lastScreenshot) { this._logger.verbose(`Ignoring ${action} (no screenshot)`); return this.getPromiseFactory().resolve(); } if (!FrameChain.isSameFrameChain(this._driver.getFrameChain(), this._lastScreenshot.getFrameChain())) { this._logger.verbose(`Ignoring ${action} (different frame)`); return this.getPromiseFactory().resolve(); } ArgumentGuard.notNull(element, 'element'); let p1; return element.getLocation() .then(loc => { p1 = loc; return element.getSize(); }) .then(ds => { const elementRegion = new Region(p1.x, p1.y, ds.width, ds.height); super.addMouseTriggerBase(action, elementRegion, elementRegion.getMiddleOffset()); }); } /** * Adds a keyboard trigger. * * @param {Region} control The control on which the trigger is activated (context relative coordinates). * @param {string} text The trigger's text. */ addTextTrigger(control, text) { if (this.getIsDisabled()) { this._logger.verbose(`Ignoring ${text} (disabled)`); return; } // Triggers are actually performed on the previous window. if (!this._lastScreenshot) { this._logger.verbose(`Ignoring ${text} (no screenshot)`); return; } if (!FrameChain.isSameFrameChain(this._driver.getFrameChain(), this._lastScreenshot.getFrameChain())) { this._logger.verbose(`Ignoring ${text} (different frame)`); return; } super.addTextTriggerBase(control, text); } /** * Adds a keyboard trigger. * * @param {EyesWebElement} element The element for which we sent keys. * @param {string} text The trigger's text. * @return {Promise<void>} */ addTextTriggerForElement(element, text) { if (this.getIsDisabled()) { this._logger.verbose(`Ignoring ${text} (disabled)`); return this.getPromiseFactory().resolve(); } // Triggers are actually performed on the previous window. if (!this._lastScreenshot) { this._logger.verbose(`Ignoring ${text} (no screenshot)`); return this.getPromiseFactory().resolve(); } if (!FrameChain.isSameFrameChain(this._driver.getFrameChain(), this._lastScreenshot.getFrameChain())) { this._logger.verbose(`Ignoring ${text} (different frame)`); return this.getPromiseFactory().resolve(); } ArgumentGuard.notNull(element, 'element'); return element.getLocation() .then(p1 => element.getSize() .then(ds => { const elementRegion = new Region(Math.ceil(p1.x), Math.ceil(p1.y), ds.width, ds.height); super.addTextTrigger(elementRegion, text); })); } /** * Use this method only if you made a previous call to {@link #open(WebDriver, string, string)} or one of its * variants. * * @override * @inheritDoc */ getViewportSize() { const viewportSize = this._viewportSizeHandler.get(); if (viewportSize) { return this.getPromiseFactory().resolve(viewportSize); } return this._driver.getDefaultContentViewportSize(); } // noinspection JSUnusedGlobalSymbols /** * Use this method only if you made a previous call to {@link #open(WebDriver, string, string)} or one of its * variants. * * @protected * @override */ setViewportSize(viewportSize) { if (this._viewportSizeHandler instanceof ReadOnlyPropertyHandler) { this._logger.verbose('Ignored (viewport size given explicitly)'); return this.getPromiseFactory().resolve(); } ArgumentGuard.notNull(viewportSize, 'viewportSize'); const that = this; const originalFrame = this._driver.getFrameChain(); return this._driver.switchTo() .defaultContent() .then(() => EyesSeleniumUtils.setViewportSize(that._logger, that._driver, new RectangleSize(viewportSize)) .catch(err => that._driver.switchTo() // Just in case the user catches that error .frames(originalFrame) .then(() => { throw new TestFailedError('Failed to set the viewport size', err); }))) .then(() => that._driver.switchTo().frames(originalFrame)) .then(() => { that._viewportSizeHandler.set(new RectangleSize(viewportSize)); }); } // noinspection JSUnusedGlobalSymbols /** * Call this method if for some reason you don't want to call {@link #open(WebDriver, string, string)} (or one of its * variants) yet. * * @param {EyesWebDriver} driver The driver to use for getting the viewport. * @return {Promise<RectangleSize>} The viewport size of the current context. */ static getViewportSize(driver) { ArgumentGuard.notNull(driver, 'driver'); return EyesSeleniumUtils.getViewportSizeOrDisplaySize(new Logger(), driver); } // noinspection JSUnusedGlobalSymbols /** * Set the viewport size using the driver. Call this method if for some reason you don't want to call * {@link #open(WebDriver, string, string)} (or one of its variants) yet. * * @param {EyesWebDriver} driver The driver to use for setting the viewport. * @param {RectangleSize} viewportSize The required viewport size. * @return {Promise<void>} */ static setViewportSize(driver, viewportSize) { ArgumentGuard.notNull(driver, 'driver'); ArgumentGuard.notNull(viewportSize, 'viewportSize'); return EyesSeleniumUtils.setViewportSize(new Logger(), driver, new RectangleSize(viewportSize)); } /** @override */ beforeOpen() { return this._tryHideScrollbars(); } /** @override */ beforeMatchWindow() { return this._tryHideScrollbars(); } /** * @private * @return {Promise<void>} */ _tryHideScrollbars() { if (this._hideScrollbars) { const that = this; const originalFC = new FrameChain(that._logger, that._driver.getFrameChain()); const fc = new FrameChain(that._logger, that._driver.getFrameChain()); return EyesSeleniumUtils.hideScrollbars(that._driver, 200, that._scrollRootElement) .then(overflow => { that._originalOverflow = overflow; if (!that._scrollRootElement) { return that._tryHideScrollbarsLoop(fc).then(() => that._driver.switchTo().frames(originalFC)); } }) .catch(err => { that._logger.log(`WARNING: Failed to hide scrollbars! Error: ${err}`); }); } return this.getPromiseFactory().resolve(); } /** * @private * @param {FrameChain} fc * @return {Promise<void>} */ _tryHideScrollbarsLoop(fc) { if (fc.size() > 0) { const that = this; return that._driver.getRemoteWebDriver() .switchTo() .parentFrame() .then(() => { const frame = fc.pop(); return EyesSeleniumUtils.hideScrollbars(that._driver, 200); }) .then(() => that._tryHideScrollbarsLoop(fc)); } return this.getPromiseFactory().resolve(); } /** * @private * @return {Promise<void>} */ _tryRestoreScrollbars() { if (this._hideScrollbars) { const that = this; const originalFC = new FrameChain(that._logger, that._driver.getFrameChain()); const fc = new FrameChain(that._logger, that._driver.getFrameChain()); return that._tryRestoreScrollbarsLoop(fc) .then(() => that._driver.switchTo().frames(originalFC)); } } /** * @private * @param {FrameChain} fc * @return {Promise<void>} */ _tryRestoreScrollbarsLoop(fc) { if (fc.size() > 0) { const that = this; return that._driver.getRemoteWebDriver() .switchTo() .parentFrame() .then(() => { const frame = fc.pop(); return frame.getReference().setOverflow(frame.getOriginalOverflow()); }) .then(() => that._tryRestoreScrollbarsLoop(fc)); } return this.getPromiseFactory().resolve(); } /* /** * @protected * @return {Promise<void>} * / _afterMatchWindow() { if (this.hideScrollbars) { try { EyesSeleniumUtils.setOverflow(this.driver, this.originalOverflow); } catch (EyesDriverOperationException e) { // Bummer, but we'll continue with the screenshot anyway :) logger.log("WARNING: Failed to revert overflow! Error: " + e.getMessage()); } } } */ // noinspection JSUnusedGlobalSymbols /** * @protected * @override */ getScreenshot() { const that = this; that._logger.verbose('getScreenshot()'); let result, scaleProviderFactory, originalBodyOverflow, error; return that._updateScalingParams() .then(scaleProviderFactory_ => { scaleProviderFactory = scaleProviderFactory_; const screenshotFactory = new EyesWebDriverScreenshotFactory(that._logger, that._driver, that.getPromiseFactory()); const originalFrameChain = new FrameChain(that._logger, that._driver.getFrameChain()); const algo = new FullPageCaptureAlgorithm(that._logger, that._userAgent, that._jsExecutor, that.getPromiseFactory()); const switchTo = that._driver.switchTo(); if (that._checkFrameOrElement) { that._logger.verbose('Check frame/element requested'); return switchTo.framesDoScroll(originalFrameChain) .then(() => algo.getStitchedRegion( that._imageProvider, that._regionToCheck, that._positionProviderHandler.get(), that.getElementPositionProvider(), scaleProviderFactory, that._cutProviderHandler.get(), that.getWaitBeforeScreenshots(), that._debugScreenshotsProvider, screenshotFactory, that.getStitchOverlap(), that._regionPositionCompensation )) .then(entireFrameOrElement => { that._logger.verbose('Building screenshot object...'); const screenshot = new EyesWebDriverScreenshot( that._logger, that._driver, entireFrameOrElement, that.getPromiseFactory() ); return screenshot.initFromFrameSize(new RectangleSize(entireFrameOrElement.getWidth(), entireFrameOrElement.getHeight())); }) .then(screenshot => { result = screenshot; }); } if (that._forceFullPageScreenshot || that._stitchContent) { that._logger.verbose('Full page screenshot requested.'); // Save the current frame path. const originalFramePosition = originalFrameChain.size() > 0 ? originalFrameChain.getDefaultContentScrollPosition() : new Location(0, 0); return switchTo.defaultContent() .then(() => algo.getStitchedRegion( that._imageProvider, Region.EMPTY, new ScrollPositionProvider(that._logger, this._jsExecutor), that._positionProviderHandler.get(), scaleProviderFactory, that._cutProviderHandler.get(), that.getWaitBeforeScreenshots(), that._debugScreenshotsProvider, screenshotFactory, that.getStitchOverlap(), that._regionPositionCompensation ) .then(fullPageImage => switchTo.frames(originalFrameChain) .then(() => { const screenshot = new EyesWebDriverScreenshot( that._logger, that._driver, fullPageImage, that.getPromiseFactory() ); return screenshot.init(null, originalFramePosition); }) .then(screenshot => { result = screenshot; }))); } let screenshotImage; return that._ensureElementVisible(that._targetElement) .then(() => { that._logger.verbose('Screenshot requested...'); return that._imageProvider.getImage(); }) .then(screenshotImage_ => { screenshotImage = screenshotImage_; return that._debugScreenshotsProvider.save(screenshotImage, 'original'); }) .then(() => { const scaleProvider = scaleProviderFactory.getScaleProvider(screenshotImage.getWidth()); if (scaleProvider.getScaleRatio() !== 1) { that._logger.verbose('scaling...'); return screenshotImage.scale(scaleProvider.getScaleRatio()).then(screenshotImage_ => { screenshotImage = screenshotImage_; return that._debugScreenshotsProvider.save(screenshotImage, 'scaled'); }); } }) .then(() => { const cutProvider = that._cutProviderHandler.get(); if (!(cutProvider instanceof NullCutProvider)) { that._logger.verbose('cutting...'); return cutProvider.cut(screenshotImage).then(screenshotImage_ => { screenshotImage = screenshotImage_; return that._debugScreenshotsProvider.save(screenshotImage, 'cut'); }); } }) .then(() => { that._logger.verbose('Creating screenshot object...'); const screenshot = new EyesWebDriverScreenshot( that._logger, that._driver, screenshotImage, that.getPromiseFactory() ); return screenshot.init(); }) .then(screenshot => { result = screenshot; }); }) .catch(error_ => { error = error_; }) .then(() => { if (originalBodyOverflow) { return EyesSeleniumUtils.setBodyOverflow(that._jsExecutor, originalBodyOverflow); } }) .then(() => { if (