diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..3c078e9f9 --- /dev/null +++ b/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + "es2015" + ] +} diff --git a/.gitignore b/.gitignore index 7bd7bf0cb..936cdea00 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ coverage +.idea node_modules -.DS_Store \ No newline at end of file +.DS_Store diff --git a/package-lock.json b/package-lock.json index 9f2a509dc..583cbe772 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1840,6 +1840,17 @@ "integrity": "sha1-4KHGA/iICteHsqNWUrJzPzKl4po=", "dev": true }, + "capture-chrome": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-chrome/-/capture-chrome-2.0.0.tgz", + "integrity": "sha512-ormq6/k6XkJEWdZWIYzIQA3JwovI3f9cJsqhNyw1VQM0BgaVFa3dN+uSh2Z7F4My8xoJr9s8QiTeENN47NSUEg==", + "dev": true, + "requires": { + "chromium-prebuilt": "2.0.1", + "shell-escape": "0.2.0", + "temp-dir": "1.0.0" + } + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -1906,6 +1917,15 @@ "readdirp": "2.1.0" } }, + "chromium-prebuilt": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/chromium-prebuilt/-/chromium-prebuilt-2.0.1.tgz", + "integrity": "sha512-NyqdaxN6zcEtuajYTgE0CyZ0zpDkH9jF9a2GRQAMv3WlaQ8OeV2WMRhqAaBJWCdkoV9zfUu8r3JJMdDHFzWP3g==", + "dev": true, + "requires": { + "download-chromium": "2.0.1" + } + }, "ci-info": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.2.tgz", @@ -2390,6 +2410,26 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "cpr": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cpr/-/cpr-2.2.0.tgz", + "integrity": "sha512-q8UoWzIT9rslJKb3Y5CcByzR2zX7GBkVcoU6jJx02d/BgbE7zJ8Aix74i7bw3iYk58TrgXhmB2XB0aGaBd7oZA==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "create-ecdh": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", @@ -2930,6 +2970,19 @@ "dev": true, "optional": true }, + "download-chromium": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/download-chromium/-/download-chromium-2.0.1.tgz", + "integrity": "sha512-kmMQ0Dx0Ybi3VEWjPUW/jabt2WT1sNtUbMzNrVrNJriRPLXph+N6DqjvnxGz6S1cXk3ytOAfurHjWsoZqeTBdg==", + "dev": true, + "requires": { + "cpr": "2.2.0", + "debug": "3.1.0", + "extract-zip": "1.6.6", + "mkdirp": "0.5.1", + "promisepipe": "2.1.2" + } + }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -3810,6 +3863,49 @@ } } }, + "extract-zip": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", + "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "debug": "2.6.9", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.5", + "typedarray": "0.0.6" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -3872,6 +3968,15 @@ } } }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "1.2.0" + } + }, "file-entry-cache": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", @@ -4098,6 +4203,15 @@ "null-check": "1.0.0" } }, + "fs-readfile-promise": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-readfile-promise/-/fs-readfile-promise-3.0.1.tgz", + "integrity": "sha512-LsSxMeaJdYH27XrW7Dmq0Gx63mioULCRel63B5VeELYLavi1wF5s0XfsIdKDFdCL9hsfQ2qBvXJszQtQJ9h17A==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6614,6 +6728,12 @@ "semver": "5.5.0" } }, + "jpeg-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.2.0.tgz", + "integrity": "sha1-U+RI7J0mPmgyZkZ+lELSxaLvVII=", + "dev": true + }, "js-base64": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz", @@ -8013,6 +8133,16 @@ } } }, + "node-resemble-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-resemble-js/-/node-resemble-js-0.2.0.tgz", + "integrity": "sha1-ijbGZ4ph5dhFX+xYAJsbAnGxkJo=", + "dev": true, + "requires": { + "jpeg-js": "0.2.0", + "pngjs": "2.2.0" + } + }, "node-sass": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.7.2.tgz", @@ -8905,6 +9035,12 @@ "sha.js": "2.4.10" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -8941,6 +9077,12 @@ "find-up": "2.1.0" } }, + "pngjs": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-2.2.0.tgz", + "integrity": "sha1-ZJZjYJoOurh8jwiz/nJASLUdnX8=", + "dev": true + }, "portfinder": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.13.tgz", @@ -9570,6 +9712,12 @@ "asap": "2.0.6" } }, + "promisepipe": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/promisepipe/-/promisepipe-2.1.2.tgz", + "integrity": "sha512-XBa14UrXiH9ThyQN/Ok3tUv9ilw6eXpK3OM2oa3vnzCUeD9vERBJAKKZKMg5l6g4lW0xRe3wmroYQTLl228kfg==", + "dev": true + }, "prop-types": { "version": "15.6.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", @@ -10669,6 +10817,12 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shell-escape": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz", + "integrity": "sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM=", + "dev": true + }, "shell-quote": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", @@ -11530,6 +11684,12 @@ "inherits": "2.0.3" } }, + "temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", + "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", + "dev": true + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -13473,6 +13633,15 @@ } } }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "1.0.1" + } + }, "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", diff --git a/package.json b/package.json index a13235a79..02a0a5d48 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,17 @@ "license": "Apache-2.0", "scripts": { "start": "webpack-dev-server --config test/screenshot/webpack.config.js --content-base test/screenshot", + "stop": "./test/screenshot/stop.sh", + "capture": "./node_modules/.bin/mocha --compilers js:babel-core/register --ui tdd test/screenshot/capture-suite.js", "commitmsg": "validate-commit-msg", "fix": "eslint --fix packages test", "lint": "eslint packages test", - "pretest": "npm run lint", - "test": "npm run test:unit", - "posttest": "istanbul report --root coverage text-summary && istanbul check-coverage --lines 95 --statements 95 --branches 95 --functions 95", + "pretest": "npm run lint && npm stop && ./test/screenshot/start.sh", + "test": "npm run test:unit && npm run test:image-diff", + "posttest": "npm stop && istanbul report --root coverage text-summary && istanbul check-coverage --lines 95 --statements 95 --branches 95 --functions 95", "test:watch": "karma start --auto-watch", - "test:unit": "karma start --single-run" + "test:unit": "karma start --single-run", + "test:image-diff": "./node_modules/.bin/mocha --compilers js:babel-core/register --ui tdd --timeout 15000 test/screenshot/diff-suite.js" }, "config": { "validate-commit-msg": { @@ -28,12 +31,14 @@ "babel-loader": "^7.1.4", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", + "capture-chrome": "^2.0.0", "chai": "^4.1.2", "css-loader": "^0.28.10", "eslint": "^3.19.0", "eslint-config-google": "^0.9.1", "eslint-plugin-react": "^7.7.0", "extract-text-webpack-plugin": "^3.0.2", + "fs-readfile-promise": "^3.0.1", "husky": "^0.14.3", "istanbul": "^0.4.5", "istanbul-instrumenter-loader": "^3.0.0", @@ -43,6 +48,7 @@ "karma-mocha": "^1.3.0", "karma-webpack": "^2.0.13", "mocha": "^5.0.2", + "node-resemble-js": "^0.2.0", "node-sass": "^4.7.2", "optimize-css-assets-webpack-plugin": "^3.2.0", "react": "^16.2.0", diff --git a/test/screenshot/capture-suite.js b/test/screenshot/capture-suite.js new file mode 100644 index 000000000..9b250714d --- /dev/null +++ b/test/screenshot/capture-suite.js @@ -0,0 +1,5 @@ +import fullSuite from './full-suite'; + +fullSuite.forEach((screenshotSuite) => { + screenshotSuite.capture(); +}); diff --git a/test/screenshot/diff-suite.js b/test/screenshot/diff-suite.js new file mode 100644 index 000000000..7fe542b53 --- /dev/null +++ b/test/screenshot/diff-suite.js @@ -0,0 +1,5 @@ +import fullSuite from './full-suite'; + +fullSuite.forEach((screenshotSuite) => { + screenshotSuite.diff(); +}); diff --git a/test/screenshot/full-suite.js b/test/screenshot/full-suite.js new file mode 100644 index 000000000..98f1878ee --- /dev/null +++ b/test/screenshot/full-suite.js @@ -0,0 +1,7 @@ +import temporaryPackageSuite from './temporary-package/screenshot-suite'; + +const fullSuite = [ + temporaryPackageSuite, +]; + +export default fullSuite; diff --git a/test/screenshot/screenshot-suite.js b/test/screenshot/screenshot-suite.js new file mode 100644 index 000000000..91aa4caf2 --- /dev/null +++ b/test/screenshot/screenshot-suite.js @@ -0,0 +1,22 @@ +export default class ScreenshotSuite { + constructor(name, screenshots) { + this.name_ = name; + this.screenshots_ = screenshots; + } + + capture() { + suite(this.name_, () => {}); + + this.screenshots_.forEach((screenshot) => { + screenshot.capture(); + }); + } + + diff() { + suite(this.name_, () => {}); + + this.screenshots_.forEach((screenshot) => { + screenshot.diff(); + }); + } +} diff --git a/test/screenshot/screenshot.js b/test/screenshot/screenshot.js new file mode 100644 index 000000000..6a2db5da2 --- /dev/null +++ b/test/screenshot/screenshot.js @@ -0,0 +1,62 @@ +import capture from 'capture-chrome'; +import {get} from 'http'; +import fs from 'fs'; +import resemble from 'node-resemble-js'; +import readFilePromise from 'fs-readfile-promise'; +import {assert} from 'chai'; + +export default class Screenshot { + constructor(urlPath, imagePath) { + this.urlPath_ = urlPath; + this.imagePath_ = imagePath; + // TODO allow clients to specify capture-chrome options, like viewport size + } + + capture() { + test(this.urlPath_, () => { + const url = 'http://localhost:8080/' + this.urlPath_; + return this.checkStatusCode_(url, + capture({ + url, + }).then((screenshot) => { + fs.writeFileSync( + './test/screenshot/' + this.imagePath_, + screenshot); + }) + ); + }); + } + + diff() { + test(this.urlPath_, () => { + const capturePromise = capture({ + url: 'http://localhost:8080/' + this.urlPath_, + }); + const readPromise = readFilePromise( + './test/screenshot/' + this.imagePath_); + return Promise.all([capturePromise, readPromise]) + .then(function([newScreenshot, oldScreenshot]) { + return new Promise(function(resolve) { + const onComplete = function(data) { + assert.isBelow(Number(data.misMatchPercentage), 0.01); + resolve(); + }; + resemble(newScreenshot) + .compareTo(oldScreenshot) + .onComplete(onComplete); + }); + }); + }); + } + + checkStatusCode_(url, success) { + return new Promise((resolve) => { + get(url, (res) => { + const {statusCode} = res; + if (statusCode === 200) { + resolve(success); + } + }); + }); + } +} diff --git a/test/screenshot/start.sh b/test/screenshot/start.sh new file mode 100755 index 000000000..d4eaebc13 --- /dev/null +++ b/test/screenshot/start.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +nohup npm start >/dev/null 2>&1 & diff --git a/test/screenshot/stop.sh b/test/screenshot/stop.sh new file mode 100755 index 000000000..0d4f124f9 --- /dev/null +++ b/test/screenshot/stop.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +kill $(ps aux | grep 'webpack' | awk '{print $2}') || echo 'already stopped' diff --git a/test/screenshot/temporary-package/foo.html b/test/screenshot/temporary-package/foo.html new file mode 100644 index 000000000..13d01fa53 --- /dev/null +++ b/test/screenshot/temporary-package/foo.html @@ -0,0 +1,10 @@ + + + + + + + +
+ + diff --git a/test/screenshot/temporary-package/foo.js b/test/screenshot/temporary-package/foo.js new file mode 100644 index 000000000..2cf009c3f --- /dev/null +++ b/test/screenshot/temporary-package/foo.js @@ -0,0 +1,6 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +ReactDOM.render(( +
foo
+), document.getElementById('app')); diff --git a/test/screenshot/temporary-package/foo.png b/test/screenshot/temporary-package/foo.png new file mode 100644 index 000000000..f55140c62 Binary files /dev/null and b/test/screenshot/temporary-package/foo.png differ diff --git a/test/screenshot/temporary-package/main.png b/test/screenshot/temporary-package/main.png new file mode 100644 index 000000000..110b39f74 Binary files /dev/null and b/test/screenshot/temporary-package/main.png differ diff --git a/test/screenshot/temporary-package/screenshot-suite.js b/test/screenshot/temporary-package/screenshot-suite.js new file mode 100644 index 000000000..b45ffa471 --- /dev/null +++ b/test/screenshot/temporary-package/screenshot-suite.js @@ -0,0 +1,11 @@ +import ScreenshotSuite from '../screenshot-suite'; +import Screenshot from '../screenshot'; + +const screenshots = [ + new Screenshot('temporary-package/index.html', 'temporary-package/main.png'), + new Screenshot('temporary-package/foo.html', 'temporary-package/foo.png'), +]; + +const screenshotSuite = new ScreenshotSuite('TemporaryPackage', screenshots); + +export default screenshotSuite; diff --git a/test/screenshot/temporary-package/webpack.config.js b/test/screenshot/temporary-package/webpack.config.js new file mode 100644 index 000000000..1fb467b43 --- /dev/null +++ b/test/screenshot/temporary-package/webpack.config.js @@ -0,0 +1,6 @@ +const {bundle} = require('../webpack-bundles'); + +module.exports = [ + bundle('temporary-package/index.js', 'temporary-package/bundle'), + bundle('temporary-package/foo.js', 'temporary-package/foo'), +]; diff --git a/test/screenshot/webpack-bundles.js b/test/screenshot/webpack-bundles.js index 51bc1e710..588f2a923 100644 --- a/test/screenshot/webpack-bundles.js +++ b/test/screenshot/webpack-bundles.js @@ -1,11 +1,11 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin'); const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); -module.exports.bundle = function(testPath) { +module.exports.bundle = function(testPath, outputPath) { return { - entry: './test/screenshot/' + testPath + '/index.js', + entry: './test/screenshot/' + testPath, output: { - filename: testPath + '/bundle.js', + filename: outputPath + '.js', }, module: { rules: [{ @@ -42,7 +42,7 @@ module.exports.bundle = function(testPath) { }], }, plugins: [ - new ExtractTextPlugin(testPath + '/bundle.css'), + new ExtractTextPlugin(outputPath + '.css'), new OptimizeCssAssetsPlugin(), ], }; diff --git a/test/screenshot/webpack.config.js b/test/screenshot/webpack.config.js index ef3b8d368..7408f2f8e 100644 --- a/test/screenshot/webpack.config.js +++ b/test/screenshot/webpack.config.js @@ -1,5 +1,6 @@ -const {bundle} = require('./webpack-bundles'); +const temporaryPackageBundles + = require('./temporary-package/webpack.config.js'); module.exports = [ - bundle('temporary-package'), + ...temporaryPackageBundles, ];