GUI -- Button and Toolbar Services with unit tests - finished implementing button widget, started modifying toolbar to use ButtonService

Change-Id: I96078516d396951175fb8d9e69dd7796da9f114e
diff --git a/web/gui/src/main/webapp/app/fw/widget/button.js b/web/gui/src/main/webapp/app/fw/widget/button.js
index 4530bb6..5eccc9a 100644
--- a/web/gui/src/main/webapp/app/fw/widget/button.js
+++ b/web/gui/src/main/webapp/app/fw/widget/button.js
@@ -33,13 +33,12 @@
     }
 
     function button(div, id, gid, cb, tooltip) {
-        if (!div) {
+        if (!div || div.empty()) {
             $log.warn('Button cannot append to div');
             return null;
         }
 
         var btnDiv = createDiv(div, 'btn', id),
-            svg = btnDiv.append('svg'),
             cbFnc = fs.isF(cb) || noop;
 
         is.loadIcon(btnDiv, gid, btnSize);
@@ -53,36 +52,81 @@
         }
     }
 
-    function toggle(div, id, gid, cb, tooltip) {
-        if (!div) {
+    function toggle(div, id, gid, initState, cb, tooltip) {
+        if (!div || div.empty()) {
             $log.warn('Toggle cannot append to div');
             return null;
         }
 
-        var sel = false,
+        var sel = !!initState,
             togDiv = createDiv(div, 'tog', id),
-            svg = togDiv.append('svg'),
             cbFnc = fs.isF(cb) || noop;
 
         is.loadIcon(togDiv, gid, btnSize);
 
+        function _toggle(b) {
+            if (b === undefined) {
+                sel = !sel;
+            } else {
+                sel = !!b;
+            }
+            cbFnc(sel);
+        }
+
+        togDiv.on('click', _toggle);
+
         return {
             id: id,
             el: togDiv,
             selected: function () { return sel; },
-            toggle: function (b) {
-                if (b === undefined) {
-                    sel = !sel;
-                } else {
-                    sel = !!b;
-                }
-                cbFnc(sel);
-            }
+            toggle: _toggle
         }
     }
 
     function radioSet(div, id, rset) {
-        return {}
+        if (!div || div.empty()) {
+            $log.warn('Radio buttons cannot append to div');
+            return null;
+        }
+        if (!fs.isA(rset)) {
+            $log.warn('Radio button set is not an array');
+            return null;
+        }
+        if (rset.length === 0) {
+            $log.warn('Cannot create radio button set from empty array');
+            return null;
+        }
+        var rDiv = div.append('div').classed('rset', true),
+            sel = 0,
+            rads = [];
+
+        rset.forEach(function (btn, index) {
+            var rid = {id: id + '-' + index},
+                rbtn = angular.extend({}, btn, rid),
+                istate = (index === 0),
+                rtog = toggle(rDiv, rbtn.id, rbtn.gid, istate,
+                    rbtn.cb, rbtn.tooltip);
+
+            rtog.el = (rtog.el).classed('tog', false).classed('rad', true);
+            rads.push(rtog);
+        });
+
+        return {
+            rads: rads,
+            selected: function (i) {
+                if (i === undefined) { return sel; }
+                else if (i < 0 || i >= rads.length) {
+                    $log.error('Cannot select radio button of index ' + i);
+                }
+                else {
+                    if (i !== sel) {
+                        rads[sel].toggle(false);
+                        rads[i].toggle(true);
+                        sel = i;
+                    }
+                }
+            }
+        }
     }
 
     angular.module('onosWidget')
diff --git a/web/gui/src/main/webapp/app/fw/widget/toolbar.js b/web/gui/src/main/webapp/app/fw/widget/toolbar.js
index 682a5ae..7740ccd 100644
--- a/web/gui/src/main/webapp/app/fw/widget/toolbar.js
+++ b/web/gui/src/main/webapp/app/fw/widget/toolbar.js
@@ -20,14 +20,20 @@
 (function () {
     'use strict';
 
-    var $log, ps;
+    var $log, fs, ps, bns;
 
     var toolBtnIds = {},
         toolbarPanel,
         toolbarDiv;
 
+    var ids = [],
+        tbarId,
+        tbarPanel,
+        tbarDiv;
+
     function init() {
         toolBtnIds = {};
+        ids = [];
     }
 
     function addButton(btn) {
@@ -90,6 +96,76 @@
         };
     }
 
+    function validId(id, caller) {
+        if (fs.inArray(id, ids) !== -1) {
+            $log.warn(caller + ': ID already exists');
+            return false;
+        }
+        return true;
+    }
+
+    function addButton1(id, gid, cb, tooltip) {
+        var btnId = tbarId + '-' + id;
+        if (!validId(btnId, 'addButton')) {
+            return null;
+        }
+        ids.push(btnId);
+        return bns.button(tbarDiv, btnId, gid, cb, tooltip);
+    }
+
+    function addToggle1(id, gid, initState, cb, tooltip) {
+        var togId = tbarId + '-' + id;
+        if (!validId(togId, 'addToggle')) {
+            return null;
+        }
+        ids.push(togId);
+        return bns.toggle(tbarDiv, togId, gid, initState, cb, tooltip);
+    }
+
+    function addRadioSet(id, rset) {
+        var radId = tbarId + '-' + id;
+        if (!validId(radId, 'addRadioSet')) {
+            return null;
+        }
+        ids.push(radId);
+        return bns.radioSet(tbarDiv, radId, rset);
+    }
+
+    // TODO: finish this and remove unneeded code
+    function addSeparator1() {
+
+    }
+
+    function createToolbar1(id, settings) {
+        if (!id) {
+            $log.warn('createToolbar: no ID given');
+            return null;
+        }
+        tbarId = 'tbar-' + id;
+        var opts = fs.isO(settings) || {}; // default settings should be put here
+
+        if (!validId(tbarId, 'createToolbar')) {
+            return null;
+        }
+        ids.push(tbarId);
+
+        tbarPanel = ps.createPanel(tbarId, opts);
+        tbarDiv = tbarPanel.classed('toolbar', true);
+
+        // TODO: change names of functions
+        return {
+            addButton1: addButton1,
+            addToggle1: addToggle1,
+            addRadioSet: addRadioSet,
+            addSeparator1: addSeparator1
+        }
+    }
+
+    // function currently not working
+    function destroyToolbar(id) {
+        ps.destroyPanel(id);
+    }
+
     function createToolbar(tbarId, tools) {
         var api;
 
@@ -143,10 +219,13 @@
     }
 
     angular.module('onosWidget')
-        .factory('ToolbarService', ['$log', 'PanelService',
-            function (_$log_, _ps_) {
+        .factory('ToolbarService', ['$log', 'FnService',
+            'PanelService', 'ButtonService',
+            function (_$log_, _fs_, _ps_, _bns_) {
                 $log = _$log_;
+                fs = _fs_;
                 ps = _ps_;
+                bns = _bns_;
 
                 return {
                     init: init,
@@ -154,7 +233,9 @@
                     makeToggle: makeToggle,
                     makeRadio: makeRadio,
                     separator: separator,
-                    createToolbar: createToolbar
+                    createToolbar: createToolbar,
+                    createToolbar1: createToolbar1,
+                    destroyToolbar: destroyToolbar
                 };
             }]);
 
diff --git a/web/gui/src/main/webapp/tests/app/fw/widget/button-spec.js b/web/gui/src/main/webapp/tests/app/fw/widget/button-spec.js
index f07664c..b0251c1 100644
--- a/web/gui/src/main/webapp/tests/app/fw/widget/button-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/widget/button-spec.js
@@ -83,8 +83,16 @@
         expect(count).toBe(0);
     });
 
+    it('should not append toggle to an undefined div', function () {
+        spyOn($log, 'warn');
+        expect(bns.toggle(undefined, 'id', 'gid', false,
+            function () {})).toBeNull();
+        expect($log.warn).toHaveBeenCalledWith('Toggle cannot append to div');
+    });
+
     it('should verify toggle glyph', function () {
-        var tog = bns.toggle(d3Elem, 'tbar0-tog-0', 'crown', function () {});
+        var tog = bns.toggle(d3Elem, 'tbar0-tog-0', 'crown',
+            false, function () {});
         expect((tog.el).classed('tog')).toBeTruthy();
         expect((tog.el).attr('id')).toBe('tbar0-tog-0');
         expect((tog.el).select('svg')).toBeTruthy();
@@ -116,10 +124,139 @@
         expect(tog.selected()).toBe(false);
     });
 
-    it('should not append toggle to an undefined div', function () {
-        spyOn($log, 'warn');
-        expect(bns.toggle(undefined, 'id', 'gid', function () {})).toBeNull();
-        expect($log.warn).toHaveBeenCalledWith('Toggle cannot append to div');
+    it('should verity toggle initial state', function () {
+        var tog = bns.toggle(d3Elem, 'id', 'gid', true);
+        expect(tog.selected()).toBe(true);
+        tog = bns.toggle(d3Elem, 'id', 'gid', false);
+        expect(tog.selected()).toBe(false);
+        tog = bns.toggle(d3Elem, 'id', 'gid', '');
+        expect(tog.selected()).toBe(false);
+        tog = bns.toggle(d3Elem, 'id', 'gid', 'something');
+        expect(tog.selected()).toBe(true);
     });
 
+    it('should not append radio button set to an undefined div', function () {
+        spyOn($log, 'warn');
+        expect(bns.radioSet(undefined, 'id', [])).toBeNull();
+        expect($log.warn).toHaveBeenCalledWith('Radio buttons cannot append ' +
+                                                'to div');
+    });
+
+    it('should not create radio button set from a non-array', function () {
+        var rads = {test: 'test'};
+        spyOn($log, 'warn');
+
+        expect(bns.radioSet(d3Elem, 'test', rads)).toBeNull();
+        expect($log.warn).toHaveBeenCalledWith('Radio button set is not ' +
+                                                'an array');
+        rads = 'rads';
+        expect(bns.radioSet(d3Elem, 'test', rads)).toBeNull();
+        expect($log.warn).toHaveBeenCalledWith('Radio button set is not ' +
+                                                'an array');
+        rads = {arr: [1, 2, 3]};
+        expect(bns.radioSet(d3Elem, 'test', rads)).toBeNull();
+        expect($log.warn).toHaveBeenCalledWith('Radio button set is not ' +
+                                                'an array');
+    });
+
+    it('should not create radio button set from empty array', function () {
+        var rads = [];
+        spyOn($log, 'warn');
+        expect(bns.radioSet(d3Elem, 'test', rads)).toBeNull();
+        expect($log.warn).toHaveBeenCalledWith('Cannot create radio button ' +
+                                                'set from empty array');
+    });
+
+    it('should verify radio button glyph structure', function () {
+        var rads = [
+            { gid: 'crown', cb: function () {}, tooltip: 'n/a'}
+        ], rdiv;
+
+        spyOn($log, 'warn');
+        expect(bns.radioSet(d3Elem, 'test', rads)).toBeTruthy();
+        expect($log.warn).not.toHaveBeenCalled();
+
+        rdiv = d3Elem.select('div');
+        expect(rdiv.classed('rset')).toBe(true);
+        expect(rdiv.select('div').classed('rad')).toBe(true);
+        expect(rdiv.select('div').classed('tog')).toBe(false);
+        expect(rdiv.select('div').attr('id')).toBe('test-0');
+        expect(rdiv.select('div').select('svg')).toBeTruthy();
+        expect(rdiv.select('use').classed('glyph')).toBeTruthy();
+        expect(rdiv.select('use').attr('xlink:href')).toBe('#crown');
+    });
+
+    it('should verify more than one radio button glyph was added', function () {
+        var rads = [
+                { gid: 'crown', cb: function () {}, tooltip: 'n/a'},
+                { gid: 'router', cb: function () {}, tooltip: 'n/a'}
+        ], rdiv;
+
+        expect(bns.radioSet(d3Elem, 'test', rads)).toBeTruthy();
+        rdiv = d3Elem.select('div');
+        expect(rdiv.select('#test-0')).toBeTruthy();
+        expect(rdiv.select('#test-1')).toBeTruthy();
+
+        expect(rdiv.select('#test-0')
+            .select('use')
+            .classed('glyph'))
+            .toBeTruthy();
+        expect(rdiv.select('#test-0')
+            .select('use')
+            .attr('xlink:href'))
+            .toBe('#crown');
+
+        expect(rdiv.select('#test-1')
+            .select('use')
+            .classed('glyph'))
+            .toBeTruthy();
+        expect(rdiv.select('#test-1')
+            .select('use')
+            .attr('xlink:href'))
+            .toBe('#router');
+    });
+
+    it('should select the correct radio button', function () {
+        var count0 = 0,
+            count1 = 9;
+        function cb0() { count0++; }
+        function cb1() { count1++; }
+
+        var rads = [
+            { gid: 'crown', cb: cb0, tooltip: 'n/a'},
+            { gid: 'router', cb: cb1, tooltip: 'n/a'}
+            ],
+            rset = bns.radioSet(d3Elem, 'test', rads);
+        spyOn($log, 'error');
+
+        expect(rset.selected()).toBe(0);
+        expect(count0).toBe(0);
+        expect(count1).toBe(9);
+        rset.selected(0);
+        expect(rset.selected()).toBe(0);
+        expect(count0).toBe(0);
+        expect(count1).toBe(9);
+
+        rset.selected(1);
+        expect(rset.selected()).toBe(1);
+        expect(count0).toBe(1);
+        expect(count1).toBe(10);
+
+        rset.selected(-1);
+        expect($log.error).toHaveBeenCalledWith('Cannot select radio button ' +
+                                                'of index -1');
+        expect(rset.selected()).toBe(1);
+        expect(count0).toBe(1);
+        expect(count1).toBe(10);
+
+        rset.selected(66);
+        expect($log.error).toHaveBeenCalledWith('Cannot select radio button ' +
+                                                'of index 66');
+        expect(rset.selected()).toBe(1);
+        expect(count0).toBe(1);
+        expect(count1).toBe(10);
+    });
+
+    // TODO: figure out how to trigger d3 onclick for buttons and toggles
+
 });
diff --git a/web/gui/src/main/webapp/tests/app/fw/widget/toolbar-spec.js b/web/gui/src/main/webapp/tests/app/fw/widget/toolbar-spec.js
index 30eec65..3bca9e7 100644
--- a/web/gui/src/main/webapp/tests/app/fw/widget/toolbar-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/widget/toolbar-spec.js
@@ -18,19 +18,23 @@
  ONOS GUI -- Widget -- Toolbar Service - Unit Tests
  */
 describe('factory: fw/widget/toolbar.js', function () {
-    var $log, fs, tbs, ps,
+    var $log, fs, tbs, ps, bns, is,
         d3Elem;
 
-    beforeEach(module('onosWidget', 'onosUtil', 'onosLayer'));
+    beforeEach(module('onosWidget', 'onosUtil', 'onosLayer', 'onosSvg'));
 
-    beforeEach(inject(function (_$log_, FnService,
-                                ToolbarService, PanelService) {
+    beforeEach(inject(function (_$log_, FnService, ToolbarService,
+                                PanelService, ButtonService, IconService) {
         $log = _$log_;
         fs = FnService;
         tbs = ToolbarService;
         ps = PanelService;
+        bns = ButtonService;
+        is = IconService;
     }));
 
+    // TODO: figure out solution for calling tests with new info instead of calling init
+
     beforeEach(function () {
         d3Elem = d3.select('body').append('div').attr('id', 'floatpanels');
         tbs.init();
@@ -50,7 +54,7 @@
     it('should define api functions', function () {
         expect(fs.areFunctions(tbs, [
             'init', 'makeButton', 'makeToggle', 'makeRadio', 'separator',
-            'createToolbar'
+            'createToolbar', 'createToolbar1', 'destroyToolbar'
         ])).toBeTruthy();
     });
 
@@ -264,4 +268,86 @@
         expect(sepDiv.style('border-style')).toBe('solid');
     });
 
+    // ==== new Toolbar Unit tests --------------------------------------------
+
+    it('should warn if createToolbar id is invalid', function () {
+        spyOn($log, 'warn');
+        expect(tbs.createToolbar1()).toBeNull();
+        expect($log.warn).toHaveBeenCalledWith('createToolbar: no ID given');
+
+        expect(tbs.createToolbar1('test')).toBeTruthy();
+        expect(tbs.createToolbar1('test')).toBeNull();
+        expect($log.warn).toHaveBeenCalledWith('createToolbar: ID already exists');
+    });
+
+    it('should create an unpopulated toolbar', function () {
+        spyOn($log, 'warn');
+        expect(tbs.createToolbar1('test')).toBeTruthy();
+        expect($log.warn).not.toHaveBeenCalled();
+    });
+
+    it('should create a button', function () {
+        spyOn($log, 'warn');
+        var toolbar = tbs.createToolbar1('test'),
+            btn = toolbar.addButton1('btn0', 'gid', function () {});
+        expect(btn).not.toBeNull();
+        expect(btn.id).toBe('tbar-test-btn0');
+        expect($log.warn).not.toHaveBeenCalled();
+    });
+
+    it('should not create a button with a duplicate id', function () {
+        spyOn($log, 'warn');
+        var toolbar = tbs.createToolbar1('test'),
+            btn = toolbar.addButton1('btn0', 'gid', function () {}),
+            btn1 = toolbar.addButton1('btn0', 'gid', function () {});
+        expect(btn).not.toBeNull();
+        expect(btn.id).toBe('tbar-test-btn0');
+        expect($log.warn).toHaveBeenCalledWith('addButton: ID already exists');
+        expect(btn1).toBeNull();
+    });
+
+    it('should create a toggle', function () {
+        spyOn($log, 'warn');
+        var toolbar = tbs.createToolbar1('test'),
+            tog = toolbar.addButton1('tog0', 'gid', false, function () {});
+        expect(tog).not.toBeNull();
+        expect(tog.id).toBe('tbar-test-tog0');
+        expect($log.warn).not.toHaveBeenCalled();
+    });
+
+    it('should not create a toggle with a duplicate id', function () {
+        spyOn($log, 'warn');
+        var toolbar = tbs.createToolbar1('test'),
+            tog = toolbar.addToggle1('tog0', 'gid', false, function () {}),
+            tog1 = toolbar.addToggle1('tog0', 'gid', true, function () {});
+        expect(tog).not.toBeNull();
+        expect(tog.id).toBe('tbar-test-tog0');
+        expect($log.warn).toHaveBeenCalledWith('addToggle: ID already exists');
+        expect(tog1).toBeNull();
+    });
+
+
+    it('should create a radio button set', function () {
+        spyOn($log, 'warn');
+        var toolbar = tbs.createToolbar1('test'),
+            rset = [
+                { gid: 'crown', cb: function () {}, tooltip: 'nothing' },
+                { gid: 'bird', cb: function () {}, tooltip: 'nothing' }
+            ],
+            rad = toolbar.addRadioSet('rad0', rset);
+        expect(rad).not.toBeNull();
+        expect(rad.rads[0].id).toBe('tbar-test-rad0-0');
+        expect(rad.rads[1].id).toBe('tbar-test-rad0-1');
+        expect($log.warn).not.toHaveBeenCalled();
+    });
+
+    //it('should not append to a destroyed toolbar', function () {
+    //    spyOn($log, 'warn');
+    //    var toolbar = tbs.createToolbar1('test');
+    //    expect(toolbar).not.toBeNull();
+    //    tbs.destroyToolbar('tbar-test');
+    //    expect(toolbar.addButton1('btn', 'gid', function () {})).toBeNull();
+    //    expect($log.warn).toHaveBeenCalledWith('Button cannot append to div');
+    //});
+
 });