diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst index a6bd703c7a..a0f1f625ea 100644 --- a/doc/source/contributing.rst +++ b/doc/source/contributing.rst @@ -125,6 +125,8 @@ The community's guidelines for etiquette are fairly simple: a piece of code, it's polite (though not required) to thank them in your commit message. +.. _translatability: + Translatability =============== Horizon gets translated into multiple languages. The pseudo translation tool @@ -432,32 +434,6 @@ Required $window.gettext('translatable text'); -JSHint ------- -JSHint is a great tool to be used during your code editing to improve -JavaScript quality by checking your code against a configurable list of checks. -Therefore, JavaScript developers should configure their editors to use JSHint -to warn them of any such errors so they can be addressed. Since JSHint has a -ton of configuration options to choose from, links are provided below to the -options Horizon wants enforced along with the instructions for setting up -JSHint for Eclipse, Sublime Text, Notepad++ and WebStorm/PyCharm. - -JSHint configuration file: `.jshintrc`_ - -Instructions for setting up JSHint: `JSHint setup instructions`_ - -.. Note :: - JSHint is part of the automated unit tests performed by Jenkins. The - automated test use the default configurations, which are less strict than - the configurations we recommended to run in your local development - environment. - -.. _.jshintrc: https://wiki.openstack.org/wiki/Horizon/Javascript/EditorConfig/Settings#.jshintrc -.. _JSHint setup instructions: https://wiki.openstack.org/wiki/Horizon/Javascript/EditorConfig -.. _provided: https://wiki.openstack.org/wiki/Horizon/Javascript/EditorConfig - - - CSS --- diff --git a/doc/source/index.rst b/doc/source/index.rst index c3f4c09874..4bf8e2a193 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -78,6 +78,8 @@ the following topic guides. topics/policy topics/testing topics/table_actions + topics/angularjs + topics/javascript_testing API Reference ------------- diff --git a/doc/source/ref/run_tests.rst b/doc/source/ref/run_tests.rst index 59e509d020..a7c747705e 100644 --- a/doc/source/ref/run_tests.rst +++ b/doc/source/ref/run_tests.rst @@ -189,15 +189,6 @@ For more detailed code analysis you can run:: The output will be saved in ``./pylint.txt``. -JsHint ------- - -For code analysis of JavaScript files:: - - ./run_tests.sh --jshint - -You need to have jshint installed before running the command. - Tab Characters -------------- diff --git a/doc/source/topics/angularjs.rst b/doc/source/topics/angularjs.rst new file mode 100644 index 0000000000..6db397d013 --- /dev/null +++ b/doc/source/topics/angularjs.rst @@ -0,0 +1,180 @@ +===================== +AngularJS Topic Guide +===================== + +.. Note:: + This guide is a work in progress. It has been uploaded to encourage faster + reviewing and code development in Angular, and to help the community + standardize on a set of guidelines. There are notes inline on sections + that are likely to change soon, and the docs will be updated promptly + after any changes. + +Getting Started +=============== + +The tooling for AngularJS testing and code linting relies on npm, the +node package manager, and thus relies on Node.js. While it is not a +prerequisite to developing with Horizon, it is advisable to install Node.js, +either through `downloading `_ or +`via a package manager `_. + +Once you have npm available on your system, run ``npm install`` from the +horizon root directory. + +.. _js_code_style: + +Code Style +========== + +We currently use the `Angular Style Guide`_ by John Papa as reference material. +When reviewing AngularJS code, it is helpful to link directly to the style +guide to reinforce a point, e.g. +https://github.com/johnpapa/angular-styleguide#style-y024 + +.. _Angular Style Guide: https://github.com/johnpapa/angular-styleguide + +ESLint +------ + +ESLint is a tool for identifying and reporting on patterns in your JS code, and +is part of the automated tests run by Jenkins. You can run ESLint from the +horizon root directory with ``npm run lint``, or alternatively on a specific +directory or file with ``eslint file.js``. + +Horizon includes a `.eslintrc` in its root directory, that is used by the +local tests. An explanation of the options, and details of others you may want +to use, can be found in the +`ESLint user guide `_. + +.. _js_file_structure: + +File Structure +============== + +Each component should have its own folder, with the code broken up into one JS +component per file. (See `Single Responsibility `_ +in the style guide). +Each folder may include styling (``.scss``), as well as templates(``.html``) +and tests (``.spec.js``). +You may also include examples, by appending ``.example``. + +Reusable components are in ``horizon/static/framework/``. These are a +collection of pieces, such as modals or wizards where the functionality +is likely to be used across many parts of horizon. +When adding code to horizon, consider whether it is panel-specific or should be +broken out as a reusable utility or widget. + +Panel-specific code is in ``openstack_dashboard/static/dashboard/``. + +The modal directive is a good example of the file structure. This is a reusable +component: +:: + + horizon/static/framework/widgets/modal/ + ├── modal.controller.js + ├── modal.factory.js + ├── modal.module.js + ├── modal.spec.js + └── simple-modal.html + +For larger components, such as workflows with multiple steps, consider breaking +the code down further. The Angular **Launch Instance** workflow, +for example, has one directory per step +(``openstack_dashboard/static/dashboard/launch-instance/``) + +Testing +======= + +1. Open /jasmine in a browser. The development server can be run + with``./run_tests.sh --runserver`` from the horizon root directory. +2. ``npm run test`` from the horizon root directory. + +For more detailed information, see :doc:`javascript_testing`. + +Translation (Internationalization and Localization) +=================================================== + +.. Note:: + This is likely to change soon, after the + `Angular Translation `_ + blueprint has been completed. + +Translations are handled in Transifex, as with Django. They are merged daily +with the horizon upstream codebase. See +`Translations `_ in the +OpenStack wiki to learn more about this process. + +Use either ``gettext`` (singular) or ``ngettext`` (plural): +:: + + gettext('text to be translated'); + ngettext('text to be translated'); + +The :ref:`translatability` section contains information about the +pseudo translation tool, and how to make sure your translations are working +locally. + +Creating your own panel +======================= + +.. Note:: + This section will be extended as standard practices are adopted upstream. + Currently, it may be useful to use + `this patch `_ and its dependants + as an example. + +.. Note:: + File inclusion is likely to be automated soon, after this + `blueprint `_ + is completed. + +This section serves as a basic introduction to writing your own panel for +horizon, using AngularJS. A panel may be included with the plugin system, or it may be +part of the upstream horizon project. + +Upstream +-------- + +If you are adding a panel to horizon, add the relevant ``.js`` and ``.spec.js`` +files to one of the dashboards in ``openstack_dashboard/enabled/``. +An example can be found at ``openstack_dashboard/enabled/_10_project.py``: +:: + + LAUNCH_INST = 'dashboard/launch-instance/' + + ADD_JS_FILES = [ + ... + LAUNCH_INST + 'launch-instance.js', + LAUNCH_INST + 'launch-instance.model.js', + LAUNCH_INST + 'source/source.js', + LAUNCH_INST + 'flavor/flavor.js', + ... + ] + + ADD_JS_SPEC_FILES = [ + ... + LAUNCH_INST + 'launch-instance.spec.js', + LAUNCH_INST + 'launch-instance.model.spec.js', + LAUNCH_INST + 'source/source.spec.js', + LAUNCH_INST + 'flavor/flavor.spec.js', + ... + ] + +Plugins +------- + +Add a new panel/ panel group/ dashboard (See :doc:`tutorial`). Add your files +to the relevant arrays in your new enabled files: +:: + + ADD_JS_FILES = [ + ... + 'path_to/my_angular_code.js', + ... + ] + + ADD_JS_SPEC_FILES = [ + ... + 'path_to/my_angular_code.spec.js', + ... + ] diff --git a/doc/source/topics/javascript_testing.rst b/doc/source/topics/javascript_testing.rst new file mode 100644 index 0000000000..f5f8dae739 --- /dev/null +++ b/doc/source/topics/javascript_testing.rst @@ -0,0 +1,294 @@ +================== +JavaScript Testing +================== + +There are multiple components in our JavaScript testing framework: + * `Jasmine`_ is our testing framework, so this defines the syntax and file + structure we use to test our JavaScript. + * `Karma`_ is our test runner. Amongst other things, this lets us run the + tests against multiple browsers and generate test coverage reports. + Alternatively, tests can be run inside the browser with the Jasmine spec + runner. + * `PhantomJS`_ provides a headless WebKit (the browser engine). This gives us + native support for many web features without relying on specific browsers + being installed. + * `ESLint`_ is a pluggable code linting utilty. This will catch small errors + and inconsistencies in your JS, which may lead to bigger issues later on. + See :ref:`js_code_style` for more detail. + +Jasmine uses specs (``.spec.js``) which are kept with the JavaScript files +that they are testing. See the :ref:`js_file_structure` section or the `Examples`_ +below for more detail on this. + +.. _Jasmine: https://jasmine.github.io/2.3/introduction.html +.. _Karma: https://karma-runner.github.io/ +.. _PhantomJS: http://phantomjs.org/ +.. _ESLint: http://eslint.org/ + +Running Tests +============= + +Tests can be run in two ways: + + 1. Open /jasmine in a browser. The development server can be + run with ``./run_tests.sh --runserver`` from the horizon root directory. + 2. ``npm run test`` from the horizon root directory. This runs Karma, + so it will run all the tests against PhantomJS and generate coverage + reports. + +The code linting job can be run with ``npm run lint``. + +Coverage Reports +---------------- + +Our Karma setup includes a plugin to generate test coverage reports. When +developing, be sure to check the coverage reports on the master branch and +compare your development branch; this will help identify missing tests. + +To generate coverage reports, run ``npm run test``. The coverage reports can be +found at ``horizon/.coverage-karma/`` (framework tests) and +``openstack_dashboard/.coverage-karma/`` (dashboard tests). Load +``/index.html`` in a browser to view the reports. + +Writing Tests +============= + +.. Note:: + File inclusion is likely to be automated soon, after this + `blueprint `_ + is completed. + +Jasmine uses suites and specs: + * Suites begin with a call to ``describe``, which takes two parameters; a + string and a function. The string is a name or title for the spec suite, + whilst the function is a block that implements the suite. + * Specs begin with a call to ``it``, which also takes a string and a function + as parameters. The string is a name or title, whilst the function is a + block with one or more expectations (``expect``) that test the state of + the code. An expectation in Jasmine is an assertion that is either true or + false; every expectation in a spec must be true for the spec to pass. + +Horizon Tests +------------- + +Horizon tests are included in +``horizon/test/jasmine/jasmine_tests.py``. + +Add your test to the ``specs`` array, code sources to the ``dashboard_sources`` +array, and any templates to the ``externalTemplates`` array. Horizon tests +cover reusable components, as well as api functionality, whilst dashboard +tests cover specific panels and their logic. The tests themselves are kept in +the same directory as the implementation they are testing. + +OpenStack Dashboard Tests +------------------------- + +Dashboard tests are included in the relevant dashboard enabled file, such as +``openstack_dashboard/enabled/_10_project.py``. + +Add your tests to the ``ADD_JS_SPEC_FILES`` array. + +Examples +======== + +.. Note:: + The code below is just for example purposes, and may not be current in + horizon. Ellipses (...) are used to represent code that has been + removed for the sake of brevity. + +Example 1 - A reusable component in the **horizon** directory +------------------------------------------------------------- + +File tree: +:: + + horizon/static/framework/widgets/modal + ├── modal.controller.js + ├── modal.factory.js + ├── modal.module.js + └── modal.spec.js + +Lines added to ``horizon/test/jasmine/jasmine_tests.py``: +:: + + class ServicesTests(test.JasmineTests): + sources = [ + ... + 'framework/widgets/modal/modal.module.js', + 'framework/widgets/modal/modal.controller.js', + 'framework/widgets/modal/modal.factory.js', + ... + ] + + specs = [ + ... + 'framework/widgets/modal/modal.spec.js', + ... + ] + +``modal.spec.js``: +:: + + ... + + (function() { + "use strict"; + + describe('horizon.framework.widgets.modal module', function() { + + beforeEach(module('horizon.framework.widgets.modal')); + + describe('simpleModalCtrl', function() { + var scope; + var modalInstance; + var context; + var ctrl; + + beforeEach(inject(function($controller) { + scope = {}; + modalInstance = { + close: function() {}, + dismiss: function() {} + }; + context = { what: 'is it' }; + ctrl = $controller('simpleModalCtrl', { + $scope: scope, + $modalInstance: modalInstance, + context: context + }); + })); + + it('establishes a controller', function() { + expect(ctrl).toBeDefined(); + }); + + it('sets context on the scope', function() { + expect(scope.context).toBeDefined(); + expect(scope.context).toEqual({ what: 'is it' }); + }); + + it('sets action functions', function() { + expect(scope.submit).toBeDefined(); + expect(scope.cancel).toBeDefined(); + }); + + it('makes submit close the modal instance', function() { + expect(scope.submit).toBeDefined(); + spyOn(modalInstance, 'close'); + scope.submit(); + expect(modalInstance.close.calls.count()).toBe(1); + }); + + it('makes cancel close the modal instance', function() { + expect(scope.cancel).toBeDefined(); + spyOn(modalInstance, 'dismiss'); + scope.cancel(); + expect(modalInstance.dismiss).toHaveBeenCalledWith('cancel'); + }); + }); + + ... + + }); + })(); + +Example 2 - Panel-specific code in the **openstack_dashboard** directory +------------------------------------------------------------------------ + +File tree: +:: + + openstack_dashboard/static/dashboard/launch-instance/network/ + ├── network.help.html + ├── network.html + ├── network.js + ├── network.scss + └── network.spec.js + + +Lines added to ``openstack_dashboard/enabled/_10_project.py``: +:: + + LAUNCH_INST = 'dashboard/launch-instance/' + + ADD_JS_FILES = [ + ... + LAUNCH_INST + 'network/network.js', + ... + ] + + ADD_JS_SPEC_FILES = [ + ... + LAUNCH_INST + 'network/network.spec.js', + ... + ] + +``network.spec.js``: +:: + + ... + + (function(){ + 'use strict'; + + describe('Launch Instance Network Step', function() { + + describe('LaunchInstanceNetworkCtrl', function() { + var scope; + var ctrl; + + beforeEach(module('hz.dashboard.launch-instance')); + + beforeEach(inject(function($controller) { + scope = { + model: { + newInstanceSpec: {networks: ['net-a']}, + networks: ['net-a', 'net-b'] + } + }; + ctrl = $controller('LaunchInstanceNetworkCtrl', {$scope:scope}); + })); + + it('has correct network statuses', function() { + expect(ctrl.networkStatuses).toBeDefined(); + expect(ctrl.networkStatuses.ACTIVE).toBeDefined(); + expect(ctrl.networkStatuses.DOWN).toBeDefined(); + expect(Object.keys(ctrl.networkStatuses).length).toBe(2); + }); + + it('has correct network admin states', function() { + expect(ctrl.networkAdminStates).toBeDefined(); + expect(ctrl.networkAdminStates.UP).toBeDefined(); + expect(ctrl.networkAdminStates.DOWN).toBeDefined(); + expect(Object.keys(ctrl.networkStatuses).length).toBe(2); + }); + + it('defines a multiple-allocation table', function() { + expect(ctrl.tableLimits).toBeDefined(); + expect(ctrl.tableLimits.maxAllocation).toBe(-1); + }); + + it('contains its own labels', function() { + expect(ctrl.label).toBeDefined(); + expect(Object.keys(ctrl.label).length).toBeGreaterThan(0); + }); + + it('contains help text for the table', function() { + expect(ctrl.tableHelpText).toBeDefined(); + expect(ctrl.tableHelpText.allocHelpText).toBeDefined(); + expect(ctrl.tableHelpText.availHelpText).toBeDefined(); + }); + + it('uses scope to set table data', function() { + expect(ctrl.tableDataMulti).toBeDefined(); + expect(ctrl.tableDataMulti.available).toEqual(['net-a', 'net-b']); + expect(ctrl.tableDataMulti.allocated).toEqual(['net-a']); + expect(ctrl.tableDataMulti.displayedAllocated).toEqual([]); + expect(ctrl.tableDataMulti.displayedAvailable).toEqual([]); + }); + }); + + ... + + }); + })();