Get unit tests working with Karma

Establish features for running Karma tests.  Separate karma configurations
are used due to current overlap of template file structure.

You can run the karma tests via:
  ./run_tests.sh --karma

The Karma framework allows for many features like unit test coverage and
provides a common configuration for different test runners.

Change-Id: I79680ef6369383c148da68e6677945886a48df81
Implements: blueprint karma
This commit is contained in:
Matt Borland 2015-03-26 14:48:55 -06:00
parent 8ae838c836
commit 241c2cf432
7 changed files with 336 additions and 0 deletions

2
.gitignore vendored
View File

@ -24,6 +24,8 @@ doc/source/sourcecode
/static/ /static/
.venv .venv
.tox .tox
node_modules
npm-debug.log
build build
dist dist
AUTHORS AUTHORS

View File

@ -134,6 +134,21 @@ Available options:
the dashboard module's directory structure. Default: A new directory within the dashboard module's directory structure. Default: A new directory within
the current directory. the current directory.
JavaScript
----------
You can also run JavaScript unit tests using Karma. Karma is a test
environment that allows for multiple test runners and reporters, including
such features as code coverage. Karma allows developer to run tests live,
as it can watch source and test files for changes.
To run the Karma tests for Horizon and Dashboard::
./run_tests.sh --karma
The default configuration also performs coverage reports, which are saved
to ``horizon/.coverage-karma/`` and ``openstack_dashboard/.coverage-karma/``.
Give me metrics! Give me metrics!
================ ================

99
horizon/karma.conf.js Normal file
View File

@ -0,0 +1,99 @@
module.exports = function(config){
// Path to xstatic pkg path.
var xstaticPath = '../../.venv/lib/python2.7/site-packages/xstatic/pkg/';
config.set({
preprocessors: {
// Used to collect templates for preprocessing.
// NOTE: the templates must also be listed in the files section below.
'./**/*.html': ['ng-html2js'],
// Used to indicate files requiring coverage reports.
'./**/!(*spec).js': ['coverage']
},
// Sets up module to process templates.
ngHtml2JsPreprocessor: {
prependPrefix: '/static/',
moduleName: 'templates'
},
// Assumes you're in the top-level horizon directory.
basePath : './static/',
// Contains both source and test files.
files : [
// shim, partly stolen from /i18n/js/horizon/
// Contains expected items not provided elsewhere (dynamically by
// Django or via jasmine template.
'../../test-shim.js',
// from jasmine.html
xstaticPath + 'jquery/data/jquery.js',
xstaticPath + 'angular/data/angular.js',
xstaticPath + 'angular/data/angular-mocks.js',
xstaticPath + 'angular/data/angular-cookies.js',
xstaticPath + 'angular_bootstrap/data/angular-bootstrap.js',
xstaticPath + 'angular/data/angular-sanitize.js',
xstaticPath + 'd3/data/d3.js',
xstaticPath + 'rickshaw/data/rickshaw.js',
xstaticPath + 'angular_smart_table/data/smart-table.js',
xstaticPath + 'angular_lrdragndrop/data/lrdragndrop.js',
xstaticPath + 'spin/data/spin.js',
xstaticPath + 'spin/data/spin.jquery.js',
// from jasmine_tests.py; only those that are deps for others
'horizon/js/horizon.js',
'horizon/js/angular/hz.api.module.js',
'horizon/js/angular/services/**/*.js',
'horizon/js/angular/hz.api.module.js',
'dashboard-app/dashboard-app.module.js',
'dashboard-app/utils/utils.module.js',
'dashboard-app/**/*.js',
'framework/framework.module.js',
'framework/widgets/charts/charts.js',
'framework/widgets/metadata-tree/metadata-tree.js',
'framework/widgets/table/table.js',
// Catch-all for stuff that isn't required explicitly by others.
'framework/**/!(*spec).js',
// Templates.
'./**/*.html',
// TESTS
'**/*.spec.js',
],
autoWatch : true,
frameworks: ['jasmine'],
browsers : ['PhantomJS'],
phantomjsLauncher: {
// Have phantomjs exit if a ResourceError is encountered
// (useful if karma exits without killing phantom)
exitOnResourceError: true
},
reporters : [ 'progress', 'coverage' ],
plugins : [
'karma-phantomjs-launcher',
'karma-jasmine',
'karma-ng-html2js-preprocessor',
'karma-coverage'
],
coverageReporter: {
type : 'html',
dir : '../.coverage-karma/'
}
});
};

