ONOS-1842 - GUI -- Tables have better resizing behavior -- column headers stay above their columns and table cell text is constrained to one size.

Change-Id: I89ca7d25d46d895c78c41b8250ce40408fbaba85
diff --git a/web/gui/src/main/webapp/tests/app/fw/widget/table-spec.js b/web/gui/src/main/webapp/tests/app/fw/widget/table-spec.js
index 693f42d..6df87b3 100644
--- a/web/gui/src/main/webapp/tests/app/fw/widget/table-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/widget/table-spec.js
@@ -20,71 +20,59 @@
 describe('factory: fw/widget/table.js', function () {
     var $log, $compile, $rootScope,
         fs, ts, mast, is,
-        scope, compiled,
-        table, thead, tbody, mockHeader,
-        mockH2Height = 20,
-        mockMastHeight = 20,
-        mockHeaderHeight = mockH2Height + mockMastHeight,
-        tableIconTdSize = 100,
-        numTestElems = 4;
+        scope,
+        containerDiv,
+        headerDiv, bodyDiv,
+        header, body,
+        mockHeader,
+        mockHeaderHeight = 40;
 
     var onosFixedHeaderTags =
-            '<table onos-fixed-header>' +
-            '<thead style="height:27px;">' +
-            '<tr>' +
-            '<th></th>' +
-            '<th>Device ID </th>' +
-            '<th col-width="100px">H/W Version </th>' +
-            '<th>S/W Version </th>' +
-            '</tr>' +
-            '</thead>' +
-            '<tbody>' +
-            '<tr>' +
-            '<td colspan="4">' +
-                'No Devices found' +
-            '</td>' +
-            '</tr>' +
-            '<tr>' +
-            '<td class="table-icon">' +
-                '<div icon icon-id="{{dev._iconid_available}}">' +
-                '</div>' +
-            '</td>' +
-            '<td>Some ID</td>' +
-            '<td>Some HW</td>' +
-            '<td>Some Software</td>' +
-            '</tr>' +
-            '</tbody>' +
-            '</table>',
+            '<div class="summary-list" onos-fixed-header>' +
+            '<div class="table-header">' +
+                '<table>' +
+                    '<tr>' +
+                        '<td colId="type" class="table-icon"></td>' +
+                        '<td colId="id">Host ID </td>' +
+                        '<td colId="mac" sortable>MAC Address </td>' +
+                        '<td colId="location" col-width="110px">Location </td>' +
+                    '</tr>' +
+                '</table>' +
+            '</div>' +
+
+            '<div class="table-body">' +
+                '<table>' +
+                    '<tr class="ignore-width">' +
+                        '<td class="not-picked"></td>' +
+                    '</tr>' +
+                    '<tr>' +
+                        '<td class="table-icon">Some Icon</td>' +
+                        '<td>Some ID</td>' +
+                        '<td>Some MAC Address</td>' +
+                        '<td>Some Location</td>' +
+                    '</tr>' +
+                '</table>' +
+            '</div>' +
+            '</div>',
 
         onosSortableHeaderTags =
-            '<table onos-sortable-header ' +
+            '<div onos-sortable-header ' +
             'sort-callback="sortCallback(requestParams)">' +
-            '<thead>' +
-            '<tr>' +
-            '<th colId="available"></th>' +
-            '<th colId="id" sortable>Device ID </th>' +
-            '<th colId="hw" sortable>H/W Version </th>' +
-            '<th colId="sw" sortable>S/W Version </th>' +
-            '</tr>' +
-            '</thead>' +
-            '<tbody>' +
-            '<tr>' +
-            '<td>' +
-                '<div icon icon-id="{{dev._iconid_available}}">' +
-                '</div>' +
-            '</td>' +
-            '<td>Some ID</td>' +
-            '<td>Some HW</td>' +
-            '<td>Some Software</td>' +
-            '</tr>' +
-            '</tbody>' +
-            '</table>';
+                '<table>' +
+                    '<tr>' +
+                        '<td colId="type"></td>' +
+                        '<td colId="id" sortable>Host ID </td>' +
+                        '<td colId="mac" sortable>MAC Address </td>' +
+                        '<td colId="location" sortable>Location </td>' +
+                    '</tr>' +
+                '</table>' +
+            '</div>';
 
     beforeEach(module('onosWidget', 'onosUtil', 'onosMast', 'onosSvg'));
 
     var mockWindow = {
-        innerWidth: 400,
-        innerHeight: 200,
+        innerWidth: 600,
+        innerHeight: 400,
         navigator: {
             userAgent: 'defaultUA'
         },
@@ -111,23 +99,44 @@
 
     beforeEach(function () {
         scope = $rootScope.$new();
+        scope.tableData = [];
     });
 
+    // Note: dummy header so that d3 doesn't trip up.
+    //       $compile has to be used on the directive tag element, so it can't
+    //       be included in the tag strings declared above.
     beforeEach(function () {
         mockHeader = d3.select('body')
             .append('h2')
             .classed('tabular-header', true)
-            .style('height', mockHeaderHeight + 'px')
-            .html('Some Header');
+            .style({
+                height: mockHeaderHeight + 'px',
+                margin: 0,
+                padding: 0
+            })
+            .text('Some Header');
     });
 
     afterEach(function () {
-        table = null;
-        thead = null;
-        tbody = null;
+        containerDiv = undefined;
+        headerDiv = undefined;
+        bodyDiv = undefined;
+        header = undefined;
+        body = undefined;
         mockHeader.remove();
     });
 
+    function populateTableData() {
+        scope.tableData = [
+            {
+                type: 'endstation',
+                id: '1234',
+                mac: '00:00:03',
+                location: 'USA'
+            }
+        ];
+    }
+
     it('should define TableBuilderService', function () {
         expect(ts).toBeDefined();
     });
@@ -138,83 +147,119 @@
         ])).toBeTruthy();
     });
 
