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:
parent
bc3e3b6934
commit
8d2792f19f
@ -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
|
||||
---
|
||||
|
||||
|
@ -78,6 +78,8 @@ the following topic guides.
|
||||
topics/policy
|
||||
topics/testing
|
||||
topics/table_actions
|
||||
topics/angularjs
|
||||
topics/javascript_testing
|
||||
|
||||
API Reference
|
||||
-------------
|
||||
|
@ -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
|
||||
--------------
|
||||
|
||||
|
180
doc/source/topics/angularjs.rst
Normal file
180
doc/source/topics/angularjs.rst
Normal 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',
|
||||
...
|
||||
]
|
294
doc/source/topics/javascript_testing.rst
Normal file
294
doc/source/topics/javascript_testing.rst
Normal 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([]);
|
||||
});
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
});
|
||||
})();
|
Loading…
Reference in New Issue
Block a user