From e9134314524e0bdddcb6fdbd0fcfc211e61025c1 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Fri, 8 Jan 2021 10:55:39 +0000 Subject: [PATCH 1/6] feat: Setup custom matcher - toMatchBaseline() --- src/index.ts | 2 ++ src/matchers.ts | 30 ++++++++++++++++++++++++++++++ src/take-screenshot.ts | 6 ++++-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 src/matchers.ts diff --git a/src/index.ts b/src/index.ts index 193bf6d1..5a32e1af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,3 @@ +import './matchers'; + export { takeScreenshot } from './take-screenshot'; diff --git a/src/matchers.ts b/src/matchers.ts new file mode 100644 index 00000000..2fb7e1b9 --- /dev/null +++ b/src/matchers.ts @@ -0,0 +1,30 @@ +import path from 'path'; + +import { Platform } from './cli/types'; + +declare global { + namespace jest { + interface Matchers { + /** Compares the image passed to the baseline one */ + toMatchBaseline: () => CustomMatcherResult; + } + } +} + +export const toMatchBaseline = (latestPath: string) => { + const platform = process.env.OWL_PLATFORM as Platform; + const screenshotsDir = path.join(path.dirname(latestPath), '..', '..'); + const baselinePath = path.join( + screenshotsDir, + 'baseline', + platform, + path.basename(latestPath) + ); + + return { + message: () => `expected latest to match baseline`, + pass: true, + }; +}; + +expect.extend({ toMatchBaseline }); diff --git a/src/take-screenshot.ts b/src/take-screenshot.ts index 9931ed27..ed1f7c45 100644 --- a/src/take-screenshot.ts +++ b/src/take-screenshot.ts @@ -5,7 +5,7 @@ import path from 'path'; import { Platform } from './cli/types'; import { Logger } from './logger'; -export const takeScreenshot = async (): Promise => { +export const takeScreenshot = async (): Promise => { const platform = process.env.OWL_PLATFORM as Platform; const debug = process.env.OWL_DEBUG === 'true'; const updateBaseline = process.env.OWL_UPDATE_BASELINE === 'true'; @@ -31,5 +31,7 @@ export const takeScreenshot = async (): Promise => { shell: platform === 'android', }); - logger.info(`[OWL] Screenshot saved to ${cwd}/${DEFAULT_FILENAME}.`); + const screenshotPath = `${cwd}/${DEFAULT_FILENAME}`; + logger.info(`[OWL] Screenshot saved to ${screenshotPath}.`); + return screenshotPath; }; From c1883a0bf46821cf47f44569f1cc2e49b6b10b5d Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Fri, 8 Jan 2021 14:40:24 +0000 Subject: [PATCH 2/6] feat: Use pixelmatch & pngjs to diff screenshots --- package.json | 4 ++++ src/matchers.ts | 27 ++++++++++++++++++++++++++- yarn.lock | 26 ++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f83ce9e1..c5710262 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,14 @@ "dependencies": { "ajv": "^7.0.3", "execa": "^5.0.0", + "pixelmatch": "^5.2.1", + "pngjs": "^4.0.1", "yargs": "^16.2.0" }, "devDependencies": { "@types/jest": "^26.0.19", + "@types/pixelmatch": "^5.2.2", + "@types/pngjs": "^3.4.2", "@types/yargs": "^15.0.12", "jest": "^26.6.3", "prettier": "^2.2.1", diff --git a/src/matchers.ts b/src/matchers.ts index 2fb7e1b9..42991ca0 100644 --- a/src/matchers.ts +++ b/src/matchers.ts @@ -1,4 +1,7 @@ import path from 'path'; +import pixelmatch from 'pixelmatch'; +import fs from 'fs'; +import { PNG } from 'pngjs'; import { Platform } from './cli/types'; @@ -20,10 +23,32 @@ export const toMatchBaseline = (latestPath: string) => { platform, path.basename(latestPath) ); + const diffPath = path.join( + screenshotsDir, + 'diff', + platform, + path.basename(latestPath) + ); + + const baseline = PNG.sync.read(fs.readFileSync(baselinePath)); + const latest = PNG.sync.read(fs.readFileSync(latestPath)); + const diff = new PNG({ width: baseline.width, height: baseline.height }); + + const diffPixelsCount = pixelmatch( + baseline.data, + latest.data, + diff.data, + baseline.width, + baseline.height, + { threshold: 0 } + ); + + fs.mkdirSync(path.dirname(diffPath), { recursive: true }); + fs.writeFileSync(diffPath, PNG.sync.write(diff)); return { message: () => `expected latest to match baseline`, - pass: true, + pass: diffPixelsCount === 0, }; }; diff --git a/yarn.lock b/yarn.lock index e0365d08..0ab7da4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -556,6 +556,20 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/pixelmatch@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@types/pixelmatch/-/pixelmatch-5.2.2.tgz#3403238d4b920bf2255fb6cbf9a098bef796ce62" + integrity sha512-ndpfW/H8+SAiI3wt+f8DlHGgB7OeBdgFgBJ6v/1l3SpJ0MCn9wtXFb4mUccMujN5S4DMmAh7MVy1O3WcXrHUKw== + dependencies: + "@types/node" "*" + +"@types/pngjs@^3.4.2": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.4.2.tgz#8dc49b45fbcf18a5873179e3664f049388e39ecf" + integrity sha512-LJVPDraJ5YFEnMHnzxTN4psdWz1M61MtaAAWPn3qnDk5fvs7BAmmQ9pd3KPlrdrvozMyne4ktanD4pg0L7x1Pw== + dependencies: + "@types/node" "*" + "@types/prettier@^2.0.0": version "2.1.5" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" @@ -2786,6 +2800,13 @@ pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" +pixelmatch@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.2.1.tgz#9e4e4f4aa59648208a31310306a5bed5522b0d65" + integrity sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ== + dependencies: + pngjs "^4.0.1" + pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -2793,6 +2814,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pngjs@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-4.0.1.tgz#f803869bb2fc1bfe1bf99aa4ec21c108117cfdbe" + integrity sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg== + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" From 190c20fed527b63978062357e03785f7e1aea814 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Sat, 9 Jan 2021 18:44:06 +0000 Subject: [PATCH 3/6] feat: Use native-image-diff instead of pixelmatch --- package.json | 6 ++---- src/matchers.ts | 34 +++++++++++++++------------------- yarn.lock | 42 +++++++++++++++--------------------------- 3 files changed, 32 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index c5710262..a797f235 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,12 @@ "dependencies": { "ajv": "^7.0.3", "execa": "^5.0.0", - "pixelmatch": "^5.2.1", - "pngjs": "^4.0.1", + "native-image-diff": "^0.1.11", + "node-libpng": "^0.2.18", "yargs": "^16.2.0" }, "devDependencies": { "@types/jest": "^26.0.19", - "@types/pixelmatch": "^5.2.2", - "@types/pngjs": "^3.4.2", "@types/yargs": "^15.0.12", "jest": "^26.6.3", "prettier": "^2.2.1", diff --git a/src/matchers.ts b/src/matchers.ts index 42991ca0..be452ee6 100644 --- a/src/matchers.ts +++ b/src/matchers.ts @@ -1,7 +1,6 @@ import path from 'path'; -import pixelmatch from 'pixelmatch'; -import fs from 'fs'; -import { PNG } from 'pngjs'; +import { diffImages } from 'native-image-diff'; +import { readPngFileSync, writePngFileSync, rect, xy } from 'node-libpng'; import { Platform } from './cli/types'; @@ -30,25 +29,22 @@ export const toMatchBaseline = (latestPath: string) => { path.basename(latestPath) ); - const baseline = PNG.sync.read(fs.readFileSync(baselinePath)); - const latest = PNG.sync.read(fs.readFileSync(latestPath)); - const diff = new PNG({ width: baseline.width, height: baseline.height }); + const baselineImage = readPngFileSync(baselinePath); + const { image: diff, pixels } = diffImages({ + image1: baselineImage, + image2: readPngFileSync(latestPath), + colorThreshold: 0.1, + }); - const diffPixelsCount = pixelmatch( - baseline.data, - latest.data, - diff.data, - baseline.width, - baseline.height, - { threshold: 0 } - ); - - fs.mkdirSync(path.dirname(diffPath), { recursive: true }); - fs.writeFileSync(diffPath, PNG.sync.write(diff)); + writePngFileSync(diffPath, diff!.data, { + width: diff!.width, + height: diff!.height, + }); return { - message: () => `expected latest to match baseline`, - pass: diffPixelsCount === 0, + message: () => + `Compared screenshot to match baseline. ${pixels} were different.`, + pass: pixels === 0, }; }; diff --git a/yarn.lock b/yarn.lock index 0ab7da4e..33a36705 100644 --- a/yarn.lock +++ b/yarn.lock @@ -556,20 +556,6 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== -"@types/pixelmatch@^5.2.2": - version "5.2.2" - resolved "https://registry.yarnpkg.com/@types/pixelmatch/-/pixelmatch-5.2.2.tgz#3403238d4b920bf2255fb6cbf9a098bef796ce62" - integrity sha512-ndpfW/H8+SAiI3wt+f8DlHGgB7OeBdgFgBJ6v/1l3SpJ0MCn9wtXFb4mUccMujN5S4DMmAh7MVy1O3WcXrHUKw== - dependencies: - "@types/node" "*" - -"@types/pngjs@^3.4.2": - version "3.4.2" - resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.4.2.tgz#8dc49b45fbcf18a5873179e3664f049388e39ecf" - integrity sha512-LJVPDraJ5YFEnMHnzxTN4psdWz1M61MtaAAWPn3qnDk5fvs7BAmmQ9pd3KPlrdrvozMyne4ktanD4pg0L7x1Pw== - dependencies: - "@types/node" "*" - "@types/prettier@^2.0.0": version "2.1.5" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" @@ -2582,6 +2568,13 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +native-image-diff@^0.1.11: + version "0.1.11" + resolved "https://registry.yarnpkg.com/native-image-diff/-/native-image-diff-0.1.11.tgz#978d7107fe2f37f2d5417dd9510e6ea2c462d6dd" + integrity sha512-/81SQYlMj8BjC3I0jko8CHwNDl6BAVAOXGA03ZQdw8LGc7x2xbyQ5qDfC86JRTb45DrZa1U4LjcgIJ8cVkvqNg== + dependencies: + request "^2.88.0" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2597,6 +2590,13 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= +node-libpng@^0.2.18: + version "0.2.18" + resolved "https://registry.yarnpkg.com/node-libpng/-/node-libpng-0.2.18.tgz#af0f7cb7b854cac12a1eae1c268328275c043c5a" + integrity sha512-nr2j+Qn68ocw/0mfj09Yg8AEEdjrrQYMnFAWi8wLpwe2FMdLr36OFalIEkrJnwpqQxDybfqw7l1GH2wJ98wUIw== + dependencies: + request "^2.88.2" + node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -2800,13 +2800,6 @@ pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" -pixelmatch@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.2.1.tgz#9e4e4f4aa59648208a31310306a5bed5522b0d65" - integrity sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ== - dependencies: - pngjs "^4.0.1" - pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -2814,11 +2807,6 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -pngjs@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-4.0.1.tgz#f803869bb2fc1bfe1bf99aa4ec21c108117cfdbe" - integrity sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg== - posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -2938,7 +2926,7 @@ request-promise-native@^1.0.8: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.88.2: +request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== From 07169b75cda8d0260f3a8834c6d24a17c5ae17dd Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Mon, 11 Jan 2021 16:58:53 +0000 Subject: [PATCH 4/6] chore: Fix Tests --- package.json | 3 +- src/matchers.test.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++ src/matchers.ts | 22 +++++++++---- yarn.lock | 19 +++++++---- 4 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 src/matchers.test.ts diff --git a/package.json b/package.json index a797f235..80f2b514 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,12 @@ "ajv": "^7.0.3", "execa": "^5.0.0", "native-image-diff": "^0.1.11", - "node-libpng": "^0.2.18", + "pngjs": "^6.0.0", "yargs": "^16.2.0" }, "devDependencies": { "@types/jest": "^26.0.19", + "@types/pngjs": "^3.4.2", "@types/yargs": "^15.0.12", "jest": "^26.6.3", "prettier": "^2.2.1", diff --git a/src/matchers.test.ts b/src/matchers.test.ts new file mode 100644 index 00000000..b2409234 --- /dev/null +++ b/src/matchers.test.ts @@ -0,0 +1,77 @@ +import fs from 'fs'; +import * as nativeImageDiff from 'native-image-diff'; + +import { toMatchBaseline } from './matchers'; + +describe('matchers.ts', () => { + const imageHello1Data = `iVBORw0KGgoAAAANSUhEUgAAACUAAAALCAYAAAD4OERFAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAJaADAAQAAAABAAAACwAAAADN8bJQAAABcElEQVQ4Ec2UvytGURjHr1/lHd5JlF9leMNAiQzKbqCMBt5ikUEWMhkMDGZ/gEHZDMqCsr6JQel9yW8Dg4lBJvL5ck7dTudw3UG+9el5nvM859znnnPujaIoKoMq+Ffqo5uTFB1dMacLuuEixXxNaYRNd26lO/BHcZbnjEEecu4zy82AjnARbuEIOsBqEqdkmLGDAav1luAB7mEBtLYrNdUKd27Cxjq+d1iFTtiALZCG4QYGQLlTGAXJd3zTjB9CM7SB6schpF4Sj76kmnoCu2v9+OemcBc7Z3yZKbAN+5o6Jj+hQiM1uWMDj/U2Ze+Utlu7Jb1A5tP7Om9NnDexvtIz4/tMC4MHscQ1fm0sTuTa3XkLVD8zrretM9RjByGkIommWFJHWIjFiVzbVKh4n8QIVBvWsLMQ0jaJPKheF3wI9uBX+qmpZVarAV12fSnyVyCkdRI9cAm65K/w3Z0inU56Y/1LRBJVUNQODUmKfTUfKJc7FJ+heOgAAAAASUVORK5CYII=`; + const imageHello1Buffer = Buffer.from(imageHello1Data, 'base64'); + + const imageHello2Data = `iVBORw0KGgoAAAANSUhEUgAAACUAAAALCAYAAAD4OERFAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAJaADAAQAAAABAAAACwAAAADN8bJQAAABsklEQVQ4Ec2UOyhFcRzHj1d5lEEhogyKQqEMyioDxYJBCoMMsngslDvYDQyUYlJsHimURZJQSi6RV15ZxGD0+Hxv///pdDq3bnfhW5/7e/3/5/4fv3Mcx3ESIAX+lWpZzWkcK7pmTiVUw1Uc83uYswZL0ALuwSQT/IXa+dNxgzY3DbkwC06ifpCuMAR3cATlYNWLEzYM2GQUq+dNwDM8wRjo2X7Vk5iEBdiFOWgDV7q+b5iCCliEFZCa4RYaQLVz6AAp6Pr6yR9CIZSAxneBX/kkMjzJZfyQJ3a0qHewp1aHf2kGbGGHjC/TB3bBQYs6od6tgUZa5KYNAmw6uRm4B20kIttTOm6dlvQJaRHPcYqxNTBiYjXjhfGDTBHJA0/hBj/bE3vdMoJ1ULtUwRtEZE/nyyZ89oNYu80x5GEbIZrOKBR4irrCfU9sXW12FYahFdwF4btXJj9IOyT1pqQa5rGDEE3aeSdovBq8CbbBr1ES+hRsgK5QaI4r9ZT3O6WjfDDVLOwevMArqMcyQQrqKZ1SGB5B16xF2lbAdaXaj49jtxqDox3ruEUsSmJQKegNi0u/XtRShUjycDoAAAAASUVORK5CYII=`; + const imageHello2Buffer = Buffer.from(imageHello2Data, 'base64'); + + const imageHelloDiffData = `iVBORw0KGgoAAAANSUhEUgAAACUAAAALCAYAAAD4OERFAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAJaADAAQAAAABAAAACwAAAADN8bJQAAAA0ElEQVQ4Ee2SsQsBYRiHH0qJGJSSMiib2Xir2WSWxb9hMhrMym5ktJrEIIuwSEmEuoUynN8N19Ht9xlc39P3696v3qf3++D/+RNwoLmBiQUH5ZqI+VUDaQeNLlxzsJXJXUJL0fJUol4Ic39BdQyrE8yVteiLepgOgV6aSl4kVaiIs/JQtAMHTfwogpWCh4T2omDC4aunJMpTOKbhqZz5LBp5U5IoSWI0g54NdgRun1JGsqQGouNen4QuygkRNyLjNZXAWjialJMVbhYLr/6T+xvERzxV3g04QAAAAABJRU5ErkJggg==`; + const imageHelloDiffBuffer = Buffer.from(imageHelloDiffData, 'base64'); + + const readFileMock = jest.spyOn(fs, 'readFileSync'); + const writeFileMock = jest.spyOn(fs, 'writeFileSync'); + + const diffImagesMock = jest.spyOn(nativeImageDiff, 'diffImages'); + + beforeAll(() => { + process.env.OWL_PLATFORM = 'ios'; + }); + + describe('toMatchBaseline.ts', () => { + beforeEach(() => { + readFileMock.mockReset(); + writeFileMock.mockReset(); + }); + + afterAll(() => { + delete process.env.OWL_PLATFORM; + }); + + it('should compare two identical images', () => { + readFileMock + .mockReturnValueOnce(imageHello1Buffer) + .mockReturnValueOnce(imageHello1Buffer); + + diffImagesMock.mockReturnValueOnce({ + image: { data: Buffer.alloc(100), width: 37, height: 11 }, + pixels: 0, + } as any); + + const latestPath = 'latest/ios/screen.png'; + + const result = toMatchBaseline(latestPath); + + expect(result.message()).toBe( + 'Compared screenshot to match baseline. 0 were different.' + ); + expect(result.pass).toBe(true); + expect(writeFileMock).toHaveBeenCalledTimes(1); + }); + + it('should compare two different images', () => { + readFileMock + .mockReturnValueOnce(imageHello1Buffer) + .mockReturnValueOnce(imageHello2Buffer); + + diffImagesMock.mockReturnValueOnce({ + image: { data: imageHelloDiffBuffer, width: 37, height: 11 }, + pixels: 55, + } as any); + + const latestPath = 'latest/ios/screen.png'; + + const result = toMatchBaseline(latestPath); + + expect(result.message()).toBe( + 'Compared screenshot to match baseline. 55 were different.' + ); + expect(result.pass).toBe(false); + expect(writeFileMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/matchers.ts b/src/matchers.ts index be452ee6..d24e48c1 100644 --- a/src/matchers.ts +++ b/src/matchers.ts @@ -1,6 +1,7 @@ +import fs from 'fs'; import path from 'path'; +import { PNG } from 'pngjs'; import { diffImages } from 'native-image-diff'; -import { readPngFileSync, writePngFileSync, rect, xy } from 'node-libpng'; import { Platform } from './cli/types'; @@ -22,6 +23,7 @@ export const toMatchBaseline = (latestPath: string) => { platform, path.basename(latestPath) ); + const diffPath = path.join( screenshotsDir, 'diff', @@ -29,17 +31,23 @@ export const toMatchBaseline = (latestPath: string) => { path.basename(latestPath) ); - const baselineImage = readPngFileSync(baselinePath); + const baselineData = fs.readFileSync(baselinePath); + const baselineImage = PNG.sync.read(baselineData); + + const latestData = fs.readFileSync(latestPath); + const latestImage = PNG.sync.read(latestData); + const { image: diff, pixels } = diffImages({ image1: baselineImage, - image2: readPngFileSync(latestPath), + image2: latestImage, colorThreshold: 0.1, }); - writePngFileSync(diffPath, diff!.data, { - width: diff!.width, - height: diff!.height, - }); + fs.mkdirSync(path.dirname(diffPath), { recursive: true }); + + const diffPng = { ...diff! } as PNG; + const diffImage = PNG.sync.write(diffPng); + fs.writeFileSync(diffPath, diffImage); return { message: () => diff --git a/yarn.lock b/yarn.lock index 33a36705..02f4ce69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -556,6 +556,13 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/pngjs@^3.4.2": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.4.2.tgz#8dc49b45fbcf18a5873179e3664f049388e39ecf" + integrity sha512-LJVPDraJ5YFEnMHnzxTN4psdWz1M61MtaAAWPn3qnDk5fvs7BAmmQ9pd3KPlrdrvozMyne4ktanD4pg0L7x1Pw== + dependencies: + "@types/node" "*" + "@types/prettier@^2.0.0": version "2.1.5" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" @@ -2590,13 +2597,6 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-libpng@^0.2.18: - version "0.2.18" - resolved "https://registry.yarnpkg.com/node-libpng/-/node-libpng-0.2.18.tgz#af0f7cb7b854cac12a1eae1c268328275c043c5a" - integrity sha512-nr2j+Qn68ocw/0mfj09Yg8AEEdjrrQYMnFAWi8wLpwe2FMdLr36OFalIEkrJnwpqQxDybfqw7l1GH2wJ98wUIw== - dependencies: - request "^2.88.2" - node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -2807,6 +2807,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pngjs@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821" + integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg== + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" From bdf69a40d27b9d31333fee0cde03aed26f6eb574 Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Mon, 11 Jan 2021 20:50:29 +0000 Subject: [PATCH 5/6] chore: Update README.md --- README.md | 28 ++++++++++++++++++++++++++++ src/matchers.test.ts | 4 ++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ef6ce040..c3a58135 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,31 @@ owl test --platform ios --update [github-image]: https://github.com/FormidableLabs/react-native-owl/workflows/Run%20Tests/badge.svg [github-url]: https://github.com/FormidableLabs/react-native-owl/actions + +## Test Suite + +### Example + +```js +import { takeScreenshot } from 'react-native-owl'; + +describe('App.tsx', () => { + it('takes a screenshot of the first screen', async () => { + const screen = await takeScreenshot(); + + expect(screen).toMatchBaseline(); + }); +}); +``` + +### Methods + +#### `takeScreenshot()` + +Grabs a screenshot from the simulator and stores it under `latest` screenshots(ie. `./owl/latest/ios/`). If running the tests using the `--update` or `-u` flag, this will store the screenshot under the `baseline` directory. See example above. + +### Jest Matchers + +#### `.toMatchBaseline()` + +This custom matcher will try to find and compare the baseline screenshot by using the path of the _latest_ screenshot (returned by `takeScreenshot()`). You will have to take a screenshot before using and pass the path of that screenshot to the `expect` method. diff --git a/src/matchers.test.ts b/src/matchers.test.ts index b2409234..db323f1b 100644 --- a/src/matchers.test.ts +++ b/src/matchers.test.ts @@ -40,7 +40,7 @@ describe('matchers.ts', () => { diffImagesMock.mockReturnValueOnce({ image: { data: Buffer.alloc(100), width: 37, height: 11 }, pixels: 0, - } as any); + } as nativeImageDiff.DiffResult); const latestPath = 'latest/ios/screen.png'; @@ -61,7 +61,7 @@ describe('matchers.ts', () => { diffImagesMock.mockReturnValueOnce({ image: { data: imageHelloDiffBuffer, width: 37, height: 11 }, pixels: 55, - } as any); + } as nativeImageDiff.DiffResult); const latestPath = 'latest/ios/screen.png'; From 3ca2469998fa207134f8f17130c895b60810efeb Mon Sep 17 00:00:00 2001 From: Emmanouil Konstantinidis Date: Mon, 11 Jan 2021 21:00:52 +0000 Subject: [PATCH 6/6] chore: Clean Up --- src/matchers.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matchers.test.ts b/src/matchers.test.ts index db323f1b..cec929b4 100644 --- a/src/matchers.test.ts +++ b/src/matchers.test.ts @@ -22,16 +22,16 @@ describe('matchers.ts', () => { process.env.OWL_PLATFORM = 'ios'; }); + afterAll(() => { + delete process.env.OWL_PLATFORM; + }); + describe('toMatchBaseline.ts', () => { beforeEach(() => { readFileMock.mockReset(); writeFileMock.mockReset(); }); - afterAll(() => { - delete process.env.OWL_PLATFORM; - }); - it('should compare two identical images', () => { readFileMock .mockReturnValueOnce(imageHello1Buffer)