-    function compileTable() {
-        compiled = $compile(table);
+    function compile(elem) {
+        var compiled = $compile(elem);
         compiled(scope);
         scope.$digest();
     }
 
-    function verifyGivenTags(dirName) {
-        expect(table).toBeDefined();
-        expect(table.attr(dirName)).toBe('');
+    function selectTables() {
+        expect(containerDiv.find('div').length).toBe(2);
 
-        thead = table.find('thead');
-        expect(thead).toBeDefined();
-        tbody = table.find('tbody');
-        expect(tbody).toBeDefined();
+        headerDiv = angular.element(containerDiv[0].querySelector('.table-header'));
+        expect(headerDiv.length).toBe(1);
+
+        bodyDiv = angular.element(containerDiv[0].querySelector('.table-body'));
+        expect(bodyDiv.length).toBe(1);
+
+        header = headerDiv.find('table');
+        expect(header.length).toBe(1);
+
+        body = bodyDiv.find('table');
+        expect(body.length).toBe(1);
     }
 
-    function verifyCssDisplay() {
-        var padding = 21, // bottom table constant 12 + mastPadding(?) 9
-            tableHeight = fs.windowSize(mockHeaderHeight).height -
-                (fs.noPx(table.find('thead').css('height')) + padding);
+    function verifyGivenTags(dirName, div) {
+        expect(div).toBeDefined();
+        expect(div.attr(dirName)).toBe('');
+    }
 
-        expect(thead.css('display')).toBe('block');
-        expect(tbody.css('display')).toBe('block');
-        expect(tbody.css('height')).toBe(tableHeight + 'px');
-        expect(tbody.css('overflow')).toBe('auto');
+    function verifyDefaultSize() {
+        expect(header.css('width')).toBe('570px');
+        expect(body.css('width')).toBe('570px');
+    }
 
-        // TODO: investigate why math for calculating the height works better
-        // in the browser window (thead height is 0 in this test?)
+    function verifyHeight() {
+        var padding = 12,
+            mastHeight = 36,
+            tableHeight = (mockWindow.innerHeight - mockHeaderHeight) -
+                (fs.noPx(headerDiv.css('height')) + mastHeight + padding);
+
+        expect(bodyDiv.css('height')).toBe(tableHeight + 'px');
     }
 
     function verifyColWidth() {
-        var winWidth = fs.windowSize().width,
-            colWidth, thElems, tr, tdElem;
+        var hdrs = header.find('td'),
+            cols = body.find('td');
 
-        colWidth = Math.floor(winWidth / numTestElems);
+        expect(angular.element(hdrs[0]).css('width')).toBe('33px');
+        expect(angular.element(hdrs[3]).css('width')).toBe('110px');
 
-        thElems = thead.find('th');
+        expect(angular.element(cols[1]).css('width')).toBe('33px');
+        expect(angular.element(cols[4]).css('width')).toBe('110px');
+    }
 
-        angular.forEach(thElems, function (thElem, i) {
-            thElem = angular.element(thElems[i]);
-            tr = angular.element(tbody.find('tr').eq(1));
-            tdElem = angular.element(tr.find('td').eq(i));
-            var custWidth = thElem.attr('col-width');
+    function verifyCallbacks(h) {
+        expect(scope.sortCallback).not.toHaveBeenCalled();
 
-            if (custWidth) {
-                expect(thElem.css('width')).toBe(custWidth);
-                expect(tdElem.css('width')).toBe(custWidth);
-            } else if (tdElem.attr('class') === 'table-icon') {
-                expect(thElem.css('width')).toBe(tableIconTdSize + 'px');
-                expect(tdElem.css('width')).toBe(tableIconTdSize + 'px');
-            } else {
-                expect(thElem.css('width')).toBe(colWidth + 'px');
-                expect(tdElem.css('width')).toBe(colWidth + 'px');
-            }
+        h[0].click();
+        expect(scope.sortCallback).not.toHaveBeenCalled();
+
+        h[1].click();
+        expect(scope.sortCallback).toHaveBeenCalledWith({
+            sortCol: 'id',
+            sortDir: 'asc'
+        });
+        h[1].click();
+        expect(scope.sortCallback).toHaveBeenCalledWith({
+            sortCol: 'id',
+            sortDir: 'desc'
+        });
+        h[1].click();
+        expect(scope.sortCallback).toHaveBeenCalledWith({
+            sortCol: 'id',
+            sortDir: 'asc'
+        });
+
+        h[2].click();
+        expect(scope.sortCallback).toHaveBeenCalledWith({
+            sortCol: 'mac',
+            sortDir: 'asc'
+        });
+        h[2].click();
+        expect(scope.sortCallback).toHaveBeenCalledWith({
+            sortCol: 'mac',
+            sortDir: 'desc'
+        });
+        h[2].click();
+        expect(scope.sortCallback).toHaveBeenCalledWith({
+            sortCol: 'mac',
+            sortDir: 'asc'
+        });
+
+        h[3].click();
+        expect(scope.sortCallback).toHaveBeenCalledWith({
+            sortCol: 'location',
+            sortDir: 'asc'
+        });
+        h[3].click();
+        expect(scope.sortCallback).toHaveBeenCalledWith({
+            sortCol: 'location',
+            sortDir: 'desc'
+        });
+        h[3].click();
+        expect(scope.sortCallback).toHaveBeenCalledWith({
+            sortCol: 'location',
+            sortDir: 'asc'
         });
     }
 
-    function verifyCallbacks(thElems) {
-        expect(scope.sortCallback).not.toHaveBeenCalled();
+    function verifyIcons(h) {
+        var currH, div;
 
-        // first test header has no 'sortable' attr
-        thElems[0].click();
-        expect(scope.sortCallback).not.toHaveBeenCalled();
-
-        // the other headers have 'sortable'
-        for(var i = 1; i < numTestElems; i += 1) {
-            thElems[i].click();
-            expect(scope.sortCallback).toHaveBeenCalled();
-        }
-    }
-
-    function verifyIcons(thElems) {
-        var currentTh, div;
-        // make sure it has the correct icon after clicking
-        thElems[1].click();
-        currentTh = angular.element(thElems[1]);
-        div = currentTh.find('div');
+        h[1].click();
+        currH = angular.element(h[1]);
+        div = currH.find('div');
         expect(div.html()).toBe(
             '<svg class="embeddedIcon" width="10" height="10" viewBox="0 0 ' +
             '50 50"><g class="icon upArrow"><rect width="50" height="50" ' +
@@ -222,8 +267,8 @@
             'xlink="http://www.w3.org/1999/xlink" xlink:href="#triangleUp">' +
             '</use></g></svg>'
         );
-        thElems[1].click();
-        div = currentTh.find('div');
+        h[1].click();
+        div = currH.find('div');
         expect(div.html()).toBe(
             '<svg class="embeddedIcon" width="10" height="10" viewBox="0 0 ' +
             '50 50"><g class="icon downArrow"><rect width="50" height="50" ' +
@@ -232,14 +277,14 @@
             '</use></g></svg>'
         );
 
-        thElems[2].click();
-        div = currentTh.children();
+        h[2].click();
+        div = currH.children();
         // clicked on a new element, so the previous icon should have been deleted
         expect(div.html()).toBeFalsy();
 
         // the new element should have the ascending icon
-        currentTh = angular.element(thElems[2]);
-        div = currentTh.children();
+        currH = angular.element(h[2]);
+        div = currH.children();
         expect(div.html()).toBe(
             '<svg class="embeddedIcon" width="10" height="10" viewBox="0 0 ' +
             '50 50"><g class="icon upArrow"><rect width="50" height="50" ' +
@@ -250,31 +295,42 @@
     }
 
     it('should affirm that onos-fixed-header is working', function () {
-        table = angular.element(onosFixedHeaderTags);
+        containerDiv = angular.element(onosFixedHeaderTags);
 
-        compileTable();
-        verifyGivenTags('onos-fixed-header');
+        compile(containerDiv);
 
-        // table will not be fixed unless it receives the 'LastElement' event
+        verifyGivenTags('onos-fixed-header', containerDiv);
+        selectTables();
+        verifyDefaultSize();
+
+        populateTableData();
+
         scope.$emit('LastElement');
         scope.$digest();
 
-        verifyCssDisplay();
+        verifyHeight();
+        verifyColWidth();
+
+        mockWindow.innerHeight = 300;
+        scope.$digest();
+        verifyHeight();
+
+        mockWindow.innerWidth = 500;
+        scope.$digest();
         verifyColWidth();
     });
 
     it('should affirm that onos-sortable-header is working', function () {
-        var thElems;
-        table = angular.element(onosSortableHeaderTags);
+        headerDiv = angular.element(onosSortableHeaderTags);
 
-        compileTable();
-        verifyGivenTags('onos-sortable-header');
+        compile(headerDiv);
+        verifyGivenTags('onos-sortable-header', headerDiv);
 
         scope.sortCallback = jasmine.createSpy('sortCallback');
 
-        thElems = thead.find('th');
-        verifyCallbacks(thElems);
-        verifyIcons(thElems);
+        header = headerDiv.find('td');
+        verifyCallbacks(header);
+        verifyIcons(header);
     });
 
     // Note: testing resetSortIcons isn't feasible because due to the nature of