GUI -- Added keyBindings() and gestureNotes() to Key Service.
- Cleaned up fn.js and added contains().
- Unit tests added too.

Change-Id: Id310675836e592af7a4a763f6624c0ee31adfbf5
diff --git a/web/gui/src/main/webapp/app/fw/lib/fn.js b/web/gui/src/main/webapp/app/fw/lib/fn.js
index 15a8843..5269d4d 100644
--- a/web/gui/src/main/webapp/app/fw/lib/fn.js
+++ b/web/gui/src/main/webapp/app/fw/lib/fn.js
@@ -22,20 +22,33 @@
 (function (onos) {
     'use strict';
 
+    function isF(f) {
+        return $.isFunction(f) ? f : null;
+    }
+
+    function isA(a) {
+        return $.isArray(a) ? a : null;
+    }
+
+    function isS(s) {
+        return typeof s === 'string' ? s : null;
+    }
+
+    function isO(o) {
+        return $.isPlainObject(o) ? o : null;
+    }
+
+    function contains(a, x) {
+        return isA(a) && a.indexOf(x) > -1;
+    }
+
     onos.factory('FnService', [function () {
         return {
-            isF: function (f) {
-                return $.isFunction(f) ? f : null;
-            },
-            isA: function (a) {
-                return $.isArray(a) ? a : null;
-            },
-            isS: function (s) {
-                return typeof s === 'string' ? s : null;
-            },
-            isO: function (o) {
-                return $.isPlainObject(o) ? o : null;
-            }
+            isF: isF,
+            isA: isA,
+            isS: isS,
+            isO: isO,
+            contains: contains
         };
     }]);
 
diff --git a/web/gui/src/main/webapp/app/fw/lib/keys.js b/web/gui/src/main/webapp/app/fw/lib/keys.js
index c5f0a19..f4318df 100644
--- a/web/gui/src/main/webapp/app/fw/lib/keys.js
+++ b/web/gui/src/main/webapp/app/fw/lib/keys.js
@@ -144,6 +144,45 @@
         return true;
     }
 
+    function setKeyBindings(keyArg) {
+        var viewKeys,
+            masked = [];
+
+        if (f.isF(keyArg)) {
+            // set general key handler callback
+            keyHandler.viewFn = keyArg;
+        } else {
+            // set specific key filter map
+            viewKeys = d3.map(keyArg).keys();
+            viewKeys.forEach(function (key) {
+                if (keyHandler.maskedKeys[key]) {
+                    masked.push('  Key "' + key + '" is reserved');
+                }
+            });
+
+            if (masked.length) {
+                // TODO: use alert service
+                window.alert('WARNING...\n\nsetKeys():\n' + masked.join('\n'));
+            }
+            keyHandler.viewKeys = keyArg;
+        }
+    }
+
+    function getKeyBindings() {
+        var gkeys = d3.map(keyHandler.globalKeys).keys(),
+            masked = d3.map(keyHandler.maskedKeys).keys(),
+            vkeys = d3.map(keyHandler.viewKeys).keys(),
+            vfn = !!f.isF(keyHandler.viewFn);
+
+        return {
+            globalKeys: gkeys,
+            maskedKeys: masked,
+            viewKeys: vkeys,
+            viewFunction: vfn
+        };
+    }
+
+    // TODO: inject alert service
     onos.factory('KeyService', ['FnService', function (fs) {
         f = fs;
         return {
@@ -154,7 +193,20 @@
             theme: function () {
                 return theme;
             },
-            whatKey: whatKey
+            keyBindings: function (x) {
+                if (x === undefined) {
+                    return getKeyBindings();
+                } else {
+                    setKeyBindings(x);
+                }
+            },
+            gestureNotes: function (g) {
+                if (g === undefined) {
+                    return keyHandler.viewGestures;
+                } else {
+                    keyHandler.viewGestures = f.isA(g) || [];
+                }
+            }
         };
     }]);
 
diff --git a/web/gui/src/main/webapp/tests/fw/lib/fn-spec.js b/web/gui/src/main/webapp/tests/fw/lib/fn-spec.js
index 4bd1b4a..44c12b5 100644
--- a/web/gui/src/main/webapp/tests/fw/lib/fn-spec.js
+++ b/web/gui/src/main/webapp/tests/fw/lib/fn-spec.js
@@ -26,7 +26,8 @@
         someObject = { foo: 'bar'},
         someNumber = 42,
         someString = 'xyyzy',
-        someDate = new Date();
+        someDate = new Date(),
+        stringArray = ['foo', 'bar'];
 
     beforeEach(module('onosApp'));
 
@@ -149,4 +150,20 @@
     it('isO(): the reference for object', function () {
         expect(fs.isO(someObject)).toBe(someObject);
     });
+
+    // === Tests for contains()
+    it('contains(): false for improper args', function () {
+        expect(fs.contains()).toBeFalsy();
+    });
+    it('contains(): false for non-array', function () {
+        expect(fs.contains(null, 1)).toBeFalsy();
+    });
+    it ('contains(): true for contained item', function () {
+        expect(fs.contains(someArray, 1)).toBeTruthy();
+        expect(fs.contains(stringArray, 'bar')).toBeTruthy();
+    });
+    it ('contains(): false for non-contained item', function () {
+        expect(fs.contains(someArray, 109)).toBeFalsy();
+        expect(fs.contains(stringArray, 'zonko')).toBeFalsy();
+    });
 });
