diff --git a/.gitignore b/.gitignore index 1e0fe2b0..13d64bde 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.egg-info dist build +cover .eggs eggs parts diff --git a/README.rst b/README.rst index 4bff1c4f..580e2c80 100644 --- a/README.rst +++ b/README.rst @@ -88,8 +88,11 @@ After this, you can run ./stack.sh from the devstack directory. The Ironic Bare Metal Provisioning plugin should now be visible in the Horizon navigation. -6. Run JavaScript unit tests by visiting - http://localhost:8000/jasmine/?spec=horizon.dashboard.admin.ironic in your +6. Run JavaScript unit tests by either: + + Running the tests locally with npm run test. + + Visiting http://localhost:8000/jasmine/?spec=horizon.dashboard.admin.ironic in your browser. Uninstallation diff --git a/ironic_ui/karma.conf.js b/ironic_ui/karma.conf.js new file mode 100644 index 00000000..ffcd4161 --- /dev/null +++ b/ironic_ui/karma.conf.js @@ -0,0 +1,153 @@ +/* + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function () { + 'use strict'; + + module.exports = function (config) { + // This tox venv is setup in the post-install npm step + var toxPath = '../.tox/py27/lib/python2.7/site-packages/'; + + config.set({ + preprocessors: { + // Used to collect templates for preprocessing. + // NOTE: the templates must also be listed in the files section below. + './static/**/*.html': ['ng-html2js'], + // Used to indicate files requiring coverage reports. + './static/**/!(*.spec).js': ['coverage'] + }, + + // Sets up module to process templates. + ngHtml2JsPreprocessor: { + prependPrefix: '/', + moduleName: 'templates' + }, + + basePath: './', + + // 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 + toxPath + 'xstatic/pkg/jquery/data/jquery.js', + toxPath + 'xstatic/pkg/angular/data/angular.js', + toxPath + 'xstatic/pkg/angular/data/angular-route.js', + toxPath + 'xstatic/pkg/angular/data/angular-mocks.js', + toxPath + 'xstatic/pkg/angular/data/angular-cookies.js', + toxPath + 'xstatic/pkg/angular_bootstrap/data/angular-bootstrap.js', + toxPath + 'xstatic/pkg/angular_gettext/data/angular-gettext.js', + toxPath + 'xstatic/pkg/angular/data/angular-sanitize.js', + toxPath + 'xstatic/pkg/d3/data/d3.js', + toxPath + 'xstatic/pkg/rickshaw/data/rickshaw.js', + toxPath + 'xstatic/pkg/angular_smart_table/data/smart-table.js', + toxPath + 'xstatic/pkg/angular_lrdragndrop/data/lrdragndrop.js', + toxPath + 'xstatic/pkg/spin/data/spin.js', + toxPath + 'xstatic/pkg/spin/data/spin.jquery.js', + toxPath + 'xstatic/pkg/tv4/data/tv4.js', + toxPath + 'xstatic/pkg/objectpath/data/ObjectPath.js', + toxPath + 'xstatic/pkg/angular_schema_form/data/schema-form.js', + toxPath + '/horizon/static/horizon/js/horizon.js', + + /** + * Include framework source code from horizon that we need. + * Otherwise, karma will not be able to find them when testing. + * These files should be mocked in the foreseeable future. + */ + toxPath + 'horizon/static/framework/**/*.module.js', + toxPath + 'horizon/static/framework/**/!(*.spec|*.mock).js', + toxPath + 'openstack_dashboard/static/**/*.module.js', + toxPath + 'openstack_dashboard/static/**/!(*.spec|*.mock).js', + toxPath + 'openstack_dashboard/dashboards/**/static/**/*.module.js', + toxPath + 'openstack_dashboard/dashboards/**/static/**/!(*.spec|*.mock).js', + + /** + * First, list all the files that defines application's angular modules. + * Those files have extension of `.module.js`. The order among them is + * not significant. + */ + './static/**/*.module.js', + + /** + * Followed by other JavaScript files that defines angular providers + * on the modules defined in files listed above. And they are not mock + * files or spec files defined below. The order among them is not + * significant. + */ + './static/**/!(*.spec|*.mock).js', + + /** + * Then, list files for mocks with `mock.js` extension. The order + * among them should not be significant. + */ + toxPath + 'openstack_dashboard/static/**/*.mock.js', + + /** + * Finally, list files for spec with `spec.js` extension. The order + * among them should not be significant. + */ + './static/**/*.spec.js', + + /** + * Angular external templates + */ + './static/**/*.html' + ], + + autoWatch: true, + + frameworks: ['jasmine'], + + browsers: ['Chrome'], + + browserNoActivityTimeout: 60000, + + phantomjsLauncher: { + // Have phantomjs exit if a ResourceError is encountered + // (useful if karma exits without killing phantom) + exitOnResourceError: true + }, + + reporters: ['progress', 'coverage', 'threshold'], + + plugins: [ + 'karma-chrome-launcher', + 'karma-jasmine', + 'karma-ng-html2js-preprocessor', + 'karma-coverage', + 'karma-threshold-reporter' + ], + + // Places coverage report in HTML format in the subdirectory below. + coverageReporter: { + type: 'html', + dir: '../cover/karma/' + }, + + // Coverage threshold values. + thresholdReporter: { + statements: 10, // target 100 + branches: 0, // target 100 + functions: 10, // target 100 + lines: 10 // target 100 + } + }); + }; + +}()); diff --git a/package.json b/package.json index ef927a78..0bc0cf2c 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,6 @@ "name": "ironic-ui", "version": "1.0.0", "description": "Horizon plugin for OpenStack Ironic.", - "scripts": { - "lint": "eslint ./" - }, "repository": { "type": "git", "url": "git://git.openstack.org/openstack/ironic-ui" @@ -12,8 +9,23 @@ "author": "Openstack ", "license": "Apache-2.0", "devDependencies": { - "eslint": "^1.10.3", - "eslint-config-openstack": "^1.2.4", - "eslint-plugin-angular": "1.0.0" - } + "eslint": "1.10.3", + "eslint-config-openstack": "1.2.4", + "eslint-plugin-angular": "1.0.1", + "jasmine-core": "2.4.1", + "karma": "1.1.2", + "karma-chrome-launcher": "1.0.1", + "karma-cli": "1.0.1", + "karma-coverage": "1.1.1", + "karma-jasmine": "1.0.2", + "karma-ng-html2js-preprocessor": "1.0.0", + "karma-threshold-reporter": "0.1.15" + }, + "scripts": { + "postinstall": "if [ ! -d .tox ] || [ ! -d .tox/py27 ]; then tox -epy27 --notest; fi", + "test": "karma start ironic_ui/karma.conf.js --single-run", + "lint": "eslint --no-color ironic_ui/static", + "lintq": "eslint --quiet ironic_ui/static" + }, + "dependencies": {} } diff --git a/test-requirements.txt b/test-requirements.txt index b310362d..9a7715e2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,9 +2,6 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -# Require Horizon --e git://github.com/openstack/horizon.git#egg=horizon - hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 @@ -19,3 +16,5 @@ testtools>=1.4.0 # MIT sphinx!=1.6.1,>=1.5.1 # BSD oslosphinx>=4.7.0 # Apache-2.0 reno>=1.8.0 # Apache-2.0 +# Include horizon as test requirement +http://tarballs.openstack.org/horizon/horizon-master.tar.gz#egg=horizon diff --git a/test-shim.js b/test-shim.js new file mode 100644 index 00000000..3146c45a --- /dev/null +++ b/test-shim.js @@ -0,0 +1,108 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * 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 horizonPlugInModules = []; + + +(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; + globals.STATIC_URL = '/static/'; + globals.WEBROOT = '/'; + +}(this));