diff --git a/.gitignore b/.gitignore index 925e338c25..9dbd711479 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ doc/source/sourcecode /static/ .venv .tox +node_modules +npm-debug.log build dist AUTHORS diff --git a/doc/source/ref/run_tests.rst b/doc/source/ref/run_tests.rst index 2a703a66a6..59e509d020 100644 --- a/doc/source/ref/run_tests.rst +++ b/doc/source/ref/run_tests.rst @@ -134,6 +134,21 @@ Available options: the dashboard module's directory structure. Default: A new directory within 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! ================ diff --git a/horizon/karma.conf.js b/horizon/karma.conf.js new file mode 100644 index 0000000000..5eabc514a8 --- /dev/null +++ b/horizon/karma.conf.js @@ -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/' + } + + }); +}; + diff --git a/openstack_dashboard/karma.conf.js b/openstack_dashboard/karma.conf.js new file mode 100644 index 0000000000..572575401f --- /dev/null +++ b/openstack_dashboard/karma.conf.js @@ -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/' + } + + }); +}; + diff --git a/package.json b/package.json new file mode 100644 index 0000000000..0a101010eb --- /dev/null +++ b/package.json @@ -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": {} +} diff --git a/run_tests.sh b/run_tests.sh index 3180396f19..7269684db8 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -28,6 +28,7 @@ function usage { echo " -y, --pylint Just run pylint" echo " -j, --jshint Just run jshint" echo " -s, --jscs Just run jscs" + echo " -k, --karma Just run karma" echo " -q, --quiet Run non-interactively. (Relatively) quiet." echo " Implies -V if -N is not set." echo " --only-selenium Run only the Selenium unit tests" @@ -72,6 +73,7 @@ just_docs=0 just_tabs=0 just_jscs=0 just_jshint=0 +just_karma=0 never_venv=0 quiet=0 restore_env=0 @@ -109,6 +111,7 @@ function process_option { -y|--pylint) just_pylint=1;; -j|--jshint) just_jshint=1;; -s|--jscs) just_jscs=1;; + -k|--karma) just_karma=1;; -f|--force) force=1;; -t|--tabs) just_tabs=1;; -q|--quiet) quiet=1;; @@ -174,6 +177,12 @@ function run_jscs { fi } +function run_karma { + echo "Running karma ..." + npm install + npm run test +} + function warn_on_flake8_without_venv { set +o errexit ${command_wrapper} python -c "import hacking" 2>/dev/null @@ -578,6 +587,12 @@ if [ $just_jscs -eq 1 ]; then exit $? fi +# Karma +if [ $just_karma -eq 1 ]; then + run_karma + exit $? +fi + # Tab checker if [ $just_tabs -eq 1 ]; then tab_check diff --git a/test-shim.js b/test-shim.js new file mode 100644 index 0000000000..e8f185f665 --- /dev/null +++ b/test-shim.js @@ -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)); + + + +