├── .github └── workflows │ ├── npm-publish.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── __test__ ├── codecept.conf.ts ├── index.spec.ts ├── package.json ├── screenshots │ ├── base │ │ ├── Playwright_doc.png │ │ └── element.png │ └── diff │ │ ├── Diff_Playwright_doc.png │ │ └── Diff_element.png ├── steps.d.ts ├── steps_file.ts ├── tsconfig.json └── visual_test.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── rome.json ├── src └── index.ts └── tsconfig.json /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Publish NPM package 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - master 11 | 12 | jobs: 13 | 14 | publish-npm: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | registry-url: https://registry.npmjs.org/ 22 | - run: npm i 23 | - run: npm run build 24 | - run: npx semantic-release 25 | env: 26 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 27 | GH_TOKEN: ${{secrets.GH_TOKEN}} 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | test: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - name: run unit tests 27 | run: | 28 | npm i && npx playwright install chromium 29 | npm test 30 | - name: lint check 31 | run: | 32 | npm i 33 | npm run lint 34 | - name: Build 35 | run: | 36 | npm i 37 | npm run build 38 | - name: Run acceptance tests 39 | working-directory: ./__test__ 40 | run: | 41 | npm i && npx playwright install chromium 42 | npm run test 43 | env: 44 | MAILINATOR_TOKEN: ${{secrets.MAILINATOR_TOKEN}} 45 | MAILINATOR_DOMAIN: ${{secrets.MAILINATOR_DOMAIN}} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | #webstorm 64 | .idea 65 | dist 66 | coverage 67 | __test__/node_modules 68 | __test__/package-lock.json 69 | __test__/output 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Percona 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # codeceptjs-resemblehelper 2 | Helper for resemble.js, used for image comparison in tests with Playwright, Webdriver, Puppeteer, Appium, TestCafe! 3 | 4 | codeceptjs-resemblehelper is a [CodeceptJS](https://codecept.io/) helper which can be used to compare screenshots and make the tests fail/pass based on the tolerance allowed. 5 | 6 | If two screenshot comparisons have difference greater then the tolerance provided, the test will fail. 7 | 8 | NPM package: https://www.npmjs.com/package/codeceptjs-resemblehelper 9 | 10 | To install the package, just run `npm install codeceptjs-resemblehelper`. 11 | 12 | ### Configuration 13 | 14 | This helper should be added in codecept.json/codecept.conf.js 15 | 16 | Example: 17 | 18 | ```json 19 | { 20 | "helpers": { 21 | "ResembleHelper" : { 22 | "require": "codeceptjs-resemblehelper", 23 | "baseFolder": "./tests/screenshots/base/", 24 | "diffFolder": "./tests/screenshots/diff/", 25 | "prepareBaseImage": true 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | To use the Helper, users may provide the parameters: 32 | 33 | `baseFolder`: Mandatory. This is the folder for base images, which will be used with screenshot for comparison. 34 | 35 | `diffFolder`: Mandatory. This will the folder where resemble would try to store the difference image, which can be viewed later. 36 | 37 | `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. 38 | 39 | `compareWithImage`: Optional. A custom filename to compare the screenshot with. The `compareWithImage` file must be located inside the `baseFolder`. 40 | 41 | ### Usage 42 | 43 | These are the major functions that help in visual testing: 44 | 45 | First one is the `seeVisualDiff` which basically takes two parameters 46 | 1) `baseImage` Name of the base image, this will be the image used for comparison with the screenshot image. It is mandatory to have the same image file names for base and screenshot image. 47 | 2) `options` options can be passed which include `prepaseBaseImage`, `tolerance` and `needsSameDimension`. 48 | 49 | ```js 50 | /** 51 | * Check Visual Difference for Base and Screenshot Image 52 | * @param baseImage Name of the Base Image (Base Image path is taken from Configuration) 53 | * @param options Options ex {prepareBaseImage: true, tolerance: 5, needsSameDimension: false} along with Resemble JS Options, read more here: https://github.com/rsmbl/Resemble.js 54 | * @returns {Promise} 55 | */ 56 | async seeVisualDiff(baseImage, options) {} 57 | ``` 58 | Second one is the `seeVisualDiffForElement` which basically compares elements on the screenshot, selector for element must be provided. 59 | 60 | It is exactly same as `seeVisualDiff` function, only an additional `selector` CSS|XPath|ID locators is provided 61 | ```js 62 | /** 63 | * See Visual Diff for an Element on a Page 64 | * 65 | * @param selector Selector which has to be compared, CSS|XPath|ID 66 | * @param baseImage Base Image for comparison 67 | * @param options Options ex {prepareBaseImage: true, tolerance: 5} along with Resemble JS Options, read more here: https://github.com/rsmbl/Resemble.js 68 | * @returns {Promise} 69 | */ 70 | async seeVisualDiffForElement(selector, baseImage, options){} 71 | ``` 72 | > Note: 73 | `seeVisualDiffForElement` only works when the page for baseImage is open in the browser, so that WebdriverIO can fetch coordinates of the provided selector. 74 | 75 | Third one is the `screenshotElement` which basically takes screenshot of the element. Selector for the element must be provided. It saves the image in the output directory as mentioned in the config folder. 76 | 77 | ```js 78 | I.screenshotElement("selectorForElement", "nameForImage"); 79 | ``` 80 | > Note: This method only works with puppeteer. 81 | 82 | Finally to use the helper in your test, you can write something like this: 83 | 84 | ```js 85 | Feature('to verify monitoried Remote Db instances'); 86 | 87 | Scenario('Open the System Overview Dashboard', async (I, adminPage, loginPage) => { 88 | adminPage.navigateToDashboard("OS", "System Overview"); 89 | I.saveScreenshot("Complete_Dashboard_Image.png"); 90 | adminPage.applyTimer("1m"); 91 | adminPage.viewMetric("CPU Usage"); 92 | I.saveScreenshot("Complete_Metric_Image.png"); 93 | }); 94 | 95 | Scenario('Compare CPU Usage Images', async (I) => { 96 | 97 | // setting tolerance and prepareBaseImage in the options array 98 | I.seeVisualDiff("Complete_Metric_Image.png", {prepareBaseImage: false, tolerance: 5}); 99 | 100 | // passing a selector, to only compare that element on both the images now 101 | 102 | // We need to navigate to that page first, so that webdriver can fetch coordinates for the selector 103 | adminPage.navigateToDashboard("OS", "System Overview"); 104 | I.seeVisualDiffForElement("//div[@class='panel-container']", "Complete_Dashboard_Image.png", {prepareBaseImage: false, tolerance: 3}); 105 | }); 106 | ``` 107 | > Note: `seeVisualDiff` and `seeVisualDiffElement` work only when the dimensions of the screenshot as well as the base image are same so as to avoid unexpected results. 108 | 109 | ### Ignored Box 110 | You can also exclude part of the image from comparison, by specifying the excluded area in pixels from the top left. 111 | Just declare an object and pass it in options as `ignoredBox`: 112 | ```js 113 | const box = { 114 | left: 0, 115 | top: 10, 116 | right: 0, 117 | bottom: 10 118 | }; 119 | 120 | I.seeVisualDiff("image.png", {prepareBaseImage: true, tolerance: 1, ignoredBox: box}); 121 | ``` 122 | After this, that specific mentioned part will be ignored while comparison. 123 | This works for `seeVisualDiff` and `seeVisualDiffForElement`. 124 | 125 | ### resemble.js Output Settings 126 | You can set further output settings used by resemble.js. Declare an object specifying them and pass it in the options as `outputSettings`: 127 | 128 | ```js 129 | const outputSettings = { 130 | ignoreAreasColoredWith: {r: 250, g: 250, b: 250, a: 0}, 131 | // read more here: https://github.com/rsmbl/Resemble.js 132 | }; 133 | I.seeVisualDiff("image.png", {prepareBaseImage: true, tolerance: 1, outputSettings: outputSettings}); 134 | ``` 135 | 136 | Refer to the [resemble.js](https://github.com/rsmbl/Resemble.js) documentation for available output settings. 137 | 138 | ### Skip Failure 139 | You can avoid the test fails for a given threshold but yet generates the difference image. 140 | Just declare an object and pass it in options as `skipFailure`: 141 | ``` 142 | I.seeVisualDiff("image.png", {prepareBaseImage: true, tolerance: 1, skipFailure: true}); 143 | ``` 144 | After this, the system generates the difference image but does not fail the test. 145 | This works for `seeVisualDiff` and `seeVisualDiffForElement`. 146 | 147 | 148 | ### Allure Reporter 149 | Allure reports may also be generated directly from the tool. To do so, add 150 | 151 | ``` 152 | "plugins": { 153 | "allure": {} 154 | } 155 | ``` 156 | 157 | in the config file. 158 | The attachments will be added to the report only when the calulated mismatch is greater than the given tolerance. 159 | Set `output` to where the generated report is to be stored. Default is the output directory of the project. 160 | 161 | ### AWS Support 162 | AWS S3 support to upload and download various images is also provided. 163 | It can be used by adding the *aws* code inside `"ResembleHelper"` in the `"helpers"` section in config file. The final result should look like: 164 | ```json 165 | { 166 | "helpers": { 167 | "ResembleHelper" : { 168 | "require": "codeceptjs-resemblehelper", 169 | "baseFolder": "", 170 | "diffFolder": "", 171 | "aws": { 172 | "accessKeyId" : "", 173 | "secretAccessKey": "", 174 | "region": "", 175 | "bucketName": "" 176 | } 177 | } 178 | } 179 | } 180 | ``` 181 | When this option has been provided, the helper will download the base image from the S3 bucket. 182 | This base image has to be located inside a folder named "*base*". 183 | The resultant output image will be uploaded in a folder named "*output*" and diff image will be uploaded to a folder named "*diff*" in the S3 bucket. 184 | If the `prepareBaseImage` option is marked `true`, then the generated base image will be uploaded to a folder named "*base*" in the S3 bucket. 185 | > 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. 186 | 187 | ### Other S3 Providers 188 | The same configuration as above, but with *endpoint* field: 189 | 190 | ```json 191 | { 192 | "helpers": { 193 | "ResembleHelper" : { 194 | "require": "codeceptjs-resemblehelper", 195 | "baseFolder": "", 196 | "diffFolder": "", 197 | "aws": { 198 | "accessKeyId" : "", 199 | "secretAccessKey": "", 200 | "region": "", 201 | "bucketName": "", 202 | "endpoint": "" 203 | } 204 | } 205 | } 206 | } 207 | ``` 208 | 209 | ### Compare with custom image 210 | 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. 211 | 212 | 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. 213 | ```js 214 | I.seeVisualDiffForElement("#element", "image.png", {compareWithImage: "dashboard.png"}); 215 | I.seeVisualDiffForElement("#element", "image.png", {compareWithImage: "account.png"}); 216 | ``` 217 | 218 | Or, in some cases there are intended visual differences for different browsers or operating systems: 219 | ```js 220 | const os = "win32" === process.platform ? "win" : "mac"; 221 | 222 | // Compare "image.png" either with "image-win.png" or "image-mac.png": 223 | I.seeVisualDiff("image.png", {compareWithImage: `image-${os}.png`}); 224 | ``` 225 | 226 | ### Known Issues: 227 | 228 | > Issue in Windows where the image comparison is not carried out, and therefore no Mismatch Percentage is shown. See 'loadImageData' function in resemble.js 229 | -------------------------------------------------------------------------------- /__test__/codecept.conf.ts: -------------------------------------------------------------------------------- 1 | export const config: CodeceptJS.MainConfig = { 2 | tests: './*_test.ts', 3 | output: './output', 4 | helpers: { 5 | Playwright: { 6 | url: 'https://codecept.io/', 7 | show: false, 8 | browser: 'chromium' 9 | }, 10 | "ResembleHelper" : { 11 | "require": "../src/index", 12 | "baseFolder": "./screenshots/base/", 13 | "diffFolder": "./screenshots/diff/", 14 | "prepareBaseImage": false 15 | } 16 | }, 17 | include: { 18 | I: './steps_file' 19 | }, 20 | name: '__test__' 21 | } 22 | -------------------------------------------------------------------------------- /__test__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import ResembleHelper from "../src"; 2 | const { container } = require('codeceptjs'); 3 | const helpers = container.helpers(); 4 | 5 | let helper = new ResembleHelper({ baseFolder: './__test__/screenshots/base/', 6 | diffFolder: './__test__/screenshots/diff/', 7 | screenshotFolder: './__test__/output', 8 | prepareBaseImage: true }) 9 | 10 | describe('_getHelper()', () => { 11 | test('should return error when no matching helper found', () => { 12 | try { 13 | helper._getHelper() 14 | } catch (e: any) { 15 | expect(e.message).toEqual('No matching helper found. Supported helpers: Playwright/Puppeteer/WebDriver/TestCafe/Appium') 16 | } 17 | }); 18 | }) 19 | 20 | describe('_getPrepareBaseImage()', () => { 21 | beforeAll(() => { 22 | helpers['Playwright'] = { hello: 1 } 23 | }) 24 | test('should return false when no prepareBaseImage is provided', () => { 25 | expect(helper._getPrepareBaseImage({ prepareBaseImage: false, tolerance: 1 })).toBeFalsy() 26 | }); 27 | 28 | test('should return true when prepareBaseImage matched with config', () => { 29 | expect(helper._getPrepareBaseImage({ prepareBaseImage: true })).toBeTruthy() 30 | }); 31 | }) 32 | 33 | describe('_getDiffImagePath()', () => { 34 | beforeAll(() => { 35 | helpers['Playwright'] = { hello: 1 } 36 | }) 37 | test('should return diffImagePath', () => { 38 | expect(helper._getDiffImagePath('hello')).toContain('Diff_hello.png') 39 | }); 40 | 41 | }) 42 | 43 | describe('_getActualImagePath()', () => { 44 | beforeAll(() => { 45 | helpers['Playwright'] = { hello: 1 } 46 | }) 47 | test('should return ActualImagePath', () => { 48 | expect(helper._getActualImagePath('hello')).toContain('hello') 49 | }); 50 | 51 | }) 52 | 53 | describe('_getBaseImagePath()', () => { 54 | beforeAll(() => { 55 | helpers['Playwright'] = { hello: 1 } 56 | }) 57 | test('should return BaseImagePath', () => { 58 | expect(helper._getBaseImagePath('hello', {})).toContain('hello') 59 | }); 60 | 61 | }) 62 | 63 | describe('resolvePath()', () => { 64 | beforeAll(() => { 65 | helpers['Playwright'] = { hello: 1 } 66 | }) 67 | test('should return resolvePath', () => { 68 | expect(helper.resolvePath('hello')).toContain('hello') 69 | }); 70 | }) 71 | -------------------------------------------------------------------------------- /__test__/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__test__", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "codeceptjs run --verbose" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "codeceptjs": "^3.4.1", 14 | "playwright": "^1.32.3" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^18.15.11", 18 | "ts-node": "^10.9.1", 19 | "typescript": "^5.0.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /__test__/screenshots/base/Playwright_doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/codeceptjs-resemblehelper/6a255fd8e86d0743f480e5afb863eebc02de6d00/__test__/screenshots/base/Playwright_doc.png -------------------------------------------------------------------------------- /__test__/screenshots/base/element.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/codeceptjs-resemblehelper/6a255fd8e86d0743f480e5afb863eebc02de6d00/__test__/screenshots/base/element.png -------------------------------------------------------------------------------- /__test__/screenshots/diff/Diff_Playwright_doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/codeceptjs-resemblehelper/6a255fd8e86d0743f480e5afb863eebc02de6d00/__test__/screenshots/diff/Diff_Playwright_doc.png -------------------------------------------------------------------------------- /__test__/screenshots/diff/Diff_element.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeceptjs/codeceptjs-resemblehelper/6a255fd8e86d0743f480e5afb863eebc02de6d00/__test__/screenshots/diff/Diff_element.png -------------------------------------------------------------------------------- /__test__/steps.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | type steps_file = typeof import('./steps_file'); 3 | type ResembleHelper = import('../src/index'); 4 | 5 | declare namespace CodeceptJS { 6 | interface SupportObject { I: I, current: any } 7 | interface Methods extends Playwright, ResembleHelper {} 8 | interface I extends ReturnType, WithTranslation, WithTranslation {} 9 | namespace Translation { 10 | interface Actions {} 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /__test__/steps_file.ts: -------------------------------------------------------------------------------- 1 | // in this file you can append custom step methods to 'I' object 2 | 3 | module.exports = function() { 4 | return actor({ 5 | 6 | // Define custom steps here, use 'this' to access default methods of I. 7 | // It is recommended to place a general 'login' function here. 8 | 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /__test__/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "files": true 4 | }, 5 | "compilerOptions": { 6 | "target": "es2018", 7 | "lib": ["es2018", "DOM"], 8 | "esModuleInterop": true, 9 | "module": "commonjs", 10 | "strictNullChecks": false, 11 | "types": ["codeceptjs", "node","jest", "node", "mocha"], 12 | "declaration": true, 13 | "skipLibCheck": true, 14 | }, 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /__test__/visual_test.ts: -------------------------------------------------------------------------------- 1 | const { I } = inject() 2 | Feature('visual tests'); 3 | 4 | Before(() => { 5 | I.amOnPage('https://codecept.io/helpers/Playwright/') 6 | }) 7 | 8 | Scenario('seeVisualDiff', () => { 9 | I.saveScreenshot('Playwright_doc.png'); 10 | I.seeVisualDiff('Playwright_doc.png', {prepareBaseImage: false, tolerance: 20}) 11 | }); 12 | 13 | Scenario('seeVisualDiffForElement', () => { 14 | I.saveElementScreenshot('h2#playwright','element.png'); 15 | I.seeVisualDiffForElement('h2#playwright','element.png', {prepareBaseImage: false, tolerance: 20}) 16 | }); 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeceptjs-resemblehelper", 3 | "version": "1.9.7", 4 | "description": "Resemble Js helper for CodeceptJS, with Support for Playwright, Webdriver, TestCafe, Puppeteer & Appium", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:Percona-Lab/codeceptjs-resemblehelper.git" 8 | }, 9 | "scripts": { 10 | "build": "tsc", 11 | "lint": "rome ci src", 12 | "format": "rome format src --write", 13 | "lint:fix": "rome check src --apply-unsafe", 14 | "test": "jest --coverage" 15 | }, 16 | "dependencies": { 17 | "assert": "^2.1.0", 18 | "aws-sdk": "2.1692.0", 19 | "canvas": "^3.1.0", 20 | "image-size": "2.0.1", 21 | "mkdirp": "^3.0.1", 22 | "mz": "2.7.0", 23 | "path": "^0.12.7", 24 | "resemblejs": "^5.0.0" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^29.5.0", 28 | "@types/mocha": "^10.0.1", 29 | "allure-commandline": "^2.13.0", 30 | "codeceptjs": "^3.7.3", 31 | "jest": "^29.5.0", 32 | "mocha": "^11.1.0", 33 | "mochawesome": "^7.1.3", 34 | "rome": "^12.0.0", 35 | "ts-jest": "^29.1.0", 36 | "typescript": "^5.0.4" 37 | }, 38 | "keywords": [ 39 | "codeceptJS", 40 | "codeceptjs", 41 | "resemblejs", 42 | "codeceptjs-resemble" 43 | ], 44 | "author": "Puneet Kala ", 45 | "license": "MIT", 46 | "files": [ 47 | "dist/*", 48 | "README.md" 49 | ], 50 | "release": { 51 | "branches": [ 52 | "main", 53 | "master" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /rome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/rome/configuration_schema.json", 3 | "formatter": { 4 | "enabled": true, 5 | "indentSize": 4, 6 | "indentStyle": "tab", 7 | "lineWidth": 120, 8 | "ignore": [".gitingore", "*.json", ".github/workflows/*.yml", ".npmignore"] 9 | }, 10 | "linter": { 11 | "enabled": true, 12 | "rules": { 13 | "recommended": true, 14 | "suspicious": { 15 | "recommended": true, 16 | "noExplicitAny": "off" 17 | }, 18 | "style": { 19 | "recommended": true 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | const { Helper } = require("codeceptjs"); 2 | const resemble = require("resemblejs"); 3 | const fs = require("fs"); 4 | const assert = require("assert"); 5 | const mkdirp = require("mkdirp"); 6 | const getDirName = require("path").dirname; 7 | const AWS = require("aws-sdk"); 8 | const path = require("path"); 9 | const sizeOf = require("image-size"); 10 | const Container = require("codeceptjs/lib/container"); 11 | const supportedHelper = ["Playwright", "Puppeteer", "WebDriver", "TestCafe", "Appium"]; 12 | let outputDir: string; 13 | 14 | /** 15 | * Resemble.js helper class for CodeceptJS, this allows screen comparison 16 | * @author Puneet Kala 17 | */ 18 | 19 | interface Config { 20 | baseFolder: string; 21 | diffFolder: string; 22 | screenshotFolder: string; 23 | prepareBaseImage: string; 24 | } 25 | 26 | interface Options { 27 | tolerance?: any; 28 | ignoredBox?: any; 29 | boundingBox?: any; 30 | needsSameDimension?: boolean; 31 | outputSettings?: any; 32 | prepareBaseImage?: boolean; 33 | compareWithImage?: any; 34 | } 35 | 36 | interface Endpoint { 37 | /** 38 | * The host portion of the endpoint including the port, e.g., example.com:80. 39 | */ 40 | host: string; 41 | /** 42 | * The host portion of the endpoint, e.g., example.com. 43 | */ 44 | hostname: string; 45 | /** 46 | * The full URL of the endpoint. 47 | */ 48 | href: string; 49 | /** 50 | * The port of the endpoint. 51 | */ 52 | port: number; 53 | /** 54 | * The protocol (http or https) of the endpoint URL. 55 | */ 56 | protocol: string; 57 | } 58 | 59 | class ResembleHelper extends Helper { 60 | baseFolder: string; 61 | diffFolder: string; 62 | screenshotFolder?: string; 63 | prepareBaseImage?: boolean; 64 | config?: any; 65 | 66 | constructor(config: any) { 67 | // @ts-ignore 68 | super(config); 69 | outputDir = require("codeceptjs").config.get().output || "output"; 70 | this.baseFolder = this.resolvePath(config.baseFolder); 71 | this.diffFolder = this.resolvePath(config.diffFolder); 72 | this.screenshotFolder = this.resolvePath(config.screenshotFolder || "output"); 73 | this.prepareBaseImage = config.prepareBaseImage; 74 | } 75 | 76 | resolvePath(folderPath: string) { 77 | if (!path.isAbsolute(folderPath)) { 78 | return `${path.resolve(folderPath)}/`; 79 | } 80 | return folderPath; 81 | } 82 | 83 | _resolveRelativePath(folderPath: string) { 84 | let absolutePathOfImage = folderPath; 85 | if (!path.isAbsolute(absolutePathOfImage)) { 86 | absolutePathOfImage = `${path.resolve(outputDir, absolutePathOfImage)}/`; 87 | } 88 | let absolutePathOfReportFolder = outputDir; 89 | // support mocha 90 | if (Container.mocha() && typeof Container.mocha().options.reporterOptions.reportDir !== "undefined") { 91 | absolutePathOfReportFolder = Container.mocha().options.reporterOptions.reportDir; 92 | } 93 | // support mocha-multi-reporters 94 | if ( 95 | Container.mocha() && 96 | typeof Container.mocha().options.reporterOptions.mochawesomeReporterOptions?.reportDir !== "undefined" 97 | ) { 98 | absolutePathOfReportFolder = Container.mocha().options.reporterOptions.mochawesomeReporterOptions.reportDir; 99 | } 100 | return path.relative(absolutePathOfReportFolder, absolutePathOfImage); 101 | } 102 | 103 | /** 104 | * Compare Images 105 | * 106 | * @param image 107 | * @param options 108 | * @returns {Promise} 109 | */ 110 | async _compareImages(image: any, options: Options) { 111 | const baseImage = this._getBaseImagePath(image, options); 112 | const actualImage = this._getActualImagePath(image); 113 | const diffImage = this._getDiffImagePath(image); 114 | 115 | // check whether the base and the screenshot images are present. 116 | fs.access(baseImage, fs.constants.F_OK | fs.constants.R_OK, (err: any) => { 117 | if (err) { 118 | throw new Error( 119 | `${baseImage} ${err.code === "ENOENT" ? "base image does not exist" : "base image has an access error"}`, 120 | ); 121 | } 122 | }); 123 | 124 | fs.access(actualImage, fs.constants.F_OK | fs.constants.R_OK, (err: any) => { 125 | if (err) { 126 | throw new Error( 127 | `${actualImage} ${ 128 | err.code === "ENOENT" ? "screenshot image does not exist" : "screenshot image has an access error" 129 | }`, 130 | ); 131 | } 132 | }); 133 | 134 | return new Promise((resolve, reject) => { 135 | if (!options.outputSettings) { 136 | options.outputSettings = {}; 137 | } 138 | if (typeof options.needsSameDimension === "undefined") { 139 | options.needsSameDimension = true; 140 | } 141 | resemble.outputSettings({ 142 | boundingBox: options.boundingBox, 143 | ignoredBox: options.ignoredBox, 144 | ...options.outputSettings, 145 | }); 146 | 147 | this.debug(`Tolerance Level Provided ${options.tolerance}`); 148 | const tolerance = options.tolerance; 149 | 150 | resemble.compare(actualImage, baseImage, options, (err: any, data: any) => { 151 | if (err) { 152 | reject(err); 153 | } else { 154 | if (options.needsSameDimension && !data.isSameDimensions) { 155 | const dimensions1 = sizeOf(baseImage); 156 | const dimensions2 = sizeOf(actualImage); 157 | reject( 158 | new Error( 159 | `The base image is of ${dimensions1.height} X ${dimensions1.width} and actual image is of ${dimensions2.height} X ${dimensions2.width}. Please use images of same dimensions so as to avoid any unexpected results.`, 160 | ), 161 | ); 162 | } 163 | resolve(data); 164 | if (data.misMatchPercentage >= tolerance) { 165 | if (!fs.existsSync(getDirName(diffImage))) { 166 | fs.mkdirSync(getDirName(diffImage)); 167 | } 168 | fs.writeFileSync(diffImage, data.getBuffer()); 169 | const diffImagePath = path.join(process.cwd(), diffImage); 170 | this.debug(`Diff Image File Saved to: ${diffImagePath}`); 171 | } 172 | } 173 | }); 174 | }); 175 | } 176 | 177 | /** 178 | * 179 | * @param image 180 | * @param options 181 | * @returns {Promise<*>} 182 | */ 183 | async _fetchMisMatchPercentage(image: any, options: Options) { 184 | const result = this._compareImages(image, options); 185 | const data: any = await Promise.resolve(result); 186 | return data.misMatchPercentage; 187 | } 188 | 189 | /** 190 | * Take screenshot of individual element. 191 | * @param selector selector of the element to be screenshotted 192 | * @param name name of the image 193 | * @returns {Promise} 194 | */ 195 | async screenshotElement(selector: any, name: string) { 196 | const helper = this._getHelper(); 197 | 198 | if (!helper) throw new Error("Method only works with Playwright, Puppeteer, WebDriver or TestCafe helpers."); 199 | 200 | await helper.waitForVisible(selector); 201 | const els = await helper._locate(selector); 202 | 203 | if (this.helpers["Puppeteer"] || this.helpers["Playwright"] || this.helpers["WebDriver"]) { 204 | if (!els.length) throw new Error(`Element ${selector} couldn't be located`); 205 | const el = els[0]; 206 | 207 | if (this.helpers["Puppeteer"] || this.helpers["Playwright"]) { 208 | await el.screenshot({ path: `${outputDir}/${name}.png` }); 209 | } 210 | 211 | if (this.helpers["WebDriver"]) { 212 | await el.saveScreenshot(`${this.screenshotFolder}${name}.png`); 213 | } 214 | } 215 | 216 | if (this.helpers["TestCafe"]) { 217 | if (!(await els.count)) throw new Error(`Element ${selector} couldn't be located`); 218 | const { t } = this.helpers["TestCafe"]; 219 | 220 | await t.takeElementScreenshot(els, name); 221 | } 222 | } 223 | 224 | /** 225 | * This method attaches image attachments of the base, screenshot and diff to the allure reporter when the mismatch exceeds tolerance. 226 | * @param baseImage 227 | * @param misMatch 228 | * @param options 229 | * @returns {Promise} 230 | */ 231 | 232 | async _addAttachment(baseImage: any, misMatch: any, options: Options) { 233 | const allure: any = require("codeceptjs").container.plugins("allure"); 234 | 235 | if (allure !== undefined && misMatch >= options.tolerance) { 236 | allure.addAttachment("Base Image", fs.readFileSync(this._getBaseImagePath(baseImage, options)), "image/png"); 237 | allure.addAttachment("Screenshot Image", fs.readFileSync(this._getActualImagePath(baseImage)), "image/png"); 238 | allure.addAttachment("Diff Image", fs.readFileSync(this._getDiffImagePath(baseImage)), "image/png"); 239 | } 240 | } 241 | 242 | /** 243 | * This method attaches context, and images to Mochawesome reporter when the mismatch exceeds tolerance. 244 | * @param baseImage 245 | * @param misMatch 246 | * @param options 247 | * @returns {Promise} 248 | */ 249 | 250 | async _addMochaContext(baseImage: any, misMatch: any, options: any) { 251 | const mocha = this.helpers["Mochawesome"]; 252 | 253 | if (mocha !== undefined && misMatch >= options.tolerance) { 254 | await mocha.addMochawesomeContext("Base Image"); 255 | await mocha.addMochawesomeContext(this._resolveRelativePath(this._getBaseImagePath(baseImage, options))); 256 | await mocha.addMochawesomeContext("ScreenShot Image"); 257 | await mocha.addMochawesomeContext(this._resolveRelativePath(this._getActualImagePath(baseImage))); 258 | await mocha.addMochawesomeContext("Diff Image"); 259 | await mocha.addMochawesomeContext(this._resolveRelativePath(this._getDiffImagePath(baseImage))); 260 | } 261 | } 262 | 263 | /** 264 | * This method uploads the diff and screenshot images into the bucket with diff image under bucketName/diff/diffImage and the screenshot image as 265 | * bucketName/output/ssImage 266 | * @param accessKeyId 267 | * @param secretAccessKey 268 | * @param region 269 | * @param bucketName 270 | * @param baseImage 271 | * @param options 272 | * @param {string | Endpoint } [endpoint] 273 | * @returns {Promise} 274 | */ 275 | 276 | async _upload( 277 | accessKeyId: any, 278 | secretAccessKey: any, 279 | region: any, 280 | bucketName: any, 281 | baseImage: any, 282 | options: any, 283 | endpoint: Endpoint, 284 | ) { 285 | console.log("Starting Upload... "); 286 | const s3 = new AWS.S3({ 287 | accessKeyId: accessKeyId, 288 | secretAccessKey: secretAccessKey, 289 | region: region, 290 | endpoint, 291 | }); 292 | fs.readFile(this._getActualImagePath(baseImage), (err: any, data: any) => { 293 | if (err) throw err; 294 | const base64data = new Buffer(data, "binary"); 295 | const params = { 296 | Bucket: bucketName, 297 | Key: `output/${baseImage}`, 298 | Body: base64data, 299 | }; 300 | s3.upload(params, (uErr: any, uData: { Location: any }) => { 301 | if (uErr) throw uErr; 302 | console.log(`Screenshot Image uploaded successfully at ${uData.Location}`); 303 | }); 304 | }); 305 | fs.readFile(this._getDiffImagePath(baseImage), (err: any, data: any) => { 306 | if (err) console.log("Diff image not generated"); 307 | else { 308 | const base64data = new Buffer(data, "binary"); 309 | const params = { 310 | Bucket: bucketName, 311 | Key: `diff/Diff_${baseImage}`, 312 | Body: base64data, 313 | }; 314 | s3.upload(params, (uErr: any, uData: { Location: any }) => { 315 | if (uErr) throw uErr; 316 | console.log(`Diff Image uploaded successfully at ${uData.Location}`); 317 | }); 318 | } 319 | }); 320 | 321 | // 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. 322 | if (this._getPrepareBaseImage(options)) { 323 | const baseImageName = this._getBaseImageName(baseImage, options); 324 | 325 | fs.readFile(this._getBaseImagePath(baseImage, options), (err: any, data: any) => { 326 | if (err) throw err; 327 | else { 328 | const base64data = new Buffer(data, "binary"); 329 | const params = { 330 | Bucket: bucketName, 331 | Key: `base/${baseImageName}`, 332 | Body: base64data, 333 | }; 334 | s3.upload(params, (uErr: any, uData: { Location: any }) => { 335 | if (uErr) throw uErr; 336 | console.log(`Base Image uploaded at ${uData.Location}`); 337 | }); 338 | } 339 | }); 340 | } else { 341 | console.log("Not Uploading base Image"); 342 | } 343 | } 344 | 345 | /** 346 | * This method downloads base images from specified bucket into the base folder as mentioned in config file. 347 | * @param accessKeyId 348 | * @param secretAccessKey 349 | * @param region 350 | * @param bucketName 351 | * @param baseImage 352 | * @param options 353 | * @param {string | Endpoint } [endpoint] 354 | * @returns {Promise} 355 | */ 356 | 357 | _download( 358 | accessKeyId: any, 359 | secretAccessKey: any, 360 | region: any, 361 | bucketName: any, 362 | baseImage: any, 363 | options: any, 364 | endpoint: Endpoint, 365 | ) { 366 | console.log("Starting Download..."); 367 | const baseImageName = this._getBaseImageName(baseImage, options); 368 | const s3 = new AWS.S3({ 369 | accessKeyId: accessKeyId, 370 | secretAccessKey: secretAccessKey, 371 | region: region, 372 | endpoint, 373 | }); 374 | const params = { 375 | Bucket: bucketName, 376 | Key: `base/${baseImageName}`, 377 | }; 378 | return new Promise((resolve) => { 379 | s3.getObject(params, (err: any, data: { Body: any }) => { 380 | if (err) console.error(err); 381 | console.log(this._getBaseImagePath(baseImage, options)); 382 | fs.writeFileSync(this._getBaseImagePath(baseImage, options), data.Body); 383 | resolve("File Downloaded Successfully"); 384 | }); 385 | }); 386 | } 387 | 388 | /** 389 | * Check Visual Difference for Base and Screenshot Image 390 | * @param baseImage Name of the Base Image (Base Image path is taken from Configuration) 391 | * @param {any} [options] Options ex {prepareBaseImage: true, tolerance: 5} along with Resemble JS Options, read more here: https://github.com/rsmbl/Resemble.js 392 | * @returns {Promise} 393 | */ 394 | async seeVisualDiff(baseImage: any, options?: Options) { 395 | await this._assertVisualDiff(undefined, baseImage, options); 396 | } 397 | 398 | /** 399 | * See Visual Diff for an Element on a Page 400 | * 401 | * @param selector Selector which has to be compared expects these -> CSS|XPath|ID 402 | * @param baseImage Base Image for comparison 403 | * @param {any} [options] Options ex {prepareBaseImage: true, tolerance: 5} along with Resemble JS Options, read more here: https://github.com/rsmbl/Resemble.js 404 | * @returns {Promise} 405 | */ 406 | async seeVisualDiffForElement(selector: any, baseImage: any, options: Options) { 407 | await this._assertVisualDiff(selector, baseImage, options); 408 | } 409 | 410 | async _assertVisualDiff( 411 | selector: undefined, 412 | baseImage: string, 413 | options?: { tolerance?: any; boundingBox?: any; skipFailure?: any }, 414 | ) { 415 | let newOptions = options; 416 | 417 | if (!newOptions) { 418 | newOptions = {}; 419 | newOptions.tolerance = 0; 420 | } 421 | 422 | const awsC = this.config.aws; 423 | 424 | if (this._getPrepareBaseImage(newOptions)) { 425 | await this._prepareBaseImage(baseImage, newOptions); 426 | } else if (awsC !== undefined) { 427 | await this._download( 428 | awsC.accessKeyId, 429 | awsC.secretAccessKey, 430 | awsC.region, 431 | awsC.bucketName, 432 | baseImage, 433 | options, 434 | awsC.endpoint, 435 | ); 436 | } 437 | 438 | // BoundingBox for Playwright not necessary 439 | if (selector && !this.helpers["Playwright"]) { 440 | newOptions.boundingBox = await this._getBoundingBox(selector); 441 | } 442 | const misMatch = await this._fetchMisMatchPercentage(baseImage, newOptions); 443 | await this._addAttachment(baseImage, misMatch, newOptions); 444 | await this._addMochaContext(baseImage, misMatch, newOptions); 445 | if (awsC !== undefined) { 446 | await this._upload( 447 | awsC.accessKeyId, 448 | awsC.secretAccessKey, 449 | awsC.region, 450 | awsC.bucketName, 451 | baseImage, 452 | options, 453 | awsC.endpoint, 454 | ); 455 | } 456 | 457 | this.debug(`MisMatch Percentage Calculated is ${misMatch} for baseline ${baseImage}`); 458 | 459 | if (!newOptions.skipFailure) { 460 | assert( 461 | misMatch <= newOptions.tolerance, 462 | `Screenshot does not match with the baseline ${baseImage} when MissMatch Percentage is ${misMatch}`, 463 | ); 464 | } 465 | } 466 | 467 | /** 468 | * Function to prepare Base Images from Screenshots 469 | * 470 | * @param screenShotImage Name of the screenshot Image (Screenshot Image Path is taken from Configuration) 471 | * @param options 472 | */ 473 | async _prepareBaseImage(screenShotImage: string, options: { tolerance?: any; boundingBox?: any; skipFailure?: any }) { 474 | const baseImage = this._getBaseImagePath(screenShotImage, options); 475 | const actualImage = this._getActualImagePath(screenShotImage); 476 | 477 | await this._createDir(baseImage); 478 | 479 | fs.access(actualImage, fs.constants.F_OK | fs.constants.W_OK, (err: any) => { 480 | if (err) { 481 | throw new Error(`${actualImage} ${err.code === "ENOENT" ? "does not exist" : "is read-only"}`); 482 | } 483 | }); 484 | 485 | fs.access(this.baseFolder, fs.constants.F_OK | fs.constants.W_OK, (err: any) => { 486 | if (err) { 487 | throw new Error(`${this.baseFolder} ${err.code === "ENOENT" ? "does not exist" : "is read-only"}`); 488 | } 489 | }); 490 | 491 | fs.copyFileSync(actualImage, baseImage); 492 | } 493 | 494 | /** 495 | * Function to create Directory 496 | * @param directory 497 | * @returns {Promise} 498 | * @private 499 | */ 500 | _createDir(directory: any) { 501 | mkdirp.sync(getDirName(directory)); 502 | } 503 | 504 | /** 505 | * Function to fetch Bounding box for an element, fetched using selector 506 | * 507 | * @param selector CSS|XPath|ID selector 508 | * @returns {Promise<{boundingBox: {left: *, top: *, right: *, bottom: *}}>} 509 | */ 510 | async _getBoundingBox(selector: any) { 511 | const helper = this._getHelper(); 512 | await helper.waitForVisible(selector); 513 | const els = await helper._locate(selector); 514 | 515 | if (this.helpers["TestCafe"]) { 516 | if ((await els.count) !== 1) 517 | throw new Error(`Element ${selector} couldn't be located or isn't unique on the page`); 518 | } else { 519 | if (!els.length) throw new Error(`Element ${selector} couldn't be located`); 520 | } 521 | 522 | let location; 523 | let size; 524 | 525 | if (this.helpers["Puppeteer"] || this.helpers["Playwright"]) { 526 | const el = els[0]; 527 | const box = await el.boundingBox(); 528 | size = location = box; 529 | } 530 | 531 | if (this.helpers["WebDriver"] || this.helpers["Appium"]) { 532 | const el = els[0]; 533 | location = await el.getLocation(); 534 | size = await el.getSize(); 535 | } 536 | 537 | if (this.helpers["WebDriverIO"]) { 538 | location = await helper.browser.getLocation(selector); 539 | size = await helper.browser.getElementSize(selector); 540 | } 541 | if (this.helpers["TestCafe"]) { 542 | return await els.boundingClientRect; 543 | } 544 | 545 | if (!size) { 546 | throw new Error("Cannot get element size!"); 547 | } 548 | 549 | const bottom = size.height + location.y; 550 | const right = size.width + location.x; 551 | const boundingBox = { 552 | left: location.x, 553 | top: location.y, 554 | right: right, 555 | bottom: bottom, 556 | }; 557 | 558 | this.debugSection("Area", JSON.stringify(boundingBox)); 559 | 560 | return boundingBox; 561 | } 562 | 563 | _getHelper() { 564 | if (this.helpers["Puppeteer"]) { 565 | return this.helpers["Puppeteer"]; 566 | } 567 | 568 | if (this.helpers["WebDriver"]) { 569 | return this.helpers["WebDriver"]; 570 | } 571 | if (this.helpers["Appium"]) { 572 | return this.helpers["Appium"]; 573 | } 574 | if (this.helpers["WebDriverIO"]) { 575 | return this.helpers["WebDriverIO"]; 576 | } 577 | if (this.helpers["TestCafe"]) { 578 | return this.helpers["TestCafe"]; 579 | } 580 | 581 | if (this.helpers["Playwright"]) { 582 | return this.helpers["Playwright"]; 583 | } 584 | 585 | throw Error(`No matching helper found. Supported helpers: ${supportedHelper.join("/")}`); 586 | } 587 | 588 | /** 589 | * Returns the final name of the expected base image, without a path 590 | * @param image Name of the base-image, without path 591 | * @param options Helper options 592 | * @returns {string} 593 | */ 594 | _getBaseImageName(image: any, options: { compareWithImage?: any }) { 595 | return options.compareWithImage ? options.compareWithImage : image; 596 | } 597 | 598 | /** 599 | * Returns the path to the expected base image 600 | * @param image Name of the base-image, without path 601 | * @param options Helper options 602 | * @returns {string} 603 | */ 604 | _getBaseImagePath(image: string, options: Options) { 605 | return this.baseFolder + this._getBaseImageName(image, options); 606 | } 607 | 608 | /** 609 | * Returns the path to the actual screenshot image 610 | * @param image Name of the image, without path 611 | * @returns {string} 612 | */ 613 | _getActualImagePath(image: string) { 614 | return this.screenshotFolder + image; 615 | } 616 | 617 | /** 618 | * Returns the path to the image that displays differences between base and actual image. 619 | * @param image Name of the image, without path 620 | * @returns {string} 621 | */ 622 | _getDiffImagePath(image: string) { 623 | const diffImage = `Diff_${image.split(".")[0]}.png`; 624 | return this.diffFolder + diffImage; 625 | } 626 | 627 | /** 628 | * Returns the final `prepareBaseImage` flag after evaluating options and config values 629 | * @param options Helper options 630 | * @returns {boolean} 631 | */ 632 | _getPrepareBaseImage(options: Options) { 633 | if ("undefined" !== typeof options.prepareBaseImage) { 634 | // Cast to bool with `!!` for backwards compatibility 635 | return !!options.prepareBaseImage; 636 | } else { 637 | // Compare with `true` for backwards compatibility 638 | return true === this.prepareBaseImage; 639 | } 640 | } 641 | } 642 | 643 | export = ResembleHelper; 644 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["es2017"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "strict": true, 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "removeComments": false, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "rootDir": "./src", 15 | "outDir": "./dist", 16 | "skipLibCheck": true, 17 | "types": ["jest", "node", "mocha"] 18 | }, 19 | "exclude": [ 20 | "node_modules", "dist", "__test__" 21 | ], 22 | "compileOnSave": false 23 | } 24 | --------------------------------------------------------------------------------