Bri Prebilic Cole
Committed by Gerrit Code Review

GUI -- WIP Device View details panel. Egress Links backend added, updated FnServ…

…ice, added CSS for panel, populates panel with properties and a close button.

Change-Id: Ia510b1e47fecc9140adcb1596c365a4114784b88
...@@ -15,12 +15,15 @@ ...@@ -15,12 +15,15 @@
15 */ 15 */
16 package org.onosproject.ui.impl; 16 package org.onosproject.ui.impl;
17 17
18 +import com.fasterxml.jackson.databind.ObjectMapper;
18 import com.fasterxml.jackson.databind.node.ArrayNode; 19 import com.fasterxml.jackson.databind.node.ArrayNode;
19 import com.fasterxml.jackson.databind.node.ObjectNode; 20 import com.fasterxml.jackson.databind.node.ObjectNode;
20 import com.google.common.collect.ImmutableSet; 21 import com.google.common.collect.ImmutableSet;
21 import org.onosproject.mastership.MastershipService; 22 import org.onosproject.mastership.MastershipService;
23 +import org.onosproject.net.ConnectPoint;
22 import org.onosproject.net.Device; 24 import org.onosproject.net.Device;
23 import org.onosproject.net.DeviceId; 25 import org.onosproject.net.DeviceId;
26 +import org.onosproject.net.Link;
24 import org.onosproject.net.Port; 27 import org.onosproject.net.Port;
25 import org.onosproject.net.device.DeviceService; 28 import org.onosproject.net.device.DeviceService;
26 import org.onosproject.net.link.LinkService; 29 import org.onosproject.net.link.LinkService;
...@@ -28,31 +31,61 @@ import org.onosproject.net.link.LinkService; ...@@ -28,31 +31,61 @@ import org.onosproject.net.link.LinkService;
28 import java.util.ArrayList; 31 import java.util.ArrayList;
29 import java.util.Arrays; 32 import java.util.Arrays;
30 import java.util.List; 33 import java.util.List;
31 - 34 +import java.util.Set;
32 -//import org.onosproject.net.Link;
33 -//import java.util.Set;
34 35
35 /** 36 /**
36 * Message handler for device view related messages. 37 * Message handler for device view related messages.
37 */ 38 */
38 public class DeviceViewMessageHandler extends AbstractTabularViewMessageHandler { 39 public class DeviceViewMessageHandler extends AbstractTabularViewMessageHandler {
39 40
41 + private static final String ID = "id";
42 + private static final String TYPE = "type";
43 + private static final String AVAILABLE = "available";
44 + private static final String AVAILABLE_IID = "_iconid_available";
45 + private static final String TYPE_IID = "_iconid_type";
46 + private static final String DEV_ICON_PREFIX = "devIcon_";
47 + private static final String NUM_PORTS = "num_ports";
48 + private static final String LINK_DEST = "elinks_dest";
49 + private static final String MFR = "mfr";
50 + private static final String HW = "hw";
51 + private static final String SW = "sw";
52 + private static final String PROTOCOL = "protocol";
53 + private static final String MASTER_ID = "masterid";
54 + private static final String CHASSIS_ID = "chassisid";
55 + private static final String SERIAL = "serial";
56 + private static final String PORTS = "ports";
57 + private static final String ENABLED = "enabled";
58 + private static final String SPEED = "speed";
59 +
60 + private static final ObjectMapper MAPPER = new ObjectMapper();
61 +
62 +
40 /** 63 /**
41 * Creates a new message handler for the device messages. 64 * Creates a new message handler for the device messages.
42 */ 65 */
43 protected DeviceViewMessageHandler() { 66 protected DeviceViewMessageHandler() {
44 - super(ImmutableSet.of("deviceDataRequest")); 67 + super(ImmutableSet.of("deviceDataRequest", "deviceDetailsRequest"));
45 } 68 }
46 69
47 @Override 70 @Override
48 - public void process(ObjectNode message) { 71 + public void process(ObjectNode event) {
49 - ObjectNode payload = payload(message); 72 + String type = string(event, "event", "unknown");
73 + if (type.equals("deviceDataRequest")) {
74 + dataRequest(event);
75 + } else if (type.equals("deviceDetailsRequest")) {
76 + detailsRequest(event);
77 + }
78 + }
79 +
80 + private void dataRequest(ObjectNode event) {
81 + ObjectNode payload = payload(event);
50 String sortCol = string(payload, "sortCol", "id"); 82 String sortCol = string(payload, "sortCol", "id");
51 String sortDir = string(payload, "sortDir", "asc"); 83 String sortDir = string(payload, "sortDir", "asc");
52 84
53 DeviceService service = get(DeviceService.class); 85 DeviceService service = get(DeviceService.class);
54 MastershipService mastershipService = get(MastershipService.class); 86 MastershipService mastershipService = get(MastershipService.class);
55 LinkService linkService = get(LinkService.class); 87 LinkService linkService = get(LinkService.class);
88 +
56 TableRow[] rows = generateTableRows(service, 89 TableRow[] rows = generateTableRows(service,
57 mastershipService, 90 mastershipService,
58 linkService); 91 linkService);
...@@ -66,6 +99,37 @@ public class DeviceViewMessageHandler extends AbstractTabularViewMessageHandler ...@@ -66,6 +99,37 @@ public class DeviceViewMessageHandler extends AbstractTabularViewMessageHandler
66 connection().sendMessage("deviceDataResponse", 0, rootNode); 99 connection().sendMessage("deviceDataResponse", 0, rootNode);
67 } 100 }
68 101
102 + private void detailsRequest(ObjectNode event) {
103 + ObjectNode payload = payload(event);
104 + String id = string(payload, "id", "of:0000000000000000");
105 +
106 + DeviceId deviceId = DeviceId.deviceId(id);
107 + DeviceService service = get(DeviceService.class);
108 + MastershipService ms = get(MastershipService.class);
109 + Device device = service.getDevice(deviceId);
110 + ObjectNode data = MAPPER.createObjectNode();
111 +
112 + data.put(ID, deviceId.toString());
113 + data.put(TYPE, device.type().toString());
114 + data.put(MFR, device.manufacturer());
115 + data.put(HW, device.hwVersion());
116 + data.put(SW, device.swVersion());
117 + data.put(SERIAL, device.serialNumber());
118 + data.put(CHASSIS_ID, device.chassisId().toString());
119 + data.put(MASTER_ID, ms.getMasterFor(deviceId).toString());
120 + data.put(PROTOCOL, device.annotations().value(PROTOCOL));
121 +
122 + ArrayNode ports = MAPPER.createArrayNode();
123 + for (Port p : service.getPorts(deviceId)) {
124 + ports.add(portData(p, deviceId));
125 + }
126 + data.set(PORTS, ports);
127 +
128 + ObjectNode rootNode = mapper.createObjectNode();
129 + rootNode.set("details", data);
130 + connection().sendMessage("deviceDetailsResponse", 0, rootNode);
131 + }
132 +
69 private TableRow[] generateTableRows(DeviceService service, 133 private TableRow[] generateTableRows(DeviceService service,
70 MastershipService mastershipService, 134 MastershipService mastershipService,
71 LinkService linkService) { 135 LinkService linkService) {
...@@ -79,48 +143,44 @@ public class DeviceViewMessageHandler extends AbstractTabularViewMessageHandler ...@@ -79,48 +143,44 @@ public class DeviceViewMessageHandler extends AbstractTabularViewMessageHandler
79 return list.toArray(new TableRow[list.size()]); 143 return list.toArray(new TableRow[list.size()]);
80 } 144 }
81 145
146 + private ObjectNode portData(Port p, DeviceId id) {
147 + ObjectNode port = MAPPER.createObjectNode();
148 + LinkService ls = get(LinkService.class);
149 +
150 + port.put(ID, p.number().toString());
151 + port.put(TYPE, p.type().toString());
152 + port.put(SPEED, p.portSpeed());
153 + port.put(ENABLED, p.isEnabled());
154 +
155 + Set<Link> links = ls.getEgressLinks(new ConnectPoint(id, p.number()));
156 + if (!links.isEmpty()) {
157 + String egressLinks = "";
158 + for (Link l : links) {
159 + ConnectPoint dest = l.dst();
160 + egressLinks += dest.elementId().toString()
161 + + "/" + dest.port().toString() + " ";
162 + }
163 + port.put(LINK_DEST, egressLinks);
164 + }
165 +
166 + return port;
167 + }
168 +
82 /** 169 /**
83 * TableRow implementation for {@link Device devices}. 170 * TableRow implementation for {@link Device devices}.
84 */ 171 */
85 private static class DeviceTableRow extends AbstractTableRow { 172 private static class DeviceTableRow extends AbstractTableRow {
86 173
87 - private static final String ID = "id";
88 - private static final String AVAILABLE = "available";
89 - private static final String AVAILABLE_IID = "_iconid_available";
90 - private static final String TYPE_IID = "_iconid_type";
91 - private static final String DEV_ICON_PREFIX = "devIcon_";
92 - private static final String NUM_PORTS = "num_ports";
93 - private static final String NUM_EGRESS_LINKS = "num_elinks";
94 - private static final String MFR = "mfr";
95 - private static final String HW = "hw";
96 - private static final String SW = "sw";
97 - private static final String PROTOCOL = "protocol";
98 - private static final String MASTERID = "masterid";
99 - private static final String CHASSISID = "chassisid";
100 - private static final String SERIAL = "serial";
101 -
102 private static final String[] COL_IDS = { 174 private static final String[] COL_IDS = {
103 AVAILABLE, AVAILABLE_IID, TYPE_IID, ID, 175 AVAILABLE, AVAILABLE_IID, TYPE_IID, ID,
104 - NUM_PORTS, NUM_EGRESS_LINKS, MASTERID, MFR, HW, SW, 176 + NUM_PORTS, MASTER_ID, MFR, HW, SW,
105 - PROTOCOL, CHASSISID, SERIAL 177 + PROTOCOL, CHASSIS_ID, SERIAL
106 }; 178 };
107 179
108 private static final String ICON_ID_ONLINE = "deviceOnline"; 180 private static final String ICON_ID_ONLINE = "deviceOnline";
109 private static final String ICON_ID_OFFLINE = "deviceOffline"; 181 private static final String ICON_ID_OFFLINE = "deviceOffline";
110 182
111 // TODO: use in details pane 183 // TODO: use in details pane
112 -// private String getPorts(List<Port> ports) {
113 -// String formattedString = "";
114 -// int numPorts = 0;
115 -//
116 -// for (Port p : ports) {
117 -// numPorts++;
118 -// formattedString += p.number().toString() + ", ";
119 -// }
120 -// return formattedString + "Total: " + numPorts;
121 -// }
122 -
123 - // TODO: use in details pane
124 // private String getEgressLinks(Set<Link> links) { 184 // private String getEgressLinks(Set<Link> links) {
125 // String formattedString = ""; 185 // String formattedString = "";
126 // 186 //
...@@ -139,7 +199,6 @@ public class DeviceViewMessageHandler extends AbstractTabularViewMessageHandler ...@@ -139,7 +199,6 @@ public class DeviceViewMessageHandler extends AbstractTabularViewMessageHandler
139 String iconId = available ? ICON_ID_ONLINE : ICON_ID_OFFLINE; 199 String iconId = available ? ICON_ID_ONLINE : ICON_ID_OFFLINE;
140 DeviceId id = d.id(); 200 DeviceId id = d.id();
141 List<Port> ports = service.getPorts(id); 201 List<Port> ports = service.getPorts(id);
142 -// Set<Link> links = ls.getDeviceEgressLinks(id);
143 202
144 add(ID, id.toString()); 203 add(ID, id.toString());
145 add(AVAILABLE, Boolean.toString(available)); 204 add(AVAILABLE, Boolean.toString(available));
...@@ -148,12 +207,9 @@ public class DeviceViewMessageHandler extends AbstractTabularViewMessageHandler ...@@ -148,12 +207,9 @@ public class DeviceViewMessageHandler extends AbstractTabularViewMessageHandler
148 add(MFR, d.manufacturer()); 207 add(MFR, d.manufacturer());
149 add(HW, d.hwVersion()); 208 add(HW, d.hwVersion());
150 add(SW, d.swVersion()); 209 add(SW, d.swVersion());
151 -// add(SERIAL, d.serialNumber());
152 add(PROTOCOL, d.annotations().value(PROTOCOL)); 210 add(PROTOCOL, d.annotations().value(PROTOCOL));
153 add(NUM_PORTS, Integer.toString(ports.size())); 211 add(NUM_PORTS, Integer.toString(ports.size()));
154 -// add(NUM_EGRESS_LINKS, Integer.toString(links.size())); 212 + add(MASTER_ID, ms.getMasterFor(d.id()).toString());
155 -// add(CHASSISID, d.chassisId().toString());
156 - add(MASTERID, ms.getMasterFor(d.id()).toString());
157 } 213 }
158 214
159 private String getTypeIconId(Device d) { 215 private String getTypeIconId(Device d) {
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
5 - Key Handler 5 - Key Handler
6 - Theme Service 6 - Theme Service
7 - Alert Service 7 - Alert Service
8 + - Preference Service
8 9
9 - Mast 10 - Mast
10 - Masthead 11 - Masthead
...@@ -27,4 +28,6 @@ ...@@ -27,4 +28,6 @@
27 28
28 - Widget 29 - Widget
29 - Table Styling Directives 30 - Table Styling Directives
31 + - Table Builder Service
30 - Toolbar Service 32 - Toolbar Service
33 + - Button Service
......
...@@ -161,7 +161,7 @@ ...@@ -161,7 +161,7 @@
161 161
162 // return the given string with the first character capitalized. 162 // return the given string with the first character capitalized.
163 function cap(s) { 163 function cap(s) {
164 - return s.replace(/^[a-z]/, function (m) { 164 + return s.toLowerCase().replace(/^[a-z]/, function (m) {
165 return m.toUpperCase(); 165 return m.toUpperCase();
166 }); 166 });
167 } 167 }
......
...@@ -18,5 +18,86 @@ ...@@ -18,5 +18,86 @@
18 ONOS GUI -- Device View -- CSS file 18 ONOS GUI -- Device View -- CSS file
19 */ 19 */
20 20
21 -#ov-device td { 21 +/* More in generic panel.css */
22 +
23 +#device-details-panel.floatpanel {
24 + -moz-border-radius: 0;
25 + border-radius: 0;
26 +}
27 +
28 +.light #device-details-panel.floatpanel {
29 + background-color: rgb(226, 248, 255);
30 +}
31 +.dark #device-details-panel.floatpanel {
32 + background-color: #444;
33 +}
34 +
35 +#device-details-panel .container {
36 + padding: 0 12px;
37 +}
38 +
39 +#device-details-panel .close-btn {
40 + position: absolute;
41 + right: 10px;
42 + top: 0;
43 +}
44 +.close-btn svg.embeddedIcon .icon.appPlus .glyph {
45 + /* works for both dark and light themes */
46 + fill: #ccc;
47 +}
48 +
49 +#device-details-panel h2 {
50 + margin: 8px 0;
51 +}
52 +
53 +#device-details-panel td.label {
54 + font-style: italic;
55 + padding-right: 12px;
56 + /* works for both light and dark themes ... */
57 + color: #777;
58 +}
59 +
60 +#device-details-panel hr {
61 + margin: 12px 0;
62 +}
63 +
64 +.light #device-details-panel hr {
65 + opacity: .5;
66 + border-color: #FFF;
67 +}
68 +.dark #device-details-panel hr {
69 + border-color: #666;
70 +}
71 +
72 +#device-details-panel .bottom table {
73 + border-spacing: 0;
74 +}
75 +
76 +#device-details-panel .bottom th {
77 + letter-spacing: 0.02em;
78 +}
79 +
80 +.light #device-details-panel .bottom th {
81 + background-color: #D0E1ED;
82 + /* default text color */
83 +}
84 +.dark #device-details-panel .bottom th {
85 + background-color: #2b2b2b;
86 + color: #ccc;
87 +}
88 +
89 +#device-details-panel .bottom th,
90 +#device-details-panel .bottom td {
91 + padding: 6px 12px;
92 + text-align: center;
93 +}
94 +
95 +.light #device-details-panel .bottom tr:nth-child(odd) {
96 + background-color: #f9f9f9;
97 +}
98 +.light #device-details-panel .bottom tr:nth-child(even) {
99 + background-color: #EBEDF2;
100 +}
101 +.dark #device-details-panel .bottom tr:nth-child(odd) {
102 + background-color: #333;
22 } 103 }
......
...@@ -27,6 +27,8 @@ ...@@ -27,6 +27,8 @@
27 </tr> 27 </tr>
28 28
29 <tr ng-repeat="dev in ctrl.tableData" 29 <tr ng-repeat="dev in ctrl.tableData"
30 + ng-click="selectCallback(dev)"
31 + ng-class="{selected: dev === sel}"
30 ng-repeat-done> 32 ng-repeat-done>
31 <td class="table-icon"> 33 <td class="table-icon">
32 <div icon icon-id="{{dev._iconid_available}}"></div> 34 <div icon icon-id="{{dev._iconid_available}}"></div>
......
...@@ -21,15 +21,209 @@ ...@@ -21,15 +21,209 @@
21 (function () { 21 (function () {
22 'use strict'; 22 'use strict';
23 23
24 + // injected refs
25 + var $log, $scope, fs, mast, ps, wss, is;
26 +
27 + // internal state
28 + var self,
29 + detailsPane,
30 + container, top, bottom, closeBtn;
31 +
32 + // constants
33 + // TODO: consider having a set y height that all tables start at
34 + var h2Pdg = 40,
35 + mastPdg = 8,
36 + tbodyPdg = 5,
37 + cntrPdg = 24,
38 + pName = 'device-details-panel',
39 + detailsReq = 'deviceDetailsRequest',
40 + detailsResp = 'deviceDetailsResponse',
41 + propOrder = [
42 + 'type', 'masterid', 'chassisid',
43 + 'mfr', 'hw', 'sw', 'protocol', 'serial'
44 + ],
45 + friendlyProps = [
46 + 'Type', 'Master ID', 'Chassis ID',
47 + 'Vendor', 'H/W Version', 'S/W Version', 'Protocol', 'Serial #'
48 + ],
49 + portCols = [
50 + 'enabled', 'id', 'speed', 'type', 'elinks_dest'
51 + ],
52 + friendlyPortCols = [
53 + 'Enabled', 'ID', 'Speed', 'Type', 'Egress Links'
54 + ];
55 +
56 + function setUpPanel() {
57 + detailsPane.empty();
58 +
59 + container = detailsPane.append('div').classed('container', true);
60 +
61 + top = container.append('div').classed('top', true);
62 + closeBtn = top.append('div').classed('close-btn', true);
63 + addCloseBtn(closeBtn);
64 + top.append('h2');
65 + top.append('table');
66 +
67 + container.append('hr');
68 +
69 + bottom = container.append('div').classed('bottom', true);
70 + bottom.append('h2').text('Ports');
71 + bottom.append('table');
72 + }
73 +
74 + function createDetailsPane() {
75 + var headerHeight = h2Pdg + fs.noPxStyle(d3.select('h2'), 'height'),
76 + panelTop = headerHeight + tbodyPdg + mast.mastHeight() + mastPdg,
77 + wSize = fs.windowSize(panelTop);
78 +
79 + detailsPane = ps.createPanel(pName, {
80 + height: wSize.height,
81 + width: wSize.width / 2,
82 + margin: 0,
83 + hideMargin: 0
84 + });
85 +
86 + detailsPane.el().style({
87 + position: 'absolute',
88 + top: panelTop + 'px'
89 + });
90 +
91 + setUpPanel();
92 +
93 + detailsPane.hide();
94 + }
95 +
96 + function addCloseBtn(div) {
97 + is.loadEmbeddedIcon(div, 'appPlus', 30);
98 + div.select('g').attr('transform', 'translate(25, 0) rotate(45)');
99 + div.on('click', function () {
100 + detailsPane.hide();
101 + // TODO: deselect the table row when button is clicked
102 + //$scope.sel = null;
103 + });
104 + }
105 +
106 + function populateTopHalf(tbody, details) {
107 + top.select('h2').text(details['id']);
108 +
109 + propOrder.forEach(function (prop, i) {
110 + addProp(tbody, i, details[prop]);
111 + });
112 + }
113 +
114 + function populateBottomHalf(table, ports) {
115 + var theader = table.append('thead').append('tr'),
116 + tbody = table.append('tbody'),
117 + tbWidth, tbHeight,
118 + scrollSize = 20,
119 + btmPdg = 50;
120 +
121 + friendlyPortCols.forEach(function (header) {
122 + theader.append('th').html(header);
123 + });
124 + ports.forEach(function (port) {
125 + addPortRow(tbody, port);
126 + });
127 +
128 + tbWidth = fs.noPxStyle(tbody, 'width') + scrollSize;
129 + tbHeight = detailsPane.height()
130 + - (fs.noPxStyle(detailsPane.el().select('.top'), 'height')
131 + + fs.noPxStyle(detailsPane.el().select('hr'), 'height')
132 + + fs.noPxStyle(detailsPane.el().select('h2'), 'height')
133 + + btmPdg);
134 +
135 + table.style({
136 + height: tbHeight + 'px',
137 + width: tbWidth + 'px',
138 + overflow: 'auto',
139 + display: 'block'
140 + });
141 +
142 + detailsPane.width(tbWidth + cntrPdg);
143 + }
144 +
145 + function addProp(tbody, index, value) {
146 + var tr = tbody.append('tr');
147 +
148 + function addCell(cls, txt) {
149 + tr.append('td').attr('class', cls).html(txt);
150 + }
151 + addCell('label', friendlyProps[index] + ' :');
152 + addCell('value', value);
153 + }
154 +
155 + function addPortRow(tbody, port) {
156 + var tr = tbody.append('tr');
157 +
158 + portCols.forEach(function (col) {
159 + if (col === 'type' || col === 'id') {
160 + port[col] = fs.cap(port[col]);
161 + }
162 + tr.append('td').html(port[col]);
163 + });
164 + }
165 +
166 + function populateDetails(details) {
167 + setUpPanel();
168 +
169 + var toptbody = top.select('table').append('tbody'),
170 + btmTable = bottom.select('table'),
171 + ports = details.ports;
172 +
173 + populateTopHalf(toptbody, details);
174 + populateBottomHalf(btmTable, ports);
175 + }
176 +
177 + function respDetailsCb(data) {
178 + self.panelData = data['details'];
179 + populateDetails(self.panelData);
180 + detailsPane.show();
181 + }
182 +
24 angular.module('ovDevice', []) 183 angular.module('ovDevice', [])
25 .controller('OvDeviceCtrl', 184 .controller('OvDeviceCtrl',
26 - ['$log', '$scope', 'TableBuilderService', 185 + ['$log', '$scope', 'TableBuilderService', 'FnService',
186 + 'MastService', 'PanelService', 'WebSocketService', 'IconService',
187 +
188 + function (_$log_, _$scope_, tbs, _fs_, _mast_, _ps_, _wss_, _is_) {
189 + $log = _$log_;
190 + $scope = _$scope_;
191 + fs = _fs_;
192 + mast = _mast_;
193 + ps = _ps_;
194 + wss = _wss_;
195 + is = _is_;
196 + self = this;
197 + var handlers = {};
198 + self.panelData = [];
199 +
200 + function selCb(row) {
201 + // request the server for more information
202 + // get the id from the row to request details with
203 + if ($scope.sel) {
204 + wss.sendEvent(detailsReq, { id: row.id });
205 + } else {
206 + detailsPane.hide();
207 + }
208 + $log.debug('Got a click on:', row);
209 + }
27 210
28 - function ($log, $scope, tbs) {
29 tbs.buildTable({ 211 tbs.buildTable({
30 - self: this, 212 + self: self,
31 scope: $scope, 213 scope: $scope,
32 - tag: 'device' 214 + tag: 'device',
215 + selCb: selCb
216 + });
217 +
218 + createDetailsPane();
219 +
220 + // bind websocket handlers
221 + handlers[detailsResp] = respDetailsCb;
222 + wss.bindHandlers(handlers);
223 +
224 + $scope.$on('$destroy', function () {
225 + ps.destroyPanel(pName);
226 + wss.unbindHandlers(handlers);
33 }); 227 });
34 228
35 $log.log('OvDeviceCtrl has been created'); 229 $log.log('OvDeviceCtrl has been created');
......
...@@ -386,6 +386,8 @@ describe('factory: fw/util/fn.js', function() { ...@@ -386,6 +386,8 @@ describe('factory: fw/util/fn.js', function() {
386 expect(fs.cap('Foo')).toEqual('Foo'); 386 expect(fs.cap('Foo')).toEqual('Foo');
387 expect(fs.cap('foo')).toEqual('Foo'); 387 expect(fs.cap('foo')).toEqual('Foo');
388 expect(fs.cap('foo bar')).toEqual('Foo bar'); 388 expect(fs.cap('foo bar')).toEqual('Foo bar');
389 + expect(fs.cap('FOO BAR')).toEqual('Foo bar');
390 + expect(fs.cap('foo Bar')).toEqual('Foo bar');
389 }); 391 });
390 392
391 // === Tests for noPx() 393 // === Tests for noPx()
......