From 82eef9340f08e3491ef4d93277c5f6361b4e1a8b Mon Sep 17 00:00:00 2001 From: squidfunk Date: Fri, 7 Oct 2016 16:38:13 +0200 Subject: [PATCH] Integrated karma for unit tests --- .eslintrc | 7 +- .gitignore | 2 +- Gulpfile.babel.js | 17 ++++ package.json | 16 +++- src/assets/stylesheets/helpers/_break.scss | 12 +-- tasks/tests/unit/watch.js | 37 ++++++++ tests/.babelrc | 9 ++ tests/.eslintrc | 23 +++++ tests/karma.conf.js | 103 +++++++++++++++++++++ tests/unit/Marker.spec.jsx | 24 +++++ tests/unit/_lib/create-element.js | 73 +++++++++++++++ 11 files changed, 310 insertions(+), 13 deletions(-) create mode 100644 tasks/tests/unit/watch.js create mode 100644 tests/.babelrc create mode 100644 tests/.eslintrc create mode 100644 tests/karma.conf.js create mode 100644 tests/unit/Marker.spec.jsx create mode 100644 tests/unit/_lib/create-element.js diff --git a/.eslintrc b/.eslintrc index 5f3f52927..efff6969e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -29,10 +29,6 @@ "node": true }, "globals": { - "before": true, - "describe": true, - "expect": true, - "it": true, "Modernizr": true, "navigator": true }, @@ -198,5 +194,6 @@ "requireReturnDescription": true }], "yield-star-spacing": 2 - } + }, + "root": true } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2866009ac..a4589e6b4 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,4 @@ # Distribution files /dist -/mkdocs_material.egg-info \ No newline at end of file +/mkdocs_material.egg-info diff --git a/Gulpfile.babel.js b/Gulpfile.babel.js index b6ca93f98..6c0e837f6 100755 --- a/Gulpfile.babel.js +++ b/Gulpfile.babel.js @@ -43,6 +43,7 @@ const config = { const args = yargs .default("clean", false) /* Clean before build */ + .default("karma", false) /* Karma watchdog */ .default("lint", true) /* Lint sources */ .default("mkdocs", false) /* MkDocs watchdog */ .default("optimize", true) /* Optimize sources */ @@ -272,6 +273,16 @@ gulp.task("mkdocs:clean", gulp.task("mkdocs:serve", load("mkdocs/serve")) +/* ---------------------------------------------------------------------------- + * Tests + * ------------------------------------------------------------------------- */ + +/* + * Start karma test runner + */ +gulp.task("tests:unit:watch", + load("tests/unit/watch")) + /* ---------------------------------------------------------------------------- * Interface * ------------------------------------------------------------------------- */ @@ -306,9 +317,15 @@ gulp.task("watch", [ "assets:build", "views:build" ], () => { + + /* Start MkDocs server */ if (args.mkdocs) gulp.start("mkdocs:serve") + /* Start karma test runner */ + if (args.karma) + gulp.start("tests:unit:watch") + /* Rebuild stylesheets */ gulp.watch([ `${config.assets.src}/stylesheets/**/*.scss` diff --git a/package.json b/package.json index d85eff354..b1644c34d 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,9 @@ "scripts": { "build": "./node_modules/.bin/gulp build --clean", "clean": "./node_modules/.bin/gulp clean", + "pre-commit": "./node_modules/.bin/gulp assets:lint", "start": "./node_modules/.bin/gulp watch --mkdocs --no-lint --no-revision", - "pre-commit": "./node_modules/.bin/gulp assets:lint" + "test": "./node_modules/.bin/karma start ./tests/karma.conf.js" }, "repository": { "type": "git", @@ -32,11 +33,15 @@ "babel-eslint": "^6.1.2", "babel-loader": "^6.2.4", "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-transform-react-jsx": "^6.8.0", + "babel-polyfill": "^6.16.0", "babel-preset-es2015": "^6.13.2", "babel-register": "^6.16.3", + "chai": "^3.5.0", "css-mqpacker": "^4.0.0", "del": "^2.2.0", "eslint": "^3.6.1", + "eslint-plugin-mocha": "^4.6.0", "git-hooks": "^1.1.6", "gulp": "^3.9.1", "gulp-changed": "^1.3.2", @@ -57,6 +62,15 @@ "gulp-svgmin": "^1.2.2", "gulp-uglify": "^1.5.2", "gulp-util": "^3.0.7", + "karma": "^1.3.0", + "karma-chrome-launcher": "^2.0.0", + "karma-coverage": "^1.1.1", + "karma-mocha": "^1.2.0", + "karma-notify-reporter": "^1.0.1", + "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "0.0.26", + "karma-webpack": "^1.8.0", + "mocha": "^3.1.0", "node-notifier": "^4.5.0", "sass-lint": "^1.9.1", "through2": "^2.0.1", diff --git a/src/assets/stylesheets/helpers/_break.scss b/src/assets/stylesheets/helpers/_break.scss index 34d024f5b..42dc8efd5 100644 --- a/src/assets/stylesheets/helpers/_break.scss +++ b/src/assets/stylesheets/helpers/_break.scss @@ -123,7 +123,7 @@ $break-devices: () !default; /// /// @group helpers /// @access public -/// @param {Number|List} $breakpoint Number or number pair +/// @param {Number|List} $breakpoint Number or number pair /// @mixin break-at($breakpoint) { @if type-of($breakpoint) == number { @@ -150,7 +150,7 @@ $break-devices: () !default; /// /// @group helpers /// @access public -/// @param {String} $breakpoint Orientation +/// @param {String} $breakpoint Orientation /// @mixin break-at-orientation($breakpoint) { @if type-of($breakpoint) == string { @@ -167,7 +167,7 @@ $break-devices: () !default; /// /// @group helpers /// @access public -/// @param {Number} $breakpoint Ratio +/// @param {Number} $breakpoint Ratio /// @mixin break-at-ratio($breakpoint) { @if type-of($breakpoint) == number { @@ -184,7 +184,7 @@ $break-devices: () !default; /// /// @group helpers /// @access public -/// @param {String|List} $breakpoint Device +/// @param {String|List} $breakpoint Device /// @mixin break-at-device($device) { @if type-of($device) == string { @@ -211,7 +211,7 @@ $break-devices: () !default; /// /// @group helpers /// @access public -/// @param {String|List} $breakpoint Device +/// @param {String|List} $breakpoint Device /// @mixin break-from-device($device) { @if type-of($device) == string { @@ -233,7 +233,7 @@ $break-devices: () !default; /// /// @group helpers /// @access public -/// @param {String|List} $breakpoint Device +/// @param {String|List} $breakpoint Device /// @mixin break-to-device($device) { @if type-of($device) == string { diff --git a/tasks/tests/unit/watch.js b/tasks/tests/unit/watch.js new file mode 100644 index 000000000..c92197523 --- /dev/null +++ b/tasks/tests/unit/watch.js @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016 Martin Donath + * + * 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: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 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 NON-INFRINGEMENT. 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. + */ + +import path from "path" +import { Server } from "karma" + +/* ---------------------------------------------------------------------------- + * Task: start karma test runner + * ------------------------------------------------------------------------- */ + +export default () => { + return cb => { + process.env.GULP = true + new Server({ + configFile: path.join(process.cwd(), "tests/karma.conf.js") + }, cb).start() + } +} diff --git a/tests/.babelrc b/tests/.babelrc new file mode 100644 index 000000000..61a632adb --- /dev/null +++ b/tests/.babelrc @@ -0,0 +1,9 @@ +{ + "presets": ["es2015"], + "plugins": [ + "add-module-exports", + ["transform-react-jsx", { + "pragma": "createElement" + }] + ] +} \ No newline at end of file diff --git a/tests/.eslintrc b/tests/.eslintrc new file mode 100644 index 000000000..bda0f6c45 --- /dev/null +++ b/tests/.eslintrc @@ -0,0 +1,23 @@ +{ + "env": { + "mocha": true + }, + "parserOptions": { + "ecmaFeatures": { + "jsx": true + } + }, + "plugins": [ + "mocha" + ], + "rules": { + "mocha/no-exclusive-tests": 2, + "mocha/no-global-tests": 2, + "mocha/no-identical-title": 2, + "mocha/no-mocha-arrows": 2, + "mocha/no-pending-tests": 1, + "mocha/no-skipped-tests": 1, + "no-use-before-define": 0, + "prefer-arrow-callback": 0 + } +} \ No newline at end of file diff --git a/tests/karma.conf.js b/tests/karma.conf.js new file mode 100644 index 000000000..017a5132b --- /dev/null +++ b/tests/karma.conf.js @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016 Martin Donath + * + * 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: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 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 NON-INFRINGEMENT. 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. + */ + +const webpack = require("webpack") + +/* ---------------------------------------------------------------------------- + * Definition + * ------------------------------------------------------------------------- */ + +module.exports = karma => { + const config = { + basePath: "../", + frameworks: ["mocha"], + + /* Include babel polyfill to support older browsers */ + files: [ + "node_modules/babel-polyfill/dist/polyfill.js", + "tests/unit/*.spec.jsx" + ], + + /* Test reporters */ + reporters: ["spec", "coverage"], + + /* Silence calls to console.log */ + client: { + captureConsole: false + }, + + /* Preprocess test files */ + preprocessors: { + "tests/unit/*.spec.jsx": ["webpack", "sourcemap"] + }, + + /* Configuration for webpack */ + webpack: { + devtool: "inline-source-map", + plugins: [ + + /* Inject DOM node creation helper for JSX support */ + new webpack.ProvidePlugin({ + createElement: "./_lib/create-element.js" + }) + ], + module: { + loaders: [ + { + test: /\.jsx?$/, + loader: "babel-loader" + } + ] + } + }, + + /* Suppress messages by webpack */ + webpackServer: { + noInfo: true + }, + + /* Code coverage */ + coverageReporter: { + dir: "./coverage", + reporters: [ + { type: "json" } + ] + } + } + + /* Setup for continuous integration */ + if (process.env.CONTINUOUS_INTEGRATION) { + // TODO TBD + + /* Setup for local development environment */ + } else if (process.env.GULP) { + delete config.reporters + + /* Setup for local testing */ + } else { + config.browsers = ["Chrome"] + config.singleRun = true + } + + /* Persist configuration */ + karma.set(config) +} diff --git a/tests/unit/Marker.spec.jsx b/tests/unit/Marker.spec.jsx new file mode 100644 index 000000000..df335d9b3 --- /dev/null +++ b/tests/unit/Marker.spec.jsx @@ -0,0 +1,24 @@ + +import chai from "chai" + +describe("Karma test runner", function() { + chai.should() + + let sandbox = null + + beforeEach(function() { + sandbox = ( +
    + {[...Array(10)].map((x, i) => { + return
  • Element {i + 1}
  • + })} +
