From 1aa51620a7f09b2b20453a928a6fdc270c655076 Mon Sep 17 00:00:00 2001 From: Travis Kaufman Date: Tue, 19 Jul 2016 14:02:05 -0400 Subject: [PATCH] chore(infrastructure): Set up testing * `npm test` lints files, runs karma, reports coverages, checks coverage thresholds * `npm run test:watch` runs karma in auto-watch mode, with source maps for both source and test files. * [karma](https://karma-runner.github.io/1.0/index.html) is used for running tests. * [tape](https://github.com/substack/tape) is used as the actual test runner. * [bel](https://github.com/shama/bel) is used for easy DOM fixtures. * [testdouble](https://github.com/testdouble/testdouble.js) is used for mocking/doubles. * [isparta](https://github.com/douglasduteil/isparta) is used to instrument source files for coverage. * [istanbul](https://github.com/gotwarlost/istanbul) is used to check and report coverage. resolves #4465 --- .gitignore | 1 + karma.conf.js | 51 ++++++++++ package.json | 23 ++++- packages/mdl-auto-init/index.js | 20 +++- test/unit/index.js | 4 + test/unit/mdl-auto-init/mdl-auto-init.test.js | 92 +++++++++++++++++++ 6 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 karma.conf.js create mode 100644 test/unit/index.js create mode 100644 test/unit/mdl-auto-init/mdl-auto-init.test.js diff --git a/.gitignore b/.gitignore index 2277292c3f..abd977c329 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules .DS_Store /build +/coverage packages/*/dist *.log diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000000..0f37f97109 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,51 @@ +const path = require('path'); +const webpackConfig = require('./webpack.config')[0]; + +// TODO: Sourcemaps +// TODO: eslint-plugin-tape +module.exports = function(config) { + config.set({ + basePath: '', + frameworks: ['tap'], + files: [ + 'test/unit/index.js' + ], + preprocessors: { + 'test/unit/index.js': ['webpack', 'sourcemap'] + }, + reporters: ['dots', 'coverage'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + browsers: ['Chrome'], + concurrency: Infinity, + + coverageReporter: { + dir: 'coverage', + reporters: [ + {type: 'json', subdir: '.', file: 'coverage.json'}, + {type: 'html'} + ] + }, + + webpack: Object.assign({}, webpackConfig, { + devtool: 'inline-source-map', + node: { + fs: 'empty' + }, + module: Object.assign({}, webpackConfig.module, { + // Cover source files when not debugging tests. Otherwise, omit coverage instrumenting to get + // uncluttered source maps. + loaders: webpackConfig.module.loaders.concat([config.singleRun ? { + test: /\.js$/, + include: path.resolve('./packages'), + loader: 'isparta' + } : undefined]).filter(Boolean) + }) + }), + + webpackMiddleware: { + noInfo: true + } + }); +}; diff --git a/package.json b/package.json index 111e4fa0d4..f7960d153d 100644 --- a/package.json +++ b/package.json @@ -9,24 +9,39 @@ "build:min": "mkdir -p build && MDL_ENV=production webpack -p", "dist": "npm run clean && npm run build && npm run build:min", "dev": "npm run clean && MDL_ENV=development webpack-dev-server --content-base demos --inline --hot", - "lint:js": "eslint --fix packages webpack.config.js", - "postinstall": "lerna bootstrap" + "lint:js": "eslint --fix packages test webpack.config.js karma.conf.js", + "postinstall": "lerna bootstrap", + "pretest": "npm run lint:js", + "test": "karma start --single-run", + "posttest": "istanbul report --root coverage text-summary && istanbul check-coverage --lines 85 --statements 85 --branches 85 --functions 85", + "test:watch": "karma start --auto-watch" }, "devDependencies": { "autoprefixer": "^6.3.6", "babel-loader": "^6.2.4", "babel-plugin-transform-object-assign": "^6.8.0", "babel-preset-es2015": "^6.9.0", + "bel": "^4.4.3", "css-loader": "^0.23.1", "eslint": "^2.12.0", "eslint-config-google": "^0.5.0", "extract-text-webpack-plugin": "^1.0.1", + "isparta-loader": "^2.0.0", + "istanbul": "^0.4.4", + "karma": "^1.1.1", + "karma-chrome-launcher": "^1.0.1", + "karma-coverage": "^1.1.0", + "karma-sourcemap-loader": "^0.3.7", + "karma-tap": "^2.0.1", + "karma-webpack": "^1.7.0", "lerna": "2.0.0-beta.18", "node-sass": "^3.7.0", "postcss-loader": "^0.9.1", "raw-loader": "^0.5.1", "sass-loader": "^3.2.0", "style-loader": "^0.13.1", + "tape": "^4.6.0", + "testdouble": "^1.6.0", "webpack": "^1.13.1", "webpack-dev-server": "^1.14.1" }, @@ -34,6 +49,8 @@ "presets": [ "es2015" ], - "plugins": ["transform-object-assign"] + "plugins": [ + "transform-object-assign" + ] } } diff --git a/packages/mdl-auto-init/index.js b/packages/mdl-auto-init/index.js index b8a8bba9f0..ccc38fc5e4 100644 --- a/packages/mdl-auto-init/index.js +++ b/packages/mdl-auto-init/index.js @@ -1,10 +1,12 @@ const registry = Object.create(null); +const CONSOLE_WARN = console.warn.bind(console); + /** * Auto-initializes all mdl components on a page. */ -export default function mdlAutoInit() { - const nodes = document.querySelectorAll('[data-mdl-auto-init]'); +export default function mdlAutoInit(root = document, warn = CONSOLE_WARN) { + const nodes = root.querySelectorAll('[data-mdl-auto-init]'); for (let i = 0, node; (node = nodes[i]); i++) { const ctorName = node.dataset.mdlAutoInit; if (!ctorName) { @@ -18,7 +20,7 @@ export default function mdlAutoInit() { } if (node[ctorName]) { - console.warn(`(mdl-auto-init) Component already initialized for ${node}. Skipping...`); + warn(`(mdl-auto-init) Component already initialized for ${node}. Skipping...`); continue; } @@ -33,14 +35,22 @@ export default function mdlAutoInit() { } } -mdlAutoInit.register = function(componentName, Ctor) { +mdlAutoInit.register = function(componentName, Ctor, warn = CONSOLE_WARN) { if (typeof Ctor !== 'function') { throw new Error(`(mdl-auto-init) Invalid Ctor value ${Ctor}. Expected function`); } if (registry[componentName]) { - console.warn( + warn( `(mdl-auto-init) Overriding registration for ${componentName} with ${Ctor}. ` + `Was: ${registry[componentName]}`); } registry[componentName] = Ctor; }; + +mdlAutoInit.deregister = function(componentName) { + delete registry[componentName]; +}; + +mdlAutoInit.deregisterAll = function() { + Object.keys(registry).forEach(this.deregister, this); +}; diff --git a/test/unit/index.js b/test/unit/index.js new file mode 100644 index 0000000000..1070734ac7 --- /dev/null +++ b/test/unit/index.js @@ -0,0 +1,4 @@ +/** @fileoverview Bootstraps the test bundle for karma-webpack. */ + +const testsContext = require.context('.', true, /\.test\.js$/); +testsContext.keys().forEach(testsContext); diff --git a/test/unit/mdl-auto-init/mdl-auto-init.test.js b/test/unit/mdl-auto-init/mdl-auto-init.test.js new file mode 100644 index 0000000000..11e3ccc301 --- /dev/null +++ b/test/unit/mdl-auto-init/mdl-auto-init.test.js @@ -0,0 +1,92 @@ +import bel from 'bel'; +import td from 'testdouble'; +import test from 'tape'; +import mdlAutoInit from '../../../packages/mdl-auto-init'; + +class FakeComponent { + static attachTo(node) { + return new this(node); + } + + constructor(node) { + this.node = node; + } +} + +const createFixture = () => bel` +
+

Fake Element

+
+`; + +const setupTest = () => { + mdlAutoInit.deregisterAll(); + mdlAutoInit.register(FakeComponent.name, FakeComponent); + return createFixture(); +}; + +test('calls attachTo() on components registered for identifier on nodes w/ data-mdl-auto-init attr', t => { + t.plan(1); + + const root = setupTest(); + mdlAutoInit(root); + + t.true(root.querySelector('.mdl-fake').FakeComponent instanceof FakeComponent); +}); + +test('passes the node where "data-mdl-auto-init" was found to attachTo()', t => { + t.plan(1); + + const root = setupTest(); + mdlAutoInit(root); + + const fake = root.querySelector('.mdl-fake'); + t.equal(fake.FakeComponent.node, fake); +}); + +test('throws when no constructor name is specified within "data-mdl-auto-init"', t => { + t.plan(1); + + const root = setupTest(); + root.querySelector('.mdl-fake').dataset.mdlAutoInit = ''; + + t.throws(() => mdlAutoInit(root)); +}); + +test('throws when constructor is not registered', t => { + t.plan(1); + + const root = setupTest(); + root.querySelector('.mdl-fake').dataset.mdlAutoInit = 'MDLUnregisteredComponent'; + + t.throws(() => mdlAutoInit(root)); +}); + +test('warns when autoInit called multiple times on a node', t => { + t.plan(1); + + const root = setupTest(); + const warn = td.func('warn'); + const {contains} = td.matchers; + + mdlAutoInit(root, warn); + mdlAutoInit(root, warn); + + t.doesNotThrow(() => td.verify(warn(contains('(mdl-auto-init) Component already initialized')))); +}); + +test('#register throws when Ctor is not a function', t => { + t.plan(1); + t.throws(() => mdlAutoInit.register('not-a-function', 'Not a function')); +}); + +test('#register warns when registered key is being overridden', t => { + t.plan(1); + + const warn = td.func('warn'); + const {contains} = td.matchers; + + mdlAutoInit.register(FakeComponent.name, () => ({overridden: true}), warn); + + t.true(() => td.verify(warn(contains('(mdl-auto-init) Overriding registration')))); +});