From ec98c2d4d2b70f9d4a5717a54025b27a7cd90458 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 9 Aug 2021 16:54:28 +0200 Subject: [PATCH 1/5] Add option `compareWithImage` --- index.js | 137 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 92 insertions(+), 45 deletions(-) diff --git a/index.js b/index.js index 680741d..90113ed 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, options); + const diffImage = this._getDiffImagePath(image, options); // 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, options)), 'image/png'); + allure.addAttachment('Diff Image', fs.readFileSync(this._getDiffImagePath(baseImage, options)), '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, options)); await mocha.addMochawesomeContext("Diff Image"); - await mocha.addMochawesomeContext(this.diffFolder + diffImage); + await mocha.addMochawesomeContext(this._getDiffImagePath(baseImage, options)); } } @@ -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, options), (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, options), (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 (options.prepareBaseImage) { + 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"); }); }); @@ -313,19 +316,19 @@ class ResembleHelper extends Helper { : (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); + await this._download(awsC.accessKeyId, awsC.secretAccessKey, awsC.region, awsC.bucketName, baseImage, options); } if (options.prepareBaseImage !== undefined && options.prepareBaseImage) { - await this._prepareBaseImage(baseImage); + await this._prepareBaseImage(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 +342,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, options); - fs.access(this.screenshotFolder + screenShotImage, fs.constants.F_OK | fs.constants.W_OK, (err) => { + await this._createDir(baseImage); + + 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 +364,7 @@ class ResembleHelper extends Helper { } }); - fs.copyFileSync(this.screenshotFolder + screenShotImage, this.baseFolder + screenShotImage); + fs.copyFileSync(actualImage, baseImage); } /** @@ -452,6 +459,46 @@ 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; + } } + module.exports = ResembleHelper; From 60a9c5feabd8e2563ec1c412562e340b07c443ee Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 9 Aug 2021 17:00:06 +0200 Subject: [PATCH 2/5] Cleanup changes from previous commit --- index.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 90113ed..74c3e86 100644 --- a/index.js +++ b/index.js @@ -38,8 +38,8 @@ class ResembleHelper extends Helper { */ async _compareImages(image, options) { const baseImage = this._getBaseImagePath(image, options); - const actualImage = this._getActualImagePath(image, options); - const diffImage = this._getDiffImagePath(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) => { @@ -152,8 +152,8 @@ class ResembleHelper extends Helper { 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, options)), 'image/png'); - allure.addAttachment('Diff Image', fs.readFileSync(this._getDiffImagePath(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'); } } @@ -172,9 +172,9 @@ class ResembleHelper extends Helper { await mocha.addMochawesomeContext("Base Image"); await mocha.addMochawesomeContext(this._getBaseImagePath(baseImage, options)); await mocha.addMochawesomeContext("ScreenShot Image"); - await mocha.addMochawesomeContext(this._getActualImagePath(baseImage, options)); + await mocha.addMochawesomeContext(this._getActualImagePath(baseImage)); await mocha.addMochawesomeContext("Diff Image"); - await mocha.addMochawesomeContext(this._getDiffImagePath(baseImage, options)); + await mocha.addMochawesomeContext(this._getDiffImagePath(baseImage)); } } @@ -197,7 +197,7 @@ class ResembleHelper extends Helper { secretAccessKey: secretAccessKey, region: region }); - fs.readFile(this._getActualImagePath(baseImage, options), (err, data) => { + fs.readFile(this._getActualImagePath(baseImage), (err, data) => { if (err) throw err; let base64data = new Buffer(data, 'binary'); const params = { @@ -210,7 +210,7 @@ class ResembleHelper extends Helper { console.log(`Screenshot Image uploaded successfully at ${uData.Location}`); }); }); - fs.readFile(this._getDiffImagePath(baseImage, options), (err, data) => { + fs.readFile(this._getDiffImagePath(baseImage), (err, data) => { if (err) console.log("Diff image not generated"); else { let base64data = new Buffer(data, 'binary'); @@ -226,8 +226,8 @@ class ResembleHelper extends Helper { } }); - // 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 (options.prepareBaseImage) { + // 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 (options.prepareBaseImage) { const baseImageName = this._getbaseImageName(baseImage, options); fs.readFile(this._getBaseImagePath(baseImage, options), (err, data) => { @@ -346,7 +346,7 @@ class ResembleHelper extends Helper { */ async _prepareBaseImage(screenShotImage, options) { const baseImage = this._getBaseImagePath(screenShotImage, options); - const actualImage = this._getActualImagePath(screenShotImage, options); + const actualImage = this._getActualImagePath(screenShotImage); await this._createDir(baseImage); @@ -500,5 +500,4 @@ class ResembleHelper extends Helper { } } - module.exports = ResembleHelper; From fe544ee4ba1ac8fe7670985cea6c55b0e7745b89 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 9 Aug 2021 17:01:19 +0200 Subject: [PATCH 3/5] Document changes for `compareWithImage` option --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 66f888d..3301b96 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,21 @@ 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: +```js +I.seeVisualDiffForElement("#element", "image.png", {compareWithImage: "login-screen.png"}); +``` +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. + +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 From c93563f2c326a26014ca58736bddc5f4ca1cfbb0 Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 9 Aug 2021 17:03:27 +0200 Subject: [PATCH 4/5] Improve `compareWithImage` documentation --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3301b96..15b20e1 100644 --- a/README.md +++ b/README.md @@ -185,11 +185,13 @@ If the `prepareBaseImage` option is marked `true`, then the generated base image > 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: +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: "login-screen.png"}); +I.seeVisualDiffForElement("#element", "image.png", {compareWithImage: "dashboard.png"}); +I.seeVisualDiffForElement("#element", "image.png", {compareWithImage: "account.png"}); ``` -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. Or, in some cases there are intended visual differences for different browsers or operating systems: ```js From 54ed706f47566eb9fb3c26615a2e26cd0033d6ef Mon Sep 17 00:00:00 2001 From: Philipp Stracker Date: Mon, 9 Aug 2021 17:40:10 +0200 Subject: [PATCH 5/5] Fix issues from previous commit 1. Code ignored the flag `config.prepareBaseImage` 2. There was a typo in the function name `_getBaseImageName()` --- index.js | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 74c3e86..89da5dd 100644 --- a/index.js +++ b/index.js @@ -227,8 +227,8 @@ class ResembleHelper extends Helper { }); // 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 (options.prepareBaseImage) { - const baseImageName = this._getbaseImageName(baseImage, options); + if (this._getPrepareBaseImage(options)) { + const baseImageName = this._getBaseImageName(baseImage, options); fs.readFile(this._getBaseImagePath(baseImage, options), (err, data) => { if (err) throw err; @@ -263,7 +263,7 @@ class ResembleHelper extends Helper { _download(accessKeyId, secretAccessKey, region, bucketName, baseImage, options) { console.log("Starting Download..."); - const baseImageName = this._getbaseImageName(baseImage, options); + const baseImageName = this._getBaseImageName(baseImage, options); const s3 = new AWS.S3({ accessKeyId: accessKeyId, secretAccessKey: secretAccessKey, @@ -311,16 +311,14 @@ 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, options); - } - if (options.prepareBaseImage !== undefined && options.prepareBaseImage) { + + 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); } @@ -498,6 +496,21 @@ class ResembleHelper extends Helper { 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;