Skip to content

New option compareWithImage to compare a screenshot with a custom file #90

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
161 changes: 110 additions & 51 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ class ResembleHelper extends Helper {
* 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;
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) => {
Expand Down Expand Up @@ -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}`);
}
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -144,40 +143,38 @@ 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<void>}
*/

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');
}
}

/**
* This method attaches context, and images to Mochawesome reporter when the mismatch exceeds tolerance.
* @param baseImage
* @param misMatch
* @param tolerance
* @param options
* @returns {Promise<void>}
*/

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));
}
}

Expand All @@ -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<void>}
*/

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 = {
Expand All @@ -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');
Expand All @@ -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) => {
Expand All @@ -256,25 +257,27 @@ class ResembleHelper extends Helper {
* @param region
* @param bucketName
* @param baseImage
* @param options
* @returns {Promise<void>}
*/

_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,
region: region
});
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");
});
});
Expand Down Expand Up @@ -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)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change also addresses the following bug report:

#88

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);
Expand All @@ -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'}`);
}
});

Expand All @@ -357,7 +362,7 @@ class ResembleHelper extends Helper {
}
});

fs.copyFileSync(this.screenshotFolder + screenShotImage, this.baseFolder + screenShotImage);
fs.copyFileSync(actualImage, baseImage);
}

/**
Expand Down Expand Up @@ -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;