AngularJS in Horizon Documentation

This patch is a first pass at reducing the levels of "tribal knowledge"
and making the AngularJS codebase more accessible. It details code
style, file structures, testing and translation.

Change-Id: I22e6e5627216739fc92a5c9a5b417c6c6b16476d
Closes-Bug: 1373310
This commit is contained in:
Rob Cresswell 2015-05-11 08:28:57 +01:00
parent bc3e3b6934
commit 8d2792f19f
5 changed files with 478 additions and 35 deletions

View File

@ -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 a piece of code, it's polite (though not required) to thank them in your
commit message. commit message.
.. _translatability:
Translatability Translatability
=============== ===============
Horizon gets translated into multiple languages. The pseudo translation tool Horizon gets translated into multiple languages. The pseudo translation tool
@ -432,32 +434,6 @@ Required
$window.gettext('translatable text'); $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 CSS
--- ---

View File

@ -78,6 +78,8 @@ the following topic guides.
topics/policy topics/policy
topics/testing topics/testing
topics/table_actions topics/table_actions
topics/angularjs
topics/javascript_testing
API Reference API Reference
------------- -------------

View File

@ -189,15 +189,6 @@ For more detailed code analysis you can run::
The output will be saved in ``./pylint.txt``. 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 Tab Characters
-------------- --------------

View File

@ -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 <https://nodejs.org/download/>`_ or
`via a package manager <https://github.com/joyent/node/wiki/Installing-Node.js-via-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 <http://eslint.org/docs/user-guide/configuring>`_.
.. _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 <https://github.com/johnpapa/angular-styleguide#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 <dev_server_ip>/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 <https://blueprints.launchpad.net/horizon/+spec/angular-translate-makemessages>`_
blueprint has been completed.
Translations are handled in Transifex, as with Django. They are merged daily
with the horizon upstream codebase. See
`Translations <https://wiki.openstack.org/wiki/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 <https://review.openstack.org/#/c/190852/>`_ and its dependants
as an example.
.. Note::
File inclusion is likely to be automated soon, after this
`blueprint <https://blueprints.launchpad.net/horizon/+spec/auto-js-file-finding>`_
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',
...
]

View File

@ -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 <dev_server_ip>/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
``<browser>/index.html`` in a browser to view the reports.
Writing Tests
=============
.. Note::
File inclusion is likely to be automated soon, after this
`blueprint <https://blueprints.launchpad.net/horizon/+spec/auto-js-file-finding>`_
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([]);
});
});
...
});
})();