diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad46b30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba64554 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# codeceptjs-resemblehelper +Helper for resemble.js, used for Image comparison in Tests with WebdriverIO + +codeceptjs-resemblehelper is [CodeceptJS](https://codecept.io/) helper which can be used to compare screenshots and make the tests fail/pass based on the tolerance allowed + +If two screenshot comparisons have difference greater then the tolerance provided, the test will fail. + +NPM package: https://www.npmjs.com/package/codeceptjs-resemblehelper + +To install the package, just run `npm install codeceptjs-resemblehelper` + +### Configuration + +This helper should be added in codecept.json/codecept.conf.js + +Example: + +```json +{ + "helpers": { + "ResembleHelper" : { + "require": "codeceptjs-resemblehelper", + "screenshotFolder" : "./tests/output/", + "baseFolder": "./tests/screenshots/base/", + "diffFolder": "./tests/screenshots/diff/" + } + } +} +``` +To use the Helper, users must provide the three parameters: +`screenshotFolder` : This will always have the same value as `output` in Codecept configuration, this is the folder where webdriverIO +saves a screenshot when using `I.saveScreenshot` method + +`baseFolder`: This is the folder for base images, which will be used with screenshot for comparison + +`diffFolder`: This will the folder where resemble would try to store the difference image, which can be viewed later, +Please remember to create empty folder if you don't have one already + +Usage, these are major functions that help in visual testing + +First one is the `verifyMisMatchPercentage` which basically takes several parameters including tolerance and PrepareBase +```js + /** + * Mis Match Percentage Verification + * @param baseImage Name of the Base Image (Base Image path is taken from Configuration) + * @param screenShotImage Name of the screenshot Image (Screenshot Image Path is taken from Configuration) + * @param diffImageName Name of the Diff Image which will be saved after comparison (Diff Image path is taken from Configuration) + * @param tolerance Tolerance Percentage, default value 10 + * @param prepareBase True | False, depending on the requirement if the base images are missing + * @param selector CSS|XPath|id, If provided locator will be used to fetch Bounding Box of the element and only that element is compared on two images + * @param options Resemble JS Options, read more here: https://github.com/rsmbl/Resemble.js + * @returns {Promise} + */ + async verifyMisMatchPercentage(baseImage, screenShotImage, diffImageName, tolerance = 10, prepareBase = false, selector, options){ +``` +Second one is the `PrepareBase` which basically prepares all the base images in case they are not available +```js + /** + * Function to prepare Base Images from Screenshots + * + * @param baseImage Name of the Base Image (Base Image path is taken from Configuration) + * @param screenShotImage Name of the screenshot Image (Screenshot Image Path is taken from Configuration) + */ + prepareBaseImage(baseImage, screenShotImage) {} +``` +Third function is to fetch the boundingBox of an element using selector, this boundingBox is then provided to resemble +so that only that element is compared on the images. + +```js + /** + * Function to fetch Bounding box for an element, fetched using selector + * + * @param selector CSS|XPath|ID locators + * @returns {Promise<{boundingBox: {left: *, top: *, right: *, bottom: *}}>} + */ + async getBoundingBox(selector){ +``` +Users can make use of the boundingBox feature by providing a selector to `verifyMisMatchPercentage` function, it will internally +check if a locator is provided, fetch it's bounding-box and compare only that element on both the images. + +Finally to use the helper in your test, you can write something like this: + +``` +Feature('to verify monitoried Remote Db instances'); + +Scenario('Open the System Overview Dashboard', async (I, adminPage, loginPage) => { + adminPage.navigateToDashboard("OS", "System Overview"); + adminPage.applyTimer("1m"); + adminPage.viewMetric("CPU Usage"); + I.saveScreenshot("System_Overview_CPU_Usage.png"); +}); + +Scenario('Compare CPU Usage Images', async (I) => { + + // passing TRUE to let the helper know to prepare base images + I.verifyMisMatchPercentage("System_Overview_CPU_Usage.png", "System_Overview_CPU_Usage.png", "DiffImage_SystemOverview_CPU_USAGE_Dashboard", 10, true); + + // passing a selector, to only compare that element on both the images now + I.verifyMisMatchPercentage("System_Overview_CPU_Usage.png", "System_Overview_CPU_Usage.png", "DiffImage_SystemOverview_CPU_USAGE_Panel", 10, false, "//div[@class='panel-container']"); +}); +``` \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..5b0d76c --- /dev/null +++ b/index.js @@ -0,0 +1,195 @@ +'use strict'; + +// use any assertion library you like +const resemble = require("resemblejs"); +const fs = require('fs'); +let assert = require('assert'); +const mkdirp = require('mkdirp'); +const getDirName = require('path').dirname; +/** + * Resemble.js helper class for CodeceptJS, this allows screen comparison + * @author Puneet Kala + */ +class ResembleHelper extends Helper { + + constructor(config) { + super(config); + } + + /** + * + * @param image1 + * @param image2 + * @param diffImage + * @param tolerance + * @param options + * @returns {Promise} + */ + async _compareImages (image1, image2, diffImage, tolerance, options) { + image1 = this.config.baseFolder + image1; + image2 = this.config.screenshotFolder + image2; + + return new Promise((resolve, reject) => { + if (options !== undefined) + { + resemble.outputSettings({ + boundingBox: options.boundingBox + }); + } + resemble.compare(image1, image2, options, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + if (data.misMatchPercentage >= tolerance) { + mkdirp(getDirName(this.config.diffFolder + diffImage), function (err) { + if (err) return cb(err); + }); + fs.writeFile(this.config.diffFolder + diffImage + '.png', data.getBuffer(), (err, data) => { + if (err) { + throw new Error(this.err); + } + }); + } + } + }); + }).catch((error) => { + console.log('caught', error.message); + }); + } + + /** + * + * @param image1 + * @param image2 + * @param diffImage + * @param tolerance + * @param options + * @returns {Promise<*>} + */ + async _fetchMisMatchPercentage (image1, image2, diffImage, tolerance, options) { + var result = this._compareImages(image1, image2, diffImage, tolerance, options); + var data = await Promise.resolve(result); + return data.misMatchPercentage; + } + + /** + * Mis Match Percentage Verification + * @param baseImage Name of the Base Image (Base Image path is taken from Configuration) + * @param screenShotImage Name of the screenshot Image (Screenshot Image Path is taken from Configuration) + * @param diffImageName Name of the Diff Image which will be saved after comparison (Diff Image path is taken from Configuration) + * @param tolerance Tolerance Percentage, default value 10 + * @param prepareBase True | False, depending on the requirement if the base images are missing + * @param selector If set, passed selector will be used to fetch Bouding Box and compared on two images + * @param options Resemble JS Options, read more here: https://github.com/rsmbl/Resemble.js + * @returns {Promise} + */ + async verifyMisMatchPercentage(baseImage, screenShotImage, diffImageName, tolerance = 10, prepareBase = false, selector, options){ + if (prepareBase) + { + await this.prepareBaseImage(baseImage, screenShotImage); + } + + if (selector !== undefined) + { + if (options !== undefined) + { + options.boundingBox = await this.getBoundingBox(selector); + } + else + { + var options = {}; + options.boundingBox = await this.getBoundingBox(selector); + } + } + + var misMatch = await this._fetchMisMatchPercentage(baseImage, screenShotImage, diffImageName, tolerance, options); + console.log("MisMatch Percentage Calculated is " + misMatch); + assert.ok(misMatch < tolerance, "MissMatch Percentage " + misMatch); + } + + /** + * Function to prepare Base Images from Screenshots + * + * @param baseImage Name of the Base Image (Base Image path is taken from Configuration) + * @param screenShotImage Name of the screenshot Image (Screenshot Image Path is taken from Configuration) + */ + async prepareBaseImage(baseImage, screenShotImage) { + var configuration = this.config; + + await this._createDir(configuration.baseFolder + baseImage); + + fs.access(configuration.screenshotFolder + screenShotImage, fs.constants.F_OK | fs.constants.W_OK, (err) => { + if (err) { + console.error( + `${configuration.screenshotFolder + screenShotImage} ${err.code === 'ENOENT' ? 'does not exist' : 'is read-only'}`); + } + }); + + fs.access(configuration.baseFolder, fs.constants.F_OK | fs.constants.W_OK, (err) => { + if (err) { + console.error( + `${configuration.baseFolder} ${err.code === 'ENOENT' ? 'does not exist' : 'is read-only'}`); + } + }); + + fs.copyFileSync(configuration.screenshotFolder + screenShotImage, configuration.baseFolder + baseImage); + } + + /** + * Function to create Directory + * @param directory + * @returns {Promise} + * @private + */ + async _createDir (directory) { + mkdirp.sync(getDirName(directory)); + } + + /** + * 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 browser = this._getBrowser(); + + var ele = await browser.element(selector) + .then((res) => { + return res; + }) + .catch((err) => { + // Catch the error because webdriver.io throws if the element could not be found + // Source: https://github.com/webdriverio/webdriverio/blob/master/lib/protocol/element.js + return null; + }); + var location = await browser.getLocation(selector); + var size = await browser.getElementSize(selector); + var bottom = size.height + location.y; + var right = size.width + location.x; + var boundingBox = { + left: location.x, + top: location.y, + right: right, + bottom: bottom + }; + + return boundingBox; + } + + _getBrowser() { + if (this.helpers['WebDriver']) { + return this.helpers['WebDriver'].browser; + } + if (this.helpers['Appium']) { + return this.helpers['Appium'].browser; + } + if (this.helpers['WebDriverIO']) { + return this.helpers['WebDriverIO'].browser; + } + throw new Error('No matching helper found. Supported helpers: WebDriver/Appium/WebDriverIO'); + } +} + +module.exports = ResembleHelper; diff --git a/package.json b/package.json new file mode 100644 index 0000000..72dde23 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "codeceptjs-resemblehelper", + "version": "1.2.1", + "description": "Resemble Js helper for CodeceptJS, with WebdriverIO", + "repository": { + "type": "git", + "url": "git@github.com:Percona-Lab/codeceptjs-resemblehelper.git" + }, + "dependencies": { + "assert": "^1.4.1", + "canvas": "^2.2.0", + "mz": "^2.7.0", + "resemblejs": "^3.0.0", + "mkdirp": "^0.5.1", + "path": "^0.12.7" + }, + "keywords": [ + "codeceptJS", + "codeceptjs", + "resemblejs", + "codeceptjs-resemble" + ], + "author": "Puneet Kala ", + "license": "MIT" +}