UNPKG

@currys-co-uk/codeceptjs-resemblehelper

Version:

Forked Resemble JS helper for CodeceptJS, with Support for Playwright, Webdriver, TestCafe, Puppeteer & Appium

683 lines (682 loc) 32.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; const getDirName = require('path').dirname; const sizeOf = require('image-size'); const Helper = require('@codeceptjs/helper'); const codeceptjs_1 = require("codeceptjs"); const resemblejs_1 = __importDefault(require("resemblejs")); const fs_1 = __importDefault(require("fs")); const mkdirp_1 = __importDefault(require("mkdirp")); const aws_sdk_1 = __importDefault(require("aws-sdk")); const path_1 = __importDefault(require("path")); const chalk_1 = __importDefault(require("chalk")); /** * Resemble.js helper class for CodeceptJS, this allows screen comparison * */ class ResembleHelper extends Helper { constructor(config) { super(config); this.baseFolder = this._resolvePath(config.baseFolder); this._baseFolder = this._resolvePath(config.baseFolder); this.diffFolder = this._resolvePath(config.diffFolder); this.screenshotFolder = `${global.output_dir}/`; this.prepareBaseImage = config.prepareBaseImage; this.tolerance = config.tolerance; this.skipFailure = config.skipFailure; this.createDiffInToleranceRange = config.createDiffInToleranceRange; this.alwaysSaveDiff = config.alwaysSaveDiff; this.createSubFoldersInBaseFolder = config.createSubFoldersInBaseFolder; this.updateMismatchedBaseImage = config.updateMismatchedBaseImage; this.ignoreNothing = config.ignoreNothing; this.scaleToSameSize = config.scaleToSameSize; } async _before() { if (this.createSubFoldersInBaseFolder) { codeceptjs_1.event.dispatcher.on(codeceptjs_1.event.test.started, (test) => { const removedTags = test.title.split(' @')[0]; const parsedTestTitle = removedTags.slice(0, 50).replace(/[<>:"/\\|\\?*,(){} ]/g, '_'); this.baseFolder = `${this._baseFolder}${parsedTestTitle}/`; }); } } _resolvePath(folderPath) { if (!path_1.default.isAbsolute(folderPath)) { return `${path_1.default.resolve(global.codecept_dir, folderPath)}/`; } return folderPath; } /** * Compare Images * * @param image * @param diffImage * @param options * @returns {Promise<resolve | reject>} */ async _compareImages(image, diffImage, options) { const baseImage = `${this.baseFolder}${image}`; const actualImage = `${this.screenshotFolder}${image}`; // check whether the base and the screenshot images are present. fs_1.default.access(baseImage, fs_1.default.constants.F_OK | fs_1.default.constants.R_OK, (err) => { if (err) { throw new Error(`${baseImage} ${err.code === 'ENOENT' ? 'base image does not exist' : 'base image has an access error'}`); } }); fs_1.default.access(actualImage, fs_1.default.constants.F_OK | fs_1.default.constants.R_OK, (err) => { if (err) { throw new Error(`${actualImage} ${err.code === 'ENOENT' ? 'screenshot image does not exist' : 'screenshot image has an access error'}`); } }); return new Promise((resolve, reject) => { if (!options.outputSettings) { options.outputSettings = {}; } if (this.ignoreNothing === true && !options.ignore) { options.ignore = 'nothing'; this.debug('Full image comparison is turn on.'); } if (this.scaleToSameSize === true) { options.scaleToSameSize = true; this.debug('Compare of different sizes of the same image is turn on.'); } resemblejs_1.default.outputSettings({ boundingBox: options.boundingBox, ignoredBox: options.ignoredBox, ignoredBoxes: options.ignoredBoxes, ...options.outputSettings, }); this.debug(`Tolerance Level Provided ${options.tolerance}`); const tolerance = options.tolerance; resemblejs_1.default.compare(baseImage, actualImage, options, (err, data) => { if (err) { reject(err); } else { if (!data.isSameDimensions) { const dimensions1 = sizeOf(baseImage); const dimensions2 = sizeOf(actualImage); reject(new Error(`The base image is of ${dimensions1.height} X ${dimensions1.width} and actual image is of ${dimensions2.height} X ${dimensions2.width}. Please use images of same dimensions so as to avoid any unexpected results.`)); } resolve(data); if (Number(data.misMatchPercentage) > tolerance && this.createDiffInToleranceRange !== true && !this.updateMismatchedBaseImage) { if (!fs_1.default.existsSync(getDirName(`${this.diffFolder}${diffImage}`))) { fs_1.default.mkdirSync(getDirName(`${this.diffFolder}${diffImage}`)); } fs_1.default.writeFileSync(`${this.diffFolder}${diffImage}.png`, data.getBuffer()); const diffImagePath = `${this.diffFolder}${diffImage}.png`; this.debug(`Diff Image File Saved to: ${diffImagePath}`); } if (this.createDiffInToleranceRange === true) { if (Number(data.misMatchPercentage) > 0 && Number(data.misMatchPercentage) <= tolerance) { this.debug(`${chalk_1.default.yellow('createDiffInToleranceRange is set as true and met conditions')}`); this.debug(chalk_1.default.yellow `Mismatch percentage: "${data.misMatchPercentage}" is less or equal than tolerance: ${tolerance}`); this.debug(`${chalk_1.default.yellow('Creating diff ...')}`); if (!fs_1.default.existsSync(getDirName(`${this.diffFolder}${diffImage}`))) { fs_1.default.mkdirSync(getDirName(`${this.diffFolder}${diffImage}`)); } fs_1.default.writeFileSync(`${this.diffFolder}${diffImage}.png`, data.getBuffer()); const diffImagePath = `${this.diffFolder}${diffImage}.png`; this.debug(`Diff Image File Saved to: ${diffImagePath}`); } else { this.debug(chalk_1.default.yellow `You have set createDiffInToleranceRange as true and your mismatch: ${data.misMatchPercentage} is not in tolerance: ${tolerance}`); this.debug(chalk_1.default.yellow `Diff Image File NOT Saved.`); } } if (this.alwaysSaveDiff === true) { this.debug(`${chalk_1.default.bgMagenta('alwaysSaveDiff is set as true')}`); this.debug(`${chalk_1.default.bgMagenta('Creating diff ...')}`); if (!fs_1.default.existsSync(getDirName(`${this.diffFolder}${diffImage}`))) { fs_1.default.mkdirSync(getDirName(`${this.diffFolder}${diffImage}`)); } fs_1.default.writeFileSync(`${this.diffFolder}${diffImage}.png`, data.getBuffer()); const diffImagePath = `${this.diffFolder}${diffImage}.png`; this.debug(`Diff Image File Saved to: ${diffImagePath}`); } } }); }); } /** * Get actual date and time in format MMMM-MM-MMTHH:MM:SS * @returns string */ _getTimestamp() { const now = new Date(); return now.toISOString().slice(0, 19).replace(/:/g, '_'); } /** * * @param image * @param options * @returns {Promise<*>} */ async _fetchMisMatchPercentage(image, options, timestamp) { const diffImage = `Diff_${image.split('.')[0]}_${timestamp}`; const result = this._compareImages(image, diffImage, options); const data = await Promise.resolve(result); return Number(data.misMatchPercentage); } /** * Take screenshot of individual element. * @param selector selector of the element to be screenshotted * @param name name of the image * @returns {Promise<void>} */ async screenshotElement(selector, name) { const helper = this._getHelper(); if (this.helpers.Puppeteer || this.helpers.Playwright) { await helper.waitForVisible(selector); const els = await helper._locate(selector); if (!els.length) throw new Error(`Element ${selector} couldn't be located`); const el = els[0]; await el.screenshot({ path: `${global.output_dir}/${name}` }); } else if (this.helpers.WebDriver) { await helper.waitForVisible(selector); const els = await helper._locate(selector); if (!els.length) throw new Error(`Element ${selector} couldn't be located`); const el = els[0]; await el.saveScreenshot(`${this.screenshotFolder}${name}`); } else if (this.helpers.TestCafe) { await helper.waitForVisible(selector); const els = await helper._locate(selector); if (!(await els.count)) throw new Error(`Element ${selector} couldn't be located`); const { t } = this.helpers.TestCafe; await t.takeElementScreenshot(els, name); } else throw new Error('Method only works with Playwright, Puppeteer, WebDriver or TestCafe helpers.'); } /** * This method attaches image attachments of the base, screenshot and diff to the allure reporter when the mismatch exceeds tolerance. * @param baseImage * @param misMatch * @param tolerance * @returns {Promise<void>} */ async _addAttachment(baseImage, misMatch, tolerance, timestamp) { const allure = codeceptjs.container.plugins('allure'); const diffImage = `Diff_${baseImage.split('.')[0]}_${timestamp}.png`; if (allure !== undefined && misMatch > tolerance) { await allure.addAttachment('Base Image', fs_1.default.readFileSync(`${this.baseFolder}${baseImage}`), 'image/png'); await allure.addAttachment('Screenshot Image', fs_1.default.readFileSync(`${this.screenshotFolder}${baseImage}`), 'image/png'); await allure.addAttachment('Diff Image', fs_1.default.readFileSync(`${this.diffFolder}${diffImage}`), 'image/png'); } } /** * This method attaches context, and images to Mochawesome reporter when the mismatch exceeds tolerance. * @param baseImage * @param misMatch * @param tolerance * @returns {Promise<void>} */ async _addMochaContext(baseImage, misMatch, tolerance) { const mocha = this.helpers.Mochawesome; const diffImage = `Diff_${baseImage.split('.')[0]}.png`; if (mocha !== undefined && misMatch > tolerance) { await mocha.addMochawesomeContext('Base Image'); await mocha.addMochawesomeContext(`${this.baseFolder}${baseImage}`); await mocha.addMochawesomeContext('ScreenShot Image'); await mocha.addMochawesomeContext(`${this.screenshotFolder}${baseImage}`); await mocha.addMochawesomeContext('Diff Image'); await mocha.addMochawesomeContext(`${this.diffFolder}${diffImage}`); } } /** * This method uploads the diff and screenshot images into the bucket with diff image under bucketName/diff/diffImage and the screenshot image as * bucketName/output/ssImage * @param accessKeyId * @param secretAccessKey * @param region * @param bucketName * @param baseImage * @param ifBaseImage - tells if the prepareBaseImage is true or false. If false, then it won't upload the baseImage. However, this parameter is not considered if the config file has a prepareBaseImage set to true. * @returns {Promise<void>} */ async _upload(accessKeyId, secretAccessKey, region, bucketName, baseImage, ifBaseImage) { console.log('Starting Upload... '); const s3 = new aws_sdk_1.default.S3({ accessKeyId, secretAccessKey, region, }); fs_1.default.readFile(`${this.screenshotFolder}${baseImage}`, { encoding: 'base64' }, (err, base64data) => { if (err) throw err; const params = { Bucket: bucketName, Key: `output/${baseImage}`, Body: base64data, }; s3.upload(params, (uErr, uData) => { if (uErr) throw uErr; console.log(`Screenshot Image uploaded successfully at ${uData.Location}`); }); }); fs_1.default.readFile(`${this.diffFolder}Diff_${baseImage}`, { encoding: 'base64' }, (err, base64data) => { if (err) console.log('Diff image not generated'); else { const params = { Bucket: bucketName, Key: `diff/Diff_${baseImage}`, Body: base64data, }; s3.upload(params, (uErr, uData) => { if (uErr) throw uErr; console.log(`Diff Image uploaded successfully at ${uData.Location}`); }); } }); if (ifBaseImage) { fs_1.default.readFile(`${this.baseFolder}${baseImage}`, { encoding: 'base64' }, (err, base64data) => { if (err) throw err; else { const params = { Bucket: bucketName, Key: `base/${baseImage}`, Body: base64data, }; s3.upload(params, (uErr, uData) => { if (uErr) throw uErr; console.log(`Base Image uploaded at ${uData.Location}`); }); } }); } else { console.log('Not Uploading base Image'); } } /** * This method downloads base images from specified bucket into the base folder as mentioned in config file. * @param accessKeyId * @param secretAccessKey * @param region * @param bucketName * @param baseImage * @returns {Promise<void>} */ _download(accessKeyId, secretAccessKey, region, bucketName, baseImage) { console.log('Starting Download...'); const s3 = new aws_sdk_1.default.S3({ accessKeyId, secretAccessKey, region, }); const params = { Bucket: bucketName, Key: `base/${baseImage}`, }; return new Promise((resolve) => { s3.getObject(params, (err, data) => { if (err) console.error(err); console.log(`${this.baseFolder}${baseImage}`); fs_1.default.writeFileSync(`${this.baseFolder}${baseImage}`, data.Body); resolve('File Downloaded Successfully'); }); }); } /** * Check Visual Difference for Base and Screenshot Image * @param baseImage Name of the Base Image (Base Image path is taken from Configuration) * @param options Options ex {prepareBaseImage: true, tolerance: 5} along with Resemble JS Options, read more here: https://github.com/rsmbl/Resemble.js * @returns {Promise<void>} */ async seeVisualDiff(baseImage, options) { await this._assertVisualDiff(undefined, baseImage, options); } /** * See Visual Diff for an Element on a Page * * @param selector Selector which has to be compared expects these -> CSS|XPath|ID * @param baseImage Base Image for comparison * @param options Options ex {prepareBaseImage: true, tolerance: 5} along with Resemble JS Options, read more here: https://github.com/rsmbl/Resemble.js * @returns {Promise<void>} */ async seeVisualDiffForElement(selector, baseImage, options) { await this._assertVisualDiff(selector, baseImage, options); } async _assertVisualDiff(selector, baseImage, options) { if (!options) { options = {}; } if (this.tolerance !== undefined) { if (options.tolerance === undefined) { options.tolerance = this.tolerance; } } if (!options.tolerance && options.tolerance !== 0) { options.tolerance = 0; } if (this.skipFailure !== undefined) { options.skipFailure = this.skipFailure; } if (options.ignoredElement !== undefined) { options.ignoredBox = await this._getElementCoordinates(options.ignoredElement); } if (options.ignoredElements !== undefined) { options.ignoredBoxes = await this._getIgnoredBoxesFromElements(options.ignoredElements); } if (options.ignoredQueryElementAll !== undefined) { options.ignoredBoxes = await this._locateAll(options.ignoredQueryElementAll); } const prepareBaseImage = options.prepareBaseImage !== undefined ? options.prepareBaseImage : this.prepareBaseImage === true; const awsC = this.config.aws; if (awsC !== undefined && prepareBaseImage === false) { await this._download(awsC.accessKeyId, awsC.secretAccessKey, awsC.region, awsC.bucketName, baseImage); } if (((this.prepareBaseImage === true && options.prepareBaseImage === undefined) || options.prepareBaseImage === true || (this.prepareBaseImage === undefined && options.prepareBaseImage === undefined)) && !this.updateMismatchedBaseImage) { await this._prepareBaseImage(baseImage, options); } if (selector) { if (options.ignoredElement) { options.ignoredBox = (await this._reCountElementCoordinatesForIgnoreInScreenshotElement(selector, [options.ignoredBox]))[0]; this.debug(`You ignore one element in screenshotted element "${selector}" ...`); this.debug(`Element coordinates were recounted to element screenshotted size as: ${JSON.stringify(options.ignoredBox)}`); } else if (options.ignoredElements || options.ignoredQueryElementAll) { options.ignoredBoxes = await this._reCountElementCoordinatesForIgnoreInScreenshotElement(selector, options.ignoredBoxes); this.debug(`You ignore more elements in screenshotted element "${selector}" ...`); this.debug(`Element coordinates were recounted to element screenshotted size as: ${JSON.stringify(options.ignoredBoxes)}`); } else { options.boundingBox = await this._getBoundingBox(selector); } } const imageTimestamp = this._getTimestamp(); let misMatch = await this._fetchMisMatchPercentage(baseImage, options, imageTimestamp); if (this.updateMismatchedBaseImage === true && misMatch > options.tolerance) { console.log(`${chalk_1.default.magenta.bold('--------------- INFO -----------------')}`); console.log(`${chalk_1.default.magenta.bold('--- "updateMismatchedBaseImage" IS TURN ON ---')}`); console.log(chalk_1.default.magenta.bold `Mismatch: ${misMatch} > Tolerance: ${options.tolerance}`); console.log(`${chalk_1.default.magenta.bold('Base image will be updated...')}`); console.log(`${chalk_1.default.magenta.bold('--------------------------------------')}`); await this._prepareBaseImage(baseImage, options); misMatch = 0; } await this._addAttachment(baseImage, misMatch, options.tolerance, imageTimestamp); await this._addMochaContext(baseImage, misMatch, options.tolerance); if (awsC !== undefined) { await this._upload(awsC.accessKeyId, awsC.secretAccessKey, awsC.region, awsC.bucketName, baseImage, options.prepareBaseImage); } this.debug(`MisMatch Percentage Calculated is ${misMatch} for baseline ${baseImage}`); if (!options.skipFailure) { if (misMatch > options.tolerance) { throw new Error(`Screenshot does not match with the baseline ${baseImage} when MisMatch Percentage is ${misMatch}`); } } if (options.skipFailure === true && misMatch > options.tolerance) { console.log(`${chalk_1.default.red.bgYellowBright.bold('--------------- WARNING ---------------')}`); console.log(chalk_1.default.red.bgYellowBright.bold `You have set "skipFailure: true"`); console.log(chalk_1.default.red.bgYellowBright.bold `Your baseline "${baseImage}" MisMatch Percentage is ${misMatch}`); console.log(`${chalk_1.default.red.bgYellowBright.bold('-------------- END WARNING --------------')}`); } } /** * Function to prepare Base Images from Screenshots * * @param screenShotImage Name of the screenshot Image (Screenshot Image Path is taken from Configuration) * @param options Options ex {prepareBaseImage: true, tolerance: 5} along with Resemble JS Options, read more here: https://github.com/rsmbl/Resemble.js */ async _prepareBaseImage(screenShotImage, options) { await this._createDir(`${this.baseFolder}${screenShotImage}`); fs_1.default.access(`${this.screenshotFolder}${screenShotImage}`, fs_1.default.constants.F_OK | fs_1.default.constants.W_OK, (err) => { if (err) { throw new Error(`${this.screenshotFolder}${screenShotImage} ${err.code === 'ENOENT' ? 'does not exist' : 'is read-only'}`); } }); fs_1.default.access(this.baseFolder, fs_1.default.constants.F_OK | fs_1.default.constants.W_OK, (err) => { if (err) { throw new Error(`${this.baseFolder} ${err.code === 'ENOENT' ? 'does not exist' : 'is read-only'}`); } }); try { await fs_1.default.promises.access(`${this.baseFolder}${screenShotImage}`, fs_1.default.constants.F_OK | fs_1.default.constants.W_OK); if (options.prepareBaseImage === true) { this.debug('Test option is set as: prepareBaseImage = true'); this._createBaseImage(screenShotImage, this.baseFolder, `${this.screenshotFolder}${screenShotImage}`, `${this.baseFolder}${screenShotImage}`); } else if (this.prepareBaseImage === true && options.prepareBaseImage === undefined) { this.debug('Global config is set as: prepareBaseImage = true'); this._createBaseImage(screenShotImage, this.baseFolder, `${this.screenshotFolder}${screenShotImage}`, `${this.baseFolder}${screenShotImage}`); } else if (this.updateMismatchedBaseImage === true) { this.debug('Global config is set as updateMismatchedBaseImage = true'); this.debug('Updating base image ...'); this.debug(`In base folder: ${this.baseFolder}`); fs_1.default.copyFileSync(`${this.screenshotFolder}${screenShotImage}`, `${this.baseFolder}${screenShotImage}`); this.debug(`Base image: ${screenShotImage} is updated.`); } else { this.debug(`Found existing base image: ${screenShotImage} and use it for compare.`); } } catch (e) { this.debug(`Existing base image with name ${screenShotImage} was not found.`); this._createBaseImage(screenShotImage, this.baseFolder, `${this.screenshotFolder}${screenShotImage}`, `${this.baseFolder}${screenShotImage}`); } } /** * Function for create base image * @param baseImage * @param baseFolder * @param screenshotImageFromScreenshotFolder * @param screenshotImageToBaseFolder * @returns void * @private */ _createBaseImage(baseImage, baseFolder, screenshotImageFromScreenshotFolder, screenshotImageToBaseFolder) { this.debug('Creating base image ...'); this.debug(`In base folder: ${baseFolder}`); fs_1.default.copyFileSync(screenshotImageFromScreenshotFolder, screenshotImageToBaseFolder); this.debug(`Base image: ${baseImage} is created.`); } /** * Function to create Directory * @param directory * @returns {Promise<void>} * @private */ _createDir(directory) { mkdirp_1.default.sync(getDirName(directory)); } /** * Function for delete screenshot image * @example * I.deleteScreenshot('./folder/image.png') * @param pathToFile string * @returns {Promise<void>} */ async deleteScreenshot(pathToFile) { fs_1.default.unlink(pathToFile, (err) => { if (err && err.code === 'ENOENT') { console.info(`Current directory: " ${process.cwd()}`); console.info("File doesn't exist, can't remove it."); } else if (err) { console.error('Error occurred while trying to remove file'); } else { console.info(`File ${pathToFile} removed.`); } }); } /** * Function to fetch Bounding box for an element, fetched using selector * * @param selector CSS|XPath|ID selector * @returns {Promise<{boundingBox: {left: *, top: *, right: *, bottom: *}}>} */ async _getBoundingBox(selector) { const helper = this._getHelper(); await helper.waitForVisible(selector); const els = await helper._locate(selector); if (this.helpers.TestCafe) { if ((await els.count) !== 1) throw new Error(`Element ${selector} couldn't be located or isn't unique on the page`); } else if (!els.length) throw new Error(`Element ${selector} couldn't be located`); let location; let size; if (this.helpers.Puppeteer || this.helpers.Playwright) { const el = els[0]; const box = await el.boundingBox(); size = location = box; } if (this.helpers.WebDriver || this.helpers.Appium) { const el = els[0]; location = await el.getLocation(); size = await el.getSize(); } if (this.helpers.TestCafe) { return await els.boundingClientRect; } if (!size) { throw new Error('Cannot get element size!'); } const bottom = size.height + location.y; const right = size.width + location.x; const boundingBox = { left: location.x, top: location.y, right, bottom, }; this.debug(`Area for selector ${selector} ${JSON.stringify(boundingBox)}`); return boundingBox; } /** * Function for get element coordinates, which should be later excluded from diff comparison * * @param selector CSS|XPath|ID selector * @returns {Promise<{ignoredBox: {left: *, top: *, right: *, bottom: *}}>} */ async _getElementCoordinates(selector) { const helper = this._getHelper(); await helper.waitForVisible(selector); const els = await helper._locate(selector); return this._countCoordinates(els[0], selector); } /** * Function for recount elements coordinates to ignoredBoxes in screenshotted element screenshot * * @selector selector CSS|XPath|ID selector * @param ignoredElementsCoordinates Options ex {ignoredElements: ['#name', '#email']} along with Resemble JS Options, read more here: https://github.com/rsmbl/Resemble.js * @returns {Promise<{ignoredBoxes: [{left: *, top: *, right: *, bottom: *}]>} */ async _reCountElementCoordinatesForIgnoreInScreenshotElement(selector, ignoredElementsCoordinates) { const helper = this._getHelper(); const boundingBox = await this._getBoundingBox(selector); const scrollOffset = await helper.executeScript(() => ({ X: window.pageXOffset, Y: window.pageYOffset })); ignoredElementsCoordinates = ignoredElementsCoordinates.map((elementCoordinates) => { const left = elementCoordinates.left - boundingBox.left + scrollOffset.X; const top = elementCoordinates.top - boundingBox.top + scrollOffset.Y; const right = elementCoordinates.right - boundingBox.left + scrollOffset.X; const bottom = elementCoordinates.bottom - boundingBox.top + scrollOffset.Y; return { left, top, right, bottom, }; }); if (scrollOffset.X || scrollOffset.Y) { this.debug(`Screenshotted element was in test scrolled "${JSON.stringify(scrollOffset.Y)}" px vertically and "${JSON.stringify(scrollOffset.X)}" px horizontal.`); } return ignoredElementsCoordinates; } /** * Function for translate elements coordinates to ignoredBoxes * * @param options Options ex {ignoredElements: ['#name', '#email']} along with Resemble JS Options, read more here: https://github.com/rsmbl/Resemble.js * @returns {Promise<{ignoredBoxes: [{left: *, top: *, right: *, bottom: *}]>} */ async _getIgnoredBoxesFromElements(options) { return await Promise.all(options.map(async (item) => await this._getElementCoordinates(item))); } /** * Function for count selector coordinates * * @param el counted element * @param selector represents counted element in human language * @returns {Promise<{ignoredBoxes: [{left: *, top: *, right: *, bottom: *},{...}]>} */ async _countCoordinates(el, selector) { const helper = this._getHelper(); let location; let size; if (this.helpers.WebDriver || this.helpers.Appium) { location = await el.getLocation(); size = await el.getSize(); } if (this.helpers.Puppeteer || this.helpers.Playwright) { const box = await el.boundingBox(); size = location = box; } if (!size) { throw new Error('Cannot get element size!'); } const scrollOffset = await helper.executeScript(() => ({ X: window.pageXOffset, Y: window.pageYOffset })); const bottom = location.y + size.height - scrollOffset.Y; const right = location.x + size.width - scrollOffset.X; const left = location.x - scrollOffset.X; const top = location.y - scrollOffset.Y; const ignoredBox = { left, top, right, bottom, }; this.debug(`Element: ${JSON.stringify(selector)} has coordinates: ${JSON.stringify(ignoredBox)}`); this.debug(`Browser screen was scrolled "${JSON.stringify(scrollOffset.Y)}" px vertically and "${JSON.stringify(scrollOffset.X)}" px horizontal.`); return ignoredBox; } /** * Function equivalent for querySelectorAll * * @param selector CSS|XPath|ID selector * @returns {Promise<{ignoredBoxes: [{left: *, top: *, right: *, bottom: *}]>} */ async _locateAll(selector) { const browser = this.helpers.WebDriver || this.helpers.Playwright; const els = await browser._locate(selector); return await Promise.all(els.map(async (item) => await this._countCoordinates(item, selector))); } _getHelper() { if (this.helpers.Puppeteer) { return this.helpers.Puppeteer; } if (this.helpers.WebDriver) { return this.helpers.WebDriver; } if (this.helpers.Appium) { return this.helpers.Appium; } if (this.helpers.TestCafe) { return this.helpers.TestCafe; } if (this.helpers.Playwright) { return this.helpers.Playwright; } throw new Error('No matching helper found. Supported helpers: Playwright/WebDriver/Appium/Puppeteer/TestCafe'); } } module.exports = ResembleHelper;