+ ) + }) + + it("should compile JSX correctly", function() { + document.body.appendChild(sandbox) + return true + }) +}) + diff --git a/tests/unit/_lib/create-element.js b/tests/unit/_lib/create-element.js new file mode 100644 index 000000000..a8d8e157e --- /dev/null +++ b/tests/unit/_lib/create-element.js @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016 Martin Donath + * + * 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: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 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 NON-INFRINGEMENT. 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. + */ + +/* ---------------------------------------------------------------------------- + * Definition + * ------------------------------------------------------------------------- */ + +/** + * Create a native DOM node + * + * @param {string} tag - Tag name + * @param {object} properties - Properties + * @param {string|number|Array} children - Child nodes + * @return {HTMLElement} Native DOM node + */ +const createElement = (tag, properties, ...children) => { + const el = document.createElement(tag) + + /* Set all properties */ + for (const attr of Object.keys(properties)) + el.setAttribute(attr, properties[attr]) + + /* Iterate child nodes */ + const iterateChildNodes = nodes => { + for (const node of nodes) { + + /* Directly append content */ + if (typeof node === "string" || + typeof node === "number") { + el.textContent += node + + /* Recurse, if we got an array */ + } else if (Array.isArray(node)) { + iterateChildNodes(node) + + /* Append regular nodes */ + } else { + el.appendChild(node) + } + } + } + + /* Iterate child nodes */ + iterateChildNodes(children) + + /* Return element */ + return el +} + +/* ---------------------------------------------------------------------------- + * Exports + * ------------------------------------------------------------------------- */ + +export default createElement