View File

@ -0,0 +1,85 @@
module.exports = function(config){
// Path to xstatic pkg path.
var xstaticPath = '../../.venv/lib/python2.7/site-packages/xstatic/pkg/';
config.set({
preprocessors: {
// Used to collect templates for preprocessing.
// NOTE: the templates must also be listed in the files section below.
'./**/*.html': ['ng-html2js'],
// Used to indicate files requiring coverage reports.
'./**/!(*spec).js': ['coverage']
},
// Sets up module to process templates.
ngHtml2JsPreprocessor: {
prependPrefix: '/static/',
moduleName: 'templates'
},
// Assumes you're in the top-level horizon directory.
basePath : './static/',
// Contains both source and test files.
files : [
// shim, partly stolen from /i18n/js/horizon/
// Contains expected items not provided elsewhere (dynamically by
// Django or via jasmine template.
'../../test-shim.js',
// from jasmine.html
xstaticPath + 'jquery/data/jquery.js',
xstaticPath + 'angular/data/angular.js',
xstaticPath + 'angular/data/angular-mocks.js',
xstaticPath + 'angular/data/angular-cookies.js',
xstaticPath + 'angular_bootstrap/data/angular-bootstrap.js',
xstaticPath + 'angular/data/angular-sanitize.js',
xstaticPath + 'd3/data/d3.js',
xstaticPath + 'rickshaw/data/rickshaw.js',
xstaticPath + 'angular_lrdragndrop/data/lrdragndrop.js',
// Needed by modal spinner
xstaticPath + 'spin/data/spin.js',
xstaticPath + 'spin/data/spin.jquery.js',
// This one seems to have to come first.
"dashboard/dashboard.module.js",
"dashboard/workflow/workflow.js",
"dashboard/launch-instance/launch-instance.js",
"dashboard/**/*.js",
// Templates.
'./**/*.html',
],
autoWatch : true,
frameworks: ['jasmine'],
browsers : ['PhantomJS'],
phantomjsLauncher: {
// Have phantomjs exit if a ResourceError is encountered
// (useful if karma exits without killing phantom)
exitOnResourceError: true
},
reporters : [ 'progress', 'coverage' ],
plugins : [
'karma-phantomjs-launcher',
'karma-jasmine',
'karma-ng-html2js-preprocessor',
'karma-coverage'
],
coverageReporter: {
type : 'html',
dir : '../.coverage-karma/'
}
});
};

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"version": "0.0.0",
"private": true,
"name": "horizon",
"description": "OpenStack Horizon - Angular",
"repository": "none",
"license": "Apache 2.0",
"devDependencies": {
"jasmine-core": "2.2.0",
"karma": "0.12.31",
"karma-chrome-launcher": "0.1.8",
"karma-coverage": "0.3.1",
"karma-jasmine": "0.3.5",
"karma-ng-html2js-preprocessor": "0.1.2",
"karma-phantomjs-launcher": "0.2.0",
"phantomjs": "^1.9.17"
},
"scripts": {
"test": "node node_modules/karma/bin/karma start horizon/karma.conf.js --single-run && node node_modules/karma/bin/karma start openstack_dashboard/karma.conf.js --single-run"
},
"dependencies": {}
}

View File

