UNPKG

@applitools/eyes.selenium

Version:

Applitools Eyes SDK for Selenium WebDriver

508 lines (452 loc) 18.6 kB
'use strict'; const { ArgumentGuard, EyesScreenshot, CoordinatesType, Region, Location, RectangleSize, CoordinatesTypeConversionError, OutOfBoundsError, } = require('@applitools/eyes.sdk.core'); const { SeleniumJavaScriptExecutor } = require('../SeleniumJavaScriptExecutor'); const { ScrollPositionProvider } = require('../positioning/ScrollPositionProvider'); const { FrameChain } = require('../frames/FrameChain'); /** * @readonly * @enum {number} */ const ScreenshotType = { VIEWPORT: 1, ENTIRE_FRAME: 2, }; class EyesWebDriverScreenshot extends EyesScreenshot { /** * !WARNING! After creating new instance of EyesWebDriverScreenshot, it should be initialized by calling to init or * initFromFrameSize method * * @param {Logger} logger A Logger instance. * @param {EyesWebDriver} driver The web driver used to get the screenshot. * @param {MutableImage} image The actual screenshot image. * @param {PromiseFactory} promiseFactory */ constructor(logger, driver, image, promiseFactory) { super(image); ArgumentGuard.notNull(logger, 'logger'); ArgumentGuard.notNull(driver, 'driver'); ArgumentGuard.notNull(promiseFactory, 'promiseFactory'); this._logger = logger; this._driver = driver; this._promiseFactory = promiseFactory; /** @type {FrameChain} */ this._frameChain = driver.getFrameChain(); /** @type {Location} */ this._currentFrameScrollPosition = null; /** @type {ScreenshotType} */ this._screenshotType = null; /** * The top/left coordinates of the frame window(!) relative to the top/left of the screenshot. Used for * calculations, so can also be outside(!) the screenshot. * * @type {Location} */ this._frameLocationInScreenshot = null; /** * The top/left coordinates of the frame window(!) relative to the top/left of the screenshot. Used for * calculations, so can also be outside(!) the screenshot. * * @type {Region} */ this._frameWindow = null; } /** * Creates a frame(!) window screenshot. * * @param {RectangleSize} entireFrameSize The full internal size of the frame. * @return {Promise<EyesWebDriverScreenshot>} */ initFromFrameSize(entireFrameSize) { // The frame comprises the entire screenshot. this._screenshotType = ScreenshotType.ENTIRE_FRAME; this._currentFrameScrollPosition = Location.ZERO; this._frameLocationInScreenshot = Location.ZERO; this._frameWindow = new Region(Location.ZERO, entireFrameSize); return this._promiseFactory.resolve(this); } /** * @param {ScreenshotType} [screenshotType] The screenshot's type (e.g., viewport/full page). * @param {Location} [frameLocationInScreenshot[ The current frame's location in the screenshot. * @return {Promise<EyesWebDriverScreenshot>} */ init(screenshotType, frameLocationInScreenshot) { const that = this; return that._updateScreenshotType(screenshotType, that._image).then(updatedScreenshotType => { that._screenshotType = updatedScreenshotType; const positionProvider = that._driver.getEyes().getPositionProvider(); that._frameChain = that._driver.getFrameChain(); return that._getFrameSize(positionProvider) .then(frameSize => EyesWebDriverScreenshot.getUpdatedScrollPosition(positionProvider) .then(currentFrameScrollPosition => { that._currentFrameScrollPosition = currentFrameScrollPosition; return that._getUpdatedFrameLocationInScreenshot(frameLocationInScreenshot); }) .then(updatedFrameLocationInScreenshot => { that._frameLocationInScreenshot = updatedFrameLocationInScreenshot; that._logger.verbose('Calculating frame window...'); that._frameWindow = new Region(updatedFrameLocationInScreenshot, frameSize); that._frameWindow.intersect(new Region(0, 0, that._image.getWidth(), that._image.getHeight())); if (that._frameWindow.getWidth() <= 0 || that._frameWindow.getHeight() <= 0) { throw new Error('Got empty frame window for screenshot!'); } that._logger.verbose('Done!'); return that; })); }); } /** * @param {Logger} logger * @param {FrameChain} currentFrames * @param {EyesWebDriver} driver * @return {Promise<Location>} */ static getDefaultContentScrollPosition(logger, currentFrames, driver) { const jsExecutor = new SeleniumJavaScriptExecutor(driver); const positionProvider = new ScrollPositionProvider(logger, jsExecutor); if (currentFrames.size() === 0) { return positionProvider.getCurrentPosition(); } const originalFC = new FrameChain(logger, currentFrames); const switchTo = driver.switchTo(); return switchTo.defaultContent() .then(() => positionProvider.getCurrentPosition()) .then(defaultContentScrollPosition => switchTo.frames(originalFC) .then(() => defaultContentScrollPosition)); } /** * @param {Logger} logger * @param {EyesWebDriver} driver * @param {FrameChain} frameChain * @param {ScreenshotType} screenshotType * @return {Promise<Location>} */ static calcFrameLocationInScreenshot(logger, driver, frameChain, screenshotType) { return EyesWebDriverScreenshot.getDefaultContentScrollPosition(logger, frameChain, driver).then(windowScroll => { logger.verbose('Getting first frame...'); const firstFrame = frameChain.getFrame(0); logger.verbose('Done!'); let locationInScreenshot = new Location(firstFrame.getLocation()); // We only consider scroll of the default content if this is a viewport screenshot. if (screenshotType === ScreenshotType.VIEWPORT) { locationInScreenshot = locationInScreenshot.offset(-windowScroll.getX(), -windowScroll.getY()); } logger.verbose('Iterating over frames..'); let frame; for (let i = 1, l = frameChain.size(); i < l; i += 1) { logger.verbose('Getting next frame...'); frame = frameChain.getFrame(i); logger.verbose('Done!'); const frameLocation = frame.getLocation(); // For inner frames we must consider the scroll const frameOriginalLocation = frame.getOriginalLocation(); // Offsetting the location in the screenshot locationInScreenshot = locationInScreenshot.offset( frameLocation.getX() - frameOriginalLocation.getX(), frameLocation.getY() - frameOriginalLocation.getY() ); } logger.verbose('Done!'); return locationInScreenshot; }); } /** * @private * @param {Location} frameLocationInScreenshot * @return {Promise<Location>} */ _getUpdatedFrameLocationInScreenshot(frameLocationInScreenshot) { this._logger.verbose(`frameLocationInScreenshot: ${frameLocationInScreenshot}`); if (this._frameChain.size() > 0) { return EyesWebDriverScreenshot.calcFrameLocationInScreenshot( this._logger, this._driver, this._frameChain, this._screenshotType ); } if (!frameLocationInScreenshot) { return this._promiseFactory.resolve(new Location(0, 0)); } return this._promiseFactory.resolve(frameLocationInScreenshot); } /** * @private * @param {PositionProvider} positionProvider * @return {Promise<Location>} */ static getUpdatedScrollPosition(positionProvider) { return positionProvider.getCurrentPosition() .then(sp => { if (!sp) { return new Location(0, 0); } return sp; }) .catch(() => new Location(0, 0)); } /** * @private * @param {PositionProvider} positionProvider * @return {Promise<RectangleSize>} */ _getFrameSize(positionProvider) { if (this._frameChain.size() === 0) { // get entire page size might throw an exception for applications which don't support Javascript (e.g., Appium). // In that case we'll use the viewport size as the frame's size. const that = this; return positionProvider.getEntireSize() .catch(() => that._driver.getDefaultContentViewportSize()); } return this._promiseFactory.resolve(this._frameChain.getCurrentFrameInnerSize()); } /** * @private * @param {ScreenshotType} screenshotType * @param {MutableImage} image * @return {Promise<ScreenshotType>} */ _updateScreenshotType(screenshotType, image) { if (!screenshotType) { const that = this; return that._driver.getEyes() .getViewportSize() .then(viewportSize => { const scaleViewport = that._driver.getEyes().shouldStitchContent(); if (scaleViewport) { const pixelRatio = that._driver.getEyes().getDevicePixelRatio(); viewportSize = viewportSize.scale(pixelRatio); } if (image.getWidth() <= viewportSize.getWidth() && image.getHeight() <= viewportSize.getHeight()) { return ScreenshotType.VIEWPORT; } return ScreenshotType.ENTIRE_FRAME; }); } return this._promiseFactory.resolve(screenshotType); } /** * @return {Region} The region of the frame which is available in the screenshot, in screenshot coordinates. */ getFrameWindow() { return this._frameWindow; } /** * @return {FrameChain} A copy of the frame chain which was available when the screenshot was created. */ getFrameChain() { return new FrameChain(this._logger, this._frameChain); } // noinspection JSUnusedGlobalSymbols /** * Returns a part of the screenshot based on the given region. * * @override * @param {Region} region The region for which we should get the sub screenshot. * @param {boolean} throwIfClipped Throw an EyesException if the region is not fully contained in the screenshot. * @return {Promise<EyesWebDriverScreenshot>} A screenshot instance containing the given region. */ getSubScreenshot(region, throwIfClipped) { this._logger.verbose(`getSubScreenshot([${region}], ${throwIfClipped})`); ArgumentGuard.notNull(region, 'region'); // We calculate intersection based on as-is coordinates. const asIsSubScreenshotRegion = this.getIntersectedRegion(region, CoordinatesType.SCREENSHOT_AS_IS); if ( asIsSubScreenshotRegion.isEmpty() || (throwIfClipped && !asIsSubScreenshotRegion.getSize().equals(region.getSize())) ) { throw new OutOfBoundsError(`Region [${region}] is out of screenshot bounds [${this._frameWindow}]`); } const that = this; return this._image.getImagePart(asIsSubScreenshotRegion) .then(imagePart => { const result = new EyesWebDriverScreenshot(that._logger, that._driver, imagePart, that._promiseFactory); return result.initFromFrameSize(new RectangleSize(imagePart.getWidth(), imagePart.getHeight())); }) .then(/** EyesWebDriverScreenshot */result => { result._frameLocationInScreenshot = new Location(-region.getLeft(), -region.getTop()); that._logger.verbose('Done!'); return result; }); } // noinspection JSUnusedGlobalSymbols /** * Converts a location's coordinates with the {@code from} coordinates type to the {@code to} coordinates type. * * @override * @param {Location} location The location which coordinates needs to be converted. * @param {CoordinatesType} from The current coordinates type for {@code location}. * @param {CoordinatesType} to The target coordinates type for {@code location}. * @return {Location} A new location which is the transformation of {@code location} to the {@code to} coordinates * type. */ convertLocation(location, from, to) { ArgumentGuard.notNull(location, 'location'); ArgumentGuard.notNull(from, 'from'); ArgumentGuard.notNull(to, 'to'); let result = new Location(location); if (from === to) { return result; } // If we're not inside a frame, and the screenshot is the entire page, then the context as-is/relative // are the same (notice screenshot as-is might be different, e.g., if it is actually a sub-screenshot of a region). if (this._frameChain.size() === 0 && this._screenshotType === ScreenshotType.ENTIRE_FRAME) { if ( (from === CoordinatesType.CONTEXT_RELATIVE || from === CoordinatesType.CONTEXT_AS_IS) && to === CoordinatesType.SCREENSHOT_AS_IS ) { // If this is not a sub-screenshot, this will have no effect. result = result.offset(this._frameLocationInScreenshot.getX(), this._frameLocationInScreenshot.getY()); } else if ( from === CoordinatesType.SCREENSHOT_AS_IS && (to === CoordinatesType.CONTEXT_RELATIVE || to === CoordinatesType.CONTEXT_AS_IS) ) { result = result.offset(-this._frameLocationInScreenshot.getX(), -this._frameLocationInScreenshot.getY()); } return result; } switch (from) { case CoordinatesType.CONTEXT_AS_IS: { switch (to) { case CoordinatesType.CONTEXT_RELATIVE: result = result.offset(this._currentFrameScrollPosition.getX(), this._currentFrameScrollPosition.getY()); break; case CoordinatesType.SCREENSHOT_AS_IS: result = result.offset(this._frameLocationInScreenshot.getX(), this._frameLocationInScreenshot.getY()); break; default: throw new CoordinatesTypeConversionError(from, to); } break; } case CoordinatesType.CONTEXT_RELATIVE: { switch (to) { case CoordinatesType.SCREENSHOT_AS_IS: // First, convert context-relative to context-as-is. result = result.offset(-this._currentFrameScrollPosition.getX(), -this._currentFrameScrollPosition.getY()); // Now convert context-as-is to screenshot-as-is. result = result.offset(this._frameLocationInScreenshot.getX(), this._frameLocationInScreenshot.getY()); break; case CoordinatesType.CONTEXT_AS_IS: result = result.offset(-this._currentFrameScrollPosition.getX(), -this._currentFrameScrollPosition.getY()); break; default: throw new CoordinatesTypeConversionError(from, to); } break; } case CoordinatesType.SCREENSHOT_AS_IS: { switch (to) { case CoordinatesType.CONTEXT_RELATIVE: // First convert to context-as-is. result = result.offset(-this._frameLocationInScreenshot.getX(), -this._frameLocationInScreenshot.getY()); // Now convert to context-relative. result = result.offset(this._currentFrameScrollPosition.getX(), this._currentFrameScrollPosition.getY()); break; case CoordinatesType.CONTEXT_AS_IS: result = result.offset(-this._frameLocationInScreenshot.getX(), -this._frameLocationInScreenshot.getY()); break; default: throw new CoordinatesTypeConversionError(from, to); } break; } default: { throw new CoordinatesTypeConversionError(from, to); } } return result; } // noinspection JSUnusedGlobalSymbols /** * @override * @param {Location} location * @param {CoordinatesType} coordinatesType * @return {Location} */ getLocationInScreenshot(location, coordinatesType) { this._location = this.convertLocation(location, coordinatesType, CoordinatesType.SCREENSHOT_AS_IS); // Making sure it's within the screenshot bounds if (!this._frameWindow.contains(location)) { throw new OutOfBoundsError(`Location ${location} ('${coordinatesType}') is not visible in screenshot!`); } return this._location; } /** * @override * @param {Region} region * @param {CoordinatesType} resultCoordinatesType * @return {Region} */ getIntersectedRegion(region, resultCoordinatesType) { if (region.isEmpty()) { return new Region(region); } const originalCoordinatesType = region.getCoordinatesType(); let intersectedRegion = this.convertRegionLocation( region, originalCoordinatesType, CoordinatesType.SCREENSHOT_AS_IS ); switch (originalCoordinatesType) { // If the request was context based, we intersect with the frame window. case CoordinatesType.CONTEXT_AS_IS: case CoordinatesType.CONTEXT_RELATIVE: intersectedRegion.intersect(this._frameWindow); break; // If the request is screenshot based, we intersect with the image case CoordinatesType.SCREENSHOT_AS_IS: intersectedRegion.intersect(new Region(0, 0, this._image.getWidth(), this._image.getHeight())); break; default: throw new CoordinatesTypeConversionError(`Unknown coordinates type: '${originalCoordinatesType}'`); } // If the intersection is empty we don't want to convert the coordinates. if (intersectedRegion.isEmpty()) { return intersectedRegion; } // Converting the result to the required coordinates type. intersectedRegion = this.convertRegionLocation( intersectedRegion, CoordinatesType.SCREENSHOT_AS_IS, resultCoordinatesType ); return intersectedRegion; } // noinspection JSUnusedGlobalSymbols /** * Gets the elements region in the screenshot. * * @param {WebElement} element The element which region we want to intersect. * @return {Promise<Region>} The intersected region, in {@code SCREENSHOT_AS_IS} coordinates type. */ getIntersectedRegionFromElement(element) { ArgumentGuard.notNull(element, 'element'); const that = this; return element.getLocation() .then(point => element.getSize() .then(size => { // Since the element coordinates are in context relative let elementRegion = new Region(point.x, point.y, size.width, size.height); // Since the element coordinates are in context relative elementRegion = that.getIntersectedRegion(elementRegion, CoordinatesType.CONTEXT_RELATIVE); if (!elementRegion.isEmpty()) { elementRegion = that.convertRegionLocation( elementRegion, CoordinatesType.CONTEXT_RELATIVE, CoordinatesType.SCREENSHOT_AS_IS ); } return elementRegion; })); } } EyesWebDriverScreenshot.ScreenshotType = Object.freeze(ScreenshotType); exports.EyesWebDriverScreenshot = EyesWebDriverScreenshot; exports.ScreenshotType = ScreenshotType;