From 3d9974df5db59206bfd2f322a5fb926ee652071d Mon Sep 17 00:00:00 2001 From: Puneet Kala Date: Tue, 10 Aug 2021 18:09:55 +0530 Subject: [PATCH] Prepare for Release V1.9.4 (#92) * New option `compareWithImage` to compare a screenshot with a custom file (#90) (#91) * Add option `compareWithImage` * Cleanup changes from previous commit * Document changes for `compareWithImage` option * Improve `compareWithImage` documentation * Fix issues from previous commit 1. Code ignored the flag `config.prepareBaseImage` 2. There was a typo in the function name `_getBaseImageName()` Co-authored-by: Philipp Stracker * Update Version Co-authored-by: Philipp Stracker --- README.md | 18 ++++++ index.js | 161 +++++++++++++++++++++++++++++++++++---------------- package.json | 2 +- 3 files changed, 129 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 66f888d..15b20e1 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ To use the Helper, users may provide the parameters: `prepareBaseImage`: Optional. When `true` then the system replaces all of the baselines related to the test case(s) you ran. This is equivalent of setting the option `prepareBaseImage: true` in all verifications of the test file. +`compareWithImage`: Optional. A custom filename to compare the screenshot with. The `compareWithImage` file must be located inside the `baseFolder`. ### Usage @@ -183,6 +184,23 @@ The resultant output image will be uploaded in a folder named "*output*" and dif If the `prepareBaseImage` option is marked `true`, then the generated base image will be uploaded to a folder named "*base*" in the S3 bucket. > Note: The tests may take a bit longer to run when the AWS configuration is provided as determined by the internet speed to upload/download images. +### Compare with custom image +Usually, every screenshot needs to have the same filename as an existing image inside the `baseFolder` directory. To change this behavior, you can use the `compareWithImage` option and specify a different image inside the `baseFolder` directory. + +This is useful, if you want to compare a single screenshot against multiple base images - for example, when you want to validate that the main menu element is identical on all app pages. +```js +I.seeVisualDiffForElement("#element", "image.png", {compareWithImage: "dashboard.png"}); +I.seeVisualDiffForElement("#element", "image.png", {compareWithImage: "account.png"}); +``` + +Or, in some cases there are intended visual differences for different browsers or operating systems: +```js +const os = "win32" === process.platform ? "win" : "mac"; + +// Compare "image.png" either with "image-win.png" or "image-mac.png": +I.seeVisualDiff("image.png", {compareWithImage: `image-${os}.png`}); +``` + ### Known Issues: > Issue in Windows where the image comparison is not carried out, and therefore no Mismatch Percentage is shown. See 'loadImageData' function in resemble.js diff --git a/index.js b/index.js index 680741d..89da5dd 100644 --- a/index.js +++ b/index.js @@ -33,13 +33,13 @@ class ResembleHelper extends Helper { * Compare Images * * @param image - * @param diffImage * @param options * @returns {Promise} */ - async _compareImages(image, diffImage, options) { - const baseImage = this.baseFolder + image; - const actualImage = this.screenshotFolder + image; + async _compareImages(image, options) { + const baseImage = this._getBaseImagePath(image, options); + const actualImage = this._getActualImagePath(image); + const diffImage = this._getDiffImagePath(image); // check whether the base and the screenshot images are present. fs.access(baseImage, fs.constants.F_OK | fs.constants.R_OK, (err) => { @@ -83,11 +83,11 @@ class ResembleHelper extends Helper { } resolve(data); if (data.misMatchPercentage >= tolerance) { - if (!fs.existsSync(getDirName(this.diffFolder + diffImage))) { - fs.mkdirSync(getDirName(this.diffFolder + diffImage)); + if (!fs.existsSync(getDirName(diffImage))) { + fs.mkdirSync(getDirName(diffImage)); } - fs.writeFileSync(this.diffFolder + diffImage + '.png', data.getBuffer()); - const diffImagePath = path.join(process.cwd(), this.diffFolder + diffImage + '.png'); + fs.writeFileSync(diffImage, data.getBuffer()); + const diffImagePath = path.join(process.cwd(), diffImage); this.debug(`Diff Image File Saved to: ${diffImagePath}`); } } @@ -102,8 +102,7 @@ class ResembleHelper extends Helper { * @returns {Promise<*>} */ async _fetchMisMatchPercentage(image, options) { - const diffImage = "Diff_" + image.split(".")[0]; - const result = this._compareImages(image, diffImage, options); + const result = this._compareImages(image, options); const data = await Promise.resolve(result); return data.misMatchPercentage; } @@ -144,18 +143,17 @@ class ResembleHelper extends Helper { * 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 + * @param options * @returns {Promise} */ - async _addAttachment(baseImage, misMatch, tolerance) { + async _addAttachment(baseImage, misMatch, options) { const allure = codeceptjs.container.plugins('allure'); - const diffImage = "Diff_" + baseImage.split(".")[0] + ".png"; - if (allure !== undefined && misMatch >= tolerance) { - allure.addAttachment('Base Image', fs.readFileSync(this.baseFolder + baseImage), 'image/png'); - allure.addAttachment('Screenshot Image', fs.readFileSync(this.screenshotFolder + baseImage), 'image/png'); - allure.addAttachment('Diff Image', fs.readFileSync(this.diffFolder + diffImage), 'image/png'); + if (allure !== undefined && misMatch >= options.tolerance) { + allure.addAttachment('Base Image', fs.readFileSync(this._getBaseImagePath(baseImage, options)), 'image/png'); + allure.addAttachment('Screenshot Image', fs.readFileSync(this._getActualImagePath(baseImage)), 'image/png'); + allure.addAttachment('Diff Image', fs.readFileSync(this._getDiffImagePath(baseImage)), 'image/png'); } } @@ -163,21 +161,20 @@ class ResembleHelper extends Helper { * This method attaches context, and images to Mochawesome reporter when the mismatch exceeds tolerance. * @param baseImage * @param misMatch - * @param tolerance + * @param options * @returns {Promise} */ - async _addMochaContext(baseImage, misMatch, tolerance) { + async _addMochaContext(baseImage, misMatch, options) { const mocha = this.helpers['Mochawesome']; - const diffImage = "Diff_" + baseImage.split(".")[0] + ".png"; - if (mocha !== undefined && misMatch >= tolerance) { + if (mocha !== undefined && misMatch >= options.tolerance) { await mocha.addMochawesomeContext("Base Image"); - await mocha.addMochawesomeContext(this.baseFolder + baseImage); + await mocha.addMochawesomeContext(this._getBaseImagePath(baseImage, options)); await mocha.addMochawesomeContext("ScreenShot Image"); - await mocha.addMochawesomeContext(this.screenshotFolder + baseImage); + await mocha.addMochawesomeContext(this._getActualImagePath(baseImage)); await mocha.addMochawesomeContext("Diff Image"); - await mocha.addMochawesomeContext(this.diffFolder + diffImage); + await mocha.addMochawesomeContext(this._getDiffImagePath(baseImage)); } } @@ -189,18 +186,18 @@ class ResembleHelper extends Helper { * @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. + * @param options * @returns {Promise} */ - async _upload(accessKeyId, secretAccessKey, region, bucketName, baseImage, ifBaseImage) { + async _upload(accessKeyId, secretAccessKey, region, bucketName, baseImage, options) { console.log("Starting Upload... "); const s3 = new AWS.S3({ accessKeyId: accessKeyId, secretAccessKey: secretAccessKey, region: region }); - fs.readFile(this.screenshotFolder + baseImage, (err, data) => { + fs.readFile(this._getActualImagePath(baseImage), (err, data) => { if (err) throw err; let base64data = new Buffer(data, 'binary'); const params = { @@ -213,7 +210,7 @@ class ResembleHelper extends Helper { console.log(`Screenshot Image uploaded successfully at ${uData.Location}`); }); }); - fs.readFile(this.diffFolder + "Diff_" + baseImage, (err, data) => { + fs.readFile(this._getDiffImagePath(baseImage), (err, data) => { if (err) console.log("Diff image not generated"); else { let base64data = new Buffer(data, 'binary'); @@ -228,14 +225,18 @@ class ResembleHelper extends Helper { }); } }); - if (ifBaseImage) { - fs.readFile(this.baseFolder + baseImage, (err, data) => { + + // If prepareBaseImage is false, then it won't upload the baseImage. However, this parameter is not considered if the config file has a prepareBaseImage set to true. + if (this._getPrepareBaseImage(options)) { + const baseImageName = this._getBaseImageName(baseImage, options); + + fs.readFile(this._getBaseImagePath(baseImage, options), (err, data) => { if (err) throw err; else { let base64data = new Buffer(data, 'binary'); const params = { Bucket: bucketName, - Key: `base/${baseImage}`, + Key: `base/${baseImageName}`, Body: base64data }; s3.upload(params, (uErr, uData) => { @@ -256,11 +257,13 @@ class ResembleHelper extends Helper { * @param region * @param bucketName * @param baseImage + * @param options * @returns {Promise} */ - _download(accessKeyId, secretAccessKey, region, bucketName, baseImage) { + _download(accessKeyId, secretAccessKey, region, bucketName, baseImage, options) { console.log("Starting Download..."); + const baseImageName = this._getBaseImageName(baseImage, options); const s3 = new AWS.S3({ accessKeyId: accessKeyId, secretAccessKey: secretAccessKey, @@ -268,13 +271,13 @@ class ResembleHelper extends Helper { }); const params = { Bucket: bucketName, - Key: `base/${baseImage}` + Key: `base/${baseImageName}` }; return new Promise((resolve) => { s3.getObject(params, (err, data) => { if (err) console.error(err); - console.log(this.baseFolder + baseImage); - fs.writeFileSync(this.baseFolder + baseImage, data.Body); + console.log(this._getBaseImagePath(baseImage, options)); + fs.writeFileSync(this._getBaseImagePath(baseImage, options), data.Body); resolve("File Downloaded Successfully"); }); }); @@ -308,24 +311,22 @@ class ResembleHelper extends Helper { options.tolerance = 0; } - 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 (options.prepareBaseImage !== undefined && options.prepareBaseImage) { - await this._prepareBaseImage(baseImage); + + if (this._getPrepareBaseImage(options)) { + await this._prepareBaseImage(baseImage, options); + } else if (awsC !== undefined) { + await this._download(awsC.accessKeyId, awsC.secretAccessKey, awsC.region, awsC.bucketName, baseImage, options); } + if (selector) { options.boundingBox = await this._getBoundingBox(selector); } const misMatch = await this._fetchMisMatchPercentage(baseImage, options); - this._addAttachment(baseImage, misMatch, options.tolerance); - this._addMochaContext(baseImage, misMatch, options.tolerance); + this._addAttachment(baseImage, misMatch, options); + this._addMochaContext(baseImage, misMatch, options); if (awsC !== undefined) { - await this._upload(awsC.accessKeyId, awsC.secretAccessKey, awsC.region, awsC.bucketName, baseImage, options.prepareBaseImage) + await this._upload(awsC.accessKeyId, awsC.secretAccessKey, awsC.region, awsC.bucketName, baseImage, options) } this.debug("MisMatch Percentage Calculated is " + misMatch + " for baseline " + baseImage); @@ -339,14 +340,18 @@ class ResembleHelper extends Helper { * Function to prepare Base Images from Screenshots * * @param screenShotImage Name of the screenshot Image (Screenshot Image Path is taken from Configuration) + * @param options */ - async _prepareBaseImage(screenShotImage) { - await this._createDir(this.baseFolder + screenShotImage); + async _prepareBaseImage(screenShotImage, options) { + const baseImage = this._getBaseImagePath(screenShotImage, options); + const actualImage = this._getActualImagePath(screenShotImage); + + await this._createDir(baseImage); - fs.access(this.screenshotFolder + screenShotImage, fs.constants.F_OK | fs.constants.W_OK, (err) => { + fs.access(actualImage, fs.constants.F_OK | fs.constants.W_OK, (err) => { if (err) { throw new Error( - `${this.screenshotFolder + screenShotImage} ${err.code === 'ENOENT' ? 'does not exist' : 'is read-only'}`); + `${actualImage} ${err.code === 'ENOENT' ? 'does not exist' : 'is read-only'}`); } }); @@ -357,7 +362,7 @@ class ResembleHelper extends Helper { } }); - fs.copyFileSync(this.screenshotFolder + screenShotImage, this.baseFolder + screenShotImage); + fs.copyFileSync(actualImage, baseImage); } /** @@ -452,6 +457,60 @@ class ResembleHelper extends Helper { throw new Error('No matching helper found. Supported helpers: Playwright/WebDriver/Appium/Puppeteer/TestCafe'); } + + /** + * Returns the final name of the expected base image, without a path + * @param image Name of the base-image, without path + * @param options Helper options + * @returns {string} + */ + _getBaseImageName(image, options) { + return (options.compareWithImage ? options.compareWithImage : image); + } + + /** + * Returns the path to the expected base image + * @param image Name of the base-image, without path + * @param options Helper options + * @returns {string} + */ + _getBaseImagePath(image, options) { + return this.baseFolder + this._getBaseImageName(image, options); + } + + /** + * Returns the path to the actual screenshot image + * @param image Name of the image, without path + * @returns {string} + */ + _getActualImagePath(image) { + return this.screenshotFolder + image; + } + + /** + * Returns the path to the image that displays differences between base and actual image. + * @param image Name of the image, without path + * @returns {string} + */ + _getDiffImagePath(image) { + const diffImage = "Diff_" + image.split(".")[0] + ".png"; + return this.diffFolder + diffImage; + } + + /** + * Returns the final `prepareBaseImage` flag after evaluating options and config values + * @param options Helper options + * @returns {boolean} + */ + _getPrepareBaseImage(options) { + if ('undefined' !== typeof options.prepareBaseImage) { + // Cast to bool with `!!` for backwards compatibility + return !! options.prepareBaseImage; + } else { + // Compare with `true` for backwards compatibility + return true === this.prepareBaseImage; + } + } } module.exports = ResembleHelper; diff --git a/package.json b/package.json index 33bfb7b..88249fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs-resemblehelper", - "version": "1.9.3", + "version": "1.9.4", "description": "Resemble Js helper for CodeceptJS, with Support for Playwright, Webdriver, TestCafe, Puppeteer & Appium", "repository": { "type": "git",