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([]);
+ });
+ });
+
+ ...
+
+ });
+ })();