diff --git a/web/gui/src/main/webapp/tests/fw/lib/keys-spec.js b/web/gui/src/main/webapp/tests/fw/lib/keys-spec.js
index 7d53c23..d8fa818 100644
--- a/web/gui/src/main/webapp/tests/fw/lib/keys-spec.js
+++ b/web/gui/src/main/webapp/tests/fw/lib/keys-spec.js
@@ -20,9 +20,7 @@
  @author Simon Hunt
  */
 describe('factory: fw/lib/keys.js', function() {
-    var ks,
-        fs,
-        d3Elem;
+    var ks, fs, d3Elem, elem, last;
 
     beforeEach(module('onosApp'));
 
@@ -30,7 +28,14 @@
         ks = KeyService;
         fs = FnService;
         d3Elem = d3.select('body').append('p').attr('id', 'ptest');
+        elem = d3Elem.node();
         ks.installOn(d3Elem);
+        last = {
+            view: null,
+            key: null,
+            code: null,
+            ev: null
+        };
     }));
 
     afterEach(function () {
@@ -74,45 +79,120 @@
         element.dispatchEvent(ev);
     }
 
+    // === Theme related tests
     it('should start in light theme', function () {
         expect(ks.theme()).toEqual('light');
     });
     it('should toggle to dark theme', function () {
-        jsKeyDown(d3Elem.node(), 84); // 'T'
+        jsKeyDown(elem, 84); // 'T'
         expect(ks.theme()).toEqual('dark');
     });
 
-    // key code lookups
-    // NOTE: should be injecting keydown events, rather than exposing whatKey()
-    it('whatKey: 13', function () {
-        expect(ks.whatKey(13)).toEqual('enter');
+    // === Key binding related tests
+    it('should start with default key bindings', function () {
+        var state = ks.keyBindings(),
+            gk = state.globalKeys,
+            mk = state.maskedKeys,
+            vk = state.viewKeys,
+            vf = state.viewFunction;
+
+        expect(gk.length).toEqual(4);
+        ['backSlash', 'slash', 'esc', 'T'].forEach(function (k) {
+            expect(fs.contains(gk, k)).toBeTruthy();
+        });
+
+        expect(mk.length).toEqual(3);
+        ['backSlash', 'slash', 'T'].forEach(function (k) {
+            expect(fs.contains(mk, k)).toBeTruthy();
+        });
+
+        expect(vk.length).toEqual(0);
+        expect(vf).toBeFalsy();
     });
-    it('whatKey: 16', function () {
-        expect(ks.whatKey(16)).toEqual('shift');
+
+    function bindTestKeys(withDescs) {
+        var keys = ['A', '1', 'F5', 'equals'],
+            kb = {};
+
+        function cb(view, key, code, ev) {
+            last.view = view;
+            last.key = key;
+            last.code = code;
+            last.ev = ev;
+        }
+
+        function bind(k) {
+            return withDescs ? [cb, 'desc for key ' + k] : cb;
+        }
+
+        keys.forEach(function (k) {
+            kb[k] = bind(k);
+        });
+
+        ks.keyBindings(kb);
+    }
+
+    function verifyCall(key, code) {
+        // TODO: update expectation, when view tokens are implemented
+        expect(last.view).toEqual('NotYetAViewToken');
+        last.view = null;
+
+        expect(last.key).toEqual(key);
+        last.key = null;
+
+        expect(last.code).toEqual(code);
+        last.code = null;
+
+        expect(last.ev).toBeTruthy();
+        last.ev = null;
+    }
+
+    function verifyNoCall() {
+        expect(last.view).toBeNull();
+        expect(last.key).toBeNull();
+        expect(last.code).toBeNull();
+        expect(last.ev).toBeNull();
+    }
+
+    function verifyTestKeys() {
+        jsKeyDown(elem, 65); // 'A'
+        verifyCall('A', 65);
+        jsKeyDown(elem, 66); // 'B'
+        verifyNoCall();
+
+        jsKeyDown(elem, 49); // '1'
+        verifyCall('1', 49);
+        jsKeyDown(elem, 50); // '2'
+        verifyNoCall();
+
+        jsKeyDown(elem, 116); // 'F5'
+        verifyCall('F5', 116);
+        jsKeyDown(elem, 117); // 'F6'
+        verifyNoCall();
+
+        jsKeyDown(elem, 187); // 'equals'
+        verifyCall('equals', 187);
+        jsKeyDown(elem, 189); // 'dash'
+        verifyNoCall();
+
+        var vk = ks.keyBindings().viewKeys;
+
+        expect(vk.length).toEqual(4);
+        ['A', '1', 'F5', 'equals'].forEach(function (k) {
+            expect(fs.contains(vk, k)).toBeTruthy();
+        });
+
+        expect(ks.keyBindings().viewFunction).toBeFalsy();
+    }
+
+    it('should allow specific key bindings', function () {
+        bindTestKeys();
+        verifyTestKeys();
     });
-    it('whatKey: 40', function () {
-        expect(ks.whatKey(40)).toEqual('downArrow');
-    });
-    it('whatKey: 65', function () {
-        expect(ks.whatKey(65)).toEqual('A');
-    });
-    it('whatKey: 84', function () {
-        expect(ks.whatKey(84)).toEqual('T');
-    });
-    it('whatKey: 49', function () {
-        expect(ks.whatKey(49)).toEqual('1');
-    });
-    it('whatKey: 55', function () {
-        expect(ks.whatKey(55)).toEqual('7');
-    });
-    it('whatKey: 112', function () {
-        expect(ks.whatKey(112)).toEqual('F1');
-    });
-    it('whatKey: 123', function () {
-        expect(ks.whatKey(123)).toEqual('F12');
-    });
-    it('whatKey: 1', function () {
-        expect(ks.whatKey(1)).toEqual('.');
+
+    it('should allow specific key bindings with descriptions', function () {
+        bindTestKeys(true);
+        verifyTestKeys();
     });
 
 });