@applitools/eyes.selenium
Version:
Applitools Eyes SDK for Selenium WebDriver
380 lines (341 loc) • 17.4 kB
JavaScript
;
const {
ArgumentGuard,
Location,
Region,
RectangleSize,
CoordinatesType,
GeneralUtils,
MutableImage,
NullCutProvider,
} = require('@applitools/eyes.sdk.core');
const { NullRegionPositionCompensation } = require('../positioning/NullRegionPositionCompensation');
const { ScrollPositionProvider } = require('../positioning/ScrollPositionProvider');
const MIN_SCREENSHOT_PART_HEIGHT = 10;
/**
* @param {Logger} logger
* @param {Region} region
* @param {MutableImage} image
* @param {number} pixelRatio
* @param {EyesScreenshot} screenshot
* @param {RegionPositionCompensation} regionPositionCompensation
* @return {Region}
*/
const getRegionInScreenshot = (logger, region, image, pixelRatio, screenshot, regionPositionCompensation) => {
// Region regionInScreenshot = screenshot.convertRegionLocation(regionProvider.getRegion(),
// regionProvider.getCoordinatesType(), CoordinatesType.SCREENSHOT_AS_IS);
let regionInScreenshot = screenshot.getIntersectedRegion(region, CoordinatesType.SCREENSHOT_AS_IS);
logger.verbose(`Done! Region in screenshot: ${regionInScreenshot}`);
regionInScreenshot = regionInScreenshot.scale(pixelRatio);
logger.verbose(`Scaled region: ${regionInScreenshot}`);
if (!regionPositionCompensation) {
regionPositionCompensation = new NullRegionPositionCompensation();
}
regionInScreenshot = regionPositionCompensation.compensateRegionPosition(regionInScreenshot, pixelRatio);
// Handling a specific case where the region is actually larger than the screenshot (e.g., when body width/height
// are set to 100%, and an internal div is set to value which is larger than the viewport).
regionInScreenshot.intersect(new Region(0, 0, image.getWidth(), image.getHeight()));
logger.verbose(`Region after intersect: ${regionInScreenshot}`);
return regionInScreenshot;
};
/**
* @param {PositionProvider} originProvider
* @param {Location} requiredPosition
* @param {number} retries
* @param {number} waitMillis
* @param {PromiseFactory} promiseFactory
* @return {Promise<Location>}
*/
const setPositionLoop = (originProvider, requiredPosition, retries, waitMillis, promiseFactory) => originProvider.setPosition(requiredPosition)
.then(() => GeneralUtils.sleep(waitMillis, promiseFactory)) // Give the scroll time to stabilize
.then(() => originProvider.getCurrentPosition())
.then(currentPosition => {
if (!currentPosition.equals(requiredPosition) && retries - 1 > 0) {
return setPositionLoop(originProvider, requiredPosition, retries, waitMillis, promiseFactory);
}
return currentPosition;
});
/**
* @param {DebugScreenshotsProvider} debugScreenshotsProvider
* @param {MutableImage} image
* @param {Region} region
* @param {string} name
* @return {Promise<void>}
*/
const saveDebugScreenshotPart = (debugScreenshotsProvider, image, region, name) => {
const suffix = `part-${name}-${region.getLeft()}_${region.getTop()}_${region.getWidth()}x${region.getHeight()}`;
return debugScreenshotsProvider.save(image, suffix);
};
class FullPageCaptureAlgorithm {
/**
* @param {Logger} logger
* @param {UserAgent} userAgent
* @param {EyesJsExecutor} jsExecutor
* @param {PromiseFactory} promiseFactory
*/
constructor(logger, userAgent, jsExecutor, promiseFactory) {
ArgumentGuard.notNull(logger, 'logger');
// TODO: why do we need userAgent here?
// ArgumentGuard.notNull(userAgent, "userAgent");
ArgumentGuard.notNull(jsExecutor, 'jsExecutor');
ArgumentGuard.notNull(promiseFactory, 'promiseFactory');
this._logger = logger;
this._userAgent = userAgent;
this._jsExecutor = jsExecutor;
this._promiseFactory = promiseFactory;
}
/**
* Returns a stitching of a region.
*
* @param {ImageProvider} imageProvider The provider for the screenshot.
* @param {Region} region The region to stitch. If {@code Region.EMPTY}, the entire image will be stitched.
* @param {PositionProvider} originProvider A provider for scrolling to initial position before starting the actual
* stitching.
* @param {PositionProvider} positionProvider A provider of the scrolling implementation.
* @param {ScaleProviderFactory} scaleProviderFactory A factory for getting the scale provider.
* @param {CutProvider} cutProvider
* @param {number} waitBeforeScreenshots Time to wait before each screenshot (milliseconds).
* @param {DebugScreenshotsProvider} debugScreenshotsProvider
* @param {EyesScreenshotFactory} screenshotFactory The factory to use for creating screenshots from the images.
* @param {number} stitchingOverlap The width of the overlapping parts when stitching an image.
* @param {RegionPositionCompensation} regionPositionCompensation A strategy for compensating region positions for
* some browsers.
* @return {Promise<MutableImage>} An image which represents the stitched region.
*/
getStitchedRegion(
imageProvider,
region,
originProvider,
positionProvider,
scaleProviderFactory,
cutProvider,
waitBeforeScreenshots,
debugScreenshotsProvider,
screenshotFactory,
stitchingOverlap,
regionPositionCompensation
) {
this._logger.verbose('getStitchedRegion()');
ArgumentGuard.notNull(region, 'region');
ArgumentGuard.notNull(originProvider, 'originProvider');
ArgumentGuard.notNull(positionProvider, 'positionProvider');
this._logger.verbose(`getStitchedRegion: originProvider: ${originProvider.constructor.name}; positionProvider: ${positionProvider.constructor.name}; cutProvider: ${cutProvider.constructor.name}`);
this._logger.verbose(`Region to check: ${region}`);
const that = this;
let originalPosition,
currentPosition,
/** @type {MutableImage} */ image,
scaleProvider,
pixelRatio,
regionInScreenshot,
entireSize;
// Saving the original position (in case we were already in the outermost frame).
return originProvider.getState()
.then(originalPosition_ => {
originalPosition = originalPosition_;
return setPositionLoop(originProvider, new Location(0, 0), 3, waitBeforeScreenshots, that._promiseFactory)
.then(currentPosition_ => {
currentPosition = currentPosition_;
if (currentPosition.getX() !== 0 || currentPosition.getY() !== 0) {
return originProvider.restoreState(originalPosition).then(() => {
throw new Error("Couldn't set position to the top/left corner!");
});
}
});
})
.then(() => {
that._logger.verbose('Getting top/left image...');
return imageProvider.getImage().then(newImage => {
image = newImage;
return debugScreenshotsProvider.save(image, 'original');
});
})
.then(() => {
// FIXME - scaling should be refactored
scaleProvider = scaleProviderFactory.getScaleProvider(image.getWidth());
// Notice that we want to cut/crop an image before we scale it, we need to change
pixelRatio = 1 / scaleProvider.getScaleRatio();
// FIXME - cropping should be overlaid, so a single cut provider will only handle a single part of the image.
cutProvider = cutProvider.scale(pixelRatio);
if (!(cutProvider instanceof NullCutProvider)) {
return cutProvider.cut(image)
.then(() => debugScreenshotsProvider.save(image, 'original-cut'));
}
})
.then(() => {
that._logger.verbose('Done! Creating screenshot object...');
// We need the screenshot to be able to convert the region to screenshot coordinates.
return screenshotFactory.makeScreenshot(image);
})
.then(/** EyesScreenshot */ screenshot => {
that._logger.verbose('Done! Getting region in screenshot...');
regionInScreenshot = getRegionInScreenshot(that._logger, region, image, pixelRatio, screenshot, regionPositionCompensation);
if (!regionInScreenshot.getSize().equals(region.getSize())) {
regionInScreenshot = getRegionInScreenshot(that._logger, region, image, pixelRatio, screenshot, regionPositionCompensation);
}
if (!regionInScreenshot.isEmpty()) {
return image.crop(regionInScreenshot)
.then(() => saveDebugScreenshotPart(debugScreenshotsProvider, image, region, 'cropped'));
}
})
.then(() => {
if (pixelRatio !== 1) {
return image.scale(scaleProvider.getScaleRatio())
.then(() => debugScreenshotsProvider.save(image, 'scaled'));
}
})
.then(() => {
const checkingAnElement = !region.isEmpty();
return positionProvider.getEntireSize().then(newEntireSize => {
entireSize = newEntireSize;
if (!checkingAnElement) {
const spp = new ScrollPositionProvider(that._logger, that._jsExecutor);
let originalCurrentPosition;
return spp.getCurrentPosition()
.then(newCurrentPosition => {
originalCurrentPosition = newCurrentPosition;
return spp.scrollToBottomRight();
})
.then(() => spp.getCurrentPosition())
.then(localCurrentPosition => {
entireSize = new RectangleSize(
localCurrentPosition.getX() + image.getWidth(),
localCurrentPosition.getY() + image.getHeight()
);
})
.then(() => {
that._logger.verbose(`Entire size of region context: ${entireSize}`);
return spp.setPosition(originalCurrentPosition);
})
.catch(err => {
that._logger.log(`WARNING: Failed to extract entire size of region context${err}`);
that._logger.log(`Using image size instead: ${image.getWidth()}x${image.getHeight()}`);
entireSize = new RectangleSize(image.getWidth(), image.getHeight());
});
}
});
})
.then(() => {
// Notice that this might still happen even if we used "getImagePart", since "entirePageSize" might be that of
// a frame.
if (image.getWidth() >= entireSize.getWidth() && image.getHeight() >= entireSize.getHeight()) {
return originProvider.restoreState(originalPosition).then(() => image);
}
// These will be used for storing the actual stitched size (it is sometimes less than the size extracted via
// "getEntireSize").
let lastSuccessfulLocation, lastSuccessfulPartSize, originalStitchedState, /** MutableImage */ partImage;
// The screenshot part is a bit smaller than the screenshot size, in order to eliminate duplicate bottom
// scroll bars, as well as fixed position footers.
const partImageSize = new RectangleSize(
image.getWidth(),
Math.max(image.getHeight() - stitchingOverlap, MIN_SCREENSHOT_PART_HEIGHT)
);
that._logger.verbose(`"Total size: ${entireSize}, image part size: ${partImageSize}`);
// Getting the list of sub-regions composing the whole region (we'll take screenshot for each one).
const entirePage = new Region(Location.ZERO, entireSize);
const imageParts = entirePage.getSubRegions(partImageSize);
that._logger.verbose(`Creating stitchedImage container. Size: ${entireSize}`);
// Notice stitchedImage uses the same type of image as the screenshots.
const stitchedImage = MutableImage.newImage(
entireSize.getWidth(),
entireSize.getHeight(),
that._promiseFactory
);
that._logger.verbose('Done! Adding initial screenshot..');
// Starting with the screenshot we already captured at (0,0).
that._logger.verbose(`Initial part:(0,0)[${image.getWidth()} x ${image.getHeight()}]`);
return stitchedImage.copyRasterData(0, 0, image)
.then(() => {
that._logger.verbose('Done!');
lastSuccessfulLocation = new Location(0, 0);
lastSuccessfulPartSize = new RectangleSize(image.getWidth(), image.getHeight());
return positionProvider.getState().then(newStitchingState => {
originalStitchedState = newStitchingState;
});
})
.then(() => {
// Take screenshot and stitch for each screenshot part.
that._logger.verbose('Getting the rest of the image parts...');
return imageParts.reduce((promise, partRegion) => promise.then(() => {
// Skipping screenshot for 0,0 (already taken)
if (partRegion.getLeft() === 0 && partRegion.getTop() === 0) {
return;
}
that._logger.verbose(`Taking screenshot for ${partRegion}`);
// Set the position to the part's top/left.
return positionProvider.setPosition(partRegion.getLocation())
.then(() => GeneralUtils.sleep(waitBeforeScreenshots, that._promiseFactory)) // Giving it time to stabilize.
.then(() => positionProvider.getCurrentPosition()
.then(newCurrentPosition => { // Screen size may cause the scroll to only reach part of the way.
currentPosition = newCurrentPosition;
that._logger.verbose(`Set position to ${currentPosition}`);
}))
.then(() => {
// Actually taking the screenshot.
that._logger.verbose('Getting image...');
return imageProvider.getImage()
.then(newPartImage => { partImage = newPartImage; })
.then(() => saveDebugScreenshotPart(debugScreenshotsProvider, partImage, partRegion, `original-scrolled-${currentPosition.toStringForFilename()}`));
})
.then(() => {
// FIXME - cropping should be overlaid (see previous comment re cropping)
if (!(cutProvider instanceof NullCutProvider)) {
that._logger.verbose('cutting...');
return cutProvider.cut(partImage)
.then(() => saveDebugScreenshotPart(debugScreenshotsProvider, partImage, partRegion, `original-scrolled-${currentPosition.toStringForFilename()}-cut-`));
}
})
.then(() => {
if (!regionInScreenshot.isEmpty()) {
that._logger.verbose('cropping...');
return partImage.crop(regionInScreenshot)
.then(() => saveDebugScreenshotPart(debugScreenshotsProvider, partImage, partRegion, `original-scrolled-${currentPosition.toStringForFilename()}-cropped-`));
}
})
.then(() => {
if (pixelRatio !== 1) {
that._logger.verbose('scaling...');
// FIXME - scaling should be refactored
return partImage.scale(scaleProvider.getScaleRatio())
.then(() => saveDebugScreenshotPart(debugScreenshotsProvider, partImage, partRegion, `original-scrolled-${currentPosition.toStringForFilename()}-scaled-`));
}
})
.then(() => {
// Stitching the current part.
that._logger.verbose('Stitching part into the image container...');
return stitchedImage.copyRasterData(currentPosition.getX(), currentPosition.getY(), partImage);
})
.then(() => {
that._logger.verbose('Done!');
lastSuccessfulLocation = currentPosition;
});
}), that._promiseFactory.resolve());
})
.then(() => {
if (partImage) {
lastSuccessfulPartSize = new RectangleSize(partImage.getWidth(), partImage.getHeight());
}
that._logger.verbose('Stitching done!');
})
.then(() => positionProvider.restoreState(originalStitchedState))
.then(() => originProvider.restoreState(originalPosition))
.then(() => {
// If the actual image size is smaller than the extracted size, we crop the image.
const actualImageWidth = lastSuccessfulLocation.getX() + lastSuccessfulPartSize.getWidth();
const actualImageHeight = lastSuccessfulLocation.getY() + lastSuccessfulPartSize.getHeight();
that._logger.verbose(`Extracted entire size: ${entireSize}`);
that._logger.verbose(`Actual stitched size: ${actualImageWidth}x${actualImageHeight}`);
if (actualImageWidth < stitchedImage.getWidth() || actualImageHeight < stitchedImage.getHeight()) {
that._logger.verbose('Trimming unnecessary margins..');
const newRegion = new Region(0, 0, Math.min(actualImageWidth, stitchedImage.getWidth()), Math.min(actualImageHeight, stitchedImage.getHeight()));
return stitchedImage.crop(newRegion).then(() => {
that._logger.verbose('Done!');
});
}
})
.then(() => debugScreenshotsProvider.save(stitchedImage, 'stitched'))
.then(() => stitchedImage);
});
}
}
exports.FullPageCaptureAlgorithm = FullPageCaptureAlgorithm;