Skip to content
This repository was archived by the owner on Oct 11, 2024. It is now read-only.

Commit 8b6cb15

Browse files
cbt1stoffeastrom
authored andcommitted
feat: chai screenshot plugin (#306)
* add chai sceenshot plugin * use screenshot plugin in protractor * fix plugin tests * support explicit type * remove plugin from protractor * preset screenshot plugin * make package private * do not require platform and browser name * use takeImageOf context when applicable * fix breaking parameter change * only create required directories * fix tests * add toImage tests * add docs * add snapshot example * Update after review * fix example
1 parent 17d44f7 commit 8b6cb15

10 files changed

Lines changed: 457 additions & 268 deletions

File tree

commands/cli/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"@after-work.js/node": "^5.0.0-beta.2",
3939
"@after-work.js/protractor": "^5.0.0-beta.2",
4040
"@after-work.js/puppeteer": "^5.0.0-beta.2",
41-
"@after-work.js/serve": "^5.0.0-beta.2"
41+
"@after-work.js/serve": "^5.0.0-beta.2",
42+
"@after-work.js/chai-plugin-screenshot": "^5.0.0-beta.2"
4243
},
4344
"files": [
4445
"/src"

commands/cli/src/preset-env.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const screenshotPlugin = require('@after-work.js/chai-plugin-screenshot');
12
const sinon = require('sinon');
23
const chai = require('chai');
34
const sinonChai = require('sinon-chai');
@@ -11,3 +12,4 @@ global.expect = chai.expect;
1112
chai.use(sinonChai);
1213
chai.use(chaiAsPromised);
1314
chai.use(chaiSubset);
15+
chai.Assertion.addMethod('matchImageOf', screenshotPlugin.matchImageOf);

commands/protractor/src/plugins/screenshoter/index.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
/* global browser */
22
const utils = require('./utils');
33

4-
global.chai.Assertion.addMethod('matchImageOf', utils.matchImageOf);
5-
64
const screenshoter = {
75
/**
86
* Sets up plugins before tests are run. This is called after the WebDriver
Lines changed: 0 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
/* globals document, Element, window */
2-
const path = require('path');
32
const fs = require('fs');
43
const jimp = require('jimp');
5-
const util = require('util');
6-
const mkdirp = require('mkdirp');
74

85
function getBoundingClientRect(selector, cb) {
96
/* eslint-disable */
@@ -29,13 +26,6 @@ const utils = {
2926
getBrowserName(browser) {
3027
return browser.getCapabilities();
3128
},
32-
fileExists(filePath) {
33-
return new Promise((resolve) => {
34-
fs.lstat(filePath, (err) => {
35-
resolve(!err);
36-
});
37-
});
38-
},
3929
removeFile(filePath) {
4030
return new Promise((resolve, reject) => {
4131
fs.unlink(filePath, (err) => {
@@ -50,28 +40,6 @@ const utils = {
5040
removeFiles(...files) {
5141
return Promise.all(files.map(file => this.removeFile(file)));
5242
},
53-
writeImage(img, filePath) {
54-
return new Promise((resolve, reject) => {
55-
img.write(filePath, (err) => {
56-
if (err) {
57-
reject(err);
58-
} else {
59-
resolve();
60-
}
61-
});
62-
});
63-
},
64-
compare(baseline, regressionImg, tolerance) {
65-
return jimp.read(baseline).then((baselineImg) => {
66-
const distance = jimp.distance(baselineImg, regressionImg);
67-
const diff = jimp.diff(baselineImg, regressionImg);
68-
return {
69-
diffImg: diff.image,
70-
isEqual: distance <= Math.max(0.02, tolerance) && diff.percent <= tolerance,
71-
equality: `distance: ${distance}, percent: ${diff.percent}`,
72-
};
73-
});
74-
},
7543
takeImageOf(browser, {
7644
selector = 'body', offsetX = 0, offsetY = 0, offsetWidth = 0, offsetHeight = 0,
7745
} = {}) {
@@ -98,60 +66,6 @@ const utils = {
9866
platform: caps.get('platform').replace(/ /g, '-').toLowerCase(),
9967
})));
10068
},
101-
matchImageOf(id, folder = '', tolerance = 0.002) {
102-
const promise = this._obj.then ? this._obj : Promise.resolve(this._obj); // eslint-disable-line
103-
return promise.then((meta) => {
104-
const imageName = util.format('%s-%s-%s.png', id, meta.platform, meta.browserName);
105-
106-
mkdirp.sync(path.resolve(meta.artifactsPath, 'baseline', folder));
107-
mkdirp.sync(path.resolve(meta.artifactsPath, 'regression', folder));
108-
mkdirp.sync(path.resolve(meta.artifactsPath, 'diff', folder));
109-
110-
const baseline = path.resolve(meta.artifactsPath, 'baseline', folder, imageName);
111-
const regression = path.resolve(meta.artifactsPath, 'regression', folder, imageName);
112-
const diff = path.resolve(meta.artifactsPath, 'diff', folder, imageName);
113-
114-
// Injecting images into assert
115-
const expected = {
116-
baseline: path.join('baseline', folder, imageName).replace(/\\/g, '/'),
117-
diff: path.join('diff', folder, imageName).replace(/\\/g, '/'),
118-
regression: path.join('regression', folder, imageName).replace(/\\/g, '/'),
119-
};
120-
121-
const actual = {};
122-
123-
return utils.fileExists(baseline).then((exists) => {
124-
if (!exists) {
125-
return utils.writeImage(meta.img, baseline).then(() => {
126-
this.assert(
127-
false,
128-
`No baseline found! New baseline generated at ${`${meta.artifactsPath}/${expected.baseline}`}`,
129-
`No baseline found! New baseline generated at ${`${meta.artifactsPath}/${expected.baseline}`}`,
130-
JSON.stringify(expected),
131-
actual,
132-
);
133-
});
134-
}
135-
return utils.compare(baseline, meta.img, tolerance).then((comparison) => {
136-
if (comparison.isEqual) {
137-
return comparison;
138-
}
139-
return Promise.all([
140-
utils.writeImage(meta.img, regression),
141-
utils.writeImage(comparison.diffImg, diff)]).then(() => {
142-
this.assert(
143-
comparison.isEqual === true,
144-
`expected ${id} equality to be less than ${tolerance}, but was ${comparison.equality}`,
145-
`expected ${id} equality to be greater than ${tolerance}, but was ${comparison.equality}`,
146-
JSON.stringify(expected),
147-
actual,
148-
);
149-
return comparison;
150-
});
151-
});
152-
});
153-
});
154-
},
15569
};
15670

15771
module.exports = utils;
Lines changed: 0 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
const path = require('path');
21
const fs = require('fs');
3-
const mkdirp = require('mkdirp');
42
const jimp = require('jimp');
53
const utils = require('../../../../src/plugins/screenshoter/utils');
64

@@ -15,18 +13,6 @@ describe('Screenshoter Utils', () => {
1513
sandbox.restore();
1614
});
1715

18-
describe('fileExists', () => {
19-
it('should return true if file exists', () => {
20-
sandbox.stub(fs, 'lstat').callsArgWith(1, false);
21-
return expect(utils.fileExists('foo')).to.eventually.equal(true);
22-
});
23-
24-
it("should return false if file doesn't exist", () => {
25-
sandbox.stub(fs, 'lstat').callsArgWith(1, true);
26-
return expect(utils.fileExists('foo')).to.eventually.equal(false);
27-
});
28-
});
29-
3016
describe('removeFile', () => {
3117
it('should resolve if the file could be removed', () => {
3218
sandbox.stub(fs, 'unlink').callsArgWith(1, false);
@@ -52,86 +38,6 @@ describe('Screenshoter Utils', () => {
5238
});
5339
});
5440

55-
describe('writeImage', () => {
56-
let img;
57-
58-
beforeEach(() => {
59-
img = { write: sandbox.stub() };
60-
});
61-
62-
it('should write an image to a file', () => {
63-
img.write.callsArgWith(1);
64-
return expect(utils.writeImage(img, 'foo')).to.eventually.be.fulfilled.and.be.an('undefined');
65-
});
66-
67-
it("should reject if image file couldn't be created", () => {
68-
img.write.callsArgWith(1, 'error');
69-
return expect(utils.writeImage(img, 'foo')).to.eventually.be.rejectedWith('error');
70-
});
71-
});
72-
73-
describe('compare', () => {
74-
let jimpRead;
75-
let jimpDistance;
76-
let jimpDiff;
77-
78-
beforeEach(() => {
79-
jimpRead = sandbox.stub(jimp, 'read');
80-
jimpDistance = sandbox.stub(jimp, 'distance');
81-
jimpDiff = sandbox.stub(jimp, 'diff');
82-
});
83-
84-
it('should be equal if the tolerance is met', () => {
85-
const distance = 0;
86-
const diffImg = {};
87-
const percent = 0;
88-
jimpRead.returns(Promise.resolve({}));
89-
jimpDistance.returns(distance);
90-
jimpDiff.returns({ image: diffImg, percent });
91-
92-
return expect(utils.compare('baseline', 'regression', 0)).to.eventually.be.fulfilled.and.deep.equal({
93-
diffImg,
94-
isEqual: true,
95-
equality: `distance: ${distance}, percent: ${percent}`,
96-
});
97-
});
98-
99-
it("should not be equal if the tolerance isn't met", () => {
100-
const distance = 0.1;
101-
const diffImg = {};
102-
const percent = 0;
103-
jimpRead.returns(Promise.resolve({}));
104-
jimpDistance.returns(distance);
105-
jimpDiff.returns({ image: diffImg, percent });
106-
107-
return expect(utils.compare('baseline', 'regression', 0)).to.eventually.be.fulfilled.and.deep.equal({
108-
diffImg,
109-
isEqual: false,
110-
equality: `distance: ${distance}, percent: ${percent}`,
111-
});
112-
});
113-
114-
it("should not be equal if the tolerance isn't met", () => {
115-
const distance = 0.1;
116-
const diffImg = {};
117-
const percent = 0;
118-
jimpRead.returns(Promise.resolve({}));
119-
jimpDistance.returns(distance);
120-
jimpDiff.returns({ image: diffImg, percent });
121-
122-
return expect(utils.compare('baseline', 'regression', 0)).to.eventually.be.fulfilled.and.deep.equal({
123-
diffImg,
124-
isEqual: false,
125-
equality: `distance: ${distance}, percent: ${percent}`,
126-
});
127-
});
128-
129-
it('should reject with error', () => {
130-
jimpRead.returns(Promise.reject(new Error('error')));
131-
expect(utils.compare('baseline', 'regression', 0)).to.eventually.be.rejected.and.have.property('message', 'error');
132-
});
133-
});
134-
13541
describe('getBoundingClientRect', () => {
13642
let querySelector;
13743
let getBoundingClientRect;
@@ -221,88 +127,4 @@ describe('Screenshoter Utils', () => {
221127
expect(result.browserName).to.equal('chrome');
222128
}));
223129
});
224-
225-
describe('matchImageOf', () => {
226-
let chaiCtx;
227-
let matchImageOf;
228-
let fileExists;
229-
let writeImage;
230-
let compare;
231-
let mkdir;
232-
const baselinePath = 'artifacts/baseline/';
233-
const baseline = `${baselinePath}id-windows-nt-chrome.png`;
234-
const regressionPath = 'artifacts/regression/';
235-
const regression = `${regressionPath}id-windows-nt-chrome.png`;
236-
const diffPath = 'artifacts/diff/';
237-
const diff = `${diffPath}id-windows-nt-chrome.png`;
238-
const img = {};
239-
240-
beforeEach(() => {
241-
sandbox.stub(path, 'resolve').callsFake((...args) => args.join('/').replace('//', '/'));
242-
fileExists = sandbox.stub(utils, 'fileExists');
243-
writeImage = sandbox.stub(utils, 'writeImage');
244-
compare = sandbox.stub(utils, 'compare');
245-
chaiCtx = {
246-
_obj: Promise.resolve({
247-
img, browserName: 'chrome', artifactsPath: 'artifacts', platform: 'windows-nt',
248-
}),
249-
assert: sinon.stub(),
250-
};
251-
matchImageOf = utils.matchImageOf.bind(chaiCtx);
252-
sandbox.stub(process, 'cwd').returns('foo');
253-
mkdir = sandbox.stub(mkdirp, 'sync');
254-
});
255-
256-
it("should write baseline if it's not existing", () => {
257-
fileExists.returns(Promise.resolve(false));
258-
writeImage.returns(Promise.resolve());
259-
return matchImageOf('id').then(() => {
260-
expect(writeImage).to.have.been.calledWith({}, baseline);
261-
});
262-
});
263-
264-
it('should compare and resolve if considered equal to baseline', () => {
265-
fileExists.returns(Promise.resolve(true));
266-
writeImage.returns(Promise.resolve());
267-
compare.returns(Promise.resolve({ equality: 0, isEqual: true }));
268-
return matchImageOf('id').then((comparison) => {
269-
expect(comparison).to.deep.equal({ equality: 0, isEqual: true });
270-
});
271-
});
272-
273-
it('should reject if not considered equal to baseline', () => {
274-
const diffImg = {};
275-
fileExists.returns(Promise.resolve(true));
276-
writeImage.returns(Promise.resolve());
277-
compare.returns(Promise.resolve({ isEqual: false, diffImg }));
278-
return matchImageOf('id').then(() => {
279-
expect(writeImage.callCount).to.equal(2);
280-
expect(writeImage.firstCall).to.have.been.calledWithExactly(img, regression);
281-
expect(writeImage.secondCall).to.have.been.calledWithExactly(diffImg, diff);
282-
});
283-
});
284-
285-
it('should create the default artifacts folders', () => {
286-
fileExists.returns(Promise.resolve(false));
287-
writeImage.returns(Promise.resolve());
288-
return matchImageOf('id').then(() => {
289-
expect(mkdir.callCount).to.equal(3);
290-
expect(mkdir.firstCall).to.have.been.calledWithExactly(baselinePath);
291-
expect(mkdir.secondCall).to.have.been.calledWithExactly(regressionPath);
292-
expect(mkdir.thirdCall).to.have.been.calledWithExactly(diffPath);
293-
});
294-
});
295-
296-
it('should create the artifacts folders with subfolders', () => {
297-
const folders = 'foo/bar';
298-
fileExists.returns(Promise.resolve(false));
299-
writeImage.returns(Promise.resolve());
300-
return matchImageOf('id', folders).then(() => {
301-
expect(mkdir.callCount).to.equal(3);
302-
expect(mkdir.firstCall).to.have.been.calledWithExactly(baselinePath + folders);
303-
expect(mkdir.secondCall).to.have.been.calledWithExactly(regressionPath + folders);
304-
expect(mkdir.thirdCall).to.have.been.calledWithExactly(diffPath + folders);
305-
});
306-
});
307-
});
308130
});

docs/node.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ describe('button', () => {
4444
</p>
4545
</details>
4646

47+
## Screenshot testing
48+
49+
When using the preset-env option. A screenshot assertion plugin is added to Chai. This allows comparisons of images.
50+
51+
```javascript
52+
describe('screenshot', () => {
53+
it('image should be equal', async () => {
54+
const img = Promise.resolve('<base64-encoded-image>'); // Promise that resolves to Buffer or a base64 encoded image
55+
await expect(img).to.matchImageOf('<name-of-my-img-on-disk>', {
56+
artifactsPath: 'tests/__artifacts__',
57+
tolerance: 0.002
58+
});
59+
});
60+
});
61+
```
4762

4863
## Options
4964

@@ -57,6 +72,7 @@ describe('button', () => {
5772
<p>
5873

5974
```javascript
75+
const screenshotPlugin = require('@after-work.js/chai-plugin-screenshot');
6076
const sinon = require('sinon');
6177
const chai = require('chai');
6278
const sinonChai = require('sinon-chai');
@@ -70,6 +86,7 @@ global.expect = chai.expect;
7086
chai.use(sinonChai);
7187
chai.use(chaiAsPromised);
7288
chai.use(chaiSubset);
89+
chai.Assertion.addMethod('matchImageOf', screenshotPlugin.matchImageOf);
7390
```
7491

7592
This enables writing your tests like this:

lerna.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"packages": [
44
"commands/*",
55
"commands-common/*",
6-
"examples/*"
6+
"examples/*",
7+
"plugins/*"
78
],
89
"npmClient": "npm",
910
"npmClientArgs": [

0 commit comments

Comments
 (0)