diff --git a/.babelrc b/.babelrc index e9d2f4d..5436124 100644 --- a/.babelrc +++ b/.babelrc @@ -4,5 +4,8 @@ "es2015", "stage-1" ], + "plugins": [ + "transform-object-assign" + ], "sourceMaps": true, } \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 5b1fd1c..4cc1689 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,8 @@ language: node_js +before_install: npm install -g npm@3 script: travis_retry npm test node_js: - '0.12' +- '4' +- '5' sudo: false diff --git a/__tests__/Sparklines.js b/__tests__/Sparklines.js index 6324124..1b2e173 100644 --- a/__tests__/Sparklines.js +++ b/__tests__/Sparklines.js @@ -3,10 +3,6 @@ import ReactDOM from 'react-dom'; import { shallow } from 'enzyme'; import { expect } from 'chai'; import { Sparklines } from '../src/Sparklines'; -import jsdom from 'jsdom' - -global.document = jsdom.jsdom(''); -global.window = document.parentWindow; describe('Sparklines', () => { it('does not throw without any parameters', () => { diff --git a/__tests__/compareSvg.js b/__tests__/compareSvg.js new file mode 100644 index 0000000..8a1f7e1 --- /dev/null +++ b/__tests__/compareSvg.js @@ -0,0 +1,23 @@ +import hiff from 'hiff'; + +function normalizeStyle(style) { + style = (style || '').split(';').map(s => s.trim()); + style.sort(); + return style.join(';'); +} + +function normalizeAttrs(...$ns) { + for (let $n of $ns) { + if ($n.attr('style')) + $n.attr('style', normalizeStyle($n.attr('style'))); + } +} + +function comparatorFn($n1, $n2, childChanges) { + normalizeAttrs($n1, $n2); + return hiff.defaultTagComparisonFn($n1, $n2, childChanges); +} + +export default function compareSvg(svg1, svg2, options = {}) { + return hiff.compare(svg1, svg2, Object.assign({}, options, {tagComparison: comparatorFn})); +} diff --git a/__tests__/data.json b/__tests__/data.json new file mode 100644 index 0000000..02de8e4 --- /dev/null +++ b/__tests__/data.json @@ -0,0 +1,67 @@ +{ + "sampleData": [0.26789611283279424, -1.5618808743590797, -0.46848826820269196, -0.02429709108986638, -0.07347501430506465, + 0.938722048681125, -0.02488170176918398, + 0.014511315562131895, -1.0920317808493079, -1.6226651458214956, + 0.6777968350341455, -1.0989601025670448, + 1.402853678778828, -1.7923422052616966, -0.37025235972161835, -0.10254054014867667, + 0.3709902985604339, + 2.5285657626539253, -0.18958673659343403, + 0.8578243085059141, + 1.7395812075504404, + 0.9723534409914075, -0.6799757002898873, + 1.153081489500828, + 1.3851189843556257, + 0.19355625368483506, + 1.262069965103209, -0.8628137671385424, -0.6118030618030503, -0.25257403618789087 + ], + "sampleData100": [-0.2809926205121489, + 0.5993223086924007, + 0.5450586119753267, -0.4794107823559421, -0.18298272472350668, + 0.3219712568468161, + 2.0566174540438324, -1.6442809970641576, + 1.971025186834513, + 0.37237930811331116, -0.4015753275277232, + 1.0601229819271032, -2.0983317267366988, + 0.26835584955818786, -0.2899975217753408, -0.6342464890422705, -0.10975205424415876, + 0.39583180670735013, + 1.4695948548213784, -1.2295606440627673, + 1.0056333434310312, + 1.006402733277956, -1.4092655719724325, + 0.17595701557726026, -0.19396518917878047, -1.4314174397644206, -0.34402041741926476, + 0.6986827111240516, -0.6157663396302129, + 1.0606864486721386, + 1.3537300165741912, -0.9521291296713654, -1.089926042595364, -0.9723342804049446, + 0.2286317959508994, + 0.2613862542298905, + 0.24840731355644413, + 2.08064561830636, + 0.44534855831763426, + 1.5511436162779393, -1.5514313805901196, -0.7497893094776009, + 0.4027674242193654, -0.38986316786208264, -1.2167765233154504, + 0.18879490542570268, -1.5284852088503573, + 0.8789559275619153, -1.2451506359938267, -0.7226040247250638, -0.07157034383583998, + 1.9901707247581082, + 0.22166972734467405, + 0.058080839429433054, -0.6324465858010533, -0.8091687560181702, -1.293296284426419, + 1.8436776591711028, -0.28314101700652944, + 1.358988312176975, -0.1152691343859452, -2.425199332455914, + 0.6696100792204956, + 1.7308347028588733, -0.9997610678433961, -0.10106296722138419, + 0.3157348177184432, -0.34931234065268996, + 0.4662049447935582, + 0.8793589099345607, + 2.069923608446714, + 1.3861543531394107, -0.2705101572065443, + 0.5980871990258989, -0.5871146157743545, -0.9844080263005216, + 0.2972697252124295, -0.6119868603373193, -1.8902200290484288, + 0.6996282188319667, -0.24883654266800448, -0.1156025389007573, + 1.0370156630612894, + 0.9750054921585302, -0.635000984672242, + 0.16076716404020402, + 0.1379262931648021, -0.6838899322825195, + 0.6088591150304701, -0.3408579041001186, -0.08790701313160872, -0.38412257182424137, -1.3319278452946857, + 0.7154759857504911, -2.8727571205730915, -1.3501468729225303, -0.0865770144109483, + 0.505651174224522, -2.2111682240498753, + 2.035381345199811 + ] +} \ No newline at end of file diff --git a/__tests__/fixtures.js b/__tests__/fixtures.js new file mode 100644 index 0000000..f178336 --- /dev/null +++ b/__tests__/fixtures.js @@ -0,0 +1,42 @@ +// This file is auto-updated by ../bootstrap-tests.js + +import React from 'react'; +import { Sparklines, SparklinesBars, SparklinesLine, SparklinesCurve, SparklinesNormalBand, SparklinesReferenceLine, SparklinesSpots } from '../src/Sparklines'; +import { sampleData, sampleData100 } from './data.json'; + +export default { +// AUTO-GENERATED PART STARTS HERE + "Header": {jsx: (), svg: ""}, + "Simple": {jsx: (), svg: ""}, + "SimpleCurve": {jsx: (), svg: ""}, + "Customizable1": {jsx: (), svg: ""}, + "Customizable2": {jsx: (), svg: ""}, + "Customizable3": {jsx: (), svg: ""}, + "Customizable4": {jsx: (), svg: ""}, + "Customizable5": {jsx: (), svg: ""}, + "Customizable6": {jsx: (), svg: ""}, + "Bounds1": {jsx: (), svg: ""}, + "Spots1": {jsx: (), svg: ""}, + "Spots2": {jsx: (), svg: ""}, + "Spots3": {jsx: (), svg: ""}, + "Bars1": {jsx: (), svg: ""}, + "Bars2": {jsx: (), svg: ""}, + "ReferenceLine1": {jsx: (), svg: ""}, + "ReferenceLine2": {jsx: (), svg: ""}, + "ReferenceLine3": {jsx: (), svg: ""}, + "ReferenceLine4": {jsx: (), svg: ""}, + "ReferenceLine5": {jsx: (), svg: ""}, + "ReferenceLine6": {jsx: (), svg: ""}, + "NormalBand1": {jsx: (), svg: ""}, + "NormalBand2": {jsx: (), svg: ""}, + "RealWorld1": {jsx: (), svg: ""}, + "RealWorld2": {jsx: (), svg: ""}, + "RealWorld3": {jsx: (), svg: ""}, + "RealWorld4": {jsx: (), svg: ""}, + "RealWorld5": {jsx: (), svg: ""}, + "RealWorld6": {jsx: (), svg: ""}, + "RealWorld7": {jsx: (), svg: ""}, + "RealWorld8": {jsx: (), svg: ""}, + "RealWorld9": {jsx: (), svg: ""}, +// AUTO-GENERATED PART ENDS HERE +}; diff --git a/__tests__/graphical-tests.js b/__tests__/graphical-tests.js new file mode 100644 index 0000000..6dd5878 --- /dev/null +++ b/__tests__/graphical-tests.js @@ -0,0 +1,17 @@ +import fixtures from './fixtures'; +import {render} from 'enzyme'; +import { expect } from 'chai'; +import compareSvg from './compareSvg'; + +describe('Graphical tests from fixtures.js', function() { + for (let key of Object.keys(fixtures)) { + describe(`${key}`, function() { + it('should render as specified', function() { + const wrapper = render(fixtures[key].jsx); + const result = compareSvg(wrapper.html(), fixtures[key].svg); + const errorMessage = 'SVG output changed:\n' + result.changes.map(change => change.message).join('\n') + '\n'; + expect(result.changes, errorMessage).to.be.empty; + }); + }); + } +}); \ No newline at end of file diff --git a/bootstrap-tests.js b/bootstrap-tests.js new file mode 100644 index 0000000..10cd82a --- /dev/null +++ b/bootstrap-tests.js @@ -0,0 +1,104 @@ +// bootstrap-tests.js - A tool for updating the test cases in __tests__/fixtures.js +// +// 1) Reads __tests__/fixtures.js and looks for a "dynamic part", which should be a list of fields +// belonging to that file's default export, enclosed in a pair of markers (see "signal" constants +// below). +// 2) Imports the same fixtures file and (re-)renders each ReactElement to a static SVG string. +// 3) On success, overwrites __tests__/fixtures.js with an updated copy. +// +// Run with babel-node or using "npm run test:bootstrap". + +import path from 'path'; +import {render} from 'enzyme'; +import LineByLineReader from 'line-by-line'; +import reactElementToJsx from 'react-element-to-jsx-string'; +import {writeFileSync} from 'fs'; +import replaceAll from 'replaceall'; +import React from 'react'; + +const fixturesFile = path.resolve(__dirname, './__tests__/fixtures.js'); +const dynamicPartStartSignal = '// AUTO-GENERATED PART STARTS HERE'; +const dynamicPartEndSignal = '// AUTO-GENERATED PART ENDS HERE'; + +const fixtures = require(fixturesFile).default; + +// Handle recurring data constants +import {sampleData, sampleData100} from './__tests__/data.json'; +const recognizedDataConstants = { + sampleData, sampleData100 +}; +const recognizedDataStrings = {}; +for (let dataKey of Object.keys(recognizedDataConstants)) { + recognizedDataStrings[dataKey] = markupToOneLine(reactElementToJsx(
) + .replace(/[^{]*\{|\}[^}]*/g, '')); +} + +// Output control +let outData = ''; +const write = content => { outData += content + '\n'; } +const save = () => writeFileSync(fixturesFile, outData); +function writeFixtures() { + for (let key of Object.keys(fixtures)) { + const jsx = fixtures[key].jsx; + const wrapper = render(jsx); + let jsxCode = `(${markupToOneLine(reactElementToJsx(jsx))})`; + const htmlCode = JSON.stringify(wrapper.html()); + for (let dataKey of Object.keys(recognizedDataStrings)) { + jsxCode = replaceAll(recognizedDataStrings[dataKey], dataKey, jsxCode); + } + write(`\t${JSON.stringify(key)}: {jsx: ${jsxCode}, svg: ${htmlCode}},`); + } +} +function markupToOneLine(code) { + return code.replace(/\s*[\r\n]\s*/g, ' ').replace(/\s+/g, ' ').replace(/\s*([<>])\s*/g, '$1'); +} + +// Input control +const lr = new LineByLineReader(fixturesFile, {skipEmptyLines: false}); +let inDynamicPart = false, dynamicPartCount = 0, lineCount = 0; + +lr.on('line', line => { + ++lineCount; + if (line === dynamicPartStartSignal) { + if (inDynamicPart) + throw new LineError('Dynamic part opened again'); + ++dynamicPartCount; + if (dynamicPartCount > 1) + throw new LineError('Multiple dynamic parts found'); + inDynamicPart = true; + write(line); + try { + writeFixtures(); + } catch(e) { + throw new LineError(e); + } + } + else if (line === dynamicPartEndSignal) { + if (!inDynamicPart) + throw new LineError('Dynamic part closed again'); + inDynamicPart = false; + write(line); + } + else if (!inDynamicPart) + write(line); +}); + +lr.on('end', () => { + if (inDynamicPart) { + throw new LineError('Dynamic part not closed'); + } + if (!dynamicPartCount) { + throw new LineError('No dynamic part found in file!'); + } + save(); +}); + +lr.on('error', function (err) { + throw new LineError(err); +}); + +class LineError extends Error { + constructor(message) { + super(`${fixturesFile}:${lineCount}: ${message}`); + } +} \ No newline at end of file diff --git a/package.json b/package.json index a0680ab..39a56a5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "test": "mocha --compilers js:babel-core/register __tests__", "test:watch": "mocha --compilers js:babel-core/register --watch __tests__", "compile": "webpack", - "prepublish": "npm run compile" + "prepublish": "npm run compile", + "test:bootstrap": "node -r babel-core/register bootstrap-tests.js" }, "repository": { "type": "git", @@ -36,15 +37,21 @@ "babel": "^6.5.2", "babel-core": "^6.7.6", "babel-loader": "^6.2.4", + "babel-plugin-transform-object-assign": "^6.5.0", "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", "babel-preset-stage-1": "^6.5.0", "babel-runtime": "^6.6.1", "chai": "^3.5.0", "enzyme": "^2.2.0", - "jsdom": "^8.3.1", + "hiff": "^0.3.0", + "line-by-line": "^0.1.4", "mocha": "^2.4.5", + "react": "^15.0.1", "react-addons-test-utils": "^15.0.1", + "react-dom": "^15.0.1", + "react-element-to-jsx-string": "=2.5.0", + "replaceall": "^0.1.6", "webpack": "^2.1.0-beta.4", "webpack-dev-server": "^2.0.0-beta" },