← Back to team overview

yellow team mailing list archive

[Merge] lp:~benji/juju-gui/bug-1074336 into lp:juju-gui

 

Benji York has proposed merging lp:~benji/juju-gui/bug-1074336 into lp:juju-gui.

Requested reviews:
  Juju GUI Hackers (juju-gui)

For more details, see:
https://code.launchpad.net/~benji/juju-gui/bug-1074336/+merge/135261

refactored manageUnitsMixin and added tests

Full details:

- refactored manageUnitsMixin and added tests for it
- added documentation for manageUnitsMixin
- reconsidered our object literal style, updated the style guide to reflect my
  current notions, and used the style in the code as a proof of concept
- captured current naming practice in the style guide (camelCase everywhere)
- tweaked all of the tests so the test container element is "display: none"
  when possible and "visibility: hidden" otherwise
- transmuted an "apply" to a slightly simpler "call"

https://codereview.appspot.com/6845065/

-- 
https://code.launchpad.net/~benji/juju-gui/bug-1074336/+merge/135261
Your team Juju GUI Hackers is requested to review the proposed merge of lp:~benji/juju-gui/bug-1074336 into lp:juju-gui.
=== modified file 'app/store/env.js'
--- app/store/env.js	2012-10-15 13:20:38 +0000
+++ app/store/env.js	2012-11-20 22:04:20 +0000
@@ -103,7 +103,7 @@
         var tid = msg.request_id;
         if (tid in this._txn_callbacks) {
           console.log('Env: Dispatch Rpc');
-          this._txn_callbacks[tid].apply(this, [msg]);
+          this._txn_callbacks[tid].call(this, msg);
           delete this._txn_callbacks[tid];
         }
       }

=== modified file 'app/views/service.js'
--- app/views/service.js	2012-11-08 16:23:16 +0000
+++ app/views/service.js	2012-11-20 22:04:20 +0000
@@ -15,61 +15,118 @@
     // Mixin attributes
     events: {
       '#num-service-units': {
-        keydown: 'modifyUnits',
-        blur: 'resetUnits'
+        keydown: 'handleKeyEvent',
+        blur: 'resetUnitInputBox'
       }
     },
 