@ -28,6 +28,7 @@ function usage {
echo " -y, --pylint Just run pylint" echo " -y, --pylint Just run pylint"
echo " -j, --jshint Just run jshint" echo " -j, --jshint Just run jshint"
echo " -s, --jscs Just run jscs" echo " -s, --jscs Just run jscs"
echo " -k, --karma Just run karma"
echo " -q, --quiet Run non-interactively. (Relatively) quiet." echo " -q, --quiet Run non-interactively. (Relatively) quiet."
echo " Implies -V if -N is not set." echo " Implies -V if -N is not set."
echo " --only-selenium Run only the Selenium unit tests" echo " --only-selenium Run only the Selenium unit tests"
@ -72,6 +73,7 @@ just_docs=0
just_tabs=0 just_tabs=0
just_jscs=0 just_jscs=0
just_jshint=0 just_jshint=0
just_karma=0
never_venv=0 never_venv=0
quiet=0 quiet=0
restore_env=0 restore_env=0
@ -109,6 +111,7 @@ function process_option {
-y|--pylint) just_pylint=1;; -y|--pylint) just_pylint=1;;
-j|--jshint) just_jshint=1;; -j|--jshint) just_jshint=1;;
-s|--jscs) just_jscs=1;; -s|--jscs) just_jscs=1;;
-k|--karma) just_karma=1;;
-f|--force) force=1;; -f|--force) force=1;;
-t|--tabs) just_tabs=1;; -t|--tabs) just_tabs=1;;
-q|--quiet) quiet=1;; -q|--quiet) quiet=1;;
@ -174,6 +177,12 @@ function run_jscs {
fi fi
} }
function run_karma {
echo "Running karma ..."
npm install
npm run test
}
function warn_on_flake8_without_venv { function warn_on_flake8_without_venv {
set +o errexit set +o errexit
${command_wrapper} python -c "import hacking" 2>/dev/null ${command_wrapper} python -c "import hacking" 2>/dev/null
@ -578,6 +587,12 @@ if [ $just_jscs -eq 1 ]; then
exit $? exit $?
fi fi
# Karma
if [ $just_karma -eq 1 ]; then
run_karma
exit $?
fi
# Tab checker # Tab checker
if [ $just_tabs -eq 1 ]; then if [ $just_tabs -eq 1 ]; then
tab_check tab_check

98
test-shim.js Normal file
View File

@ -0,0 +1,98 @@
/*
* Shim for Javascript unit tests; supplying expected global features.
* This should be removed from the codebase once i18n services are provided.
* Taken from default i18n file provided by Django.
*/
var angularModuleExtension = [];
(function (globals) {
var django = globals.django || (globals.django = {});
django.pluralidx = function (count) { return (count == 1) ? 0 : 1; };
/* gettext identity library */
django.gettext = function (msgid) { return msgid; };
django.ngettext = function (singular, plural, count) { return (count == 1) ? singular : plural; };
django.gettext_noop = function (msgid) { return msgid; };
django.pgettext = function (context, msgid) { return msgid; };
django.npgettext = function (context, singular, plural, count) { return (count == 1) ? singular : plural; };
django.interpolate = function (fmt, obj, named) {
if (named) {
return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
} else {
return fmt.replace(/%s/g, function(match){return String(obj.shift())});
}
};
/* formatting library */
django.formats = {
"DATETIME_FORMAT": "N j, Y, P",
"DATETIME_INPUT_FORMATS": [
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M:%S.%f",
"%Y-%m-%d %H:%M",
"%Y-%m-%d",
"%m/%d/%Y %H:%M:%S",
"%m/%d/%Y %H:%M:%S.%f",
"%m/%d/%Y %H:%M",
"%m/%d/%Y",
"%m/%d/%y %H:%M:%S",
"%m/%d/%y %H:%M:%S.%f",
"%m/%d/%y %H:%M",
"%m/%d/%y"
],
"DATE_FORMAT": "N j, Y",
"DATE_INPUT_FORMATS": [
"%Y-%m-%d",
"%m/%d/%Y",
"%m/%d/%y"
],
"DECIMAL_SEPARATOR": ".",
"FIRST_DAY_OF_WEEK": "0",
"MONTH_DAY_FORMAT": "F j",
"NUMBER_GROUPING": "3",
"SHORT_DATETIME_FORMAT": "m/d/Y P",
"SHORT_DATE_FORMAT": "m/d/Y",
"THOUSAND_SEPARATOR": ",",
"TIME_FORMAT": "P",
"TIME_INPUT_FORMATS": [
"%H:%M:%S",
"%H:%M:%S.%f",
"%H:%M"
],
"YEAR_MONTH_FORMAT": "F Y"
};
django.get_format = function (format_type) {
var value = django.formats[format_type];
if (typeof(value) == 'undefined') {
return format_type;
} else {
return value;
}
};
/* add to global namespace */
globals.pluralidx = django.pluralidx;
globals.gettext = django.gettext;
globals.ngettext = django.ngettext;
globals.gettext_noop = django.gettext_noop;
globals.pgettext = django.pgettext;
globals.npgettext = django.npgettext;
globals.interpolate = django.interpolate;
globals.get_format = django.get_format;
}(this));