diff --git a/lib/browser/commands/assert-view/index.js b/lib/browser/commands/assert-view/index.js index 9749e166a..faad178e0 100644 --- a/lib/browser/commands/assert-view/index.js +++ b/lib/browser/commands/assert-view/index.js @@ -56,7 +56,8 @@ module.exports = (browser) => { ['allowViewportOverflow', 'compositeImage', 'screenshotDelay', 'selectorToScroll'] ); const currImgInst = await screenShooter.capture(page, screenshoterOpts); - const currImg = {path: temp.path(Object.assign(tempOpts, {suffix: '.png'})), size: currImgInst.getSize()}; + const currSize = await currImgInst.getSize(); + const currImg = {path: temp.path(Object.assign(tempOpts, {suffix: '.png'})), size: currSize}; await currImgInst.save(currImg.path); const test = getTestContext(session.executionContext); diff --git a/lib/core/browser/camera/index.js b/lib/core/browser/camera/index.js index d0f7b942f..904ffd537 100644 --- a/lib/core/browser/camera/index.js +++ b/lib/core/browser/camera/index.js @@ -19,38 +19,50 @@ module.exports = class Camera { this._calibration = calibration; } - captureViewportImage(page) { - return this._takeScreenshot() - .then((base64) => this._applyCalibration(Image.fromBase64(base64))) - .then((image) => this._cropToViewport(image, page)); + async captureViewportImage(page) { + const base64 = await this._takeScreenshot(); + const image = Image.fromBase64(base64); + + const {width, height} = await image.getSize(); + const imageArea = {left: 0, top: 0, width, height}; + + const calibratedArea = this._calibrateArea(imageArea); + const viewportCroppedArea = this._cropAreaToViewport(calibratedArea, page); + + if (viewportCroppedArea.width !== width || viewportCroppedArea.height !== height) { + await image.crop(viewportCroppedArea); + } + + return image; } - _applyCalibration(image) { + _calibrateArea(imageArea) { if (!this._calibration) { - return image; + return imageArea; } const {left, top} = this._calibration; - const {width, height} = image.getSize(); - return image.crop({left, top, width: width - left, height: height - top}); + return {left, top, width: imageArea.width - left, height: imageArea.height - top}; } - _cropToViewport(image, page) { + _cropAreaToViewport(imageArea, page) { if (!page) { - return image; + return imageArea; } - const isFullPage = utils.isFullPage(image, page, this._screenshotMode); + const isFullPage = utils.isFullPage(imageArea, page, this._screenshotMode); const cropArea = _.clone(page.viewport); if (!isFullPage) { - _.extend(cropArea, { - top: 0, - left: 0 - }); + _.extend(cropArea, {top: 0, left: 0}); } - return image.crop(cropArea, {scaleFactor: page.pixelRatio}); + return { + left: (imageArea.left + cropArea.left) * page.pixelRatio, + top: (imageArea.top + cropArea.top) * page.pixelRatio, + width: Math.min(imageArea.width - cropArea.left, cropArea.width) * page.pixelRatio, + height: Math.min(imageArea.height - cropArea.top, cropArea.height) * page.pixelRatio + }; } }; diff --git a/lib/core/browser/camera/utils.js b/lib/core/browser/camera/utils.js index af3c6316a..31bcab44d 100644 --- a/lib/core/browser/camera/utils.js +++ b/lib/core/browser/camera/utils.js @@ -1,24 +1,27 @@ 'use strict'; -exports.isFullPage = (image, page, screenshotMode) => { +exports.isFullPage = (imageArea, page, screenshotMode) => { switch (screenshotMode) { case 'fullpage': return true; case 'viewport': return false; - case 'auto': return compareDimensions(image, page); + case 'auto': return compareDimensions(imageArea, page); } }; /** - * @param {Image} image - PngImg wrapper + * @param {Object} imageArea - area + * @param {number} imageArea.left - left offset + * @param {number} imageArea.top - top offset + * @param {number} imageArea.width - area width + * @param {number} imageArea.height - area height * @param {Object} page - capture meta information object * @returns {boolean} * @private */ -function compareDimensions(image, page) { +function compareDimensions(imageArea, page) { const pixelRatio = page.pixelRatio; const documentWidth = page.documentWidth * pixelRatio; const documentHeight = page.documentHeight * pixelRatio; - const imageSize = image.getSize(); - return imageSize.height >= documentHeight && imageSize.width >= documentWidth; + return imageArea.height >= documentHeight && imageArea.width >= documentWidth; } diff --git a/lib/core/browser/client-scripts/index.js b/lib/core/browser/client-scripts/index.js index 34a8975c4..442ab1efc 100644 --- a/lib/core/browser/client-scripts/index.js +++ b/lib/core/browser/client-scripts/index.js @@ -66,10 +66,10 @@ function prepareScreenshotUnsafe(areas, opts) { return rect; } - var viewportHeight = document.documentElement.clientHeight, - viewportWidth = document.documentElement.clientWidth, - documentHeight = document.documentElement.scrollHeight, + var viewportWidth = document.documentElement.clientWidth, + viewportHeight = document.documentElement.clientHeight, documentWidth = document.documentElement.scrollWidth, + documentHeight = document.documentElement.scrollHeight, viewPort = new Rect({ left: util.getScrollLeft(scrollElem), top: util.getScrollTop(scrollElem), @@ -97,8 +97,8 @@ function prepareScreenshotUnsafe(areas, opts) { captureArea: rect.serialize(), ignoreAreas: findIgnoreAreas(opts.ignoreSelectors, scrollElem), viewport: { - top: util.getScrollTop(scrollElem), left: util.getScrollLeft(scrollElem), + top: util.getScrollTop(scrollElem), width: Math.round(viewportWidth), height: Math.round(viewportHeight) }, diff --git a/lib/core/browser/client-scripts/rect.js b/lib/core/browser/client-scripts/rect.js index a59d5c930..d46758556 100644 --- a/lib/core/browser/client-scripts/rect.js +++ b/lib/core/browser/client-scripts/rect.js @@ -45,8 +45,8 @@ Rect.prototype = { translate: function(x, y) { return new Rect({ - top: this.top + y, left: this.left + x, + top: this.top + y, width: this.width, height: this.height }); @@ -80,8 +80,8 @@ Rect.prototype = { serialize: function() { return { - top: this.top, left: this.left, + top: this.top, width: this.width, height: this.height }; diff --git a/lib/core/calibrator/index.js b/lib/core/calibrator/index.js index 90860396d..f9a2ada1c 100644 --- a/lib/core/calibrator/index.js +++ b/lib/core/calibrator/index.js @@ -27,10 +27,10 @@ module.exports = class Calibrator { return Promise.resolve(browser.open('about:blank')) .then(() => browser.evalScript(clientScriptCalibrate)) .then((features) => [features, browser.captureViewportImage()]) - .spread((features, image) => { + .spread(async (features, image) => { const {innerWidth, pixelRatio} = features; const hasPixelRatio = Boolean(pixelRatio && pixelRatio > 1.0); - const imageFeatures = this._analyzeImage(image, {calculateColorLength: hasPixelRatio}); + const imageFeatures = await this._analyzeImage(image, {calculateColorLength: hasPixelRatio}); if (!imageFeatures) { return Promise.reject(new CoreError( @@ -50,11 +50,11 @@ module.exports = class Calibrator { }); } - _analyzeImage(image, params) { - const imageHeight = image.getSize().height; + async _analyzeImage(image, params) { + const imageHeight = (await image.getSize()).height; for (var y = 0; y < imageHeight; y++) { - var result = analyzeRow(y, image, params); + var result = await analyzeRow(y, image, params); if (result) { return result; @@ -65,8 +65,8 @@ module.exports = class Calibrator { } }; -function analyzeRow(row, image, params = {}) { - const markerStart = findMarkerInRow(row, image, DIRECTION.FORWARD); +async function analyzeRow(row, image, params = {}) { + const markerStart = await findMarkerInRow(row, image, DIRECTION.FORWARD); if (markerStart === -1) { return null; @@ -78,14 +78,14 @@ function analyzeRow(row, image, params = {}) { return result; } - const markerEnd = findMarkerInRow(row, image, DIRECTION.REVERSE); + const markerEnd = await findMarkerInRow(row, image, DIRECTION.REVERSE); const colorLength = markerEnd - markerStart + 1; return _.extend(result, {colorLength}); } -function findMarkerInRow(row, image, searchDirection) { - const imageWidth = image.getSize().width; +async function findMarkerInRow(row, image, searchDirection) { + const imageWidth = (await image.getSize()).width; const searchColor = {R: 148, G: 250, B: 0}; if (searchDirection === DIRECTION.REVERSE) { @@ -94,26 +94,31 @@ function findMarkerInRow(row, image, searchDirection) { return searchForward_(); } - function searchForward_() { + async function searchForward_() { for (var x = 0; x < imageWidth; x++) { - if (compare_(x)) { + var isSame = await compare_(x); + + if (isSame) { return x; } } return -1; } - function searchReverse_() { + async function searchReverse_() { for (var x = imageWidth - 1; x >= 0; x--) { - if (compare_(x)) { + var isSame = await compare_(x); + + if (isSame) { return x; } } return -1; } - function compare_(x) { - var color = pickRGB(image.getRGBA(x, row)); + async function compare_(x) { + var pixel = await image.getRGBA(x, row); + var color = pickRGB(pixel); return looksSame.colors(color, searchColor); } } diff --git a/lib/core/image/index.js b/lib/core/image/index.js index 18ec265d5..249b88d68 100644 --- a/lib/core/image/index.js +++ b/lib/core/image/index.js @@ -2,9 +2,7 @@ const Promise = require('bluebird'); const looksSame = require('looks-same'); -const {PngImg} = require('png-img'); -const utils = require('png-img/utils'); -const SafeRect = require('./safe-rect'); +const sharp = require('sharp'); module.exports = class Image { static create(buffer) { @@ -12,73 +10,145 @@ module.exports = class Image { } constructor(buffer) { - this._img = new PngImg(buffer); + this._img = sharp(buffer); + this._imageData = null; + this._ignoreData = []; + this._composeImages = []; } - crop(rect, opts = {}) { - rect = this._scale(rect, (opts).scaleFactor); - const imageSize = this.getSize(); - const safeRect = SafeRect.create(rect, imageSize); + async getSize() { + const imgSizes = await Promise.map([this].concat(this._composeImages), img => img._img.metadata()); - this._img.crop( - safeRect.left, - safeRect.top, - safeRect.width, - safeRect.height - ); + return imgSizes.reduce((totalSize, img) => { + return { + width: Math.max(totalSize.width, img.width), + height: totalSize.height + img.height + }; + }, {width: 0, height: 0}); + } + + async crop(rect) { + const {height, width} = await this._img.metadata(); + + this._img.extract({ + left: rect.left, + top: rect.top, + width: Math.min(width, rect.left + rect.width) - rect.left, + height: Math.min(height, rect.top + rect.height) - rect.top + }); - return Promise.resolve(this); + await this._forceRefreshImageData(); } - getSize() { - return this._img.size(); + addJoin(attachedImages) { + this._composeImages = this._composeImages.concat(attachedImages); + } + + async applyJoin() { + if (!this._composeImages.length) { + return; + } + + const {height, width} = await this._img.metadata(); + const imagesData = await Promise.all(this._composeImages.map(img => img._getImageData())); + const compositeData = []; + + let newHeight = height; + + for (const {data, info} of imagesData) { + compositeData.push({ + input: data, + left: 0, + top: newHeight, + raw: { + width: info.width, + height: info.height, + channels: info.channels + } + }); + + newHeight += info.height; + } + + this._img.resize({ + width, + height: newHeight, + fit: 'contain', + position: 'top' + }); + + this._img.composite(compositeData); } - getRGBA(x, y) { - return this._img.get(x, y); + async addClear({width, height, left, top}) { + const {channels} = await this._img.metadata(); + + this._ignoreData.push({ + input: { + create: { + channels, + background: {r: 0, g: 0, b: 0, alpha: 1}, + width, + height + } + }, + left, + top + }); } - save(file) { - return this._img.save(file); + applyClear() { + this._img.composite(this._ignoreData); } - clear(area, opts = {}) { - area = this._scale(area, (opts).scaleFactor); - this._img.fill( - area.left, - area.top, - area.width, - area.height, - '#000000' - ); + async _getImageData() { + if (!this._imageData) { + this._imageData = await this._img.raw().toBuffer({resolveWithObject: true}); + } + + return this._imageData; } - join(newImage) { - const imageSize = this.getSize(); - this._img - .setSize(imageSize.width, imageSize.height + newImage.getSize().height) - .insert(newImage._img, 0, imageSize.height); + async _forceRefreshImageData() { + this._imageData = await this._img.raw().toBuffer({resolveWithObject: true}); + this._img = sharp(this._imageData.data, { + raw: { + width: this._imageData.info.width, + height: this._imageData.info.height, + channels: this._imageData.info.channels + } + }); - return this; + this._composeImages = []; + this._ignoreData = []; } - _scale(area, scaleFactor) { - scaleFactor = scaleFactor || 1; + async getRGBA(x, y) { + const {data, info} = await this._getImageData(); + const idx = (info.width * y + x) * info.channels; return { - left: area.left * scaleFactor, - top: area.top * scaleFactor, - width: area.width * scaleFactor, - height: area.height * scaleFactor + r: data[idx], + g: data[idx + 1], + b: data[idx + 2], + a: info.channels === 4 ? data[idx + 3] : 1 }; } + async save(file) { + await this._img.png().toFile(file); + } + static fromBase64(base64) { - return new Image(new Buffer(base64, 'base64')); + return new this(Buffer.from(base64, 'base64')); } - static RGBToString(rgb) { - return utils.RGBToString(rgb); + async toPngBuffer(opts = {resolveWithObject: true}) { + const imgData = await this._img.png().toBuffer(opts); + + return opts.resolveWithObject + ? {data: imgData.data, size: {height: imgData.info.height, width: imgData.info.width}} + : imgData; } static compare(path1, path2, opts = {}) { @@ -92,7 +162,6 @@ module.exports = class Image { compareOptions[option] = opts[option]; } }); - return looksSame(path1, path2, compareOptions); } diff --git a/lib/core/image/safe-rect.js b/lib/core/image/safe-rect.js deleted file mode 100644 index 55dfa739e..000000000 --- a/lib/core/image/safe-rect.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -module.exports = class SafeRect { - static create(rect, imageSize) { - return new SafeRect(rect, imageSize); - } - - constructor(rect, imageSize) { - this._rect = rect; - this._imageSize = imageSize; - } - - get left() { - return this._calcCoord('left'); - } - - get top() { - return this._calcCoord('top'); - } - - _calcCoord(coord) { - return Math.max(this._rect[coord], 0); - } - - get width() { - return this._calcSize('width', 'left'); - } - - get height() { - return this._calcSize('height', 'top'); - } - - _calcSize(size, coord) { - const rectCoord = this._calcCoord(coord); - - return Math.min(this._rect[size], this._imageSize[size] - rectCoord); - } -}; diff --git a/lib/core/screen-shooter/index.js b/lib/core/screen-shooter/index.js index c63e891ae..126b28a08 100644 --- a/lib/core/screen-shooter/index.js +++ b/lib/core/screen-shooter/index.js @@ -1,55 +1,52 @@ 'use strict'; -const Promise = require('bluebird'); const Viewport = require('./viewport'); -const HeightViewportError = require('./viewport/coord-validator/errors/height-viewport-error'); module.exports = class ScreenShooter { static create(browser) { - return new ScreenShooter(browser); + return new this(browser); } constructor(browser) { this._browser = browser; } - capture(page, opts = {}) { + async capture(page, opts = {}) { const {allowViewportOverflow, compositeImage, screenshotDelay, selectorToScroll} = opts; const viewportOpts = {allowViewportOverflow, compositeImage}; const cropImageOpts = {screenshotDelay, compositeImage, selectorToScroll}; - return this._browser.captureViewportImage(page, screenshotDelay) - .then((viewportImage) => Viewport.create(page.viewport, viewportImage, page.pixelRatio, viewportOpts)) - .then((viewport) => this._cropImage(viewport, page, cropImageOpts)); + const capturedImage = await this._browser.captureViewportImage(page, screenshotDelay); + const viewport = Viewport.create(page, capturedImage, viewportOpts); + await viewport.handleImage(capturedImage); + + return this._extendScreenshot(viewport, page, cropImageOpts); } - _cropImage(viewport, page, opts) { - try { - viewport.validate(page.captureArea, this._browser); - } catch (e) { - return e instanceof HeightViewportError && opts.compositeImage - ? this._extendImage(viewport, page, opts) - : Promise.reject(e); - } + async _extendScreenshot(viewport, page, opts) { + let shouldExtend = viewport.validate(this._browser); - viewport.ignoreAreas(page.ignoreAreas); + while (shouldExtend) { + await this._extendImage(viewport, page, opts); + + shouldExtend = viewport.validate(this._browser); + } - return viewport.crop(page.captureArea); + return viewport.composite(); } - _extendImage(viewport, page, opts) { + async _extendImage(viewport, page, opts) { const scrollHeight = Math.min( - viewport.getVerticalOverflow(page.captureArea), + viewport.getVerticalOverflow(), page.viewport.height ); - return this._browser - .scrollBy({x: 0, y: scrollHeight, selector: opts.selectorToScroll}) - .then(() => { - page.viewport.top += scrollHeight; - return this._browser.captureViewportImage(page, opts.screenshotDelay); - }) - .then((newImage) => viewport.extendBy(scrollHeight, newImage)) - .then(() => this._cropImage(viewport, page, opts)); + await this._browser.scrollBy({x: 0, y: scrollHeight, selector: opts.selectorToScroll}); + + page.viewport.top += scrollHeight; + + const newImage = await this._browser.captureViewportImage(page, opts.screenshotDelay); + + await viewport.extendBy(scrollHeight, newImage); } }; diff --git a/lib/core/screen-shooter/viewport/coord-validator/index.js b/lib/core/screen-shooter/viewport/coord-validator/index.js index 7e191d887..8351ab108 100644 --- a/lib/core/screen-shooter/viewport/coord-validator/index.js +++ b/lib/core/screen-shooter/viewport/coord-validator/index.js @@ -41,7 +41,7 @@ module.exports = class CoordValidator { } if (cropArea.top + cropArea.height > viewport.top + viewport.height) { - return this._reportHeightViewportError(viewport, cropArea); + return this._opts.compositeImage || this._reportHeightViewportError(viewport, cropArea); } } diff --git a/lib/core/screen-shooter/viewport/index.js b/lib/core/screen-shooter/viewport/index.js index 102efbb22..b080fa332 100644 --- a/lib/core/screen-shooter/viewport/index.js +++ b/lib/core/screen-shooter/viewport/index.js @@ -6,71 +6,132 @@ const CoordValidator = require('./coord-validator'); module.exports = class Viewport { static create(...args) { - return new Viewport(...args); + return new this(...args); } - constructor(viewport, image, pixelRatio, opts) { - this._viewport = _.clone(viewport); + constructor(page, image, opts) { + this._pixelRatio = page.pixelRatio; + this._viewport = this._scale(page.viewport); + this._captureArea = this._scale(this._sanitize(page.captureArea)); + this._ignoreAreas = page.ignoreAreas.map(area => this._scale(area)); this._image = image; - this._pixelRatio = pixelRatio; this._opts = opts; + this._summaryHeight = 0; } - validate(captureArea, browser) { - CoordValidator.create(browser, this._opts).validate(this._viewport, captureArea); - } + validate(browser) { + const coordValidator = CoordValidator.create(browser, this._opts); - ignoreAreas(areas) { - _(areas) - .map((area) => this._getIntersectionWithViewport(area)) - .compact() - .forEach((area) => this._image.clear(this._transformToViewportOrigin(area), {scaleFactor: this._pixelRatio})); + return coordValidator.validate(this._viewport, this._captureArea); } - crop(captureArea) { - return this._image.crop(this._transformToViewportOrigin(captureArea), {scaleFactor: this._pixelRatio}); + async ignoreAreas(image, imageArea) { + for (const area of this._ignoreAreas) { + const imageClearArea = this._getIntersection(area, imageArea); + + if (imageClearArea !== null) { + await image.addClear(this._shiftArea(imageClearArea, -imageArea.left, -imageArea.top)); + } + } + + image.applyClear(); } - _getIntersectionWithViewport(area) { - const top = Math.max(this._viewport.top, area.top); - const bottom = Math.min(getAreaBottom(this._viewport), getAreaBottom(area)); - const left = Math.max(this._viewport.left, area.left); - const right = Math.min(getAreaRight(this._viewport), getAreaRight(area)); + async handleImage(image, area = {}) { + const {width, height} = await image.getSize(); + _.defaults(area, {left: 0, top: 0, width, height}); + const capturedArea = this._transformToCaptureArea(area); - if (left >= right || top >= bottom) { - return null; - } - return {top, left, width: right - left, height: bottom - top}; + await this.ignoreAreas(image, this._shiftArea(capturedArea, -area.left, -area.top)); + await image.crop(this._sanitize(this._transformToViewportOrigin(capturedArea))); + + this._summaryHeight += capturedArea.height; } - _transformToViewportOrigin(area) { - return _.extend({}, area, { - top: area.top - this._viewport.top, - left: area.left - this._viewport.left - }); + async composite() { + await this._image.applyJoin(); + + return this._image; } - save(path) { + async save(path) { return this._image.save(path); } - extendBy(scrollHeight, newImage) { - const newImageSize = newImage.getSize(); + async extendBy(scrollHeight, newImage) { const physicalScrollHeight = scrollHeight * this._pixelRatio; + this._viewport.height += physicalScrollHeight; + const {width, height} = await newImage.getSize(); - this._viewport.height += scrollHeight; - - return newImage.crop({ + await this.handleImage(newImage, { left: 0, - top: newImageSize.height - physicalScrollHeight, - width: newImageSize.width, + top: height - physicalScrollHeight, + width, height: physicalScrollHeight - }) - .then((croppedImage) => this._image.join(croppedImage)); + }); + + this._image.addJoin(newImage); + } + + getVerticalOverflow() { + return (getAreaBottom(this._captureArea) - getAreaBottom(this._viewport)) / this._pixelRatio; + } + + _scale(area, scaleFactor = this._pixelRatio) { + return { + left: area.left * scaleFactor, + top: area.top * scaleFactor, + width: area.width * scaleFactor, + height: area.height * scaleFactor + }; } - getVerticalOverflow(captureArea) { - return (captureArea.top + captureArea.height) - (this._viewport.top + this._viewport.height); + _sanitize(area) { + return { + left: Math.max(area.left, 0), + top: Math.max(area.top, 0), + width: Math.max(area.width, 0), + height: Math.max(area.height, 0) + }; + } + + _getIntersection(...areas) { + const top = Math.max(...areas.map(area => area.top)); + const bottom = Math.min(...areas.map(getAreaBottom)); + const left = Math.max(...areas.map(area => area.left)); + const right = Math.min(...areas.map(getAreaRight)); + + if (left >= right || top >= bottom) { + return null; + } + + return {left, top, width: right - left, height: bottom - top}; + } + + _shiftArea(area, left = 0, top = 0) { + return { + left: area.left + left, + top: area.top + top, + width: area.width, + height: area.height + }; + } + + _transformToCaptureArea(area) { + const intersectingArea = this._getIntersection( + this._shiftArea(area, this._viewport.left, this._viewport.top + this._summaryHeight), + this._shiftArea(this._captureArea, area.left, area.top) + ); + + if (!this._summaryHeight) { + intersectingArea.height = Math.min(area.height, this._captureArea.height); + } + + return intersectingArea; + } + + _transformToViewportOrigin(area) { + return this._shiftArea(area, -this._viewport.left, -this._viewport.top - this._summaryHeight); } }; diff --git a/lib/worker/runner/test-runner/one-time-screenshooter.js b/lib/worker/runner/test-runner/one-time-screenshooter.js index b19f00861..4f2dbf151 100644 --- a/lib/worker/runner/test-runner/one-time-screenshooter.js +++ b/lib/worker/runner/test-runner/one-time-screenshooter.js @@ -73,8 +73,8 @@ module.exports = class OneTimeScreenshooter { const pageSize = await this._getPageSize(); const page = await this._browser.prepareScreenshot([{ - top: 0, left: 0, + top: 0, width: pageSize.width, height: pageSize.height }], { @@ -103,21 +103,18 @@ module.exports = class OneTimeScreenshooter { temp.attach(tempOpts); const path = temp.path(Object.assign({}, tempOpts, {suffix: '.png'})); - await image.save(path); + const {data, size} = await image.toPngBuffer(); + const base64 = data.toString('base64'); - // read the recorded image to get base64, otherwise we will break the API - // TODO: need to teach the Image to return base64 - const buffer = await fs.promises.readFile(path); + await fs.promises.writeFile(path, data); - return { - base64: buffer.toString('base64'), - size: image.getSize() - }; + return {base64, size}; } async _makeViewportScreenshot() { const base64 = await this._browser.publicAPI.takeScreenshot(); - const size = Image.fromBase64(base64).getSize(); + const image = Image.fromBase64(base64); + const size = await image.getSize(); return {base64, size}; } diff --git a/package-lock.json b/package-lock.json index 2ece761f3..7c55c494f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1751,7 +1751,7 @@ "anchor-markdown-header": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/anchor-markdown-header/-/anchor-markdown-header-0.5.7.tgz", - "integrity": "sha1-BFBj125qH5zTJ6V6ASaqD97Dcac=", + "integrity": "sha512-AmikqcK15r3q99hPvTa1na9n3eLkW0uE+RL9BZMSgwYalQeDnNXbYrN06BIcBPfGlmsGIE2jvkuvl/x0hyPF5Q==", "dev": true, "requires": { "emoji-regex": "~6.1.0" @@ -1795,7 +1795,7 @@ "app-module-path": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz", - "integrity": "sha1-ZBqlXft9am8KgUHEucCqULbCTdU=", + "integrity": "sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==", "dev": true }, "append-transform": { @@ -1907,13 +1907,13 @@ "array-from": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "integrity": "sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==", "dev": true }, "array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", "dev": true }, "array-union": { @@ -1925,7 +1925,7 @@ "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "dev": true }, "asn1.js": { @@ -1984,7 +1984,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, "bail": { @@ -2329,7 +2329,7 @@ "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" }, "buffer-from": { "version": "1.1.2", @@ -2395,7 +2395,7 @@ "caller-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "integrity": "sha512-UJiE1otjXPF5/x+T3zTnSFiTOEmJoGTD9HmBoxnCUwho61a2eSNn/VwtwuIBDAo2SEOv1AJ7ARI5gCmohFLu/g==", "requires": { "callsites": "^0.2.0" } @@ -2403,7 +2403,7 @@ "callsites": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=" + "integrity": "sha512-Zv4Dns9IbXXmPkgRRUjAaJQgfN4xX5p6+RQFhWUqscdvvK2xK/ZL8b3IXIJsj+4sD+f24NwnWy2BY8AJ82JB0A==" }, "camelcase": { "version": "6.3.0", @@ -2476,7 +2476,7 @@ "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", "requires": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", @@ -2488,17 +2488,17 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==" }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "requires": { "ansi-regex": "^2.0.0" } @@ -2506,7 +2506,7 @@ "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==" } } }, @@ -2531,7 +2531,7 @@ "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", "dev": true }, "chokidar": { @@ -2596,7 +2596,7 @@ "clear-require": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/clear-require/-/clear-require-1.0.1.tgz", - "integrity": "sha1-X+/mPx9XhpmgSbWF0xNq0qoPrX8=", + "integrity": "sha512-CmocmREIWAY0uKBGb+5rl9pBYfAP8t1hXkSqM/uGdAzxjkBcJei1BJFjBel0xtOwVeOKbLTy/5q4ogKZGLltCA==", "requires": { "caller-path": "^0.1.0", "resolve-from": "^1.0.0" @@ -2693,6 +2693,15 @@ } } }, + "color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2711,6 +2720,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "combine-source-map": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", @@ -2780,7 +2798,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "concat-stream": { "version": "1.6.2", @@ -3425,7 +3443,7 @@ "css-value": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", - "integrity": "sha1-Xv1sLupeof1rasV+wEJ7GEUkJOo=" + "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==" }, "cssom": { "version": "0.4.4", @@ -3494,7 +3512,7 @@ "decamelize-keys": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "integrity": "sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==", "dev": true, "requires": { "decamelize": "^1.1.0", @@ -3576,7 +3594,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, "deps-sort": { @@ -3979,7 +3997,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" }, "escodegen": { "version": "2.0.0", @@ -4419,7 +4437,7 @@ "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==" }, "fast-glob": { "version": "3.2.12", @@ -4442,7 +4460,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "fast-safe-stringify": { @@ -4470,7 +4488,7 @@ "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "requires": { "pend": "~1.2.0" } @@ -4487,7 +4505,7 @@ "fill-keys": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", - "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==", "dev": true, "requires": { "is-object": "~1.0.1", @@ -4573,7 +4591,7 @@ "format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", "dev": true }, "fromentries": { @@ -4600,7 +4618,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "fsevents": { "version": "2.3.2", @@ -4641,7 +4659,7 @@ "get-func-name": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", "dev": true }, "get-package-type": { @@ -4676,7 +4694,7 @@ "git-remote-origin-url": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", - "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", + "integrity": "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==", "dev": true, "requires": { "gitconfiglocal": "^1.0.0", @@ -4704,7 +4722,7 @@ "gitconfiglocal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", - "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", + "integrity": "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==", "dev": true, "requires": { "ini": "^1.3.2" @@ -4850,7 +4868,7 @@ "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", "requires": { "ansi-regex": "^2.0.0" }, @@ -4858,7 +4876,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" } } }, @@ -5073,7 +5091,7 @@ "husky": { "version": "0.11.9", "resolved": "https://registry.npmjs.org/husky/-/husky-0.11.9.tgz", - "integrity": "sha1-KM0dwWv/3KHU2TWSgU5fPDJ7OO4=", + "integrity": "sha512-5iRwfa/2o8lhT2rxoW18MhPVzRwhwg1abpXvVNl0vvJPit5dmpohTjStDLIk8uo3/eUeaY/KZkBPiNFvb0V5Gg==", "dev": true, "requires": { "is-ci": "^1.0.9", @@ -5083,7 +5101,7 @@ "normalize-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz", - "integrity": "sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=", + "integrity": "sha512-7WyT0w8jhpDStXRq5836AMmihQwq2nrUVQrgjvUo/p/NZf9uy/MeJ246lBJVmWuYXMlJuG9BNZHF0hWjfTbQUA==", "dev": true } } @@ -5129,7 +5147,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, "indent-string": { @@ -5146,7 +5164,7 @@ "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -5218,7 +5236,7 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, "is-binary-path": { @@ -5265,7 +5283,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -5307,7 +5325,7 @@ "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true }, "is-potential-custom-element-name": { @@ -5325,7 +5343,7 @@ "is-text-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", + "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", "dev": true, "requires": { "text-extensions": "^1.0.0" @@ -5359,12 +5377,12 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "istanbul-lib-coverage": { "version": "3.2.0", @@ -5548,7 +5566,7 @@ "jsdom-global": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsdom-global/-/jsdom-global-3.0.2.tgz", - "integrity": "sha1-a9KZwTsMRiay2iwDk81DhdYGrLk=", + "integrity": "sha512-t1KMcBkz/pT5JrvcJbpUR2u/w1kO9jXctaaGJ0vZDzwFnIvGWw9IDSRciT83kIs8Bnw4qpOl8bQK08V01YgMPg==", "dev": true }, "jsesc": { @@ -5597,7 +5615,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true }, "json5": { @@ -5609,7 +5627,7 @@ "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "requires": { "graceful-fs": "^4.1.6" } @@ -5622,7 +5640,7 @@ "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==" }, "just-extend": { "version": "4.0.2", @@ -5676,7 +5694,7 @@ "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", "dev": true, "requires": { "prelude-ls": "~1.1.2", @@ -5751,22 +5769,22 @@ "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, "lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" }, "lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" }, "lodash.flattendeep": { "version": "4.4.0", @@ -5777,7 +5795,7 @@ "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, "lodash.ismatch": { @@ -5789,12 +5807,12 @@ "lodash.isobject": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", - "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=" + "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==" }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" }, "lodash.memoize": { "version": "3.0.4", @@ -5809,12 +5827,12 @@ "lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" }, "lodash.zip": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", - "integrity": "sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=" + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==" }, "log-symbols": { "version": "4.1.0", @@ -5864,9 +5882,9 @@ "dev": true }, "looks-same": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/looks-same/-/looks-same-8.0.0.tgz", - "integrity": "sha512-9lOGjMO8MY8LRDqDtqVBSlzRwygSoLQt8LeuBXSxjWqFAGF/eASUCPKSbp6FRvLKTq1BuLf7hGXfutVLziTLXw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/looks-same/-/looks-same-8.0.1.tgz", + "integrity": "sha512-AvkyfE+NqhfpUexMJYEpqroSjgwD5brxidWNb1sPHZiV8MS0vnh1f0mFnSycXL+y5QfLFtEZOt3G+z8BNA4OCA==", "requires": { "color-diff": "^1.1.0", "concat-stream": "^1.6.2", @@ -6121,7 +6139,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", "dev": true }, "merge-stream": { @@ -6408,7 +6426,7 @@ "module-not-found-error": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", - "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", + "integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==", "dev": true }, "ms": { @@ -6476,11 +6494,6 @@ "semver": "^7.3.5" } }, - "node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" - }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -6717,7 +6730,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "requires": { "wrappy": "1" } @@ -6920,7 +6933,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, "path-key": { "version": "3.1.1", @@ -6982,7 +6995,7 @@ "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "picocolors": { "version": "1.0.0", @@ -6998,7 +7011,7 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true }, "pkg-dir": { @@ -7062,15 +7075,6 @@ "lodash": "^4.16.4" } }, - "png-img": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/png-img/-/png-img-5.0.1.tgz", - "integrity": "sha512-AsN9HL7oDdW5D6g+euoMmcGe6LImj09aaHUuABe+jcFkSQpo+JZ/L24LgV0RJrzfpUs29nLDk2+qpQctrC6Z3g==", - "requires": { - "node-addon-api": "^4.3.0", - "prebuild-install": "^7.0.0" - } - }, "pngjs": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", @@ -7098,7 +7102,7 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "dev": true }, "process": { @@ -7133,7 +7137,7 @@ "proxyquire": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-1.8.0.tgz", - "integrity": "sha1-AtUUpb7ZhvBMuyCTrxZ0FTX3ntw=", + "integrity": "sha512-mZZq4F50qaBkngvlf9paNfaSb5gtJ0mFPnBjda4NxCpXpMAaVfSLguRr9y2KXF6koOSBf4AanD2inuEQw3aCcA==", "dev": true, "requires": { "fill-keys": "^1.0.2", @@ -7144,7 +7148,7 @@ "resolve": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", "dev": true } } @@ -7152,7 +7156,7 @@ "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==" }, "psl": { "version": "1.8.0", @@ -7508,12 +7512,12 @@ "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==" }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, "require-from-string": { "version": "2.0.2", @@ -7545,7 +7549,7 @@ "resolve-from": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=" + "integrity": "sha512-kT10v4dhrlLNcnO084hEjvXCI1wUG9qZLoz2RogxqDQQYy7IxjI/iMUkOtQTNEh6rzHxvdQWHsJyel1pKOVCxg==" }, "resolve-global": { "version": "1.0.0", @@ -7668,7 +7672,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, "sha.js": { @@ -7680,6 +7684,28 @@ "safe-buffer": "^5.0.1" } }, + "sharp": { + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.7.tgz", + "integrity": "sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==", + "requires": { + "color": "^4.2.3", + "detect-libc": "^2.0.1", + "node-addon-api": "^5.0.0", + "prebuild-install": "^7.1.1", + "semver": "^7.3.7", + "simple-get": "^4.0.1", + "tar-fs": "^2.1.1", + "tunnel-agent": "^0.6.0" + }, + "dependencies": { + "node-addon-api": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" + } + } + }, "shasum": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", @@ -7738,6 +7764,21 @@ "simple-concat": "^1.0.0" } }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, "sinon": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", @@ -7884,7 +7925,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, "standard-version": { @@ -8262,7 +8303,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, "through2": { "version": "2.0.5", @@ -8289,7 +8330,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true }, "to-regex-range": { @@ -8339,7 +8380,7 @@ "traverse": { "version": "0.6.6", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", + "integrity": "sha512-kdf4JKs8lbARxWdp7RKdNzoJBhGUcIalSYibuGyHJbmk40pOysQ0+QPvlkCOICOivDWU2IJo2rkrxyTK2AH4fw==", "dev": true }, "trim-newlines": { @@ -8411,7 +8452,7 @@ "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", "dev": true, "requires": { "prelude-ls": "~1.1.2" @@ -8431,7 +8472,7 @@ "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "typedarray-to-buffer": { "version": "3.1.5", @@ -8634,7 +8675,7 @@ "update-section": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/update-section/-/update-section-0.3.3.tgz", - "integrity": "sha1-RY8Xgg03gg3GDiC4bZQ5GwASMVg=", + "integrity": "sha512-BpRZMZpgXLuTiKeiu7kK0nIPwGdyrqrs6EDSaXtjD/aQ2T+qVo9a5hRC3HN3iJjCMxNT/VxoLGQ7E/OzE5ucnw==", "dev": true }, "uri-js": { @@ -8686,7 +8727,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "uuid": { "version": "8.3.2", @@ -8996,7 +9037,7 @@ "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", "dev": true }, "window-size": { @@ -9043,7 +9084,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "write-file-atomic": { "version": "3.0.3", @@ -9188,7 +9229,7 @@ "yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/package.json b/package.json index cfe4063be..3b9273e0e 100644 --- a/package.json +++ b/package.json @@ -52,10 +52,10 @@ "glob-extra": "^5.0.2", "inherit": "^2.2.2", "lodash": "^4.17.21", - "looks-same": "^8.0.0", + "looks-same": "^8.0.1", "micromatch": "^4.0.5", "plugins-loader": "^1.1.0", - "png-img": "^5.0.1", + "sharp": "~0.30.7", "sizzle": "^2.3.6", "temp": "^0.8.3", "uglify-js": "^2.8.29", diff --git a/test/lib/core/browser/camera/index.js b/test/lib/core/browser/camera/index.js index 58504aa2d..5c533f2de 100644 --- a/test/lib/core/browser/camera/index.js +++ b/test/lib/core/browser/camera/index.js @@ -6,9 +6,14 @@ const utils = require('lib/core/browser/camera/utils'); describe('browser/camera', () => { const sandbox = sinon.sandbox.create(); + let image; beforeEach(() => { - sandbox.stub(Image, 'fromBase64'); + image = sinon.createStubInstance(Image); + image.getSize.resolves({width: 100500, height: 500100}); + image.crop.resolves(); + + sandbox.stub(Image, 'fromBase64').returns(image); }); afterEach(() => sandbox.restore()); @@ -18,90 +23,88 @@ describe('browser/camera', () => { const takeScreenshot = sinon.stub().resolves({foo: 'bar'}); const camera = Camera.create(null, takeScreenshot); - Image.fromBase64.withArgs({foo: 'bar'}).returns('foo bar'); + Image.fromBase64.withArgs({foo: 'bar'}).returns(image); - return assert.becomes(camera.captureViewportImage(), 'foo bar'); + return assert.becomes(camera.captureViewportImage(), image); }); describe('crop', () => { - let image, camera; - - beforeEach(() => { - image = sinon.createStubInstance(Image); - Image.fromBase64.returns(image); - }); - describe('calibration', () => { - beforeEach(() => { - camera = Camera.create(null, sinon.stub().resolves()); - }); - - it('should apply calibration on taken screenshot', () => { - image.getSize.returns({width: 100, height: 200}); + it('should apply calibration on taken screenshot', async () => { + const camera = Camera.create(null, sinon.stub().resolves()); + image.getSize.resolves({width: 10, height: 10}); camera.calibrate({top: 6, left: 4}); - - return camera.captureViewportImage() - .then(() => { - assert.calledWith(image.crop, { - left: 4, - top: 6, - width: 100 - 4, - height: 200 - 6 - }); - }); - }); - - it('should not apply calibration if camera is not calibrated', () => { - return camera.captureViewportImage() - .then(() => assert.notCalled(image.crop)); + await camera.captureViewportImage(); + + assert.calledOnceWith(image.crop, { + left: 4, + top: 6, + width: 10 - 4, + height: 10 - 6 + }); }); }); describe('crop to viewport', () => { + let page; + const mkCamera_ = (browserOptions) => { const screenshotMode = (browserOptions || {}).screenshotMode || 'auto'; return new Camera(screenshotMode, sinon.stub().resolves()); }; - const page = { - viewport: { - top: 1, - left: 1, - width: 100, - height: 100 - } - }; - beforeEach(() => { sandbox.stub(utils, 'isFullPage'); + + page = { + pixelRatio: 1, + viewport: { + left: 1, + top: 1, + width: 100, + height: 100 + } + }; }); - it('should not crop to viewport if page disposition was not passed', () => { - return mkCamera_().captureViewportImage() - .then(() => assert.notCalled(image.crop)); + it('should not crop to viewport if page disposition was not passed', async () => { + await mkCamera_().captureViewportImage(); + + assert.notCalled(image.crop); }); - it('should crop fullPage image with viewport value if page disposition was set', () => { - utils.isFullPage.withArgs(image, page, 'fullPage').returns(true); + it('should crop fullPage image with viewport value if page disposition was set', async () => { + utils.isFullPage.returns(true); - return mkCamera_({screenshotMode: 'fullPage'}).captureViewportImage(page) - .then(() => { - assert.calledWith(image.crop, page.viewport); - }); + await mkCamera_({screenshotMode: 'fullPage'}).captureViewportImage(page); + + assert.calledOnceWith(image.crop, page.viewport); }); - it('should crop not fullPage image to the left and right', () => { - utils.isFullPage.withArgs(image, page, 'viewport').returns(false); - - return mkCamera_({screenshotMode: 'viewport'}).captureViewportImage(page) - .then(() => { - assert.calledWith(image.crop, { - top: 0, left: 0, - width: page.viewport.width, - height: page.viewport.height - }); - }); + it('should crop not fullPage image to the left and right', async () => { + utils.isFullPage.returns(false); + + await mkCamera_({screenshotMode: 'viewport'}).captureViewportImage(page); + + assert.calledOnceWith(image.crop, { + left: 0, top: 0, + height: page.viewport.height, + width: page.viewport.width + }); + }); + + it('should crop considering pixel ratio', async () => { + utils.isFullPage.returns(true); + + const scaledPage = { + pixelRatio: 2, + viewport: {left: 2, top: 3, width: 10, height: 12} + }; + + await mkCamera_({screenshotMode: 'fullPage'}).captureViewportImage(scaledPage); + + assert.calledOnceWith(image.crop, {left: 4, top: 6, width: 20, height: 24}); }); }); }); diff --git a/test/lib/core/browser/camera/utils.js b/test/lib/core/browser/camera/utils.js deleted file mode 100644 index 36f78691a..000000000 --- a/test/lib/core/browser/camera/utils.js +++ /dev/null @@ -1,94 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const Image = require('lib/core/image'); -const utils = require('lib/core/browser/camera/utils'); - -describe('camera utils.isFullPage', () => { - const sandbox = sinon.sandbox.create(); - - afterEach(() => sandbox.restore()); - - const isFullPage_ = (image, browserOpts, page) => { - page = _.defaults(page || {}, { - documentWidth: 100, - documentHeight: 100, - pixelRatio: 1 - }); - - return utils.isFullPage(image, page, browserOpts.screenshotMode); - }; - - const imageStub_ = (imageSize) => { - const imageStub = sinon.createStubInstance(Image); - imageStub.getSize.returns(imageSize); - return imageStub; - }; - - it('should return true for "fullpage" screenshotMode', () => { - const image = imageStub_({width: 100, height: 100}); - const result = isFullPage_(image, {screenshotMode: 'fullpage'}); - - assert.isTrue(result); - }); - - it('should return false for "viewport" screenshotMode', () => { - const image = imageStub_({width: 100, height: 100}); - const result = isFullPage_(image, {screenshotMode: 'viewport'}); - - assert.isFalse(result); - }); - - describe('"auto" screenshotMode', () => { - it('should return true if image size is bigger than document size', () => { - const image = imageStub_({width: 100, height: 100}); - const result = isFullPage_(image, {screenshotMode: 'auto'}, { - documentWidth: 99, - documentHeight: 99 - }); - - assert.isTrue(result); - }); - - it('should return true if image size and document size are the same', () => { - const image = imageStub_({width: 100, height: 100}); - const result = isFullPage_(image, {screenshotMode: 'auto'}, { - documentWidth: 100, - documentHeight: 100 - }); - - assert.isTrue(result); - }); - - it('should return false if image width is smaller than document width', () => { - const image = imageStub_({width: 100, height: 100}); - const result = isFullPage_(image, {screenshotMode: 'auto'}, { - documentWidth: 101, - documentHeight: 100 - }); - - assert.isFalse(result); - }); - - it('should return false if image height is smaller than document height', () => { - const image = imageStub_({width: 100, height: 100}); - const result = isFullPage_(image, {screenshotMode: 'auto'}, { - documentWidth: 100, - documentHeight: 101 - }); - - assert.isFalse(result); - }); - - it('should apply scale for document size if usePixelRatio was set', () => { - const image = imageStub_({width: 100, height: 100}); - const result = isFullPage_(image, {screenshotMode: 'auto', usePixelRatio: true}, { - documentWidth: 51, - documentHeight: 51, - pixelRatio: 2 - }); - - assert.isFalse(result); - }); - }); -}); diff --git a/test/lib/core/browser/client-scripts/rect.js b/test/lib/core/browser/client-scripts/rect.js index 9e0eb61aa..5277d5e39 100644 --- a/test/lib/core/browser/client-scripts/rect.js +++ b/test/lib/core/browser/client-scripts/rect.js @@ -4,8 +4,8 @@ const Rect = require('lib/core/browser/client-scripts/rect').Rect; describe('Rect', () => { const rect = new Rect({ - top: 10, left: 20, + top: 10, width: 100, height: 100 }); @@ -14,8 +14,8 @@ describe('Rect', () => { it('should create instance using width/height properties', () => { assert.doesNotThrow(() => { return new Rect({ - top: 10, left: 20, + top: 10, width: 100, height: 100 }); @@ -124,8 +124,8 @@ describe('Rect', () => { it('should return true when rect is inside', () => { assert.isTrue(rect.rectInside( new Rect({ - top: rect.top + 10, left: rect.left + 10, + top: rect.top + 10, width: rect.width - 50, height: rect.height - 50 }) @@ -135,8 +135,8 @@ describe('Rect', () => { it('should return false when rect is not inside', () => { assert.isFalse(rect.rectInside( new Rect({ - top: rect.top - 5, left: rect.left - 5, + top: rect.top - 5, width: rect.width, height: rect.width }) @@ -146,8 +146,8 @@ describe('Rect', () => { it('should return false when rect intersects on top-left', () => { assert.isFalse(rect.rectInside( new Rect({ - top: rect.top + 5, left: rect.left - 5, + top: rect.top + 5, width: rect.width + 5, height: rect.height - 5 }) @@ -156,8 +156,8 @@ describe('Rect', () => { it('should return false when rect intersects on bottom-right', () => { assert.isFalse(new Rect({ - top: rect.top + 5, left: rect.left - 5, + top: rect.top + 5, width: rect.width + 5, height: rect.height - 5 }).rectInside(rect)); @@ -169,8 +169,8 @@ describe('Rect', () => { it('intersects on left side', () => { assert.isTrue(rect.rectIntersects( new Rect({ - top: rect.top + 5, left: rect.left - 5, + top: rect.top + 5, width: rect.width - 5, height: rect.height - 5 }) @@ -180,8 +180,8 @@ describe('Rect', () => { it('intersects on top side', () => { assert.isTrue(rect.rectIntersects( new Rect({ - top: rect.top + 5, left: rect.left + 5, + top: rect.top + 5, width: rect.width - 5, height: rect.height + 5 }) @@ -191,8 +191,8 @@ describe('Rect', () => { it('intersects on right side', () => { assert.isTrue(rect.rectIntersects( new Rect({ - top: rect.top + 5, left: rect.left + 5, + top: rect.top + 5, width: rect.width + 5, height: rect.height - 5 }) @@ -202,8 +202,8 @@ describe('Rect', () => { it('intersects on bottom side', () => { assert.isTrue(rect.rectIntersects( new Rect({ - top: rect.top - 5, left: rect.left + 5, + top: rect.top - 5, width: rect.width - 5, height: rect.height - 5 }) @@ -213,8 +213,8 @@ describe('Rect', () => { it('intersects on left and right sides', () => { assert.isTrue(rect.rectIntersects( new Rect({ - top: rect.top + 5, left: rect.left - 5, + top: rect.top + 5, width: rect.width + 5, height: rect.height - 5 }) @@ -226,8 +226,8 @@ describe('Rect', () => { it('top', () => { assert.isFalse(rect.rectIntersects( new Rect({ - top: rect.top - 1, left: rect.left + 1, + top: rect.top - 1, width: 1, height: 1 }) @@ -237,8 +237,8 @@ describe('Rect', () => { it('left', () => { assert.isFalse(rect.rectIntersects( new Rect({ - top: rect.top + 1, left: rect.left - 1, + top: rect.top + 1, width: 1, height: 1 }) @@ -248,8 +248,8 @@ describe('Rect', () => { it('bottom', () => { assert.isFalse(rect.rectIntersects( new Rect({ - top: rect.top + rect.height, left: rect.left + 1, + top: rect.top + rect.height, width: 1, height: 1 }) @@ -259,8 +259,8 @@ describe('Rect', () => { it('right', () => { assert.isFalse(rect.rectIntersects( new Rect({ - top: rect.top + 1, left: rect.left + rect.width, + top: rect.top + 1, width: 1, height: 1 }) diff --git a/test/lib/core/calibrator/index.js b/test/lib/core/calibrator/index.js index fc9b9e8d5..845ab5e13 100644 --- a/test/lib/core/calibrator/index.js +++ b/test/lib/core/calibrator/index.js @@ -39,50 +39,46 @@ describe('calibrator', () => { {data: {innerWidth: 984}, name: 'wd'}, {data: {value: {innerWidth: 984}}, name: 'webdriverio'} ].forEach(({data, name}) => { - it(`should calculate correct crop area for ${name}`, () => { + it(`should calculate correct crop area for ${name}`, async () => { setScreenshot('calibrate.png'); browser.evalScript.returns(Promise.resolve(data)); - const result = calibrator.calibrate(browser); + const result = await calibrator.calibrate(browser); - return Promise.all([ - assert.eventually.propertyVal(result, 'top', 2), - assert.eventually.propertyVal(result, 'left', 2) - ]); + assert.match(result.top, 2); + assert.match(result.left, 2); }); }); - it('should return also features detected by script', () => { + it('should return also features detected by script', async () => { setScreenshot('calibrate.png'); browser.evalScript.returns(Promise.resolve({feature: 'value', innerWidth: 984})); - const result = calibrator.calibrate(browser); + const result = await calibrator.calibrate(browser); - return assert.eventually.propertyVal(result, 'feature', 'value'); + assert.match(result.feature, 'value'); }); - it('should not perform the calibration process two times', () => { + it('should not perform the calibration process two times', async () => { setScreenshot('calibrate.png'); - return calibrator.calibrate(browser) - .then(() => calibrator.calibrate(browser)) - .then(() => { - assert.calledOnce(browser.open); - assert.calledOnce(browser.evalScript); - assert.calledOnce(browser.captureViewportImage); - }); + await calibrator.calibrate(browser); + await calibrator.calibrate(browser); + + assert.calledOnce(browser.open); + assert.calledOnce(browser.evalScript); + assert.calledOnce(browser.captureViewportImage); }); - it('should return cached result second time', () => { + it('should return cached result second time', async () => { setScreenshot('calibrate.png'); - const result = calibrator.calibrate(browser) - .then(() => calibrator.calibrate(browser)); + const result = await calibrator.calibrate(browser); + + await calibrator.calibrate(browser); - return Promise.all([ - assert.eventually.propertyVal(result, 'top', 2), - assert.eventually.propertyVal(result, 'left', 2) - ]); + assert.match(result.top, 2); + assert.match(result.left, 2); }); it('should fail on broken calibration page', () => { diff --git a/test/lib/core/image/index.js b/test/lib/core/image/index.js index 4da74df6c..a4cf5fb9b 100644 --- a/test/lib/core/image/index.js +++ b/test/lib/core/image/index.js @@ -1,184 +1,302 @@ 'use strict'; -const {PngImg} = require('png-img'); -const utils = require('png-img/utils'); const proxyquire = require('proxyquire'); -const looksSameStub = sinon.stub(); -const Image = proxyquire('lib/core/image', { - 'looks-same': looksSameStub -}); -const SafeRect = require('lib/core/image/safe-rect'); - describe('Image', () => { const sandbox = sinon.sandbox.create(); + let Image; let image; + let looksSameStub; + let sharpStub; + let mkSharpInstance; + + const mkSharpStub_ = () => { + const stub = {}; + + stub.metadata = sandbox.stub().resolves({channels: 3, width: 100500, height: 500100}); + stub.toBuffer = sandbox.stub().resolves({data: 'buffer', info: {channels: 3, width: 100500, height: 500100}}); + stub.extract = sandbox.stub().returns(stub); + stub.composite = sandbox.stub().returns(stub); + stub.resize = sandbox.stub().returns(stub); + stub.toFile = sandbox.stub().resolves(); + stub.raw = () => stub; + stub.png = () => stub; + + return stub; + }; + + const transformAreaToClearData_ = ({top, left, height, width, channels}) => ({ + top, + left, + input: { + create: { + background: {alpha: 1, b: 0, g: 0, r: 0}, + channels, + height, + width + } + } + }); beforeEach(() => { - // Image 1x1 pixel - const bufferString = `iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAA - CQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjw - v8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSUR - BVBhXY2BgYAAAAAQAAVzN/2kAAAAASUVORK5CYII=`; - const imgBuffer = new Buffer(bufferString, 'base64'); - - image = Image.create(imgBuffer); + looksSameStub = sandbox.stub(); + sharpStub = mkSharpStub_(); + mkSharpInstance = sandbox.stub().callsFake(() => sharpStub); + Image = proxyquire('lib/core/image', { + 'looks-same': looksSameStub, + 'sharp': mkSharpInstance + }); - sandbox.stub(PngImg.prototype, 'size').returns({width: 100500, height: 500100}); + image = Image.create('imgBuffer'); }); afterEach(() => sandbox.restore()); - describe('crop', () => { + describe('getSize', () => { beforeEach(() => { - sandbox.stub(SafeRect, 'create').returns({}); + sharpStub.metadata.resolves({width: 15, height: 12}); + sharpStub.toBuffer.resolves({data: 'buffer', info: {width: 15, height: 12}}); + }); - sandbox.stub(PngImg.prototype, 'crop'); + it('should return image size', async () => { + const size = await image.getSize(); + + assert.deepEqual(size, {width: 15, height: 12}); }); - it('should create instance of "SafeRect"', () => { - const rect = {left: 1, top: 2, width: 3, height: 4}; + it('should return updated image size after composite with another image', async () => { + image.addJoin(image); + await image.applyJoin(); + const size = await image.getSize(); - return image.crop(rect) - .then(() => { - assert.calledOnce(SafeRect.create); - assert.calledWith(SafeRect.create, rect, image.getSize()); - }); + assert.deepEqual(size, {width: 15, height: 12 * 2}); }); + }); - it('should consider scale factor before creating of instance of "SafeRect"', () => { - const rect = {left: 1, top: 2, width: 3, height: 4}; - const scaledRect = {left: 2, top: 4, width: 6, height: 8}; + describe('crop', () => { + it('should recreate image instance from buffer after crop', async () => { + sharpStub.toBuffer.resolves({data: 'croppedBuffer', info: {channels: 3, width: 10, height: 15}}); + await image.crop({left: 20, top: 10, width: 40, height: 30}); + + assert.calledTwice(mkSharpInstance); + assert.calledWithMatch(mkSharpInstance.secondCall, 'croppedBuffer', { + raw: { + width: 10, + height: 15, + channels: 3 + } + }); + }); + + it('should extract area from image', async () => { + const area = {left: 20, top: 10, width: 40, height: 30}; - return image.crop(rect, {scaleFactor: 2}) - .then(() => assert.calledWith(SafeRect.create, scaledRect)); + await image.crop(area); + + assert.calledOnceWith(sharpStub.extract, area); }); - it('should use default scale factor (one) if the passed value is not positive', () => { - const rect = {left: 1, top: 2, width: 3, height: 4}; + it('should consider image sizes', async () => { + sharpStub.metadata.resolves({width: 10, height: 10}); + + await image.crop({left: 3, top: 3, width: 10, height: 10}); - return image.crop(rect, {scaleFactor: null}) - .then(() => assert.calledWith(SafeRect.create, rect)); + assert.calledOnceWith(sharpStub.extract, {left: 3, top: 3, width: 7, height: 7}); }); + }); + + describe('should clear', () => { + it('a region of an image', async () => { + sharpStub.metadata.resolves({channels: 4}); + const clearArea = {left: 20, top: 10, width: 40, height: 30}; - it('should crop an image', () => { - SafeRect.create.returns({left: 1, top: 2, width: 3, height: 4}); + await image.addClear(clearArea); + image.applyClear(); - return image.crop({}) - .then(() => { - assert.calledOnce(PngImg.prototype.crop); - assert.calledWith(PngImg.prototype.crop, 1, 2, 3, 4); - }); + assert.calledOnceWith(sharpStub.composite, [transformAreaToClearData_({...clearArea, channels: 4})]); }); - it('should be resolved with the same instance of "Image" after crop', () => { - return image.crop({}) - .then((res) => assert.deepEqual(res, image)); + it('multiple regions of an image', async () => { + sharpStub.metadata.resolves({channels: 3}); + const firstArea = {left: 20, top: 10, width: 40, height: 30}; + const secondArea = {left: 70, top: 50, width: 200, height: 100}; + + await image.addClear(firstArea); + await image.addClear(secondArea); + image.applyClear(); + + assert.calledOnceWith(sharpStub.composite, [ + transformAreaToClearData_({...firstArea, channels: 3}), + transformAreaToClearData_({...secondArea, channels: 3}) + ]); }); }); - it('should return correct size', () => { - PngImg.prototype.size.returns({w: 10, h: 20}); - const result = image.getSize(); + describe('composite images', () => { + let image2; - assert.deepEqual(result, {w: 10, h: 20}); - }); + beforeEach(() => { + sharpStub.toBuffer.resolves({data: 'buf', info: {width: 12, height: 7, channels: 3}}); + sharpStub.metadata.resolves({width: 12, height: 7}); - it('should return RGBA', () => { - const color = {r: 123, g: 234, b: 231, a: 132}; + const sharpStub2 = mkSharpStub_(); + sharpStub2.toBuffer.resolves({data: 'buf2', info: {width: 12, height: 5, channels: 3}}); + sharpStub2.metadata.resolves({width: 12, height: 5}); + mkSharpInstance.withArgs('buf2').returns(sharpStub2); - sandbox.stub(PngImg.prototype, 'get').returns(color); - const result = image.getRGBA(0, 0); + image2 = Image.create('buf2'); + }); - assert.calledWith(PngImg.prototype.get, 0, 0); - assert.deepEqual(result, color); - }); + it('should resize image before join', async () => { + image.addJoin(image2); + await image.applyJoin(); + + assert.callOrder(sharpStub.resize, sharpStub.composite); + assert.calledOnceWith(sharpStub.resize, { + width: 12, + height: 7 + 5, + fit: 'contain', + position: 'top' + }); + }); - it('should clear a region of an image according to scale factor', () => { - sandbox.stub(PngImg.prototype, 'fill'); + it('should composite images with incrementing top offset', async () => { + image.addJoin(image2); + image.addJoin(image2); + await image.applyJoin(); + + assert.calledOnce(sharpStub.composite); + assert.calledWithMatch(sharpStub.composite, [ + {input: 'buf2', left: 0, top: 7, raw: {width: 12, height: 5, channels: 3}}, + {input: 'buf2', left: 0, top: 12, raw: {width: 12, height: 5, channels: 3}} + ]); + }); + + it('should composite images after resize', async () => { + image.addJoin(image2); + await image.applyJoin(); + + assert.callOrder(sharpStub.resize, sharpStub.composite); + }); + + it('should update size after join', async () => { + image.addJoin(image2); + await image.applyJoin(); + + assert.becomes(image.getSize(), {width: 12, height: 7 + 5}); + }); - const clearArea = {top: 10, left: 20, width: 40, height: 30}; - const scaleOpts = {scaleFactor: 5}; + it('should not resize image if no images were added to join', async () => { + const oldSize = await image.getSize(); - image.clear(clearArea, scaleOpts); + await image.applyJoin(); + const newSize = await image.getSize(); - assert.calledWith(PngImg.prototype.fill, 20 * 5, 10 * 5, 40 * 5, 30 * 5, '#000000'); + assert.match(newSize, oldSize); + assert.notCalled(sharpStub.composite); + assert.notCalled(sharpStub.resize); + }); }); - it('should convert RGB to string', () => { - sandbox.stub(utils, 'RGBToString'); + describe('getRGBA', () => { + beforeEach(() => { + sharpStub.toBuffer.resolves({ + data: Buffer.from([1, 2, 3, 4, 5, 6]), + info: { + channels: 3, + width: 2 + } + }); + }); - const rgb = {r: 111, g: 222, b: 123}; + it('should return RGBA pixel', async () => { + const pixel = await image.getRGBA(1, 0); - Image.RGBToString(rgb); + assert.match(pixel, {r: 4, g: 5, b: 6, a: 1}); + }); - assert.calledWith(utils.RGBToString, rgb); + it('should call "toBuffer" once', async () => { + await image.getRGBA(0, 1); + await image.getRGBA(0, 2); + + assert.calledOnce(sharpStub.toBuffer); + }); }); - it('should save file', () => { - const stubSave = sandbox.stub(PngImg.prototype, 'save').resolves(); + it('should save image', async () => { + const fileName = 'image.png'; - return image.save('some/path') - .then(() => { - assert.calledOnce(stubSave); - assert.calledWith(stubSave, 'some/path'); - }); + await image.save(fileName); + + assert.calledOnceWith(sharpStub.toFile, fileName); }); - it('should join new image to current image', () => { - sandbox.stub(PngImg.prototype, 'insert'); - sandbox.stub(PngImg.prototype, 'setSize').returns(PngImg.prototype); - PngImg.prototype.size.returns({width: 150, height: 200}); + it('should create image from base64', () => { + const base64 = 'base64 image'; + + Image.fromBase64(base64); + + assert.calledWith(mkSharpInstance, Buffer.from(base64, 'base64')); + }); + + describe('toPngBuffer', () => { + beforeEach(() => { + sharpStub.toBuffer.resolves({data: 'pngBuffer', info: {width: 15, height: 10, channels: 3}}); + sharpStub.toBuffer.withArgs({resolveWithObject: false}).resolves('pngBuffer'); + }); + + it('should resolve png buffer with object', async () => { + const buffObj = await image.toPngBuffer(); + + assert.deepEqual(buffObj, {data: 'pngBuffer', size: {width: 15, height: 10}}); + }); - image.join(image); + it('should resolve png buffer without object', async () => { + const buffer = await image.toPngBuffer({resolveWithObject: false}); - assert.calledWith(PngImg.prototype.setSize, 150, 200 + 200); - assert.calledWith(PngImg.prototype.insert, sinon.match.any, 0, 200); + assert.equal(buffer, 'pngBuffer'); + }); }); - it('should compare two images', () => { + it('should compare two images', async () => { looksSameStub.resolves(); - return Image.compare('some/path', 'other/path', { + await Image.compare('some/path', 'other/path', { canHaveCaret: true, pixelRatio: 11, tolerance: 250, antialiasingTolerance: 100500, compareOpts: {stopOnFirstFail: true} - }) - .then(() => { - assert.calledOnce(looksSameStub); - assert.calledWith(looksSameStub, 'some/path', 'other/path', { - ignoreCaret: true, - pixelRatio: 11, - tolerance: 250, - antialiasingTolerance: 100500, - stopOnFirstFail: true - }); - }); + }); + + assert.calledOnceWith(looksSameStub, 'some/path', 'other/path', { + ignoreCaret: true, + pixelRatio: 11, + tolerance: 250, + antialiasingTolerance: 100500, + stopOnFirstFail: true + }); }); - it('should build diff image', () => { + it('should build diff image', async () => { const createDiffStub = sinon.stub(); looksSameStub.createDiff = createDiffStub; createDiffStub.resolves(); - return Image.buildDiff({ + await Image.buildDiff({ reference: 100, current: 200, diff: 500, tolerance: 300, diffColor: 400 - }) - .then(() => { - assert.calledOnce(createDiffStub); - assert.calledWith(createDiffStub, { - reference: 100, - current: 200, - diff: 500, - tolerance: 300, - highlightColor: 400 - }); - }); + }); + + assert.calledOnceWith(createDiffStub, { + reference: 100, + current: 200, + diff: 500, + tolerance: 300, + highlightColor: 400 + }); }); }); diff --git a/test/lib/core/image/safe-rect.js b/test/lib/core/image/safe-rect.js deleted file mode 100644 index 858c346a1..000000000 --- a/test/lib/core/image/safe-rect.js +++ /dev/null @@ -1,97 +0,0 @@ -'use strict'; - -const SafeRect = require('lib/core/image/safe-rect'); - -describe('SafeRect', () => { - describe('left', () => { - it('should return rectangle left coordinate if it is a positive number', () => { - const safeRect = SafeRect.create({left: 1}); - - assert.equal(safeRect.left, 1); - }); - - it('should handle cases when rectangle left coordinate is a negative number', () => { - const safeRect = SafeRect.create({left: -1}); - - assert.equal(safeRect.left, 0); - }); - - it('should handle cases when rectangle left coordinate is 0', () => { - const safeRect = SafeRect.create({left: 0}); - - assert.equal(safeRect.left, 0); - }); - }); - - describe('top', () => { - it('should return rectangle top coordinate if it is a positive number', () => { - const safeRect = SafeRect.create({top: 1}); - - assert.equal(safeRect.top, 1); - }); - - it('should handle cases when rectangle top coordinate is a negative number', () => { - const safeRect = SafeRect.create({top: -1}); - - assert.equal(safeRect.top, 0); - }); - - it('should handle cases when rectangle top coordinate is 0', () => { - const safeRect = SafeRect.create({top: 0}); - - assert.equal(safeRect.top, 0); - }); - }); - - describe('width', () => { - it('should return rectangle width when its position is not out of the bounds of image width', () => { - const safeRect = SafeRect.create({left: 1, width: 2}, {width: 4}); - - assert.equal(safeRect.width, 2); - }); - - it('should return rectangle width when its position is at the bounds of image width', () => { - const safeRect = SafeRect.create({left: 1, width: 3}, {width: 4}); - - assert.equal(safeRect.width, 3); - }); - - it('should handle cases when rectangle position is out of the bounds of image width', () => { - const safeRect = SafeRect.create({left: 1, width: 5}, {width: 4}); - - assert.equal(safeRect.width, 4 - 1); - }); - - it('should handle cases when rectangle left coordinate is a negative number', () => { - const safeRect = SafeRect.create({left: -1, width: 5}, {width: 4}); - - assert.equal(safeRect.width, 4); - }); - }); - - describe('height', () => { - it('should return rectangle height when its position is not out of the bounds of image height', () => { - const safeRect = SafeRect.create({top: 1, height: 2}, {height: 4}); - - assert.equal(safeRect.height, 2); - }); - - it('should return rectangle height when its position is at the bounds of image height', () => { - const safeRect = SafeRect.create({top: 1, height: 3}, {height: 4}); - - assert.equal(safeRect.height, 3); - }); - - it('should handle cases when rectangle position is out of the bounds of image height', () => { - const safeRect = SafeRect.create({top: 1, height: 5}, {height: 4}); - - assert.equal(safeRect.height, 4 - 1); - }); - - it('should handle cases when rectangle top coordinate is a negative number', () => { - const safeRect = SafeRect.create({top: -1, height: 5}, {height: 4}); - - assert.equal(safeRect.height, 4); - }); - }); -}); diff --git a/test/lib/core/screen-shooter/index.js b/test/lib/core/screen-shooter/index.js index 2261b2f1a..2afc4fbe5 100644 --- a/test/lib/core/screen-shooter/index.js +++ b/test/lib/core/screen-shooter/index.js @@ -9,51 +9,59 @@ describe('screen-shooter', () => { beforeEach(() => { sandbox.spy(Viewport, 'create'); - sandbox.stub(Viewport.prototype, 'crop'); sandbox.stub(Viewport.prototype, 'ignoreAreas'); - sandbox.spy(Viewport.prototype, 'extendBy'); + sandbox.stub(Viewport.prototype, 'composite'); + sandbox.stub(Viewport.prototype, 'handleImage'); + sandbox.stub(Viewport.prototype, 'extendBy'); + sandbox.stub(Viewport.prototype, 'validate'); }); afterEach(() => sandbox.restore()); describe('capture', () => { let browser; + const imageStub = sinon.createStubInstance(Image); - const stubPage = (page) => Object.assign({viewport: {}, captureArea: {}}, page); + const stubPage = (page) => Object.assign({viewport: {}, captureArea: {}, ignoreAreas: [], pixelRatio: 1}, page); const capture = (page, opts) => ScreenShooter.create(browser).capture(stubPage(page), opts); beforeEach(() => { browser = { config: {}, - captureViewportImage: sandbox.stub().resolves(), + captureViewportImage: sandbox.stub().resolves(imageStub), scrollBy: sandbox.stub().resolves() }; }); - it('should take vieport image', () => { - return capture({viewport: 'foo', captureArea: 'bar'}) - .then(() => assert.calledOnceWith(browser.captureViewportImage, sinon.match({viewport: 'foo', captureArea: 'bar'}))); + it('should take vieport image', async () => { + await capture({viewport: 'foo', captureArea: 'bar'}); + + assert.calledOnceWith(browser.captureViewportImage, sinon.match({viewport: 'foo', captureArea: 'bar'})); + }); + + it('should process image with Viewport.handleImage', async () => { + await capture({viewport: 'foo', captureArea: 'bar'}); + + assert.calledOnceWith(Viewport.prototype.handleImage, imageStub); }); describe('should create Viewport instance', () => { - it('with viewport data', async () => { + it('with viewport page', async () => { await capture({viewport: 'foo'}); - assert.calledOnceWith(Viewport.create, 'foo'); + assert.calledOnceWith(Viewport.create, {captureArea: {}, ignoreAreas: [], pixelRatio: 1, viewport: 'foo'}); }); it('with viewport image', async () => { - browser.captureViewportImage.resolves({bar: 'baz'}); - await capture(); - assert.calledOnceWith(Viewport.create, sinon.match.any, {bar: 'baz'}); + assert.calledOnceWith(Viewport.create, sinon.match.any, imageStub); }); it('with pixelRatio data', async () => { - await capture({pixelRatio: 'qux'}); + await capture({pixelRatio: 100500}); - assert.calledOnceWith(Viewport.create, sinon.match.any, sinon.match.any, 'qux'); + assert.calledOnceWith(Viewport.create, {captureArea: {}, ignoreAreas: [], pixelRatio: 100500, viewport: {}}); }); ['allowViewportOverflow', 'compositeImage'].forEach((option) => { @@ -61,7 +69,7 @@ describe('screen-shooter', () => { await capture({}, {[option]: true}); assert.calledOnceWith( - Viewport.create, sinon.match.any, sinon.match.any, sinon.match.any, sinon.match({[option]: true}) + Viewport.create, sinon.match.any, sinon.match.any, sinon.match({[option]: true}) ); }); }); @@ -73,27 +81,23 @@ describe('screen-shooter', () => { assert.calledWithMatch(browser.captureViewportImage, sinon.match.any, 2000); }); - it('should crop image of passed size', () => { - return capture({captureArea: {foo: 'bar'}}) - .then(() => assert.calledOnceWith(Viewport.prototype.crop, {foo: 'bar'})); - }); + it('should extract image of passed size', async () => { + await capture({captureArea: {foo: 'bar'}}); - it('should clear configured ignore areas', () => { - return capture({ignoreAreas: {foo: 'bar'}}) - .then(() => assert.calledWith(Viewport.prototype.ignoreAreas, {foo: 'bar'})); + assert.calledOnceWith(Viewport.prototype.composite); }); - it('should return croped image', () => { - Viewport.prototype.crop.resolves({foo: 'bar'}); + it('should return composited image', () => { + Viewport.prototype.composite.resolves({foo: 'bar'}); return assert.becomes(capture(), {foo: 'bar'}); }); describe('if validation fails', () => { describe('with NOT `HeightViewportError`', () => { - it('should not crop image', () => { + it('should not extract image', () => { return capture({captureArea: {top: -1}}) - .catch(() => assert.notCalled(Viewport.prototype.crop)); + .catch(() => assert.notCalled(Viewport.prototype.extract)); }); }); @@ -107,18 +111,13 @@ describe('screen-shooter', () => { }); describe('option "compositeImage" is switched on', () => { - let image; - beforeEach(() => { - image = sinon.createStubInstance(Image); - image.crop.resolves({}); - image.getSize.returns({}); - image.save.resolves(); - - browser.captureViewportImage.resolves(image); + Viewport.prototype.validate + .onFirstCall().returns(true) + .onSecondCall().returns(false); }); - it('should scroll vertically if capture area is higher then viewport', async () => { + it('should scroll vertically if capture area is higher than viewport', async () => { const page = {captureArea: {top: 0, height: 7}, viewport: {top: 0, height: 5}}; await capture(page, {compositeImage: true}); @@ -136,6 +135,13 @@ describe('screen-shooter', () => { it('should scroll vertically until the end of capture area', async () => { const page = {captureArea: {top: 0, height: 11}, viewport: {top: 0, height: 5}}; + Viewport.prototype.validate + .onFirstCall().returns(true) + .onSecondCall().returns(true) + .onThirdCall().returns(false); + sandbox.stub(Viewport.prototype, 'getVerticalOverflow') + .onFirstCall().returns(6) + .onSecondCall().returns(1); await capture(page, {compositeImage: true}); @@ -155,8 +161,10 @@ describe('screen-shooter', () => { // Test does not fairly check that `captureViewportImage` was called after resolving of `scrollBy` it('should capture viewport image after scroll', async () => { const page = {captureArea: {top: 0, height: 7}, viewport: {top: 0, height: 5}}; - const scrolledPage = {captureArea: {top: 0, height: 7}, viewport: {top: 2, height: 5}}; - + const scrolledPage = { + captureArea: {top: 0, height: 7}, viewport: {top: 2, height: 5}, + ignoreAreas: [], pixelRatio: 1 + }; const captureViewportImage = browser.captureViewportImage.withArgs(scrolledPage).named('captureViewportImage'); const scroll = browser.scrollBy.withArgs({x: 0, y: 2, selector: undefined}).named('scroll'); @@ -168,8 +176,7 @@ describe('screen-shooter', () => { it('should extend original image by scrolled viewport image', async () => { const page = {captureArea: {top: 0, height: 7}, viewport: {top: 0, height: 5}}; const scrolledPage = {captureArea: {top: 0, height: 7}, viewport: {top: 2, height: 5}}; - const scrolledViewportScreenshot = image; - + const scrolledViewportScreenshot = imageStub; browser.captureViewportImage.withArgs(scrolledPage).returns(Promise.resolve(scrolledViewportScreenshot)); await capture(page, {compositeImage: true}); @@ -177,22 +184,8 @@ describe('screen-shooter', () => { assert.calledOnceWith(Viewport.prototype.extendBy, 2, scrolledViewportScreenshot); }); - it('should crop capture area which is higher then viewport', async () => { - const page = {captureArea: {top: 0, height: 7}, viewport: {top: 0, height: 5}}; - - await capture(page, {compositeImage: true}); - - assert.calledOnceWith(Viewport.prototype.crop, page.captureArea); - }); - - it('should clear configured ignore areas', async () => { - await capture({ignoreAreas: {foo: 'bar'}}, {compositeImage: true}); - - assert.calledWith(Viewport.prototype.ignoreAreas, {foo: 'bar'}); - }); - - it('should return cropped image', () => { - Viewport.prototype.crop.resolves('foo bar'); + it('should return composed image', () => { + Viewport.prototype.composite.resolves('foo bar'); return assert.becomes(capture(), 'foo bar'); }); diff --git a/test/lib/core/screen-shooter/viewport/coord-validator.js b/test/lib/core/screen-shooter/viewport/coord-validator.js index 63d25e674..9b7a0c1c8 100644 --- a/test/lib/core/screen-shooter/viewport/coord-validator.js +++ b/test/lib/core/screen-shooter/viewport/coord-validator.js @@ -68,6 +68,12 @@ describe('CoordValidator', () => { assert.doesNotThrow(() => validate_({left: -1})); }); + it('should return "true" if crop area height bigger than viewport height and "compositeImage" is set', () => { + coordValidator = new CoordValidator({id: 'some-browser-id'}, {compositeImage: true}); + + assert.equal(validate_({height: +1}), true); + }); + it('should not throw on passed validation', () => { const fn = () => validate_({left: 0}); diff --git a/test/lib/core/screen-shooter/viewport/index.js b/test/lib/core/screen-shooter/viewport/index.js index 20cd3d697..fe070a8f4 100644 --- a/test/lib/core/screen-shooter/viewport/index.js +++ b/test/lib/core/screen-shooter/viewport/index.js @@ -1,7 +1,6 @@ 'use strict'; const _ = require('lodash'); -const Promise = require('bluebird'); const Image = require('lib/core/image'); const Viewport = require('lib/core/screen-shooter/viewport'); @@ -9,30 +8,39 @@ const CoordValidator = require('lib/core/screen-shooter/viewport/coord-validator describe('Viewport', () => { const sandbox = sinon.sandbox.create(); - - const createViewport = (opts) => new Viewport( - opts.coords || {top: 0, left: 0}, - opts.image, - opts.pixelRatio, + let image; + + const createViewport = (opts = {}) => new Viewport( + { + pixelRatio: opts.pixelRatio || 1, + captureArea: opts.captureArea || {}, + viewport: opts.viewport || {}, + ignoreAreas: opts.ignoreAreas || [] + }, + opts.image || image, {allowViewportOverflow: opts.allowViewportOverflow, compositeImage: opts.compositeImage} ); + beforeEach(() => { + image = sandbox.createStubInstance(Image); + image.getSize.resolves({width: 100500, height: 500100}); + }); + afterEach(() => sandbox.restore()); describe('validate', () => { const validate = (opts) => { opts = _.defaults(opts || {}, { - viewport: { - allowViewportOverflow: opts.allowViewportOverflow, - compositeImage: opts.compositeImage - }, - captureArea: {}, + captureArea: opts.captureArea, + viewport: opts.viewport, + allowViewportOverflow: opts.allowViewportOverflow, + compositeImage: opts.compositeImage, browser: 'default-bro' }); - const viewport = createViewport(opts.viewport); + const viewport = createViewport(opts); - viewport.validate(opts.captureArea, opts.browser); + return viewport.validate(opts.browser); }; beforeEach(() => { @@ -55,12 +63,12 @@ describe('Viewport', () => { }); it('should validate passed capture area', () => { - validate({ - viewport: {coords: {top: 0, left: 0}}, - captureArea: {top: 1, left: 1} - }); + const viewport = {left: 0, top: 0, width: 2, height: 2}; + const captureArea = {left: 1, top: 1, width: 1, height: 1}; - assert.calledWith(CoordValidator.prototype.validate, {top: 0, left: 0}, {top: 1, left: 1}); + validate({viewport, captureArea}); + + assert.calledWith(CoordValidator.prototype.validate, viewport, captureArea); }); }); @@ -69,145 +77,266 @@ describe('Viewport', () => { beforeEach(() => image = sinon.createStubInstance(Image)); - it('should ignore passed areas', () => { - const viewport = createViewport({coords: {top: 0, left: 0, width: 20, height: 20}, image, pixelRatio: 1}); + it('should ignore passed area', async () => { + const viewport = createViewport({ignoreAreas: [{left: 1, top: 1, width: 10, height: 10}]}); - viewport.ignoreAreas([{top: 1, left: 1, width: 10, height: 10}]); + await viewport.ignoreAreas(image, {left: 0, top: 0, width: 100, height: 100}); - assert.calledWith(image.clear, {top: 1, left: 1, width: 10, height: 10}, {scaleFactor: 1}); + assert.calledOnceWith(image.addClear, {left: 1, top: 1, width: 10, height: 10}); + assert.calledOnceWith(image.applyClear); }); - it('should transform area coordinates to a viewport origin', () => { - const viewport = createViewport({coords: {top: 1, left: 1, width: 20, height: 20}, image}); + it('should ignore multiple areas', async () => { + const firstArea = {left: 20, top: 12, width: 12, height: 13}; + const secondArea = {left: 12, top: 42, width: 30, height: 23}; + const viewport = createViewport({ignoreAreas: [firstArea, secondArea]}); - viewport.ignoreAreas([{top: 1, left: 1, width: 10, height: 10}]); + await viewport.ignoreAreas(image, {left: 0, top: 0, width: 100, height: 100}); - assert.calledWith(image.clear, {top: 0, left: 0, width: 10, height: 10}); + assert.calledWith(image.addClear.firstCall, firstArea); + assert.calledWith(image.addClear.secondCall, secondArea); + assert.calledOnceWith(image.applyClear); }); - it('should crop area size to a viewport origin (inside)', () => { - const viewport = createViewport({coords: {top: 0, left: 0, width: 30, height: 20}, image}); + it('should consider pixel ratio', async () => { + const firstArea = {left: 20, top: 12, width: 30, height: 5}; + const secondArea = {left: 12, top: 35, width: 25, height: 15}; + const pixelRatio = 2; + const viewport = createViewport({ + ignoreAreas: [firstArea, secondArea], + pixelRatio + }); - const area = {top: 10, left: 5, width: 20, height: 5}; - viewport.ignoreAreas([area]); + await viewport.ignoreAreas(image, {left: 0, top: 0, width: 100, height: 100}); - assert.calledWith(image.clear, area); + assert.calledWith(image.addClear.firstCall, {left: 40, top: 24, width: 60, height: 10}); + assert.calledWith(image.addClear.secondCall, {left: 24, top: 70, width: 50, height: 30}); }); - it('should crop area size to a viewport origin (bottom right)', () => { - const viewport = createViewport({coords: {top: 0, left: 0, width: 30, height: 20}, image}); + describe('should crop ignore area to image area', () => { + it('inside', async () => { + const viewport = createViewport({ignoreAreas: [{left: 0, top: 0, width: 1000, height: 1000}]}); - viewport.ignoreAreas([{top: 10, left: 5, width: 30, height: 5}]); + await viewport.ignoreAreas(image, {left: 10, top: 10, width: 100, height: 100}); - assert.calledWith(image.clear, {top: 10, left: 5, width: 25, height: 5}); - }); + assert.calledOnceWith(image.addClear, {left: 0, top: 0, width: 100, height: 100}); + }); - it('should crop area size to a viewport origin (top left)', () => { - const viewport = createViewport({coords: {top: 20, left: 15, width: 30, height: 20}, image}); + it('top left', async () => { + const viewport = createViewport({ignoreAreas: [{left: 10, top: 10, width: 1000, height: 1000}]}); - viewport.ignoreAreas([{top: 10, left: 5, width: 20, height: 20}]); + await viewport.ignoreAreas(image, {left: 0, top: 0, width: 100, height: 100}); - assert.calledWith(image.clear, {top: 0, left: 0, width: 10, height: 10}); - }); + assert.calledOnceWith(image.addClear, {left: 10, top: 10, width: 90, height: 90}); + }); + + it('top right', async () => { + const viewport = createViewport({ignoreAreas: [{left: 0, top: 10, width: 100, height: 100}]}); + + await viewport.ignoreAreas(image, {left: 50, top: 0, width: 100, height: 100}); - it('should not clear image if area is outside of viewport (bottom right)', () => { - const viewport = createViewport({coords: {top: 0, left: 0, width: 30, height: 20}, image}); + assert.calledOnceWith(image.addClear, {left: 0, top: 10, width: 50, height: 90}); + }); + + it('bottom left', async () => { + const viewport = createViewport({ignoreAreas: [{left: 10, top: 10, width: 100, height: 100}]}); - viewport.ignoreAreas([{top: 21, left: 31, width: 30, height: 5}]); + await viewport.ignoreAreas(image, {left: 0, top: 50, width: 100, height: 100}); + + assert.calledOnceWith(image.addClear, {left: 10, top: 0, width: 90, height: 60}); + }); - assert.notCalled(image.clear); + it('bottom right', async () => { + const viewport = createViewport({ + ignoreAreas: [{left: 10, top: 10, width: 100, height: 100}], + pixelRatio: 1 + }); + + await viewport.ignoreAreas(image, {left: 50, top: 50, width: 100, height: 100}); + + assert.calledOnceWith(image.addClear, {left: 0, top: 0, width: 60, height: 60}); + }); }); - it('should not clear image if area is outside of viewport (top left)', () => { - const viewport = createViewport({coords: {top: 21, left: 31, width: 30, height: 20}, image}); + describe('should not clear area if area is outside of image area', () => { + it('bottom right', async () => { + const viewport = createViewport({ignoreAreas: [{left: 0, top: 0, width: 30, height: 30}]}); + + await viewport.ignoreAreas(image, {left: 50, top: 50, width: 100, height: 100}); - viewport.ignoreAreas([{top: 0, left: 0, width: 30, height: 20}]); + assert.notCalled(image.addClear); + }); + + it ('top left', async () => { + const viewport = createViewport({ignoreAreas: [{left: 50, top: 50, width: 100, height: 100}]}); - assert.notCalled(image.clear); + await viewport.ignoreAreas(image, {left: 0, top: 0, width: 40, height: 40}); + + assert.notCalled(image.addClear); + }); }); }); - describe('crop', () => { - let image; + describe('handleImage', () => { + it('should apply "ignoreAreas" to the image', async () => { + const vieport = createViewport(); + sandbox.stub(vieport, 'ignoreAreas'); - beforeEach(() => { - image = sinon.createStubInstance(Image); + await vieport.handleImage(image); - image.crop.returns(Promise.resolve()); + assert.calledOnceWith(vieport.ignoreAreas, image); }); - it('should crop an image', () => { - const viewport = createViewport({image, pixelRatio: 1}); + it('should call apply "ignoreAreas" before crop', async () => { + const vieport = createViewport(); + sandbox.stub(vieport, 'ignoreAreas'); - return viewport.crop({top: 1, left: 1}) - .then(() => assert.calledWith(image.crop, {top: 1, left: 1}, {scaleFactor: 1})); + await vieport.handleImage(image); + + assert.callOrder(vieport.ignoreAreas, image.crop); }); - it('should transform area coordinates to a viewport origin', () => { - const viewport = createViewport({image, coords: {top: 1, left: 1}}); + describe('should crop to captureArea', () => { + beforeEach(() => image.getSize.resolves({width: 7, height: 10})); + + it('with default area', async () => { + const vieport = createViewport({ + captureArea: {left: 1, top: 2, width: 3, height: 4}, + viewport: {left: 0, top: 0, width: 10, height: 10} + }); + + await vieport.handleImage(image); + + assert.calledOnceWith(image.crop, {left: 1, top: 2, width: 3, height: 4}); + }); + + it('considering pixel ratio', async () => { + const vieport = createViewport({ + captureArea: {left: 1, top: 2, width: 3, height: 4}, + viewport: {left: 0, top: 0, width: 10, height: 10}, + pixelRatio: 2 + }); + + await vieport.handleImage(image); + + assert.calledOnceWith(image.crop, {left: 2, top: 4, width: 5, height: 8}); + }); + + it('with given area', async () => { + const vieport = createViewport({ + captureArea: {left: 1, top: 2, width: 3, height: 4}, + viewport: {left: 0, top: 0, width: 10, height: 10} + }); + + await vieport.handleImage(image, {left: 0, top: 0, width: 7, height: 10}); + + assert.calledOnceWith(image.crop, {left: 1, top: 2, width: 3, height: 4}); + }); + + it('with top offset', async () => { + const vieport = createViewport({ + captureArea: {left: 1, top: 2, width: 3, height: 4}, + viewport: {left: 0, top: 0, width: 10, height: 10} + }); + + await vieport.handleImage(image, {left: 0, top: 7, width: 7, height: 3}); + + assert.calledOnceWith(image.crop, {left: 1, top: 9, width: 3, height: 3}); + }); + + it('with negative offsets in captureAreas', async () => { + const vieport = createViewport({ + captureArea: {left: -1, top: -2, width: 7, height: 8}, + viewport: {left: 4, top: 3, width: 10, height: 10} + }); + + await vieport.handleImage(image); - return viewport.crop({top: 1, left: 1}) - .then(() => assert.calledWith(image.crop, {top: 0, left: 0})); + assert.calledOnceWith(image.crop, {left: 0, top: 0, width: 3, height: 8}); + }); }); }); - describe('save', () => { - let image; + describe('composite', () => { + it('should call "applyJoin"', async () => { + const viewport = createViewport(); - beforeEach(() => image = sinon.createStubInstance(Image)); + await viewport.composite(); + + assert.calledOnce(image.applyJoin); + }); - it('should save viewport image', () => { - const viewport = createViewport({image}); + it('should return composed image', async () => { + const viewport = createViewport(); - viewport.save('path/to/img'); + const newImage = await viewport.composite(); + + assert.match(image, newImage); + }); + }); + + describe('save', () => { + it('should save viewport image', async () => { + const viewport = createViewport(); + + await viewport.save('path/to/img'); assert.calledWith(image.save, 'path/to/img'); }); }); describe('extendBy', () => { - let image; let newImage; beforeEach(() => { - image = sinon.createStubInstance(Image); newImage = sinon.createStubInstance(Image); - newImage.crop.returns(Promise.resolve()); - newImage.getSize.returns({}); + newImage.crop.resolves(); + newImage.getSize.resolves({}); }); - it('should increase viewport height value by scroll height', () => { - const viewport = createViewport({coords: {top: 0, height: 5}, image}); + it('should increase viewport height value by scroll height', async () => { + const viewport = createViewport({ + captureArea: {top: 0, height: 7}, + viewport: {top: 0, height: 5} + }); - return viewport.extendBy(2, newImage) - .then(() => assert.equal(viewport.getVerticalOverflow({top: 0, height: 7}), 0)); + await viewport.extendBy(2, newImage); + + assert.equal(viewport.getVerticalOverflow(), 0); }); - it('should crop new image by passed scroll height', () => { - const viewport = createViewport({image, pixelRatio: 0.5}); + it('should crop new image by passed scroll height', async () => { + newImage.getSize.resolves({height: 4, width: 2}); + const viewport = createViewport({ + captureArea: {left: 0, top: 0, width: 4, height: 20}, + viewport: {left: 0, top: 0, width: 4, height: 8}, + pixelRatio: 0.5 + }); - newImage.getSize.returns({height: 4, width: 2}); + await viewport.extendBy(2, newImage); - return viewport.extendBy(2, newImage) - .then(() => assert.calledWith(newImage.crop, {left: 0, top: 3, width: 2, height: 1})); + assert.calledWith(newImage.crop, {left: 0, top: 3, width: 2, height: 1}); }); - it('should join original image with cropped image', () => { - const viewport = createViewport({image}); + it('should join original image with cropped image', async () => { + const viewport = createViewport(); - newImage.crop.returns(Promise.resolve('cropped-image')); + await viewport.extendBy(null, newImage); - return viewport.extendBy(null, newImage) - .then(() => assert.calledWith(image.join, 'cropped-image')); + assert.calledOnce(newImage.crop); + assert.calledWith(image.addJoin, newImage); }); }); describe('getVerticalOverflow', () => { it('should get outside height', () => { - const viewport = createViewport({coords: {top: 0, height: 5}}); + const viewport = createViewport({ + captureArea: {top: 0, height: 15}, + viewport: {top: 0, height: 5} + }); - assert.equal(viewport.getVerticalOverflow({top: 0, height: 15}), 10); + assert.equal(viewport.getVerticalOverflow(), 10); }); }); }); diff --git a/test/lib/worker/runner/test-runner/one-time-screenshooter.js b/test/lib/worker/runner/test-runner/one-time-screenshooter.js index 679276b74..c524bfe13 100644 --- a/test/lib/worker/runner/test-runner/one-time-screenshooter.js +++ b/test/lib/worker/runner/test-runner/one-time-screenshooter.js @@ -43,7 +43,8 @@ describe('worker/runner/test-runner/one-time-screenshooter', () => { const stubImage_ = (size = {width: 100500, height: 500100}) => { return { getSize: sandbox.stub().named('getSize').returns(size), - save: sandbox.stub().named('save').resolves() + save: sandbox.stub().named('save').resolves(), + toPngBuffer: sandbox.stub().named('toPngBuffer').resolves({data: Buffer.from('buffer'), size}) }; }; @@ -54,7 +55,7 @@ describe('worker/runner/test-runner/one-time-screenshooter', () => { sandbox.stub(temp, 'path').named('path').returns(); sandbox.stub(RuntimeConfig, 'getInstance').returns({tempOpts: {}}); sandbox.stub(logger, 'warn'); - sandbox.stub(fs.promises, 'readFile').resolves(Buffer.from('buffer')); + sandbox.stub(fs.promises, 'writeFile').resolves(); }); afterEach(() => sandbox.restore()); @@ -95,7 +96,7 @@ describe('worker/runner/test-runner/one-time-screenshooter', () => { const browser = mkBrowser_(); browser.evalScript.resolves({width: 100, height: 500}); browser.prepareScreenshot - .withArgs([{top: 0, left: 0, width: 100, height: 500}], { + .withArgs([{left: 0, top: 0, width: 100, height: 500}], { ignoreSelectors: [], captureElementFromTop: true, allowViewportOverflow: true @@ -112,17 +113,14 @@ describe('worker/runner/test-runner/one-time-screenshooter', () => { temp.path .withArgs({some: 'opts', suffix: '.png'}) .returns('some-path'); - fs.promises.readFile - .withArgs('some-path') - .resolves(Buffer.from('base64')); const config = {takeScreenshotOnFailsMode: 'fullpage'}; const screenshooter = mkScreenshooter_({browser, config}); await screenshooter[method](...getArgs()); - assert.calledOnceWith(imgStub.save, 'some-path'); + assert.calledOnceWith(fs.promises.writeFile, 'some-path', Buffer.from('buffer')); assert.deepEqual(screenshooter.getScreenshot(), { - base64: 'YmFzZTY0', + base64: Buffer.from('buffer').toString('base64'), size: {width: 100, height: 500} }); });