-    resetUnits: function() {
-      var container = this.get('container'),
-          field = container.one('#num-service-units');
+    /**
+     * Retrieve the input box that holds the user-specified number of units.
+     *
+     * @return {Object} An input box node.
+     */
+    _getInputBox: function() {
+      return this.get('container').one('#num-service-units');
+    },
+
+    /**
+     * Overwrite the number of units in the input field to the real number of
+     * units contained in the model and enable the input field.
+     *
+     * @method resetUnitInputBox
+     * @return {undefined} Side effects only.
+     */
+    resetUnitInputBox: function() {
+      var field = this._getInputBox();
       field.set('value', this.get('model').get('unit_count'));
       field.set('disabled', false);
     },
 
-    modifyUnits: function(ev) {
-      if (ev.keyCode !== ESC && ev.keyCode !== ENTER) {
-        return;
-      }
-      var container = this.get('container'),
-          field = container.one('#num-service-units');
-
-      if (ev.keyCode === ESC) {
-        this.resetUnits();
-      }
-      if (ev.keyCode !== ENTER) { // If not Enter keyup...
-        return;
-      }
-      ev.halt(true);
-
-      if (/^\d+$/.test(field.get('value'))) {
-        this._modifyUnits(parseInt(field.get('value'), 10));
-      } else {
-        this.resetUnits();
-      }
-    },
-
+    /**
+     * Interpret a key pressed inside the requested number of units field.
+     *
+     * @param {Number} keyCode The key that was pressed.
+     * @return {Boolean} True if the key press event should be halted.
+     */
+    _handleKeyPress: function(keyCode, fieldValue) {
+      if (keyCode === ESC) {
+        this.resetUnitInputBox();
+        return false;
+      } else if (keyCode === ENTER) {
+        var numberRegex = /^\d+$/;
+        if (numberRegex.test(fieldValue)) {
+          this._modifyUnits(parseInt(fieldValue, 10));
+        } else {
+          // The user entered something crazy.  Reset the input box.
+          this.resetUnitInputBox();
+        }
+        return true;
+      }
+      return false;
+    },
+
+    /**
+     * Extract the relevant information from a key press event and call the key
+     * press handler.
+     *
+     * @param {Object} ev The event.
+     * @return {undefined} Side effects only.
+     */
+    handleKeyEvent: function(ev) {
+      var field = this._getInputBox();
+      ev.halt(this._handleKeyPress(ev.keyCode, field.get('value')));
+    },
+
+    /**
+     * Tell the backend that we want a new number of units for a service.
+     *
+     * @method _modifyUnits
+     * @param {Number} requested_unit_count The number of units the user wants.
+     * @return {undefined} Side effects only.
+     */
     _modifyUnits: function(requested_unit_count) {
       var service = this.get('model'),
           unit_count = service.get('unit_count'),
-          field = this.get('container').one('#num-service-units'),
-          env = this.get('env');
-
+          field = this._getInputBox(),
+          env = this.get('env'),
+          db = this.get('db');
       if (requested_unit_count < 1) {
         console.log('You must have at least one unit');
         field.set('value', unit_count);
         return;
       }
-
+      // We do not want the user changing the requested value while a request
+      // is outstanding.
+      field.set('disabled', true);
       var delta = requested_unit_count - unit_count;
+      this._requestUnitDelta(delta, service);
+    },
+
+    /**
+     * Ask the backend to add or remove a given number of units.
+     *
+     * @param {Number} delta The number of units to add or remove.
+     * @param {String} service The service for which units should be added or
+     *   removed.
+     * @param {Function} getUnits A function that returns all the units for a
+     *   given service.
+     * @param {Function} addUnits A function that will add units for a service.
+     * @param {Function} removeUnits A function that will remove units for a
+     *   service.
+     * @return {undefined} Side effects only.
+     */
+    _requestUnitDelta: function(delta, service) {
+      var env = this.get('env'),
+          db = this.get('db');
       if (delta > 0) {
-        // Add units!
+        // We need more units.
         env.add_unit(
             service.get('id'), delta,
             Y.bind(this._addUnitCallback, this));
       } else if (delta < 0) {
+        // We need fewer units.
         delta = Math.abs(delta);
-        var units = this.get('db').units.get_units_for_service(service),
+        var units = db.units.get_units_for_service(service),
             unit_ids_to_remove = [];
 
         for (var i = units.length - 1;
@@ -82,7 +139,6 @@
             Y.bind(this._removeUnitCallback, this)
         );
       }
-      field.set('disabled', true);
     },
 
     _addUnitCallback: function(ev) {
@@ -868,6 +924,7 @@
       });
 
   views.service = ServiceView;
