├── .babelrc ├── .eslintrc ├── .gitignore ├── .scripts └── mocha-runner.js ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── react-cornea.compiled.js ├── react-cornea.js └── tests ├── fixtures ├── css-fixture.css └── react-fixture.jsx ├── react-cornea.jsx └── stylesheets.jsx /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "react"], 3 | "plugins": ["react-require"] 4 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true, 5 | "node": true 6 | }, 7 | 8 | "plugins": [ 9 | "react" 10 | ], 11 | 12 | "ecmaFeatures": { 13 | "arrowFunctions": true, 14 | "binaryLiterals": true, 15 | "blockBindings": true, 16 | "classes": true, 17 | "defaultParams": true, 18 | "destructuring": true, 19 | "experimentalObjectRestSpread": true, 20 | "forOf": true, 21 | "generators": true, 22 | "globalReturn": true, 23 | "jsx": true, 24 | "modules": true, 25 | "objectLiteralComputedProperties": true, 26 | "objectLiteralDuplicateProperties": true, 27 | "objectLiteralShorthandMethods": true, 28 | "objectLiteralShorthandProperties": true, 29 | "octalLiterals": true, 30 | "regexUFlag": true, 31 | "regexYFlag": true, 32 | "restParams": true, 33 | "spread": true, 34 | "superInFunctions": true, 35 | "templateStrings": true, 36 | "unicodeCodePointEscapes": true 37 | }, 38 | 39 | "rules": { 40 | "array-bracket-spacing": [2, "always"], 41 | "arrow-spacing": 2, 42 | "block-scoped-var": 0, 43 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 44 | "callback-return": 2, 45 | "camelcase": [2, {"properties": "always"}], 46 | "comma-dangle": 0, 47 | "comma-spacing": 0, 48 | "comma-style": [2, "last"], 49 | "complexity": 0, 50 | "computed-property-spacing": [2, "never"], 51 | "consistent-return": 0, 52 | "consistent-this": 0, 53 | "curly": [2, "all"], 54 | "default-case": 0, 55 | "dot-location": [2, "property"], 56 | "dot-notation": 0, 57 | "eol-last": 2, 58 | "eqeqeq": 2, 59 | "func-names": 0, 60 | "func-style": 0, 61 | "generator-star-spacing": [0, {"before": true, "after": false}], 62 | "guard-for-in": 2, 63 | "handle-callback-err": [2, "error"], 64 | "id-length": 0, 65 | "id-match": [2, "^(?:_?[a-zA-Z0-9]*)|[_A-Z0-9]+$"], 66 | "indent": [2, 2, {"SwitchCase": 1}], 67 | "init-declarations": 0, 68 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 69 | "linebreak-style": 2, 70 | "lines-around-comment": 0, 71 | "max-depth": 0, 72 | "max-len": [2, 100, 4], 73 | "max-nested-callbacks": 0, 74 | "max-params": 0, 75 | "max-statements": 0, 76 | "new-cap": 0, 77 | "new-parens": 2, 78 | "newline-after-var": 0, 79 | "no-array-constructor": 2, 80 | "no-bitwise": 0, 81 | "no-caller": 2, 82 | "no-catch-shadow": 0, 83 | "no-class-assign": 2, 84 | "no-cond-assign": 2, 85 | "no-console": 1, 86 | "no-const-assign": 2, 87 | "no-constant-condition": 2, 88 | "no-continue": 0, 89 | "no-control-regex": 0, 90 | "no-debugger": 1, 91 | "no-delete-var": 2, 92 | "no-div-regex": 2, 93 | "no-dupe-args": 2, 94 | "no-dupe-keys": 2, 95 | "no-duplicate-case": 2, 96 | "no-else-return": 2, 97 | "no-empty": 2, 98 | "no-empty-character-class": 2, 99 | "no-empty-label": 2, 100 | "no-eq-null": 0, 101 | "no-eval": 2, 102 | "no-ex-assign": 2, 103 | "no-extend-native": 2, 104 | "no-extra-bind": 2, 105 | "no-extra-boolean-cast": 2, 106 | "no-extra-parens": 0, 107 | "no-extra-semi": 2, 108 | "no-fallthrough": 2, 109 | "no-floating-decimal": 2, 110 | "no-func-assign": 2, 111 | "no-implicit-coercion": 2, 112 | "no-implied-eval": 2, 113 | "no-inline-comments": 0, 114 | "no-inner-declarations": [2, "functions"], 115 | "no-invalid-regexp": 2, 116 | "no-invalid-this": 0, 117 | "no-irregular-whitespace": 2, 118 | "no-iterator": 2, 119 | "no-label-var": 2, 120 | "no-labels": 0, 121 | "no-lone-blocks": 2, 122 | "no-lonely-if": 2, 123 | "no-loop-func": 0, 124 | "no-mixed-requires": [2, true], 125 | "no-mixed-spaces-and-tabs": 2, 126 | "no-multi-spaces": 2, 127 | "no-multi-str": 2, 128 | "no-multiple-empty-lines": 0, 129 | "no-native-reassign": 0, 130 | "no-negated-in-lhs": 2, 131 | "no-nested-ternary": 0, 132 | "no-new": 2, 133 | "no-new-func": 0, 134 | "no-new-object": 2, 135 | "no-new-require": 2, 136 | "no-new-wrappers": 2, 137 | "no-obj-calls": 2, 138 | "no-octal": 2, 139 | "no-octal-escape": 2, 140 | "no-param-reassign": 2, 141 | "no-path-concat": 2, 142 | "no-plusplus": 0, 143 | "no-process-env": 0, 144 | "no-process-exit": 0, 145 | "no-proto": 2, 146 | "no-redeclare": 2, 147 | "no-regex-spaces": 2, 148 | "no-restricted-modules": 0, 149 | "no-return-assign": 2, 150 | "no-script-url": 2, 151 | "no-self-compare": 0, 152 | "no-sequences": 2, 153 | "no-shadow": 2, 154 | "no-shadow-restricted-names": 2, 155 | "no-spaced-func": 2, 156 | "no-sparse-arrays": 2, 157 | "no-sync": 0, 158 | "no-ternary": 0, 159 | "no-this-before-super": 2, 160 | "no-throw-literal": 2, 161 | "no-trailing-spaces": 2, 162 | "no-undef": 2, 163 | "no-undef-init": 2, 164 | "no-undefined": 0, 165 | "no-underscore-dangle": 0, 166 | "no-unexpected-multiline": 2, 167 | "no-unneeded-ternary": 2, 168 | "no-unreachable": 2, 169 | "no-unused-expressions": 2, 170 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 171 | "no-use-before-define": 0, 172 | "no-useless-call": 2, 173 | "no-var": 0, 174 | "no-void": 2, 175 | "no-warning-comments": 0, 176 | "no-with": 2, 177 | "object-curly-spacing": [0, "always"], 178 | "object-shorthand": [2, "always"], 179 | "one-var": [2, "never"], 180 | "operator-assignment": [2, "always"], 181 | "operator-linebreak": [2, "after"], 182 | "padded-blocks": 0, 183 | "prefer-const": 0, 184 | "prefer-reflect": 0, 185 | "prefer-spread": 0, 186 | "quote-props": [2, "as-needed"], 187 | "quotes": [2, "single"], 188 | "radix": 2, 189 | "require-yield": 2, 190 | "semi": [2, "always"], 191 | "semi-spacing": [2, {"before": false, "after": true}], 192 | "sort-vars": 0, 193 | "space-after-keywords": [2, "always"], 194 | "space-before-blocks": [2, "always"], 195 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 196 | "space-in-parens": 0, 197 | "space-infix-ops": [2, {"int32Hint": false}], 198 | "space-return-throw-case": 2, 199 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 200 | "spaced-comment": [2, "always"], 201 | "strict": 0, 202 | "use-isnan": 2, 203 | "valid-jsdoc": 0, 204 | "valid-typeof": 2, 205 | "vars-on-top": 0, 206 | "wrap-iife": 2, 207 | "wrap-regex": 0, 208 | "yoda": [2, "never", {"exceptRange": true}], 209 | "react/jsx-uses-react": 1 210 | }, 211 | 212 | "globals": { 213 | "Meteor": false 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ -------------------------------------------------------------------------------- /.scripts/mocha-runner.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | require('babel-polyfill'); 3 | 4 | process.on('unhandledRejection', function (error) { 5 | console.error('Unhandled Promise Rejection:'); 6 | console.error(error && error.stack || error); 7 | }); 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 5.5.0 4 | script: 5 | - "npm test" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 The Julie Ann Wrigley Global Institute of Sustainability, ASU 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React Cornea 2 | ============ 3 | 4 | [![Build Status](https://travis-ci.org/gios-asu/react-cornea.svg?branch=develop)](https://travis-ci.org/gios-asu/react-cornea) 5 | 6 | A testing utility for generating visual diffs of your React components. 7 | 8 | This tool will create 3 files - `theirs-{componentName}.png`, `yours-{componentName}.png` and `difference.png`. When `differ.cleanup` is called, `yours-{componentName}.png` and `difference.png` will be deleted. 9 | 10 | # Installation 11 | 12 | ``` 13 | npm install --save-dev react-cornea 14 | ``` 15 | or 16 | ``` 17 | yarn add --dev react-cornea 18 | ``` 19 | 20 | This NPM package has a dependency on [ImageMagick](http://www.imagemagick.org/). To install it, see the [ImageMagick documentation](http://www.imagemagick.org/script/binary-releases.php). Installation on some Linux systems, such as Ubuntu is easy: 21 | 22 | ```sh 23 | sudo apt-get install imagemagick 24 | ``` 25 | 26 | For OSX using Brew: 27 | 28 | ```sh 29 | brew install ImageMagick 30 | ``` 31 | 32 | Note that when using TravisCI, ImageMagick is already installed. 33 | 34 | # Usage 35 | 36 | React Cornea provides utility functions for you to test your React components. 37 | 38 | Here is an example using Mocha: 39 | 40 | ```js 41 | import React from 'react'; 42 | import {expect} from 'chai'; 43 | const {describe, it} = global; 44 | import { Differ } from 'react-cornea'; 45 | import { default as Program } from '../program.jsx'; 46 | 47 | describe('A Program Component', () => { 48 | const componentName = 'program'; 49 | const program = { 50 | name: 'Save the whale' 51 | }; 52 | 53 | it( 'has not visually changed', (done) => { 54 | var differ = new Differ({ 55 | component: , 56 | componentName: 'program', 57 | onSnapshotCreated: done, 58 | savePath: __dirname + '/' 59 | }); 60 | 61 | differ.compare().then((areTheSame) => { 62 | expect(areTheSame).to.equal(true); 63 | 64 | differ.cleanup(); 65 | 66 | done(); 67 | }); 68 | }); 69 | }); 70 | 71 | ``` 72 | 73 | For your first set of snapshots, you can use the environment variables to generate snapshots for components that do not yet have snapshots: 74 | 75 | ```sh 76 | env CREATE_SNAPSHOTS=1 npm test 77 | ``` 78 | 79 | Or by passing in a createSnapshots option: 80 | 81 | ```js 82 | var differ = new Differ({ 83 | component: , 84 | componentName: 'program', 85 | savePath: __dirname + '/', 86 | threshold: 0, 87 | onSnapshotCreated: done, 88 | createSnapshots: true 89 | }); 90 | ``` 91 | 92 | If you are working on a large visual change, you can force new screenshots to be generated without running assertions by using environment variables in the command line: 93 | 94 | ```sh 95 | env UPDATE_SNAPSHOTS=1 npm test 96 | ``` 97 | 98 | You can also specify components by name: 99 | 100 | ```sh 101 | env UPDATE_SNAPSHOTS="my-component" npm test 102 | ``` 103 | 104 | Or by passing in an updateSnapshots option: 105 | 106 | ```js 107 | var differ = new Differ({ 108 | component: , 109 | componentName: 'program', 110 | savePath: __dirname + '/', 111 | threshold: 0, 112 | onSnapshotUpdated: done, 113 | updateSnapshots: true 114 | }); 115 | ``` 116 | 117 | ## Using with your current system 118 | 119 | If you are using this utility, use `env CREATE_SNAPSHOTS=1 npm run test` to generate a first, initial set of snaphots (called `theirs-{componentName}.png`). You should check these into your version control. 120 | 121 | Then, whenever you run tests `npm run test`, the utility will diff the current version of your component with the `theirs-{componentName}.png` file. 122 | 123 | Watch out, it is not very useful to check in the `your-{componentName}.png` or `difference.png`; however, you can if you want ;) 124 | 125 | # API 126 | 127 | ``` 128 | new Differ(options) :=> Object{Differ} 129 | ``` 130 | 131 | Create a new Differ object 132 | 133 | - options 134 | - component - The React component you want to test 135 | - componentName - The name of your component, used to save your file 136 | - savePath - The folder where your screenshots should be saved 137 | - threshold - The percentage difference allowed. Defaults to 0 138 | - css - A CSS string of custom styles you would like injected. Defaults to '' 139 | - cssFile - The path to a css file to include 140 | - viewportSize - An object with height and width defined as numbers. Defaults to { width: 1440, height: 900 } 141 | - onSnapshotUpdated - What to do after screenshots have been updated when using the `env UPDATE_SCREENSHOTS=1` or option `updateScreenshots: true`. Defaults to noop. 142 | - updateSnapshots - Instead of running tests, simply update snapshots. Defaults to false. 143 | - onSnapshotCreated - Callback for after a snapshot was created. Calls the callback no matter what, but passes in boolean with true if the snapshot was created, false if one existed and the snapshot was not generated. Defaults to noop. 144 | - createSnapshots - Instead of running tests, simply create snapshots for components that do not already have snapshots. Defaults to false. 145 | 146 | 147 | ``` 148 | compare() :=> Promise 149 | ``` 150 | 151 | Will snap a picture of the your version of the React component, then compare it to the baseline, then generate a difference image. Once complete, the given 152 | Promise will resolve with whether the difference is within the threshold 153 | 154 | ``` 155 | cleanup() :=> nil 156 | ``` 157 | 158 | Will remove `yours-{componentName}.png` and `difference.png`. 159 | 160 | ## Internal calls 161 | 162 | The following are provided, but their interfaces may change in the future: 163 | 164 | ``` 165 | snap(options) :=> Promise 166 | ``` 167 | 168 | Will take a screenshot. 169 | 170 | - options 171 | - path - The path to save the screenshot to 172 | 173 | 174 | ``` 175 | compareTo(options) :=> Promise 176 | ``` 177 | 178 | Will compare the screenshots and generate diff, as well as resolve the Promise with whether the images are within the threshold difference. 179 | 180 | - options 181 | - path - Path to save to 182 | - filename - File to check the currentSnap against 183 | 184 | ``` 185 | moveSnapshot(options) :=> boolean 186 | ``` 187 | 188 | Will attempt to move the snapshot from `yours-{componentName}.png` to `theirs-{componentName}.png` 189 | 190 | - options 191 | - path - Folder to move to 192 | - filename - Filename to move to 193 | 194 | 195 | # Development 196 | 197 | We have a build script to transpile the ES2015 code to ES2013 code. 198 | 199 | ```sh 200 | npm run build 201 | ``` 202 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-cornea", 3 | "version": "1.0.5", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/gios-asu/react-cornea.git" 7 | }, 8 | "options": { 9 | "mocha": "--require .scripts/mocha-runner tests/*.jsx" 10 | }, 11 | "description": "A testing utility for generating visual diffs of your React components", 12 | "main": "./react-cornea.compiled.js", 13 | "scripts": { 14 | "build": "babel react-cornea.js --out-file react-cornea.compiled.js", 15 | "lint": "eslint ./libs ./client ./server --ext .js --ext .jsx", 16 | "lintfix": "npm run lint -- --fix", 17 | "testonly": "mocha $npm_package_options_mocha", 18 | "test": "npm run lint && npm run testonly" 19 | }, 20 | "author": "GIOS ASU", 21 | "url": "https://github.com/gios-asu/react-cornea", 22 | "bugs": { 23 | "url": "https://github.com/gios-asu/react-cornea/issues" 24 | }, 25 | "license": "MIT", 26 | "devDependencies": { 27 | "babel-core": "6.x.x", 28 | "babel-cli": "^6.5.1", 29 | "babel-plugin-react-require": "2.x.x", 30 | "babel-polyfill": "6.x.x", 31 | "babel-preset-es2015": "^6.5.0", 32 | "babel-preset-react": "6.x.x", 33 | "babel-preset-stage-2": "6.x.x", 34 | "chai": "3.x.x", 35 | "enzyme": "1.x.x", 36 | "eslint": "1.10.x", 37 | "eslint-plugin-react": "3.15.x", 38 | "meteor-build-client": "^0.2.2", 39 | "mocha": "2.x.x", 40 | "react": "^0.14.7", 41 | "react-addons-test-utils": "^0.14.7", 42 | "react-dom": "^0.14.7", 43 | "sinon": "1.17.x", 44 | "underscore": "^1.8.3" 45 | }, 46 | "dependencies": { 47 | "enzyme": "^2.0.0", 48 | "file-exists": "^1.0.0", 49 | "gm": "^1.21.1", 50 | "image-diff": "^1.3.0", 51 | "phantom": "^2.0.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /react-cornea.compiled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.Differ = exports.DEVICE_SIZES = undefined; 7 | 8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 9 | 10 | var _enzyme = require('enzyme'); 11 | 12 | var _phantom = require('phantom'); 13 | 14 | var _phantom2 = _interopRequireDefault(_phantom); 15 | 16 | var _imageDiff = require('image-diff'); 17 | 18 | var _imageDiff2 = _interopRequireDefault(_imageDiff); 19 | 20 | var _fs = require('fs'); 21 | 22 | var _fs2 = _interopRequireDefault(_fs); 23 | 24 | var _fileExists = require('file-exists'); 25 | 26 | var _fileExists2 = _interopRequireDefault(_fileExists); 27 | 28 | var _gm = require('gm'); 29 | 30 | var _gm2 = _interopRequireDefault(_gm); 31 | 32 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 33 | 34 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 35 | 36 | var DEVICE_SIZES = { 37 | MOBILE: { height: 800, width: 480 }, 38 | TABLET: { height: 1200, width: 768 }, 39 | DESKTOP: { height: 900, width: 1440 }, 40 | 41 | IPAD: { height: 1024, width: 768 }, 42 | IPAD_3RD_GEN: { height: 2048, width: 1536 }, 43 | IPAD_PRO: { height: 2732, width: 2048 }, 44 | IPHONE_4S: { height: 960, width: 640 }, 45 | IPHONE_5: { height: 1136, width: 640 }, 46 | IPHONE_6: { height: 1334, width: 750 }, 47 | 48 | GALAXY_Y: { height: 320, width: 240 }, 49 | GALAXY_ACE: { height: 400, width: 240 }, 50 | GALAXY_SIII_MINI: { height: 800, width: 480 }, 51 | GALAXY_SIII: { height: 1280, width: 720 }, 52 | GALAXY_NEXUS: { height: 1280, width: 720 }, 53 | GALAXY_S4: { height: 1920, width: 1080 }, 54 | GALAXY_NOTE_III: { height: 1920, width: 1080 }, 55 | GALAXY_NOTE_II: { height: 1280, width: 720 }, 56 | GALAXY_MEGA: { height: 1280, width: 720 }, 57 | GALAXY_MEGA_TV: { height: 960, width: 540 }, 58 | GALAXY_TAB_2: { height: 960, width: 540 }, 59 | GALAXY_TAB_3: { height: 1280, width: 800 }, 60 | GALAXYNOTE_10_1: { height: 2560, width: 1600 }, 61 | 62 | NEXUS_S: { height: 800, width: 480 }, 63 | NEXUS_4: { height: 1200, width: 768 }, 64 | NEXUS_5: { height: 1920, width: 1080 }, 65 | NEXUS_7_1ST_GEN: { height: 1280, width: 800 }, 66 | NEXUS_7_2ND_GEN: { height: 1824, width: 1200 }, 67 | NEXUS_10: { height: 2560, width: 1600 }, 68 | 69 | HTC_ONE_X: { height: 1280, width: 720 }, 70 | HTC_ONE: { height: 1920, width: 1080 }, 71 | HTC_ONE_MAX: { height: 1920, width: 1080 }, 72 | 73 | KINDLE_FIRE_1ST_GEN: { height: 1024, width: 600 }, 74 | KINDLE_FIRE_2ND_GEN: { height: 1024, width: 600 }, 75 | KINDLE_FIRE_HD: { height: 1920, width: 1200 }, 76 | KINDLE_FIRE_HDX: { height: 1920, width: 1200 } 77 | }; 78 | 79 | exports.DEVICE_SIZES = DEVICE_SIZES; 80 | 81 | var Stylesheets = function () { 82 | function Stylesheets() { 83 | _classCallCheck(this, Stylesheets); 84 | 85 | this.css = ''; 86 | } 87 | 88 | _createClass(Stylesheets, [{ 89 | key: 'addCSS', 90 | value: function addCSS(css) { 91 | this.css += css; 92 | } 93 | }, { 94 | key: 'addCSSFile', 95 | value: function addCSSFile(cssFilePath) { 96 | var css = _fs2.default.readFileSync(cssFilePath).toString(); 97 | this.addCSS(css); 98 | } 99 | }, { 100 | key: 'createStyles', 101 | value: function createStyles() { 102 | return ''; 103 | } 104 | }, { 105 | key: 'createReset', 106 | value: function createReset() { 107 | var reset = '\n/* http://meyerweb.com/eric/tools/css/reset/\n v2.0 | 20110126\n License: none (public domain)\n*/\n\nhtml, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, big, cite, code,\ndel, dfn, em, img, ins, kbd, q, s, samp,\nsmall, strike, strong, sub, sup, tt, var,\nb, u, i, center,\ndl, dt, dd, ol, ul, li,\nfieldset, form, label, legend,\ntable, caption, tbody, tfoot, thead, tr, th, td,\narticle, aside, canvas, details, embed,\nfigure, figcaption, footer, header, hgroup,\nmenu, nav, output, ruby, section, summary,\ntime, mark, audio, video {\n margin: 0;\n padding: 0;\n border: 0;\n font-size: 100%;\n font: inherit;\n vertical-align: baseline;\n}\n/* HTML5 display-role reset for older browsers */\narticle, aside, details, figcaption, figure,\nfooter, header, hgroup, menu, nav, section {\n display: block;\n}\nbody {\n line-height: 1;\n}\nol, ul {\n list-style: none;\n}\nblockquote, q {\n quotes: none;\n}\nblockquote:before, blockquote:after,\nq:before, q:after {\n content: \'\';\n content: none;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n'; 108 | return reset; 109 | } 110 | }]); 111 | 112 | return Stylesheets; 113 | }(); 114 | 115 | var imagemagick = _gm2.default.subClass({ imageMagick: true }); 116 | 117 | var renderHtml = function renderHtml(component, css, cssFile) { 118 | var wrapper = (0, _enzyme.render)(component); 119 | var html = wrapper.html(); 120 | 121 | var stylesheets = new Stylesheets(); 122 | 123 | if (css) { 124 | stylesheets.addCSS(css); 125 | } 126 | 127 | if (cssFile) { 128 | stylesheets.addCSSFile(cssFile); 129 | } 130 | 131 | html = '' + stylesheets.createStyles() + '' + html + ''; 132 | 133 | return html; 134 | }; 135 | 136 | var createScreenshot = function createScreenshot(_ref) { 137 | var resolve = _ref.resolve; 138 | var componentName = _ref.componentName; 139 | var html = _ref.html; 140 | var ref = _ref.ref; 141 | var path = _ref.path; 142 | var viewportSize = _ref.viewportSize; 143 | 144 | _phantom2.default.create().then(function (ph) { 145 | ph.createPage().then(function (page) { 146 | page.property('viewportSize', viewportSize).then(function () { 147 | page.property('content', html).then(function () { 148 | // TODO figure out a better way to do this 149 | setTimeout(function () { 150 | var fullFileName = path + 'yours-' + componentName + '.png'; 151 | page.render(fullFileName).then(function () { 152 | ph.exit(); 153 | ref.currentSnap = fullFileName; 154 | resolve(ref); 155 | }); 156 | }, 1000); 157 | }); 158 | }); 159 | }); 160 | }); 161 | }; 162 | 163 | /** 164 | * Constructor 165 | */ 166 | var Differ = function Differ(_ref2) { 167 | var _this = this; 168 | 169 | var componentName = _ref2.componentName; 170 | var component = _ref2.component; 171 | var savePath = _ref2.savePath; 172 | var _ref2$viewportSize = _ref2.viewportSize; 173 | var viewportSize = _ref2$viewportSize === undefined ? DEVICE_SIZES.DESKTOP : _ref2$viewportSize; 174 | var _ref2$css = _ref2.css; 175 | var css = _ref2$css === undefined ? '' : _ref2$css; 176 | var _ref2$cssFile = _ref2.cssFile; 177 | var cssFile = _ref2$cssFile === undefined ? false : _ref2$cssFile; 178 | var _ref2$threshold = _ref2.threshold; 179 | var threshold = _ref2$threshold === undefined ? 0 : _ref2$threshold; 180 | var _ref2$onSnapshotsUpda = _ref2.onSnapshotsUpdated; 181 | var onSnapshotsUpdated = _ref2$onSnapshotsUpda === undefined ? function () {} : _ref2$onSnapshotsUpda; 182 | var _ref2$updateSnapshots = _ref2.updateSnapshots; 183 | var updateSnapshots = _ref2$updateSnapshots === undefined ? false : _ref2$updateSnapshots; 184 | var _ref2$onSnapshotCreat = _ref2.onSnapshotCreated; 185 | var onSnapshotCreated = _ref2$onSnapshotCreat === undefined ? function () {} : _ref2$onSnapshotCreat; 186 | var _ref2$createSnapshots = _ref2.createSnapshots; 187 | var createSnapshots = _ref2$createSnapshots === undefined ? false : _ref2$createSnapshots; 188 | 189 | this.currentSnap = null; 190 | this.currentDiff = null; 191 | this.html = renderHtml(component, css, cssFile); 192 | 193 | this.snap = function (_ref3) { 194 | var _ref3$path = _ref3.path; 195 | var path = _ref3$path === undefined ? './' : _ref3$path; 196 | 197 | var promise = new Promise(function (resolve, reject) { 198 | createScreenshot({ 199 | resolve: resolve, 200 | reject: reject, 201 | componentName: componentName, 202 | html: _this.html, 203 | path: path, 204 | ref: _this, 205 | viewportSize: viewportSize 206 | }); 207 | }); 208 | 209 | return promise; 210 | }; 211 | 212 | this.compareTo = function (_ref4) { 213 | var path = _ref4.path; 214 | var filename = _ref4.filename; 215 | 216 | var promise = new Promise(function (resolve, reject) { 217 | _this.currentDiff = path + 'difference.png'; 218 | (0, _imageDiff2.default)({ 219 | actualImage: path + filename, 220 | expectedImage: _this.currentSnap, 221 | diffImage: path + 'difference.png', 222 | threshold: threshold 223 | }, function (err, imagesAreSame) { 224 | if (err) { 225 | reject(err); 226 | } 227 | 228 | imagemagick().command('composite').in('-gravity', 'center').in(path + 'difference.png').in(this.currentSnap).write(path + 'difference.png', function (err2) { 229 | if (err2) { 230 | reject(err2); 231 | } 232 | 233 | resolve(imagesAreSame); 234 | }); 235 | }.bind(_this)); 236 | }); 237 | 238 | return promise; 239 | }; 240 | 241 | this.moveSnapshot = function (_ref5) { 242 | var path = _ref5.path; 243 | var filename = _ref5.filename; 244 | 245 | _fs2.default.renameSync(_this.currentSnap, path + filename); 246 | 247 | return true; 248 | }; 249 | 250 | this.cleanup = function () { 251 | if ((0, _fileExists2.default)(_this.currentSnap)) { 252 | _fs2.default.unlinkSync(_this.currentSnap); 253 | } 254 | 255 | if ((0, _fileExists2.default)(_this.currentDiff)) { 256 | _fs2.default.unlinkSync(_this.currentDiff); 257 | } 258 | }; 259 | 260 | this.compare = function () { 261 | var promise = new Promise(function (resolve) { 262 | _this.snap({ path: savePath }).then(function (differ) { 263 | var willHandleUpdate = false; 264 | 265 | if (process.env.UPDATE_SNAPSHOTS || updateSnapshots) { 266 | willHandleUpdate = true; 267 | 268 | if (typeof process.env.UPDATE_SNAPSHOTS === 'string' && process.env.UPDATE_SNAPSHOTS !== '1' && process.env.UPDATE_SNAPSHOTS !== 'true') { 269 | // We are trying to update a specific component 270 | // Flag componentNames that are not the one specified 271 | // as false. 272 | if (process.env.UPDATE_SNAPSHOTS !== componentName) { 273 | willHandleUpdate = false; 274 | } 275 | } 276 | } 277 | 278 | if (willHandleUpdate) { 279 | differ.moveSnapshot({ path: savePath, filename: 'theirs-' + componentName + '.png' }); 280 | differ.cleanup(); 281 | onSnapshotsUpdated(); 282 | } else if (process.env.CREATE_SNAPSHOTS || createSnapshots) { 283 | var created = false; 284 | if (!(0, _fileExists2.default)(savePath + 'theirs-' + componentName + '.png')) { 285 | differ.moveSnapshot({ path: savePath, filename: 'theirs-' + componentName + '.png' }); 286 | created = true; 287 | } 288 | 289 | differ.cleanup(); 290 | onSnapshotCreated(created); 291 | } else { 292 | differ.compareTo({ 293 | path: savePath, 294 | filename: 'theirs-' + componentName + '.png' 295 | }).then(function (areTheSame) { 296 | resolve(areTheSame); 297 | }); 298 | } 299 | }); 300 | }); 301 | 302 | return promise; 303 | }; 304 | }; 305 | 306 | exports.Differ = Differ; 307 | -------------------------------------------------------------------------------- /react-cornea.js: -------------------------------------------------------------------------------- 1 | import {render} from 'enzyme'; 2 | import phantom from 'phantom'; 3 | import imageDiff from 'image-diff'; 4 | import fs from 'fs'; 5 | import fileExists from 'file-exists'; 6 | import gm from 'gm'; 7 | 8 | const DEVICE_SIZES = { 9 | MOBILE: { height: 800, width: 480 }, 10 | TABLET: { height: 1200, width: 768 }, 11 | DESKTOP: { height: 900, width: 1440 }, 12 | 13 | IPAD: { height: 1024, width: 768 }, 14 | IPAD_3RD_GEN: { height: 2048, width: 1536 }, 15 | IPAD_PRO: { height: 2732, width: 2048 }, 16 | IPHONE_4S: { height: 960, width: 640 }, 17 | IPHONE_5: { height: 1136, width: 640 }, 18 | IPHONE_6: { height: 1334, width: 750 }, 19 | 20 | GALAXY_Y: { height: 320, width: 240 }, 21 | GALAXY_ACE: { height: 400, width: 240 }, 22 | GALAXY_SIII_MINI: { height: 800, width: 480 }, 23 | GALAXY_SIII: { height: 1280, width: 720 }, 24 | GALAXY_NEXUS: { height: 1280, width: 720 }, 25 | GALAXY_S4: { height: 1920, width: 1080 }, 26 | GALAXY_NOTE_III: { height: 1920, width: 1080 }, 27 | GALAXY_NOTE_II: { height: 1280, width: 720 }, 28 | GALAXY_MEGA: { height: 1280, width: 720 }, 29 | GALAXY_MEGA_TV: { height: 960, width: 540 }, 30 | GALAXY_TAB_2: { height: 960, width: 540 }, 31 | GALAXY_TAB_3: { height: 1280, width: 800 }, 32 | GALAXYNOTE_10_1: { height: 2560, width: 1600 }, 33 | 34 | NEXUS_S: { height: 800, width: 480 }, 35 | NEXUS_4: { height: 1200, width: 768 }, 36 | NEXUS_5: { height: 1920, width: 1080 }, 37 | NEXUS_7_1ST_GEN: { height: 1280, width: 800 }, 38 | NEXUS_7_2ND_GEN: { height: 1824, width: 1200 }, 39 | NEXUS_10: { height: 2560, width: 1600 }, 40 | 41 | HTC_ONE_X: { height: 1280, width: 720 }, 42 | HTC_ONE: { height: 1920, width: 1080 }, 43 | HTC_ONE_MAX: { height: 1920, width: 1080 }, 44 | 45 | KINDLE_FIRE_1ST_GEN: { height: 1024, width: 600 }, 46 | KINDLE_FIRE_2ND_GEN: { height: 1024, width: 600 }, 47 | KINDLE_FIRE_HD: { height: 1920, width: 1200 }, 48 | KINDLE_FIRE_HDX: { height: 1920, width: 1200 } 49 | }; 50 | 51 | export { DEVICE_SIZES }; 52 | 53 | class Stylesheets { 54 | constructor() { 55 | this.css = ''; 56 | } 57 | 58 | addCSS(css) { 59 | this.css += css; 60 | } 61 | 62 | addCSSFile(cssFilePath) { 63 | let css = fs.readFileSync(cssFilePath).toString(); 64 | this.addCSS(css); 65 | } 66 | 67 | createStyles() { 68 | return ''; 69 | } 70 | 71 | createReset() { 72 | let reset = ` 73 | /* http://meyerweb.com/eric/tools/css/reset/ 74 | v2.0 | 20110126 75 | License: none (public domain) 76 | */ 77 | 78 | html, body, div, span, applet, object, iframe, 79 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 80 | a, abbr, acronym, address, big, cite, code, 81 | del, dfn, em, img, ins, kbd, q, s, samp, 82 | small, strike, strong, sub, sup, tt, var, 83 | b, u, i, center, 84 | dl, dt, dd, ol, ul, li, 85 | fieldset, form, label, legend, 86 | table, caption, tbody, tfoot, thead, tr, th, td, 87 | article, aside, canvas, details, embed, 88 | figure, figcaption, footer, header, hgroup, 89 | menu, nav, output, ruby, section, summary, 90 | time, mark, audio, video { 91 | margin: 0; 92 | padding: 0; 93 | border: 0; 94 | font-size: 100%; 95 | font: inherit; 96 | vertical-align: baseline; 97 | } 98 | /* HTML5 display-role reset for older browsers */ 99 | article, aside, details, figcaption, figure, 100 | footer, header, hgroup, menu, nav, section { 101 | display: block; 102 | } 103 | body { 104 | line-height: 1; 105 | } 106 | ol, ul { 107 | list-style: none; 108 | } 109 | blockquote, q { 110 | quotes: none; 111 | } 112 | blockquote:before, blockquote:after, 113 | q:before, q:after { 114 | content: ''; 115 | content: none; 116 | } 117 | table { 118 | border-collapse: collapse; 119 | border-spacing: 0; 120 | } 121 | `; 122 | return reset; 123 | } 124 | } 125 | 126 | export { Stylesheets }; 127 | 128 | let imagemagick = gm.subClass({ imageMagick: true }); 129 | 130 | const renderHtml = (component, css, cssFile) => { 131 | const wrapper = render(component); 132 | let html = wrapper.html(); 133 | 134 | const stylesheets = new Stylesheets(); 135 | 136 | if (css) { 137 | stylesheets.addCSS(css); 138 | } 139 | 140 | if (cssFile) { 141 | stylesheets.addCSSFile(cssFile); 142 | } 143 | 144 | html = '' + stylesheets.createStyles() + '' + html + ''; 145 | 146 | return html; 147 | }; 148 | 149 | const createScreenshot = ({ 150 | resolve, 151 | componentName, 152 | html, 153 | ref, 154 | path, 155 | viewportSize 156 | }) => { 157 | phantom.create().then((ph) => { 158 | ph.createPage().then((page) => { 159 | page.property('viewportSize', viewportSize).then(() => { 160 | page.property('content', html).then(() => { 161 | // TODO figure out a better way to do this 162 | setTimeout(() => { 163 | let fullFileName = path + 'yours-' + componentName + '.png'; 164 | page.render(fullFileName).then(() => { 165 | ph.exit(); 166 | ref.currentSnap = fullFileName; 167 | resolve(ref); 168 | }); 169 | }, 1000); 170 | }); 171 | }); 172 | }); 173 | }); 174 | }; 175 | 176 | /** 177 | * Constructor 178 | */ 179 | const Differ = function ({ 180 | componentName, 181 | component, 182 | savePath, 183 | viewportSize = DEVICE_SIZES.DESKTOP, 184 | css = '', 185 | cssFile = false, 186 | threshold = 0, 187 | onSnapshotsUpdated = () => {}, 188 | updateSnapshots = false, 189 | onSnapshotCreated = () => {}, 190 | createSnapshots = false, 191 | }) { 192 | this.currentSnap = null; 193 | this.currentDiff = null; 194 | this.html = renderHtml(component, css, cssFile); 195 | 196 | this.snap = ({ path = './' }) => { 197 | let promise = new Promise((resolve, reject) => { 198 | createScreenshot({ 199 | resolve, 200 | reject, 201 | componentName, 202 | html: this.html, 203 | path, 204 | ref: this, 205 | viewportSize 206 | }); 207 | }); 208 | 209 | return promise; 210 | }; 211 | 212 | this.compareTo = ({ path, filename }) => { 213 | let promise = new Promise((resolve, reject) => { 214 | this.currentDiff = path + 'difference.png'; 215 | imageDiff({ 216 | actualImage: path + filename, 217 | expectedImage: this.currentSnap, 218 | diffImage: path + 'difference.png', 219 | threshold 220 | }, function (err, imagesAreSame) { 221 | if (err) { 222 | reject(err); 223 | } 224 | 225 | imagemagick().command('composite') 226 | .in('-gravity', 'center') 227 | .in(path + 'difference.png') 228 | .in(this.currentSnap) 229 | .write(path + 'difference.png', function (err2) { 230 | if (err2) { 231 | reject(err2); 232 | } 233 | 234 | resolve(imagesAreSame); 235 | }); 236 | }.bind(this)); 237 | }); 238 | 239 | return promise; 240 | }; 241 | 242 | this.moveSnapshot = ({ path, filename }) => { 243 | fs.renameSync( this.currentSnap, path + filename ); 244 | 245 | return true; 246 | }; 247 | 248 | this.cleanup = () => { 249 | if ( fileExists( this.currentSnap ) ) { 250 | fs.unlinkSync( this.currentSnap ); 251 | } 252 | 253 | if ( fileExists( this.currentDiff ) ) { 254 | fs.unlinkSync( this.currentDiff ); 255 | } 256 | }; 257 | 258 | this.compare = () => { 259 | var promise = new Promise((resolve) => { 260 | this.snap( { path: savePath } ).then((differ) => { 261 | let willHandleUpdate = false; 262 | 263 | if (process.env.UPDATE_SNAPSHOTS || updateSnapshots) { 264 | willHandleUpdate = true; 265 | 266 | if (typeof process.env.UPDATE_SNAPSHOTS === 'string' && 267 | process.env.UPDATE_SNAPSHOTS !== '1' && 268 | process.env.UPDATE_SNAPSHOTS !== 'true') { 269 | // We are trying to update a specific component 270 | // Flag componentNames that are not the one specified 271 | // as false. 272 | if (process.env.UPDATE_SNAPSHOTS !== componentName) { 273 | willHandleUpdate = false; 274 | } 275 | } 276 | } 277 | 278 | if (willHandleUpdate) { 279 | differ.moveSnapshot({ path: savePath, filename: 'theirs-' + componentName + '.png' }); 280 | differ.cleanup(); 281 | onSnapshotsUpdated(); 282 | } else if (process.env.CREATE_SNAPSHOTS || createSnapshots) { 283 | let created = false; 284 | if ( !fileExists( savePath + 'theirs-' + componentName + '.png' ) ) { 285 | differ.moveSnapshot({ path: savePath, filename: 'theirs-' + componentName + '.png' }); 286 | created = true; 287 | } 288 | 289 | differ.cleanup(); 290 | onSnapshotCreated(created); 291 | } else { 292 | differ.compareTo({ 293 | path: savePath, 294 | filename: 'theirs-' + componentName + '.png' 295 | }).then((areTheSame) => { 296 | resolve(areTheSame); 297 | }); 298 | } 299 | }); 300 | }); 301 | 302 | return promise; 303 | }; 304 | }; 305 | 306 | export { Differ }; 307 | -------------------------------------------------------------------------------- /tests/fixtures/css-fixture.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 10px; 3 | } 4 | 5 | .name { 6 | color: red; 7 | font-size: 2em; 8 | } 9 | 10 | .description { 11 | color: blue; 12 | } -------------------------------------------------------------------------------- /tests/fixtures/react-fixture.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ({ name, description }) => ( 4 |
5 |
{name}
6 |
{description}
7 |
8 | ); 9 | -------------------------------------------------------------------------------- /tests/react-cornea.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import fileExists from 'file-exists'; 3 | import fs from 'fs'; 4 | 5 | import { expect } from 'chai'; 6 | const { describe, it } = global; 7 | 8 | import { Differ } from '../react-cornea'; 9 | 10 | import { default as ReactFixture } from './fixtures/react-fixture.jsx'; 11 | 12 | describe( 'Differ', () => { 13 | 14 | const componentName = 'fixture'; 15 | const theirFilePath = __dirname + '/fixtures/theirs-fixture.png'; 16 | const yourFilePath = __dirname + '/fixtures/yours-fixture.png'; 17 | const diffFilePath = __dirname + '/fixtures/difference.png'; 18 | const name = 'Differ Test'; 19 | const description = 'Making sure we can detect changes'; 20 | 21 | describe( 'React Snapshots', () => { 22 | it( 'will create a snapshot when creating', function ( done ) { 23 | this.timeout( 10000 ); 24 | 25 | const onSnapshotCreated = (e) => { 26 | expect( fileExists( theirFilePath ) ).to.equal( true ); 27 | expect( e ).to.equal( true ); 28 | 29 | done(); 30 | }; 31 | 32 | const differ = new Differ( { 33 | component: , 34 | componentName, 35 | savePath: __dirname + '/fixtures/', 36 | createSnapshots: true, 37 | onSnapshotCreated 38 | } ); 39 | 40 | differ.compare(); 41 | } ); 42 | 43 | it( 'will not create a snapshot if it already exists', function ( done ) { 44 | this.timeout( 10000 ); 45 | 46 | const onSnapshotCreated = () => { 47 | const onSnapshotCreatedAgain = (e) => { 48 | expect( fileExists( theirFilePath ) ).to.equal( true ); 49 | expect( e ).to.equal( false ); 50 | 51 | done(); 52 | } 53 | 54 | const anotherDiffer = new Differ( { 55 | component: , 56 | componentName, 57 | savePath: __dirname + '/fixtures/', 58 | createSnapshots: true, 59 | onSnapshotCreated: onSnapshotCreatedAgain 60 | } ); 61 | 62 | anotherDiffer.compare(); 63 | }; 64 | 65 | const differ = new Differ( { 66 | component: , 67 | componentName, 68 | savePath: __dirname + '/fixtures/', 69 | createSnapshots: true, 70 | onSnapshotCreated 71 | } ); 72 | 73 | differ.compare(); 74 | } ); 75 | 76 | /** 77 | * Update a snapshot and check that it 78 | * exists 79 | */ 80 | it( 'will create a snapshot when updating', function ( done ) { 81 | this.timeout( 10000 ); 82 | 83 | const onSnapshotsUpdated = () => { 84 | expect( fileExists( theirFilePath ) ).to.equal( true ); 85 | 86 | done(); 87 | }; 88 | 89 | const differ = new Differ( { 90 | component: , 91 | componentName, 92 | savePath: __dirname + '/fixtures/', 93 | updateSnapshots: true, 94 | onSnapshotsUpdated 95 | } ); 96 | 97 | differ.compare(); 98 | } ); 99 | 100 | /** 101 | * Generate a snapshot and check that another 102 | * snapshot matches it 103 | */ 104 | it( 'can compare snapshots', function ( done ) { 105 | this.timeout( 10000 ); 106 | 107 | const onSnapshotsUpdated = () => { 108 | const anotherDiffer = new Differ( { 109 | component: , 110 | componentName, 111 | savePath: __dirname + '/fixtures/' 112 | } ); 113 | 114 | anotherDiffer.compare().then( ( areTheSame ) => { 115 | expect( fileExists( theirFilePath ) ).to.equal( true ); 116 | expect( fileExists( yourFilePath ) ).to.equal( true ); 117 | expect( fileExists( diffFilePath ) ).to.equal( true ); 118 | 119 | expect( areTheSame ).to.equal( true ); 120 | 121 | done(); 122 | } ); 123 | }; 124 | 125 | const differ = new Differ( { 126 | component: , 127 | componentName, 128 | savePath: __dirname + '/fixtures/', 129 | updateSnapshots: true, 130 | onSnapshotsUpdated 131 | } ); 132 | 133 | differ.compare(); 134 | } ); 135 | 136 | /** 137 | * Can clean up after generating snapshots 138 | */ 139 | it( 'can cleanup snapshots', function ( done ) { 140 | this.timeout( 10000 ); 141 | 142 | const onSnapshotsUpdated = () => { 143 | const anotherDiffer = new Differ( { 144 | component: , 145 | componentName, 146 | savePath: __dirname + '/fixtures/' 147 | } ); 148 | 149 | anotherDiffer.compare().then( ( areTheSame ) => { 150 | expect( fileExists( theirFilePath ) ).to.equal( true ); 151 | expect( fileExists( yourFilePath ) ).to.equal( true ); 152 | expect( fileExists( diffFilePath ) ).to.equal( true ); 153 | 154 | anotherDiffer.cleanup(); 155 | 156 | expect( fileExists( theirFilePath ) ).to.equal( true ); 157 | expect( fileExists( yourFilePath ) ).to.equal( false ); 158 | expect( fileExists( diffFilePath ) ).to.equal( false ); 159 | 160 | done(); 161 | } ); 162 | }; 163 | 164 | const differ = new Differ( { 165 | component: , 166 | componentName, 167 | savePath: __dirname + '/fixtures/', 168 | updateSnapshots: true, 169 | onSnapshotsUpdated 170 | } ); 171 | 172 | differ.compare(); 173 | } ); 174 | 175 | after( function ( done ) { 176 | // Clean up 177 | if ( fileExists( theirFilePath ) ) { 178 | fs.unlinkSync( theirFilePath ); 179 | } 180 | 181 | if ( fileExists( yourFilePath ) ) { 182 | fs.unlinkSync( yourFilePath ); 183 | } 184 | 185 | if ( fileExists( diffFilePath ) ) { 186 | fs.unlinkSync( diffFilePath ); 187 | } 188 | 189 | done(); 190 | } ); 191 | 192 | } ); 193 | 194 | } ); 195 | -------------------------------------------------------------------------------- /tests/stylesheets.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { expect } from 'chai'; 4 | const { describe, it } = global; 5 | 6 | import { Stylesheets } from '../react-cornea'; 7 | 8 | describe( 'Stylesheets', () => { 9 | let stylesheet; 10 | const css = '.test { color: black; }'; 11 | const cssFilePath = __dirname + '/fixtures/css-fixture.css'; 12 | 13 | beforeEach( () => { 14 | stylesheet = new Stylesheets(); 15 | } ); 16 | 17 | it( 'will include the given CSS', () => { 18 | stylesheet.addCSS(css); 19 | 20 | let styles = stylesheet.createStyles(); 21 | 22 | expect(styles).to.contain(css); 23 | } ); 24 | 25 | it ( 'will include the given CSS file', () => { 26 | stylesheet.addCSSFile(cssFilePath); 27 | 28 | let styles = stylesheet.createStyles(); 29 | 30 | expect(styles).to.contain('.container'); 31 | expect(styles).to.contain('.name'); 32 | expect(styles).to.contain('.description'); 33 | } ); 34 | } ); 35 | --------------------------------------------------------------------------------