+  views.unitCountMixin = manageUnitsMixin;
 
 }, '0.1.0', {
   requires: ['panel',

=== modified file 'docs/style-guide.rst'
--- docs/style-guide.rst	2012-11-07 18:41:11 +0000
+++ docs/style-guide.rst	2012-11-20 22:04:20 +0000
@@ -15,6 +15,23 @@
 indention will be applied.
 
 
+Variable declaration
+====================
+
+Since global variables kill kittens, we use "var" when introducing a new
+variable.  Our beautifier does not handle more than one variable
+declaration per "var" very well, so each variable should have its own,
+like so:
+
+    var a = 1;
+    var b = 2;
+
+Not like this:
+
+    var a = 1,
+        b = 2;
+
+
 For loops
 =========
 
@@ -25,8 +42,16 @@
 Whitespace
 ==========
 
-No trailing whitespace on lines or at the end of the file (i.e., the file
-should end with a non-blank line).
+No trailing whitespace on lines or at the end of the file (i.e., the
+file should end with a non-blank line).  Look at the file using xxd if
+you are unsure.
+
+
+Naming
+======
+
+Class names should be (upper) CamelCase, method, attribute, and variable
+names should be (lower) camelCase.
 
 
 Object literal formatting
@@ -34,12 +59,18 @@
 
 Things you should do:
 
-- object literals have their opening brace on a new line and the first
-  value (if any) is on the same line (i.e., the opening brace is not on
-  a line by itself)
-- there is a space after the opening brace of an object literal
-- object literals have their closing brace on the same line as their
-  last value (i.e., the closing brace is not on a line by itself)
+- object literals have their opening brace on the same line as the
+  equals sign (e.g., "var foo = {"
+- multi-line object literals have a newline after the opening brace
+- multi-line object literals have their closing brace on a line by
+  itself (like functions)
+- nested object literals may have their closing brace on the same line
+  (like functions); e.g.:
+
+    var a = {
+      b: {
+        c: 42
+      }};
 
 Things the beautifier should do (given an input line with no indentation
 at all):
@@ -47,14 +78,12 @@
 - all of the keys of an object literal line up (the second and
   subsequent key names are indented 2 spaces more than the indentation
   of the opening brace of the object literal
-- lines after a line that ends in an equals sign are indented 4 (more)
-  spaces
-- lines after a line that ends in a colon are indented 6 (more) spaces
+- lines after a line that ends in a brace are indented 2 (more) spaces
 
 An example::
 
-    var values =
-        { int_good: '1',
+    var values = {
+        int_good: '1',
           int_bad: 'nope!'},
         schema =
         { int_good:

=== modified file 'test/index.html'
--- test/index.html	2012-11-20 15:35:30 +0000
+++ test/index.html	2012-11-20 22:04:20 +0000
@@ -33,8 +33,12 @@
   <script src="test_endpoints.js"></script>
   <script src="test_application_notifications.js"></script>
   <script src="test_charm_store.js"></script>
+<<<<<<< TREE
   <script src="test_app_hotkeys.js"></script>
   <script src="test_notifier_widget.js"></script>
+=======
+  <script src="test_manageUnitsMixin.js"></script>
+>>>>>>> MERGE-SOURCE
 
   <script>
   YUI().use('node', 'event', function(Y) {

=== modified file 'test/test_app.js'
--- test/test_app.js	2012-11-14 14:34:31 +0000
+++ test/test_app.js	2012-11-20 22:04:20 +0000
@@ -132,7 +132,10 @@
 
   before(function(done) {
     Y = YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils'], function(Y) {
-      container = Y.Node.create('<div id="test" class="container"></div>');
+      container = Y.Node.create('<div/>')
+        .addClass('container')
+        .hide();
+      container.hide();
       done();
         });
 
@@ -193,7 +196,9 @@
     env = new Y.juju.Environment({conn: conn});
     env.connect();
     conn.open();
-    container = Y.Node.create('<div id="test" class="container"></div>');
+    container = Y.Node.create('<div/>')
+      .addClass('container')
+      .hide();
     data = [];
     app = new Y.juju.App(
         { container: container,

=== modified file 'test/test_application_notifications.js'
--- test/test_application_notifications.js	2012-11-19 16:36:12 +0000
+++ test/test_application_notifications.js	2012-11-20 22:04:20 +0000
@@ -39,7 +39,7 @@
       'juju-tests-utils',
       'node-event-simulate'],
     function(Y) {
-      applicationContainer = Y.Node.create('<div id="test-container" />');
+      applicationContainer = Y.Node.create('<div/>').hide();
       applicationContainer.appendTo(Y.one('body'));
 
       notificationsContainer = Y.Node.create('<div id="notifications" />');

=== modified file 'test/test_charm_collection_view.js'
--- test/test_charm_collection_view.js	2012-09-26 23:31:13 +0000
+++ test/test_charm_collection_view.js	2012-11-20 22:04:20 +0000
@@ -105,7 +105,7 @@
 
     // Ensure the search results are rendered inside the container.
     it('must correctly render the search results', function() {
-      var container = Y.Node.create('<div id="test-container" />');
+      var container = Y.Node.create('<div/>').hide();
       var MyView = Y.Base.create('MyView', CharmCollectionView, [], {
         // Overriding to check the results as they are rendered in
         // the container. Subclassing is required because render() is

=== modified file 'test/test_charm_view.js'
--- test/test_charm_view.js	2012-11-07 12:32:38 +0000
+++ test/test_charm_view.js	2012-11-20 22:04:20 +0000
@@ -54,7 +54,7 @@
               created: 1340206387.539},
         proof: {}};
 
-      container = Y.Node.create('<div id="test-container" />');
+      container = Y.Node.create('<div/>').hide();
       Y.one('#main').append(container);
       CharmView = juju.views.charm;
       // Use a local charm store.

=== modified file 'test/test_environment_view.js'
--- test/test_environment_view.js	2012-11-15 20:48:30 +0000
+++ test/test_environment_view.js	2012-11-20 22:04:20 +0000
@@ -103,8 +103,15 @@
     });
 
     beforeEach(function(done) {
+<<<<<<< TREE
       container = Y.Node.create('<div id="test-container" />');
       Y.one('body').prepend(container);
+=======
+      container = Y.Node.create('<div/>')
+        .set('id', 'test-container')
+        .setStyle('visibility', 'hidden');
+      Y.one('body').append(container);
+>>>>>>> MERGE-SOURCE
       db = new models.Database();
       db.on_delta({data: environment_delta});
       done();

=== added file 'test/test_manageUnitsMixin.js'
--- test/test_manageUnitsMixin.js	1970-01-01 00:00:00 +0000
+++ test/test_manageUnitsMixin.js	2012-11-20 22:04:20 +0000
@@ -0,0 +1,142 @@
+'use strict';
+
+(function() {
+  describe('unit management mixin', function() {
+    var views, Y, container, mixin, ENTER, ESC;
+
+    before(function(done) {
+      Y = YUI(GlobalConfig).use(
+          'juju-views', 'juju-models', 'base', 'node', 'json-parse',
+          'juju-env', 'node-event-simulate', 'juju-tests-utils',
+          function(Y) {
+            views = Y.namespace('juju.views');
+            mixin = views.unitCountMixin;
+            ENTER = Y.Node.DOM_EVENTS.key.eventDef.KEY_MAP.enter;
+            ESC = Y.Node.DOM_EVENTS.key.eventDef.KEY_MAP.esc;
+            done();
+          });
+    });
+
+    after(function(done)  {
+      done();
+    });
+
+    beforeEach(function(done) {
+      container = Y.Node.create('<div id=\"test-container\"/>').hide();
+      Y.one('#main').append(container);
+      done();
+    });
+
+    afterEach(function(done) {
+      container.remove(true);
+      done();
+    });
+
+    it('should be able to get the unit number input box', function() {
+      var fauxField = {};
+      var fauxContainer = {
+        one: function(selector) {
+          assert.equal(selector, '#num-service-units');
+          return fauxField;
+        }};
+      var fauxThis = {
+        container: fauxContainer,
+        get: function(name) {
+          return this[name];
+        }};
+      var returned = mixin._getInputBox.call(fauxThis);
+      assert.strictEqual(returned, fauxField);
+    });
+
+    it('should be able to reset the unit number input box', function() {
+      var unitsRequested;
+      var fieldValues = {};
+      var fauxField = {
+        set: function(name, value) {
+          fieldValues[name] = value;
+        }};
+      var fauxThis = {
+        _getInputBox: function() {
+          return fauxField;
+        },
+        get: function(name) {
+          return this[name];
+        },
+        _modifyUnits: function(n) {
+          unitsRequested = n;
+        }};
+      mixin._handleKeyPress.call(fauxThis, ENTER, '10');
+      assert.equal(unitsRequested, 10);
+    });
+
+    it('should pass key events through to _handleKeyPress', function() {
+      // The handleKeyEvent function just extracts the pertinent details from
+      // the event and the input field and passes them to _handleKeyPress.
+      var fauxEvent = {
+        keyCode: 42,
+        halt: function() {}};
+      var fauxInputBox = {
+        get: function(name) {
+          assert.equal(name, 'value');
+          return '999';
+        }};
+      var passedKeyCode;
+      var passedFieldValue;
+      var fauxThis = {
+        _getInputBox: function() {return fauxInputBox;},
+        _handleKeyPress: function(keyCode, fieldValue) {
+          passedKeyCode = keyCode;
+          passedFieldValue = fieldValue;
+        }};
+      mixin.handleKeyEvent.call(fauxThis, fauxEvent);
+      assert.strictEqual(passedKeyCode, 42);
+      assert.strictEqual(passedFieldValue, '999');
+    });
+
+    it('should have keydown and blur event handlers', function() {
+      assert.isTrue('keydown' in mixin.events['#num-service-units']);
+      assert.isTrue('blur' in mixin.events['#num-service-units']);
+    });
+
+    it('should reset the unit count when the user presses escape', function() {
+      var inputBoxReset = false;
+      var fakeThis = {
+        resetUnitInputBox: function() {
+          inputBoxReset = true;
+        }};
+      mixin._handleKeyPress.call(fakeThis, ESC, 999);
+      assert.isTrue(inputBoxReset);
+    });
+
+    it('should reset the unit count a non-number is entered', function() {
+      var inputBoxReset = false;
+      var fakeThis = {
+        resetUnitInputBox: function() {
+          inputBoxReset = true;
+        }};
+      mixin._handleKeyPress.call(fakeThis, ENTER, 'this is not a number');
+      assert.isTrue(inputBoxReset);
+    });
+
+    it('must change the number of units when a number is entered', function() {
+      var unitsRequested;
+      var fakeThis = {
+        _modifyUnits: function(n) {
+          unitsRequested = n;
+        }};
+      mixin._handleKeyPress.call(fakeThis, ENTER, '10');
+      assert.equal(unitsRequested, 10);
+    });
+
+    it('should be able to apply a delta to the number of units', function() {
+      var unitsRequested;
+      var fakeThis = {
+        _modifyUnits: function(n) {
+          unitsRequested = n;
+        }};
+      mixin._handleKeyPress.call(fakeThis, ENTER, '10');
+      assert.equal(unitsRequested, 10);
+    });
+
+  });
+}) ();

=== modified file 'test/test_service_view.js'
--- test/test_service_view.js	2012-11-08 18:16:07 +0000
+++ test/test_service_view.js	2012-11-20 22:04:20 +0000
@@ -23,7 +23,7 @@
       env = new (Y.namespace('juju')).Environment({conn: conn});
       env.connect();
       conn.open();
-      container = Y.Node.create('<div id="test-container" />');
+      container = Y.Node.create('<div/>').hide();
       Y.one('#main').append(container);
       db = new models.Database();
       charm = new models.Charm({id: 'cs:precise/mysql-7', description: 'A DB'});

=== modified file 'undocumented'
--- undocumented	2012-11-12 15:19:43 +0000
+++ undocumented	2012-11-20 22:04:20 +0000
@@ -151,7 +151,6 @@
 app/views/service.js _addUnitCallback
 app/views/service.js _destroyCallback
 app/views/service.js _exposeServiceCallback
-app/views/service.js _modifyUnits
 app/views/service.js _removeRelationCallback
 app/views/service.js _removeUnitCallback
 app/views/service.js _setConfigCallback
@@ -167,9 +166,7 @@
 app/views/service.js getHeight
 app/views/service.js getServiceTabs
 app/views/service.js initializer
-app/views/service.js modifyUnits
 app/views/service.js render
-app/views/service.js resetUnits
 app/views/service.js saveConfig
 app/views/service.js showErrors
 app/views/service.js unexposeService


Follow ups