Implemented WebSockets for GUI2
Change-Id: I4776ce392b1e8e94ebee938cf7df22791a1e0b8f
diff --git a/web/gui2/AngularMigration.md b/web/gui2/AngularMigration.md
index b40db34..bb83d18 100644
--- a/web/gui2/AngularMigration.md
+++ b/web/gui2/AngularMigration.md
@@ -5,6 +5,7 @@
* In tsconfig.json this app is set to be compiled as ES6 (ES2015)
* See this [Compatibility Table](http://kangax.github.io/compat-table/es6/) for supported browsers
* All modern browsers are supported
+ * See https://webapplog.com/es6/ for a list of things that ES6 brings
* Each item (Service, Component, Directive, Pipe or Module) gets its own file ending with this type e.g. function.service.ts
* Each test file is the name of the item with .spec.ts e.g. function.service.spec.ts
* Modules are used to group together services, components, directives etc
@@ -83,11 +84,59 @@
* LogService - this replaces $log that was inserted in to the old code
* WindowService - this replaces $window and $location in the old code
+There is a fair bit of refactoring has to take place. An important thing to understand
+is that DOM manipulations from inside JavaScript code is not the Angular 6
+way of doing things - there was a lot of this in the old ONOS GUI, using d3.append(..)
+and so on.
+The Angular 6 way of doing things is to defined DOM objects (elements) in the
+html template of a component, and use the Component Java Script code as a base
+for logic that can influence the display of these objects in the template.
+What this means is that what were previously defined as services (e.g. VeilService or
+LoadingService) should now become Components in Angular 6 (e.g. VeilComponent or
+LoadingComponent).
+
+###How do I know whether a Service should be made a Component in this new GUI?
+The general rule to follow is _"if a service in the old GUI has an associated CSS
+file or two then is should be a component in the new GUI"_.
+
+The implication of this is that all of the d3 DOM Manipulations that happened in
+the old service should now be represented in the template of this new component.
+If it's not clear to you what the template should look like, then run the old GUI
+and inspect the element and its children to see the structure.
+
+Components (unlike services) have limited scope (that's the magic of them really -
+no more DOM is loaded at any time than is necessary). This means that they are
+self contained modules, and any CSS associated with them is private to that
+component and not accessible globally.
+
+### Do not inject components in to services
+Components are graphical elements and should not be injected in to Services.
+Services should be injected in to components, but not the other way round.
+
+Take for instance the WebSocketService - this should remain a service, but I want
+to display the LoadingComponent while it's waiting and the VeilComponent if it
+disconnects. I should not go injecting these in to WebSocketService - instead
+there is a setLoadingDelegate() and a setVeilDelegate() function on WSS that I
+can pass in a reference to these two components. When they need to be displayed
+a method call is made on the delegate and the component gets enabled and displays.
+Also note inside WSS any time we call a method on this LoadingComponent delegate
+we check that it the delegate had actually been set.
+
+The WSS was passed in to the LoadingComponent and VeilComponent to set the
+delegate on it.
+
+Any component that needs to use WSS for data should inject the WSS service __AND__
+needs to include the components in its template by adding <onos-loading> and
+<onos-veil>.
+
## fw/remote/wsock.service
Taking for a really simple example the fw/remote/WSockService, this was originally defined in
the __app/fw/remote/wsock.js__ file and is now redefined in
__onos/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts__.
+First of all this should remain a Service, since it does not do any DOM
+manipulation and does not have an associated CSS.
+
This has one method that's called to establish the WebSocketService
```javascript
diff --git a/web/gui2/angular.json b/web/gui2/angular.json
index 887eaa4..8341e91 100644
--- a/web/gui2/angular.json
+++ b/web/gui2/angular.json
@@ -20,9 +20,7 @@
"src/main/webapp/data",
"src/main/webapp/app/fw/layer/loading.service.css"
],
- "styles": [
- "src/main/webapp/app/onos.css"
- ],
+ "styles": ["src/main/webapp/onos.theme.css"],
"scripts": []
},
"configurations": {
@@ -71,7 +69,7 @@
"tsConfig": "src/main/webapp/tsconfig.spec.json",
"scripts": [],
"styles": [
- "src/main/webapp/app/onos.css"
+ "src/main/webapp/app/onos.theme.css"
],
"assets": [
"src/main/webapp/data",
diff --git a/web/gui2/src/main/webapp/app/detectbrowser.directive.ts b/web/gui2/src/main/webapp/app/detectbrowser.directive.ts
index b69ca39..b8a3c77 100644
--- a/web/gui2/src/main/webapp/app/detectbrowser.directive.ts
+++ b/web/gui2/src/main/webapp/app/detectbrowser.directive.ts
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import { Inject } from '@angular/core';
import { Directive } from '@angular/core';
import { FnService } from './fw/util/fn.service';
import { LogService } from './log.service';
@@ -28,10 +29,9 @@
constructor(
private fs: FnService,
private log: LogService,
- private onos: OnosService
+ private onos: OnosService,
+ @Inject(Window) private w: Window
) {
- log.debug('DetectBrowserDirective constructed');
-
const body: HTMLBodyElement = document.getElementsByTagName('body')[0];
// let body = d3.select('body');
let browser = '';
@@ -44,7 +44,10 @@
} else if (fs.isFirefox()) {
browser = 'firefox';
} else {
- this.log.warn('Unknown browser:', window.navigator.vendor);
+ this.log.warn('Unknown browser. ',
+ 'Vendor:', this.w.navigator.vendor,
+ 'Agent:', this.w.navigator.userAgent);
+ return;
}
body.classList.add(browser);
// body.classed(browser, true);
diff --git a/web/gui2/src/main/webapp/app/fw/layer/layer.module.ts b/web/gui2/src/main/webapp/app/fw/layer/layer.module.ts
index e01b004..6292a8d 100644
--- a/web/gui2/src/main/webapp/app/fw/layer/layer.module.ts
+++ b/web/gui2/src/main/webapp/app/fw/layer/layer.module.ts
@@ -24,21 +24,23 @@
import { LoadingService } from './loading.service';
import { PanelService } from './panel.service';
import { QuickHelpService } from './quickhelp.service';
-import { VeilService } from './veil.service';
+import { VeilComponent } from './veil/veil.component';
/**
* ONOS GUI -- Layers Module
*/
@NgModule({
exports: [
- FlashComponent
+ FlashComponent,
+ VeilComponent
],
imports: [
CommonModule,
UtilModule
],
declarations: [
- FlashComponent
+ FlashComponent,
+ VeilComponent
],
providers: [
DetailsPanelService,
@@ -46,8 +48,7 @@
EditableTextService,
LoadingService,
PanelService,
- QuickHelpService,
- VeilService
+ QuickHelpService
]
})
export class LayerModule { }
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil.service.ts b/web/gui2/src/main/webapp/app/fw/layer/veil.service.ts
deleted file mode 100644
index d755e5d..0000000
--- a/web/gui2/src/main/webapp/app/fw/layer/veil.service.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2015-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { Injectable } from '@angular/core';
-import { FnService } from '../util/fn.service';
-import { GlyphService } from '../svg/glyph.service';
-import { KeyService } from '../util/key.service';
-import { LogService } from '../../log.service';
-import { WebSocketService } from '../remote/websocket.service';
-
-/**
- * ONOS GUI -- Layer -- Veil Service
- *
- * Provides a mechanism to display an overlaying div with information.
- * Used mainly for web socket connection interruption.
- */
-@Injectable()
-export class VeilService {
-
- constructor(
- private fs: FnService,
- private gs: GlyphService,
- private ks: KeyService,
- private log: LogService,
- private wss: WebSocketService
- ) {
- this.log.debug('VeilService constructed');
- }
-
-}
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.css b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.css
new file mode 100644
index 0000000..851d6e3
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.css
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Veil Service (layout) -- CSS file
+ */
+
+#veil {
+ z-index: 5000;
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ padding: 60px;
+}
+
+#veil p {
+ display: block;
+ text-align: left;
+ font-size: 14pt;
+ font-style: italic;
+}
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.html b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.html
new file mode 100644
index 0000000..79d202b
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.html
@@ -0,0 +1,9 @@
+<div id="veil" *ngIf="enabled">
+ <p *ngFor="let msg of messages">{{ msg }}</p>
+ <svg [attr.width]="fs.windowSize().width" [attr.height]="fs.windowSize().height">
+ <use [attr.width]="birdDim" [attr.height]="birdDim" class="glyph"
+ style="opacity: 0.2;"
+ xlink:href = "#bird" [attr.transform]="trans"/>
+
+ </svg>
+</div>
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts
new file mode 100644
index 0000000..0ed493f
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.spec.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Layer -- Veil Service - Unit Tests
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { VeilComponent } from './veil.component';
+import { ConsoleLoggerService } from '../../../consolelogger.service';
+import { LogService } from '../../../log.service';
+import { KeyService } from '../../util/key.service';
+import { GlyphService } from '../../svg/glyph.service';
+
+class MockKeyService {}
+
+class MockGlyphService {}
+
+describe('VeilComponent', () => {
+ let log: LogService;
+
+ beforeEach(() => {
+ log = new ConsoleLoggerService();
+
+ TestBed.configureTestingModule({
+ declarations: [ VeilComponent ],
+ providers: [
+ { provide: LogService, useValue: log },
+ { provide: KeyService, useClass: MockKeyService },
+ { provide: GlyphService, useClass: MockGlyphService },
+ ]
+ });
+ });
+
+ it('should create', () => {
+ const fixture = TestBed.createComponent(VeilComponent);
+ const component = fixture.componentInstance;
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.theme.css b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.theme.css
new file mode 100644
index 0000000..7a3bded
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.theme.css
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Veil Service (theme) -- CSS file
+ */
+
+.light, #veil {
+ background-color: rgba(0,0,0,0.75);
+}
+.dark, #veil {
+ background-color: rgba(64,64,64,0.75);
+}
+
+#veil p {
+ color: #ddd;
+}
+
+#veil svg .glyph {
+ fill: #222;
+}
diff --git a/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.ts b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.ts
new file mode 100644
index 0000000..cea6bed
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/fw/layer/veil/veil.component.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Component, OnInit } from '@angular/core';
+import { FnService } from '../../util/fn.service';
+import { GlyphService } from '../../svg/glyph.service';
+import { KeyService } from '../../util/key.service';
+import { LogService } from '../../../log.service';
+import { SvgUtilService } from '../../svg/svgutil.service';
+import { WebSocketService } from '../../remote/websocket.service';
+
+const BIRD = 'bird';
+
+/**
+ * ONOS GUI -- Layer -- Veil Component
+ *
+ * Provides a mechanism to display an overlaying div with information.
+ * Used mainly for web socket connection interruption.
+ *
+ * It can be added to an component's template as follows:
+ * <onos-veil #veil></onos-veil>
+ * <p (click)="veil.show(['t1','t2','t3'])">Test Veil</p>
+ */
+@Component({
+ selector: 'onos-veil',
+ templateUrl: './veil.component.html',
+ styleUrls: ['./veil.component.css', './veil.component.theme.css']
+})
+export class VeilComponent implements OnInit {
+ ww: number;
+ wh: number;
+ birdSvg: string;
+ birdDim: number;
+ enabled: boolean = false;
+ trans: string;
+ messages: string[] = [];
+ veilStyle: string;
+
+ constructor(
+ private fs: FnService,
+ private gs: GlyphService,
+ private ks: KeyService,
+ private log: LogService,
+ private sus: SvgUtilService,
+ private wss: WebSocketService
+ ) {
+ const wSize = this.fs.windowSize();
+ this.ww = wSize.width;
+ this.wh = wSize.height;
+ const shrink = this.wh * 0.3;
+ this.birdDim = this.wh - shrink;
+ const birdCenter = (this.ww - this.birdDim) / 2;
+ this.trans = this.sus.translate([birdCenter, shrink / 2]);
+
+ this.log.debug('VeilComponent with ' + BIRD + ' constructed');
+ }
+
+ ngOnInit() {
+ }
+
+ // msg should be an array of strings
+ show(msgs: string[]): void {
+ this.messages = msgs;
+ this.enabled = true;
+// this.ks.enableKeys(false);
+ }
+
+ hide(): void {
+ this.veilStyle = 'display: none';
+// this.ks.enableKeys(true);
+ }
+
+
+}
diff --git a/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.ts b/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.ts
index 4685a65..381a0c9 100644
--- a/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/remote/urlfn.service.ts
@@ -13,27 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Injectable } from '@angular/core';
+import { Injectable, Inject } from '@angular/core';
import { LogService } from '../../log.service';
-const uiContext = '/onos/ui/';
-const rsSuffix = uiContext + 'rs/';
-const wsSuffix = uiContext + 'websock/';
+const UICONTEXT = '/onos/ui/';
+const RSSUFFIX = UICONTEXT + 'rs/';
+const WSSUFFIX = UICONTEXT + 'websock/';
/**
* ONOS GUI -- Remote -- General Purpose URL Functions
*/
-@Injectable()
+@Injectable({
+ providedIn: 'root',
+})
export class UrlFnService {
constructor(
private log: LogService,
- private window: Window
+ @Inject(Window) private w: Window
) {
this.log.debug('UrlFnService constructed');
}
matchSecure(protocol: string): string {
- const p: string = window.location.protocol;
+ const p: string = this.w.location.protocol;
const secure: boolean = (p === 'https' || p === 'wss');
return secure ? protocol + 's' : protocol;
}
@@ -42,32 +44,35 @@
* behind a proxy and has an app prefix, e.g.
* http://host:port/my/app/onos/ui...
* This bit of regex grabs everything after the host:port and
- * before the uiContext (/onos/ui/) and uses that as an app
+ * before the UICONTEXT (/onos/ui/) and uses that as an app
* prefix by inserting it back into the WS URL.
* If no prefix, then no insert.
*/
- urlBase(protocol: string, port: string, host: string): string {
- const match = window.location.href.match('.*//[^/]+/(.+)' + uiContext);
+ urlBase(protocol: string, port: string = '', host: string = ''): string {
+ const match = this.w.location.href.match('.*//[^/]+/(.+)' + UICONTEXT);
const appPrefix = match ? '/' + match[1] : '';
- return this.matchSecure(protocol) + '://' +
- (host || window.location.hostname) + ':' +
- (port || window.location.port) + appPrefix;
+ return this.matchSecure(protocol) +
+ '://' +
+ (host === '' ? this.w.location.hostname : host) +
+ ':' +
+ (port === '' ? this.w.location.port : port) +
+ appPrefix;
}
httpPrefix(suffix: string): string {
- return this.urlBase('http', '', '') + suffix;
+ return this.urlBase('http') + suffix;
}
- wsPrefix(suffix: string, wsport: any, host: string): string {
+ wsPrefix(suffix: string, wsport: string, host: string): string {
return this.urlBase('ws', wsport, host) + suffix;
}
rsUrl(path: string): string {
- return this.httpPrefix(rsSuffix) + path;
+ return this.httpPrefix(RSSUFFIX) + path;
}
- wsUrl(path: string, wsport: any, host: string): string {
- return this.wsPrefix(wsSuffix, wsport, host) + path;
+ wsUrl(path: string, wsport?: string, host?: string): string {
+ return this.wsPrefix(WSSUFFIX, wsport, host) + path;
}
}
diff --git a/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts b/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts
index 0691c72..e470ec1 100644
--- a/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/remote/websocket.service.ts
@@ -18,37 +18,111 @@
import { GlyphService } from '../svg/glyph.service';
import { LogService } from '../../log.service';
import { UrlFnService } from './urlfn.service';
+import { VeilComponent } from '../layer/veil/veil.component';
import { WSock } from './wsock.service';
/**
* Event Type structure for the WebSocketService
*/
-interface EventType {
+export interface EventType {
event: string;
- payload: any;
+ payload: Object;
+}
+
+export interface Callback {
+ id: number;
+ error: string;
+ cb(host: string, url: string): void;
+}
+
+interface ClusterNode {
+ id: string;
+ ip: string;
+ m_uiAttached: boolean;
+}
+
+interface Bootstrap {
+ user: string;
+ clusterNodes: ClusterNode[];
+ glyphs: any[]; // TODO: narrow this down to a known type
+}
+
+interface ErrorData {
+ message: string;
+}
+
+export interface WsOptions {
+ wsport: number;
}
/**
* ONOS GUI -- Remote -- Web Socket Service
+ *
+ * To see debug messages add ?debug=txrx to the URL
*/
-@Injectable()
+@Injectable({
+ providedIn: 'root',
+})
export class WebSocketService {
// internal state
- private webSockOpts; // web socket options
- private ws = null; // web socket reference
- private wsUp = false; // web socket is good to go
- private handlers = {}; // event handler bindings
+ private webSockOpts: WsOptions; // web socket options
+ private ws: WebSocket = null; // web socket reference
+ private wsUp: boolean = false; // web socket is good to go
+
+ // A map of event handler bindings - names and functions (that accept data and return void)
+ private handlers = new Map<string, (data: any) => void>([]);
private pendingEvents: EventType[] = []; // events TX'd while socket not up
private host: string; // web socket host
private url; // web socket URL
- private clusterNodes = []; // ONOS instances data for failover
+ private clusterNodes: ClusterNode[] = []; // ONOS instances data for failover
private clusterIndex = -1; // the instance to which we are connected
private glyphs = [];
- private connectRetries = 0; // limit our attempts at reconnecting
- private openListeners = {}; // registered listeners for websocket open()
- private nextListenerId = 1; // internal ID for open listeners
- private loggedInUser = null; // name of logged-in user
+ private connectRetries: number = 0; // limit our attempts at reconnecting
+ // A map of registered Callbacks for websocket open()
+ private openListeners = new Map<number, Callback>([]);
+ private nextListenerId: number = 1; // internal ID for open listeners
+ private loggedInUser = null; // name of logged-in user
+ private lcd: any; // The loading component delegate
+ private vcd: any; // The veil component delegate
+
+ /**
+ * built-in handler for the 'boostrap' event
+ */
+ private bootstrap(data: Bootstrap) {
+ this.log.debug('bootstrap data', data);
+ this.loggedInUser = data.user;
+
+ this.clusterNodes = data.clusterNodes;
+ this.clusterNodes.forEach((d, i) => {
+ if (d.m_uiAttached) {
+ this.clusterIndex = i;
+ this.log.info('Connected to cluster node ' + d.ip);
+ // TODO: add connect info to masthead somewhere
+ }
+ });
+ this.glyphs = data.glyphs;
+ const glyphsMap = new Map<string, string>([]);
+ this.glyphs.forEach((d, i) => {
+ glyphsMap.set('_' + d.id, d.viewbox);
+ glyphsMap.set(d.id, d.path);
+ this.gs.registerGlyphs(glyphsMap);
+ });
+ }
+
+ private error(data: ErrorData) {
+ const m: string = data.message || 'error from server';
+ this.log.error(m, data);
+
+ // Unrecoverable error - throw up the veil...
+ if (this.vcd) {
+ this.vcd.show([
+ 'Oops!',
+ 'Server reports error...',
+ m,
+ ]);
+ }
+ }
constructor(
private fs: FnService,
@@ -59,90 +133,319 @@
private window: Window
) {
this.log.debug(window.location.hostname);
+
+ // Bind the boot strap event by default
+ this.bindHandlers(new Map<string, (data) => void>([
+ ['bootstrap', (data) => this.bootstrap(data)],
+ ['error', (data) => this.error(data)]
+ ]));
+
this.log.debug('WebSocketService constructed');
}
+
+ // ==========================
+ // === Web socket callbacks
+
+ /**
+ * Called when WebSocket has just opened
+ *
+ * Lift the Veil if it is displayed
+ * If there are any events pending, send them
+ * Mark the WSS as up and inform any listeners for this open event
+ */
+ handleOpen(): void {
+ this.log.info('Web socket open - ', this.url);
+ // Hide the veil
+ if (this.vcd) {
+ this.vcd.hide();
+ }
+
+ if (this.fs.debugOn('txrx')) {
+ this.log.debug('Sending ' + this.pendingEvents.length + ' pending event(s)...');
+ }
+ this.pendingEvents.forEach((ev) => {
+ this.send(ev);
+ });
+ this.pendingEvents = [];
+
+ this.connectRetries = 0;
+ this.wsUp = true;
+ this.informListeners(this.host, this.url);
+ }
+
+ /**
+ * Function called when WebSocket send a message
+ */
+ handleMessage(msgEvent: MessageEvent): void {
+ let ev: EventType;
+ let h;
+ try {
+ ev = JSON.parse(msgEvent.data);
+ } catch (e) {
+ this.log.error('Message.data is not valid JSON', msgEvent.data, e);
+ return null;
+ }
+ if (this.fs.debugOn('txrx')) {
+ this.log.debug(' << *Rx* ', ev.event, ev.payload);
+ }
+ h = this.handlers.get(ev.event);
+ if (h) {
+ try {
+ h(ev.payload);
+ } catch (e) {
+ this.log.error('Problem handling event:', ev, e);
+ return null;
+ }
+ } else {
+ this.log.warn('Unhandled event:', ev);
+ }
+ }
+
+ /**
+ * Called by the WebSocket if it is closed from the server end
+ *
+ * If the loading component is shown, call stop() on it
+ * Try to find another node in the cluster to connect to
+ * If this is not possible then show the Veil Component
+ */
+ handleClose(): void {
+ this.log.warn('Web socket closed');
+ if (this.lcd) {
+ this.lcd.stop();
+ }
+ this.wsUp = false;
+ let gsucc;
+
+ if (gsucc = this.findGuiSuccessor()) {
+ this.url = this.createWebSocket(this.webSockOpts, gsucc);
+ } else {
+ // If no controllers left to contact, show the Veil...
+ if (this.vcd) {
+ this.vcd.show([
+ 'Oops!', // TODO: Localize this
+ 'Web-socket connection to server closed...',
+ 'Try refreshing the page.',
+ ]);
+ }
+ }
+ }
+
+ // ==============================
+ // === Private Helper Functions
+
+ /**
+ * Find the next node in the ONOS cluster.
+ *
+ * This is used if the WebSocket connection closes because a
+ * node in the cluster ges down - fail over should be automatic
+ */
+ findGuiSuccessor(): string {
+ const ncn = this.clusterNodes.length;
+ let ip: string;
+ let node;
+
+ while (this.connectRetries < ncn && !ip) {
+ this.connectRetries++;
+ this.clusterIndex = (this.clusterIndex + 1) % ncn;
+ node = this.clusterNodes[this.clusterIndex];
+ ip = node && node.ip;
+ }
+
+ return ip;
+ }
+
+ /**
+ * When the WebSocket is opened, inform any listeners that registered
+ * for that event
+ */
+ informListeners(host: string, url: string): void {
+ for (const [key, cb] of this.openListeners.entries()) {
+ cb.cb(host, url);
+ }
+ }
+
+ send(ev: EventType): void {
+ if (this.fs.debugOn('txrx')) {
+ this.log.debug(' *Tx* >> ', ev.event, ev.payload);
+ }
+ this.ws.send(JSON.stringify(ev));
+ }
+
+ /**
+ * Check if there are no WSS event handlers left
+ */
+ noHandlersWarn(handlers: Map<string, Object>, caller: string): boolean {
+ if (!handlers || handlers.size === 0) {
+ this.log.warn('WSS.' + caller + '(): no event handlers');
+ return true;
+ }
+ return false;
+ }
+
/* ===================
* === API Functions
- *
+ */
+
+ /**
* Required for unit tests to set to known state
*/
- resetState() {
+ resetState(): void {
this.webSockOpts = undefined;
this.ws = null;
this.wsUp = false;
this.host = undefined;
this.url = undefined;
this.pendingEvents = [];
- this.handlers = {};
+ this.handlers.clear();
this.clusterNodes = [];
this.clusterIndex = -1;
this.glyphs = [];
this.connectRetries = 0;
- this.openListeners = {};
+ this.openListeners.clear();
this.nextListenerId = 1;
}
- /* Currently supported opts:
+ /*
+ * Currently supported opts:
* wsport: web socket port (other than default 8181)
* host: if defined, is the host address to use
*/
- createWebSocket(opts, _host_: string = '') {
- const wsport = (opts && opts.wsport) || null;
-
+ createWebSocket(opts?: WsOptions, host?: string) {
this.webSockOpts = opts; // preserved for future calls
+ this.host = host === undefined ? this.window.location.host : host;
+ this.url = this.ufs.wsUrl('core', opts === undefined ? '' : opts['wsport'].toString(), host);
-// this.host = _host_ || this.host();
- const url = this.ufs.wsUrl('core', wsport, _host_);
-
- this.log.debug('Attempting to open websocket to: ' + url);
- this.ws = this.wsock.newWebSocket(url);
+ this.log.debug('Attempting to open websocket to: ' + this.url);
+ this.ws = this.wsock.newWebSocket(this.url);
if (this.ws) {
- this.ws.onopen = this.handleOpen();
- this.ws.onmessage = this.handleMessage('???');
- this.ws.onclose = this.handleClose();
+ // fat arrow => syntax means that the 'this' context passed will
+ // be of WebSocketService, not the WebSocket
+ this.ws.onopen = (() => this.handleOpen());
+ this.ws.onmessage = ((msgEvent) => this.handleMessage(msgEvent));
+ this.ws.onclose = (() => this.handleClose());
-// sendEvent('authentication', { token: onosAuth });
- this.sendEvent('authentication token', '');
+ this.sendEvent('authentication token', { token: 'testAuth' });
}
// Note: Wsock logs an error if the new WebSocket call fails
- return url;
+ return this.url;
}
- handleOpen() {
- this.log.debug('WebSocketService: handleOpen() not yet implemented');
+
+ /**
+ * Binds the message handlers to their message type (event type) as
+ * specified in the given map. Note that keys are the event IDs; values
+ * are either:
+ * * the event handler function, or
+ * * an API object which has an event handler for the key
+ */
+ bindHandlers(handlerMap: Map<string, (data) => void>): void {
+ const dups: string[] = [];
+
+ if (this.noHandlersWarn(handlerMap, 'bindHandlers')) {
+ return null;
+ }
+ for (const [eventId, api] of handlerMap) {
+ this.log.debug('Adding handler for ', eventId);
+ const fn = this.fs.isF(api) || this.fs.isF(api[eventId]);
+ if (!fn) {
+ this.log.warn(eventId + ' handler not a function');
+ return;
+ }
+
+ if (this.handlers.get(eventId)) {
+ dups.push(eventId);
+ } else {
+ this.handlers.set(eventId, fn);
+ }
+ }
+ if (dups.length) {
+ this.log.warn('duplicate bindings ignored:', dups);
+ }
}
- handleMessage(msgEvent: any) {
- this.log.debug('WebSocketService: handleMessage() not yet implemented');
+ /**
+ * Unbinds the specified message handlers.
+ * Expected that the same map will be used, but we only care about keys
+ */
+ unbindHandlers(handlerMap: Map<string, (data) => void>): void {
+ if (this.noHandlersWarn(handlerMap, 'unbindHandlers')) {
+ return null;
+ }
+
+ for (const [eventId, api] of handlerMap) {
+ this.handlers.delete(eventId);
+ }
}
- handleClose() {
- this.log.debug('WebSocketService: handleClose() not yet implemented');
+ /**
+ * Add a listener function for listening for WebSocket opening.
+ * The function must give a host and url and return void
+ */
+ addOpenListener(callback: (host: string, url: string) => void ): Callback {
+ const id: number = this.nextListenerId++;
+ const cb = this.fs.isF(callback);
+ const o: Callback = <Callback>{ id: id, cb: cb };
+
+ if (cb) {
+ this.openListeners.set(id, o);
+ } else {
+ this.log.error('WSS.addOpenListener(): callback not a function');
+ o.error = 'No callback defined';
+ }
+ return o;
}
- /* Formulates an event message and sends it via the web-socket.
+ /**
+ * Remove a listener of WebSocket opening
+ */
+ removeOpenListener(lsnr: Callback): void {
+ const id = this.fs.isO(lsnr) && lsnr.id;
+ let o;
+
+ if (!id) {
+ this.log.warn('WSS.removeOpenListener(): invalid listener', lsnr);
+ return null;
+ }
+ o = this.openListeners[id];
+
+ if (o) {
+ this.openListeners.delete(id);
+ }
+ }
+
+ /**
+ * Formulates an event message and sends it via the web-socket.
* If the websocket is not up yet, we store it in a pending list.
*/
- sendEvent(evType, payload) {
+ sendEvent(evType: string, payload: Object ): void {
const ev = <EventType> {
event: evType,
payload: payload
};
if (this.wsUp) {
- this._send(ev);
+ this.send(ev);
} else {
this.pendingEvents.push(ev);
}
}
- _send(ev: EventType) {
- if (this.fs.debugOn('txrx')) {
- this.log.debug(' *Tx* >> ', ev.event, ev.payload);
- }
- this.ws.send(JSON.stringify(ev));
+ /**
+ * Binds the veil service as a delegate.
+ */
+ setVeilDelegate(vd: VeilComponent): void {
+ this.vcd = vd;
+ }
+
+ /**
+ * Binds the loading service as a delegate
+ */
+ setLoadingDelegate(ld: any): void {
+ // TODO - Investigate changing Loading Service to LoadingComponent
+ this.log.debug('Loading delegate set', ld);
+ this.lcd = ld;
}
}
diff --git a/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts b/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts
index 176b5d3..86e0625 100644
--- a/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/remote/wsock.service.ts
@@ -21,7 +21,9 @@
*
* This service provided specifically so that it can be mocked in unit tests.
*/
-@Injectable()
+@Injectable({
+ providedIn: 'root',
+})
export class WSock {
constructor(
diff --git a/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts b/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
index 674cbbf..c6a81e9 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/icon.service.ts
@@ -222,7 +222,7 @@
'xlink:href': '#' + glyphId,
width: dim,
height: dim,
- transform: this.sus.translate(xlate, xlate),
+ transform: this.sus.translate([xlate], xlate),
});
return g;
}
diff --git a/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts b/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
index 51d5d2a..9cba079 100644
--- a/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/svg/svgutil.service.ts
@@ -22,7 +22,9 @@
*
* The SVG Util Service provides a miscellany of utility functions.
*/
-@Injectable()
+@Injectable({
+ providedIn: 'root',
+})
export class SvgUtilService {
constructor(
@@ -32,7 +34,7 @@
this.log.debug('SvgUtilService constructed');
}
- translate(x, y) {
+ translate(x: number[], y?: any): string {
if (this.fs.isA(x) && x.length === 2 && !y) {
return 'translate(' + x[0] + ',' + x[1] + ')';
}
diff --git a/web/gui2/src/main/webapp/app/fw/util/fn.service.ts b/web/gui2/src/main/webapp/app/fw/util/fn.service.ts
index 0726afc..d355ce9 100644
--- a/web/gui2/src/main/webapp/app/fw/util/fn.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/fn.service.ts
@@ -13,13 +13,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Injectable } from '@angular/core';
+import { Injectable, Inject } from '@angular/core';
import { ActivatedRoute, Router} from '@angular/router';
import { LogService } from '../../log.service';
// Angular>=2 workaround for missing definition
declare const InstallTrigger: any;
+const matcher = /<\/?([a-zA-Z0-9]+)*(.*?)\/?>/igm;
+const whitelist: string[] = ['b', 'i', 'p', 'em', 'strong', 'br'];
+const evillist: string[] = ['script', 'style', 'iframe'];
+
+/**
+ * Used with the Window size function;
+ **/
+export interface WindowSize {
+ width: number;
+ height: number;
+}
+
+/**
+ * For the sanitize() and analyze() functions
+ */
+export interface Match {
+ full: string;
+ name: string;
+}
// TODO Move all this trie stuff to its own class
// Angular>=2 Tightened up on types to avoid compiler errors
@@ -133,13 +152,14 @@
@Injectable()
export class FnService {
// internal state
- debugFlags = new Map<string, boolean>([
+ private debugFlags = new Map<string, boolean>([
// [ "LoadingService", true ]
]);
constructor(
private route: ActivatedRoute,
- private log: LogService
+ private log: LogService,
+ @Inject(Window) private w: Window
) {
this.route.queryParams.subscribe(params => {
const debugparam: string = params['debug'];
@@ -149,33 +169,155 @@
log.debug('FnService constructed');
}
- isF(f) {
+ /**
+ * Test if an argument is a function
+ *
+ * Note: the need for this would go away if all functions
+ * were strongly typed
+ */
+ isF(f: any): any {
return typeof f === 'function' ? f : null;
}
- isA(a) {
+ /**
+ * Test if an argument is an array
+ *
+ * Note: the need for this would go away if all arrays
+ * were strongly typed
+ */
+ isA(a: any): any {
// NOTE: Array.isArray() is part of EMCAScript 5.1
return Array.isArray(a) ? a : null;
}
- isS(s) {
+ /**
+ * Test if an argument is a string
+ *
+ * Note: the need for this would go away if all strings
+ * were strongly typed
+ */
+ isS(s: any): string {
return typeof s === 'string' ? s : null;
}
- isO(o) {
+ /**
+ * Test if an argument is an object
+ *
+ * Note: the need for this would go away if all objects
+ * were strongly typed
+ */
+ isO(o: any): Object {
return (o && typeof o === 'object' && o.constructor === Object) ? o : null;
}
-// contains: contains,
-// areFunctions: areFunctions,
-// areFunctionsNonStrict: areFunctionsNonStrict,
-// windowSize: windowSize,
+ /**
+ * Test that an array contains an object
+ */
+ contains(a: any[], x: any): boolean {
+ return this.isA(a) && a.indexOf(x) > -1;
+ }
+
+ /**
+ * Returns width and height of window inner dimensions.
+ * offH, offW : offset width/height are subtracted, if present
+ */
+ windowSize(offH: number = 0, offW: number = 0): WindowSize {
+ return {
+ height: this.w.innerHeight - offH,
+ width: this.w.innerWidth - offW
+ };
+ }
+
+ /**
+ * Returns true if all names in the array are defined as functions
+ * on the given api object; false otherwise.
+ * Also returns false if there are properties on the api that are NOT
+ * listed in the array of names.
+ *
+ * This gets extra complicated when the api Object is an
+ * Angular service - while the functions can be retrieved
+ * by an indexed get, the ownProperties does not show the
+ * functions of the class. We have to dive in to the prototypes
+ * properties to get these - and even then we have to filter
+ * out the constructor and any member variables
+ */
+ areFunctions(api: Object, fnNames: string[]): boolean {
+ const fnLookup: Map<string, boolean> = new Map();
+ let extraFound: boolean = false;
+
+ if (!this.isA(fnNames)) {
+ return false;
+ }
+
+ const n: number = fnNames.length;
+ let i: number;
+ let name: string;
+
+ for (i = 0; i < n; i++) {
+ name = fnNames[i];
+ if (!this.isF(api[name])) {
+ return false;
+ }
+ fnLookup.set(name, true);
+ }
+
+ // check for properties on the API that are not listed in the array,
+ const keys = Object.getOwnPropertyNames(api);
+ if (keys.length === 0) {
+ return true;
+ }
+ // If the api is a class it will have a name,
+ // else it will just be called 'Object'
+ const apiObjectName: string = api.constructor.name;
+ if (apiObjectName === 'Object') {
+ Object.keys(api).forEach((key) => {
+ if (!fnLookup.get(key)) {
+ extraFound = true;
+ }
+ });
+ } else { // It is a class, so its functions will be in the child (prototype)
+ const pObj: Object = Object.getPrototypeOf(api);
+ for ( const key in Object.getOwnPropertyDescriptors(pObj) ) {
+ if (key === 'constructor') { // Filter out constructor
+ continue;
+ }
+ const value = Object.getOwnPropertyDescriptor(pObj, key);
+ // Only compare functions. Look for any not given in the map
+ if (this.isF(value.value) && !fnLookup.get(key)) {
+ extraFound = true;
+ }
+ }
+ }
+ return !extraFound;
+ }
+
+ /**
+ * Returns true if all names in the array are defined as functions
+ * on the given api object; false otherwise. This is a non-strict version
+ * that does not care about other properties on the api.
+ */
+ areFunctionsNonStrict(api, fnNames): boolean {
+ if (!this.isA(fnNames)) {
+ return false;
+ }
+ const n = fnNames.length;
+ let i;
+ let name;
+
+ for (i = 0; i < n; i++) {
+ name = fnNames[i];
+ if (!this.isF(api[name])) {
+ return false;
+ }
+ }
+ return true;
+ }
/**
* Returns true if current browser determined to be a mobile device
*/
isMobile() {
- const ua = window.navigator.userAgent;
+ const ua = this.w.navigator.userAgent;
const patt = /iPhone|iPod|iPad|Silk|Android|BlackBerry|Opera Mini|IEMobile/;
return patt.test(ua);
}
@@ -184,10 +326,10 @@
* Returns true if the current browser determined to be Chrome
*/
isChrome() {
- const isChromium = (window as any).chrome;
- const vendorName = window.navigator.vendor;
+ const isChromium = (this.w as any).chrome;
+ const vendorName = this.w.navigator.vendor;
- const isOpera = window.navigator.userAgent.indexOf('OPR') > -1;
+ const isOpera = this.w.navigator.userAgent.indexOf('OPR') > -1;
return (isChromium !== null &&
isChromium !== undefined &&
vendorName === 'Google Inc.' &&
@@ -195,8 +337,8 @@
}
isChromeHeadless() {
- const vendorName = window.navigator.vendor;
- const headlessChrome = window.navigator.userAgent.indexOf('HeadlessChrome') > -1;
+ const vendorName = this.w.navigator.vendor;
+ const headlessChrome = this.w.navigator.userAgent.indexOf('HeadlessChrome') > -1;
return (vendorName === 'Google Inc.' && headlessChrome === true);
}
@@ -205,8 +347,8 @@
* Returns true if the current browser determined to be Safari
*/
isSafari() {
- return (window.navigator.userAgent.indexOf('Safari') !== -1 &&
- window.navigator.userAgent.indexOf('Chrome') === -1);
+ return (this.w.navigator.userAgent.indexOf('Safari') !== -1 &&
+ this.w.navigator.userAgent.indexOf('Chrome') === -1);
}
/**
@@ -217,13 +359,90 @@
}
/**
+ * search through an array of objects, looking for the one with the
+ * tagged property matching the given key. tag defaults to 'id'.
+ * returns the index of the matching object, or -1 for no match.
+ */
+ find(key: string, array: Object[], tag: string = 'id'): number {
+ let idx: number;
+ const n: number = array.length;
+
+ for (idx = 0 ; idx < n; idx++) {
+ const d: Object = array[idx];
+ if (d[tag] === key) {
+ return idx;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * search through array to find (the first occurrence of) item,
+ * returning its index if found; otherwise returning -1.
+ */
+ inArray(item: any, array: any[]): number {
+ if (this.isA(array)) {
+ for (let i = 0; i < array.length; i++) {
+ if (array[i] === item) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * remove (the first occurrence of) the specified item from the given
+ * array, if any. Return true if the removal was made; false otherwise.
+ */
+ removeFromArray(item: any, array: any[]): boolean {
+ const i: number = this.inArray(item, array);
+ if (i >= 0) {
+ array.splice(i, 1);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * return true if the object is empty, return false otherwise
+ */
+ isEmptyObject(obj: Object): boolean {
+ for (const key in obj) {
+ if (true) { return false; }
+ }
+ return true;
+ }
+
+ /**
* Return the given string with the first character capitalized.
*/
- cap(s) {
+ cap(s: string): string {
return s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s;
}
/**
+ * return the parameter without a px suffix
+ */
+ noPx(num: string): number {
+ return Number(num.replace(/px$/, ''));
+ }
+
+ /**
+ * return an element's given style property without px suffix
+ */
+ noPxStyle(elem: any, prop: string): number {
+ return Number(elem.style(prop).replace(/px$/, ''));
+ }
+
+ /**
+ * Return true if a str ends with suffix
+ */
+ endsWith(str: string, suffix: string) {
+ return str.indexOf(suffix, str.length - suffix.length) !== -1;
+ }
+
+ /**
* output debug message to console, if debug tag set...
* e.g. fs.debug('mytag', arg1, arg2, ...)
*/
@@ -233,7 +452,7 @@
}
}
- parseDebugFlags(dbgstr: string): void {
+ private parseDebugFlags(dbgstr: string): void {
const bits = dbgstr ? dbgstr.split(',') : [];
bits.forEach((key) => {
this.debugFlags.set(key, true);
@@ -248,4 +467,66 @@
return this.debugFlags.get(tag);
}
+
+
+ // -----------------------------------------------------------------
+ // The next section deals with sanitizing external strings destined
+ // to be loaded via a .html() function call.
+ //
+ // See definition of matcher, evillist and whitelist at the top of this file
+
+ /*
+ * Returns true if the tag is in the evil list, (and is not an end-tag)
+ */
+ inEvilList(tag: any): boolean {
+ return (evillist.indexOf(tag.name) !== -1 && tag.full.indexOf('/') === -1);
+ }
+
+ /*
+ * Returns an array of Matches of matcher in html
+ */
+ analyze(html: string): Match[] {
+ const matches: Match[] = [];
+ let match;
+
+ // extract all tags
+ while ((match = matcher.exec(html)) !== null) {
+ matches.push({
+ full: match[0],
+ name: match[1],
+ // NOTE: ignoring attributes {match[2].split(' ')} for now
+ });
+ }
+
+ return matches;
+ }
+
+ /*
+ * Returns a cleaned version of html
+ */
+ sanitize(html: string): string {
+ const matches: Match[] = this.analyze(html);
+
+ // completely obliterate evil tags and their contents...
+ evillist.forEach((tag) => {
+ const re = new RegExp('<' + tag + '(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/' + tag + '>', 'gim');
+ html = html.replace(re, '');
+ });
+
+ // filter out all but white-listed tags and end-tags
+ matches.forEach((tag) => {
+ if (whitelist.indexOf(tag.name) === -1) {
+ html = html.replace(tag.full, '');
+ if (this.inEvilList(tag)) {
+ this.log.warn('Unsanitary HTML input -- ' +
+ tag.full + ' detected!');
+ }
+ }
+ });
+
+ // TODO: consider encoding HTML entities, e.g. '&' -> '&'
+
+ return html;
+ }
+
}
diff --git a/web/gui2/src/main/webapp/app/fw/util/lion.service.ts b/web/gui2/src/main/webapp/app/fw/util/lion.service.ts
index a33ef6b..5ab9f65 100644
--- a/web/gui2/src/main/webapp/app/fw/util/lion.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/lion.service.ts
@@ -29,19 +29,43 @@
/**
* ONOS GUI -- Lion -- Localization Utilities
*/
-@Injectable()
+@Injectable({
+ providedIn: 'root',
+})
export class LionService {
ubercache: any[];
+ /**
+ * Handler for uberlion event from WSS
+ */
+ uberlion(data: Lion) {
+ this.ubercache = data.lion;
+
+ this.log.info('LION service: Locale... [' + data.locale + ']');
+ this.log.info('LION service: Bundles installed...');
+
+ for (const p in this.ubercache) {
+ if (this.ubercache[p]) {
+ this.log.info(' :=> ', p);
+ }
+ }
+
+ this.log.debug('LION service: uber-lion bundle received:', data);
+ }
+
constructor(
private log: LogService,
private wss: WebSocketService
) {
+ this.wss.bindHandlers(new Map<string, (data) => void>([
+ ['uberlion', (data) => this.uberlion(data) ]
+ ]));
this.log.debug('LionService constructed');
}
- /* returns a lion bundle (function) for the given bundle ID
+ /**
+ * Returns a lion bundle (function) for the given bundle ID (string)
* returns a function that takes a string and returns a string
*/
bundle(bundleId: string): (string) => string {
@@ -58,21 +82,4 @@
getKey(key: string): string {
return this.bundle[key] || '%' + key + '%';
}
-
- /* handler for uberlion event..
- */
- uberlion(data: Lion) {
- this.ubercache = data.lion;
-
- this.log.info('LION service: Locale... [' + data.locale + ']');
- this.log.info('LION service: Bundles installed...');
-
- for (const p in this.ubercache) {
- if (this.ubercache[p]) {
- this.log.info(' :=> ', p);
- }
- }
-
- this.log.debug('LION service: uber-lion bundle received:', data);
- }
}
diff --git a/web/gui2/src/main/webapp/app/onos.common.css b/web/gui2/src/main/webapp/app/onos.common.css
new file mode 100644
index 0000000..9a3f5a6
--- /dev/null
+++ b/web/gui2/src/main/webapp/app/onos.common.css
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- common -- CSS file
+ */
+
+.clickable {
+ cursor: pointer;
+}
+
+.light .editable {
+ border-bottom: 1px dashed #ca504b;
+}
+
+.dark .editable {
+ border-bottom: 1px dashed #df4f4a;
+}
+
+.light svg.embeddedIcon .icon .glyph {
+ fill: #0071bd;
+}
+
+.dark svg.embeddedIcon .icon .glyph {
+ fill: #375b7f;
+}
diff --git a/web/gui2/src/main/webapp/app/onos.component.css b/web/gui2/src/main/webapp/app/onos.component.css
index e69de29..60933d8 100644
--- a/web/gui2/src/main/webapp/app/onos.component.css
+++ b/web/gui2/src/main/webapp/app/onos.component.css
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- core (layout) -- CSS file
+ */
+
+#view {
+ padding: 6px;
+ width: 100%;
+ height: 100%;
+}
+
+#view h2 {
+ -webkit-margin-before: 0;
+ -webkit-margin-after: 0;
+ margin: 32px 0 4px 16px;
+ padding: 0;
+ font-size: 18pt;
+ font-weight: lighter;
+}
diff --git a/web/gui2/src/main/webapp/app/onos.component.html b/web/gui2/src/main/webapp/app/onos.component.html
index 566c972..42d2bdd 100644
--- a/web/gui2/src/main/webapp/app/onos.component.html
+++ b/web/gui2/src/main/webapp/app/onos.component.html
@@ -1,6 +1,8 @@
<div style="text-align:center" onosDetectBrowser>
<onos-mast></onos-mast>
<onos-nav></onos-nav>
+ <onos-veil #veil></onos-veil>
+ <div>{{ wss.setVeilDelegate(veil) }}</div>
<router-outlet></router-outlet>
</div>
diff --git a/web/gui2/src/main/webapp/app/onos.component.ts b/web/gui2/src/main/webapp/app/onos.component.ts
index 02b3a44..bbec955 100644
--- a/web/gui2/src/main/webapp/app/onos.component.ts
+++ b/web/gui2/src/main/webapp/app/onos.component.ts
@@ -19,11 +19,10 @@
import { KeyService } from './fw/util/key.service';
import { ThemeService } from './fw/util/theme.service';
import { GlyphService } from './fw/svg/glyph.service';
-import { VeilService } from './fw/layer/veil.service';
import { PanelService } from './fw/layer/panel.service';
import { QuickHelpService } from './fw/layer/quickhelp.service';
import { EeService } from './fw/util/ee.service';
-import { WebSocketService } from './fw/remote/websocket.service';
+import { WebSocketService, WsOptions } from './fw/remote/websocket.service';
import { SpriteService } from './fw/svg/sprite.service';
import { OnosService, View } from './onos.service';
@@ -61,7 +60,7 @@
@Component({
selector: 'onos-root',
templateUrl: './onos.component.html',
- styleUrls: ['./onos.component.css']
+ styleUrls: ['./onos.component.css', './onos.common.css']
})
export class OnosComponent implements OnInit {
public title = 'onos';
@@ -81,7 +80,6 @@
private ks: KeyService,
private ts: ThemeService,
private gs: GlyphService,
- private vs: VeilService,
private ps: PanelService,
private qhs: QuickHelpService,
private ee: EeService,
@@ -101,6 +99,8 @@
log.warn('OnosComponent: testing logger.warn()');
log.error('OnosComponent: testing logger.error()');
+ this.wss.createWebSocket(<WsOptions>{ wsport: 8181});
+
log.debug('OnosComponent constructed');
}
diff --git a/web/gui2/src/main/webapp/app/onos.css b/web/gui2/src/main/webapp/app/onos.css
deleted file mode 100644
index fa485ac..0000000
--- a/web/gui2/src/main/webapp/app/onos.css
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2014-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
- ONOS GUI -- core (layout) -- CSS file
- */
-
-html {
- font-family: 'Open Sans', sans-serif;
- -webkit-text-size-adjust: 100%;
- -ms-text-size-adjust: 100%;
- height: 100%;
-}
-
-/*
- overflow hidden is to ensure that the body does not expand to account
- for any flyout panes, that are positioned "off screen".
- */
-body {
- height: 100%;
- margin: 0;
- overflow: hidden;
-}
-
-#view {
- padding: 6px;
- width: 100%;
- height: 100%;
-}
-
-#view h2 {
- -webkit-margin-before: 0;
- -webkit-margin-after: 0;
- margin: 32px 0 4px 16px;
- padding: 0;
- font-size: 18pt;
- font-weight: lighter;
-}
diff --git a/web/gui2/src/main/webapp/app/view/device/device.component.ts b/web/gui2/src/main/webapp/app/view/device/device.component.ts
index db42c4d..6929f9b 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.component.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device.component.ts
@@ -58,7 +58,7 @@
ngOnInit() {
this.log.debug('DeviceComponent initialized');
// TODO: Remove this - it's only for demo purposes
- this.ls.startAnim();
+// this.ls.startAnim();
}
}
diff --git a/web/gui2/src/main/webapp/app/view/device/device.module.ts b/web/gui2/src/main/webapp/app/view/device/device.module.ts
index 9cd50cb..8f0f351 100644
--- a/web/gui2/src/main/webapp/app/view/device/device.module.ts
+++ b/web/gui2/src/main/webapp/app/view/device/device.module.ts
@@ -17,7 +17,7 @@
import { CommonModule } from '@angular/common';
import { DeviceComponent } from './device.component';
import { DeviceDetailsPanelDirective } from './devicedetailspanel.directive';
-
+import { RemoteModule } from '../../fw/remote/remote.module';
/**
* ONOS GUI -- Device View Module
*/
@@ -26,7 +26,8 @@
DeviceComponent
],
imports: [
- CommonModule
+ CommonModule,
+ RemoteModule
],
declarations: [
DeviceComponent,
diff --git a/web/gui2/src/main/webapp/index.html b/web/gui2/src/main/webapp/index.html
index 83d2b6c..9ca6a02 100644
--- a/web/gui2/src/main/webapp/index.html
+++ b/web/gui2/src/main/webapp/index.html
@@ -26,6 +26,7 @@
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,300,700'
rel='stylesheet' type='text/css'>
+ <link href="onos.theme.css" type='text/css'>
<link href="app/fw/layer/loading.service.css" rel='stylesheet' type='text/css'>
<base href="/">
<title>ONOS</title>
diff --git a/web/gui2/src/main/webapp/onos.theme.css b/web/gui2/src/main/webapp/onos.theme.css
new file mode 100644
index 0000000..dc14c80
--- /dev/null
+++ b/web/gui2/src/main/webapp/onos.theme.css
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- core (theme) -- CSS file
+ */
+
+body {
+ background-color: white;
+}
+
+html {
+ font-family: 'Open Sans', sans-serif;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ height: 100%;
+}
+
+/*
+ overflow hidden is to ensure that the body does not expand to account
+ for any flyout panes, that are positioned "off screen".
+ */
+body {
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+}
+
+#view h2 {
+ color: #3c3a3a;
+}
+
+a {
+ color: #009fdb;
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+a:visited {
+ color: #7fabdb;
+ text-decoration: none;
+}
+
+/* ========== DARK Theme ========== */
+
+body.dark {
+ background-color: #282528;
+}
+
+.dark #view h2 {
+ color: #6a6e6a;
+}
+
+.dark a {
+ color: #007ca6;
+}
+.dark a:visited {
+ color: #4f6e90;
+}
+
+.dark input {
+ color: #dddddd;
+ background-color: #222222;
+ border: 1px solid #666666;
+}
+
+.dark select {
+ color: #dddddd;
+ background-color: #222222;
+}
\ No newline at end of file
diff --git a/web/gui2/src/main/webapp/tests/app/detectbrowser.directive.spec.ts b/web/gui2/src/main/webapp/tests/app/detectbrowser.directive.spec.ts
index 06715cd..9183536 100644
--- a/web/gui2/src/main/webapp/tests/app/detectbrowser.directive.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/detectbrowser.directive.spec.ts
@@ -24,8 +24,8 @@
import { of } from 'rxjs';
class MockFnService extends FnService {
- constructor(ar: ActivatedRoute, log: LogService) {
- super(ar, log);
+ constructor(ar: ActivatedRoute, log: LogService, w: Window) {
+ super(ar, log, w);
}
}
@@ -44,17 +44,25 @@
describe('DetectBrowserDirective', () => {
let log: LogService;
let ar: ActivatedRoute;
+ let mockWindow: Window;
beforeEach(() => {
log = new ConsoleLoggerService();
ar = new MockActivatedRoute(['debug', 'DetectBrowserDirective']);
+ mockWindow = <any>{
+ navigator: {
+ userAgent: 'HeadlessChrome',
+ vendor: 'Google Inc.'
+ }
+ };
TestBed.configureTestingModule({
providers: [ DetectBrowserDirective,
- { provide: FnService, useValue: new MockFnService(ar, log) },
+ { provide: FnService, useValue: new MockFnService(ar, log, mockWindow) },
{ provide: LogService, useValue: log },
{ provide: OnosService, useClass: MockOnosService },
{ provide: Document, useValue: document },
+ { provide: Window, useFactory: (() => mockWindow ) }
]
});
});
diff --git a/web/gui2/src/main/webapp/tests/app/fw/layer/veil.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/layer/veil.service.spec.ts
deleted file mode 100644
index 3d5509b..0000000
--- a/web/gui2/src/main/webapp/tests/app/fw/layer/veil.service.spec.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2015-present Open Networking Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import { TestBed, inject } from '@angular/core/testing';
-
-import { LogService } from '../../../../app/log.service';
-import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
-import { FnService } from '../../../../app/fw/util/fn.service';
-import { GlyphService } from '../../../../app/fw/svg/glyph.service';
-import { KeyService } from '../../../../app/fw/util/key.service';
-import { VeilService } from '../../../../app/fw/layer/veil.service';
-import { WebSocketService } from '../../../../app/fw/remote/websocket.service';
-
-class MockFnService {}
-
-class MockGlyphService {}
-
-class MockKeyService {}
-
-class MockWebSocketService {}
-
-/**
- * ONOS GUI -- Layer -- Veil Service - Unit Tests
- */
-describe('VeilService', () => {
- let log: LogService;
-
- beforeEach(() => {
- log = new ConsoleLoggerService();
-
- TestBed.configureTestingModule({
- providers: [VeilService,
- { provide: FnService, useClass: MockFnService },
- { provide: GlyphService, useClass: MockGlyphService },
- { provide: KeyService, useClass: MockKeyService },
- { provide: LogService, useValue: log },
- { provide: WebSocketService, useClass: MockWebSocketService },
- ]
- });
- });
-
- it('should be created', inject([VeilService], (service: VeilService) => {
- expect(service).toBeTruthy();
- }));
-});
diff --git a/web/gui2/src/main/webapp/tests/app/fw/remote/urlfn.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/remote/urlfn.service.spec.ts
index a54458b..6b229bd 100644
--- a/web/gui2/src/main/webapp/tests/app/fw/remote/urlfn.service.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/fw/remote/urlfn.service.spec.ts
@@ -18,26 +18,106 @@
import { LogService } from '../../../../app/log.service';
import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
import { UrlFnService } from '../../../../app/fw/remote/urlfn.service';
+import { FnService } from '../../../../app/fw/util/fn.service';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
/**
* ONOS GUI -- Remote -- General Functions - Unit Tests
*/
describe('UrlFnService', () => {
let log: LogService;
- const windowMock = <any>{ location: <any> { hostname: 'localhost' } };
+ let ufs: UrlFnService;
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
beforeEach(() => {
log = new ConsoleLoggerService();
+ ar = new MockActivatedRoute({'debug': 'TestService'});
+ windowMock = <any>{
+ location: <any> {
+ hostname: '',
+ host: '',
+ port: '',
+ protocol: '',
+ search: { debug: 'true'},
+ href: ''
+ }
+ };
+
+ fs = new FnService(ar, log, windowMock);
TestBed.configureTestingModule({
providers: [UrlFnService,
{ provide: LogService, useValue: log },
- { provide: Window, useValue: windowMock },
+ { provide: Window, useFactory: (() => windowMock ) },
]
});
+
+ ufs = TestBed.get(UrlFnService);
});
- it('should be created', inject([UrlFnService], (service: UrlFnService) => {
- expect(service).toBeTruthy();
- }));
+ function setLoc(prot: string, h: string, p: string, ctx: string = '') {
+ windowMock.location.host = h;
+ windowMock.location.hostname = h;
+ windowMock.location.port = p;
+ windowMock.location.protocol = prot;
+ windowMock.location.href = prot + '://' + h + ':' + p +
+ ctx + '/onos/ui/';
+ }
+
+ it('should define UrlFnService', () => {
+ expect(ufs).toBeDefined();
+ });
+
+ it('should define api functions', () => {
+ expect(fs.areFunctions(ufs, [
+ 'rsUrl', 'wsUrl', 'urlBase', 'httpPrefix',
+ 'wsPrefix', 'matchSecure'
+ ])).toBeTruthy();
+ });
+
+ it('should return the correct (http) RS url', () => {
+ setLoc('http', 'foo', '123');
+ expect(ufs.rsUrl('path')).toEqual('http://foo:123/onos/ui/rs/path');
+ });
+
+ it('should return the correct (https) RS url', () => {
+ setLoc('https', 'foo', '123');
+ expect(ufs.rsUrl('path')).toEqual('https://foo:123/onos/ui/rs/path');
+ });
+
+ it('should return the correct (ws) WS url', () => {
+ setLoc('http', 'foo', '123');
+ expect(ufs.wsUrl('path')).toEqual('ws://foo:123/onos/ui/websock/path');
+ });
+
+ it('should return the correct (wss) WS url', () => {
+ setLoc('https', 'foo', '123');
+ expect(ufs.wsUrl('path')).toEqual('wss://foo:123/onos/ui/websock/path');
+ });
+
+ it('should allow us to define an alternate WS port', () => {
+ setLoc('http', 'foo', '123');
+ expect(ufs.wsUrl('xyyzy', '456')).toEqual('ws://foo:456/onos/ui/websock/xyyzy');
+ });
+
+ it('should allow us to define an alternate host', () => {
+ setLoc('http', 'foo', '123');
+ expect(ufs.wsUrl('core', '456', 'bar')).toEqual('ws://bar:456/onos/ui/websock/core');
+ });
+
+ it('should allow us to inject an app context', () => {
+ setLoc('http', 'foo', '123', '/my/app');
+ expect(ufs.wsUrl('path')).toEqual('ws://foo:123/my/app/onos/ui/websock/path');
+ });
+
});
diff --git a/web/gui2/src/main/webapp/tests/app/fw/remote/websocket.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/remote/websocket.service.spec.ts
index d18dda4..5c8d6b7 100644
--- a/web/gui2/src/main/webapp/tests/app/fw/remote/websocket.service.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/fw/remote/websocket.service.spec.ts
@@ -16,44 +16,266 @@
import { TestBed, inject } from '@angular/core/testing';
import { LogService } from '../../../../app/log.service';
-import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
-import { WebSocketService } from '../../../../app/fw/remote/websocket.service';
+import { WebSocketService, WsOptions, Callback, EventType } from '../../../../app/fw/remote/websocket.service';
import { FnService } from '../../../../app/fw/util/fn.service';
import { GlyphService } from '../../../../app/fw/svg/glyph.service';
+import { ActivatedRoute, Params } from '@angular/router';
import { UrlFnService } from '../../../../app/fw/remote/urlfn.service';
import { WSock } from '../../../../app/fw/remote/wsock.service';
+import { of } from 'rxjs';
-class MockFnService {}
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
class MockGlyphService {}
-class MockUrlFnService {}
-
class MockWSock {}
/**
* ONOS GUI -- Remote -- Web Socket Service - Unit Tests
*/
describe('WebSocketService', () => {
- let log: LogService;
- const windowMock = <any>{ location: <any> { hostname: 'localhost' } };
+ let wss: WebSocketService;
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+
+ const noop = () => ({});
+ const send = jasmine.createSpy('send')
+ .and.callFake((ev) => ev);
+ const mockWebSocket = {
+ send: send,
+ onmessage: (msgEvent) => ({}),
+ onopen: () => ({}),
+ onclose: () => ({}),
+ };
beforeEach(() => {
- log = new ConsoleLoggerService();
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({'debug': 'txrx'});
+
+ windowMock = <any>{
+ location: <any> {
+ hostname: 'foo',
+ host: 'foo',
+ port: '80',
+ protocol: 'http',
+ search: { debug: 'true'},
+ href: 'ws://foo:123/onos/ui/websock/path',
+ absUrl: 'ws://foo:123/onos/ui/websock/path'
+ }
+ };
+ fs = new FnService(ar, logSpy, windowMock);
TestBed.configureTestingModule({
providers: [WebSocketService,
- { provide: FnService, useClass: MockFnService },
- { provide: LogService, useValue: log },
+ { provide: FnService, useValue: fs },
+ { provide: LogService, useValue: logSpy },
{ provide: GlyphService, useClass: MockGlyphService },
- { provide: UrlFnService, useClass: MockUrlFnService },
- { provide: WSock, useClass: MockWSock },
- { provide: Window, useValue: windowMock },
+ { provide: UrlFnService, useValue: new UrlFnService(logSpy, windowMock) },
+ { provide: Window, useFactory: (() => windowMock ) },
+ { provide: WSock, useFactory: (() => {
+ return {
+ newWebSocket: (() => mockWebSocket)
+ };
+ })
+ }
]
});
+
+ wss = TestBed.get(WebSocketService);
+ logServiceSpy = TestBed.get(LogService);
});
- it('should be created', inject([WebSocketService], (service: WebSocketService) => {
- expect(service).toBeTruthy();
- }));
+ it('should define WebSocketService', () => {
+ expect(wss).toBeDefined();
+ });
+
+ it('should define api functions', () => {
+ expect(fs.areFunctions(wss, ['bootstrap', 'error',
+ 'handleOpen', 'handleMessage', 'handleClose',
+ 'findGuiSuccessor', 'informListeners', 'send',
+ 'noHandlersWarn', 'resetState',
+ 'createWebSocket', 'bindHandlers', 'unbindHandlers',
+ 'addOpenListener', 'removeOpenListener', 'sendEvent',
+ 'setVeilDelegate', 'setLoadingDelegate'
+ ])).toBeTruthy();
+ });
+
+ it('should use the appropriate URL, createWebsocket', () => {
+ const url = wss.createWebSocket();
+ expect(url).toEqual('ws://foo:80/onos/ui/websock/core');
+ });
+
+ it('should use the appropriate URL with modified port, createWebsocket',
+ () => {
+ const url = wss.createWebSocket(<WsOptions>{ wsport: 1243 });
+ expect(url).toEqual('ws://foo:1243/onos/ui/websock/core');
+ });
+
+ it('should verify websocket event handlers, createWebsocket', () => {
+ wss.createWebSocket({ wsport: 1234 });
+ expect(fs.isF(mockWebSocket.onopen)).toBeTruthy();
+ expect(fs.isF(mockWebSocket.onmessage)).toBeTruthy();
+ expect(fs.isF(mockWebSocket.onclose)).toBeTruthy();
+ });
+
+ it('should invoke listener callbacks when websocket is up, handleOpen',
+ () => {
+ let num = 0;
+ function incrementNum(host: string, url: string) {
+ expect(host).toEqual('foo');
+ num++;
+ }
+ wss.addOpenListener(incrementNum);
+ wss.createWebSocket({ wsport: 1234 });
+
+ mockWebSocket.onopen();
+ expect(num).toBe(1);
+ });
+
+ it('should send pending events, handleOpen', () => {
+ const fakeEvent = {
+ event: 'mockEv',
+ payload: { mock: 'thing' }
+ };
+ wss.sendEvent(fakeEvent.event, fakeEvent.payload);
+ // on opening the socket, a single authentication event should have
+ // been sent already...
+ expect(mockWebSocket.send.calls.count()).toEqual(1);
+
+ wss.createWebSocket({ wsport: 1234 });
+ mockWebSocket.onopen();
+ expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify(fakeEvent));
+ });
+
+ it('should handle an incoming bad JSON message, handleMessage', () => {
+ const badMsg = {
+ data: 'bad message'
+ };
+ wss.createWebSocket({ wsport: 1234 });
+ expect(mockWebSocket.onmessage(badMsg)).toBeNull();
+ expect(logServiceSpy.error).toHaveBeenCalled();
+ });
+
+ it('should verify message was handled, handleMessage', () => {
+ let num = 0;
+ function fakeHandler(data1: Object) { num++; }
+ const data = JSON.stringify(<EventType>{
+ event: 'mockEvResp',
+ payload: {}
+ });
+ const event = {
+ data: data
+ };
+
+ wss.createWebSocket({ wsport: 1234 });
+ wss.bindHandlers(new Map<string, (data) => void>([
+ ['mockEvResp', (data2) => fakeHandler(data2)]
+ ]));
+ expect(mockWebSocket.onmessage(event)).toBe(undefined);
+ expect(num).toBe(1);
+ });
+
+ it('should warn if there is an unhandled event, handleMessage', () => {
+ const data = { foo: 'bar', bar: 'baz'};
+ const dataString = JSON.stringify(data);
+ const badEv = {
+ data: dataString
+ };
+ wss.createWebSocket({ wsport: 1234 });
+ mockWebSocket.onmessage(badEv);
+ expect(logServiceSpy.warn).toHaveBeenCalledWith('Unhandled event:', data);
+ });
+
+ it('should not warn if valid input, bindHandlers', () => {
+ expect(wss.bindHandlers(new Map<string, (data) => void>([
+ ['test', noop ],
+ ['bar', noop ]
+ ]))).toBe(undefined);
+
+ expect(logServiceSpy.warn).not.toHaveBeenCalled();
+ });
+
+ it('should warn if no arguments, bindHandlers', () => {
+ expect(wss.bindHandlers(
+ new Map<string, (data) => void>([])
+ )).toBeNull();
+ expect(logServiceSpy.warn).toHaveBeenCalledWith(
+ 'WSS.bindHandlers(): no event handlers'
+ );
+ });
+
+ it('should warn if duplicate handlers were given, bindHandlers',
+ () => {
+ wss.bindHandlers(
+ new Map<string, (data) => void>([
+ ['noop', noop ]
+ ])
+ );
+ expect(wss.bindHandlers(
+ new Map<string, (data) => void>([
+ ['noop', noop ]
+ ])
+ )).toBe(undefined);
+ expect(logServiceSpy.warn).toHaveBeenCalledWith('duplicate bindings ignored:',
+ ['noop']);
+ });
+
+ it('should warn if no arguments, unbindHandlers', () => {
+ expect(wss.unbindHandlers(
+ new Map<string, (data) => void>([])
+ )).toBeNull();
+ expect(logServiceSpy.warn).toHaveBeenCalledWith(
+ 'WSS.unbindHandlers(): no event handlers'
+ );
+ });
+ // Note: cannot test unbindHandlers' forEach due to it using closure variable
+
+ it('should not warn if valid argument, addOpenListener', () => {
+ let o = wss.addOpenListener(noop);
+ expect(o.id === 1);
+ expect(o.cb === noop);
+ expect(logServiceSpy.warn).not.toHaveBeenCalled();
+ o = wss.addOpenListener(noop);
+ expect(o.id === 2);
+ expect(o.cb === noop);
+ expect(logServiceSpy.warn).not.toHaveBeenCalled();
+ });
+
+ it('should log error if callback not a function, addOpenListener',
+ () => {
+ const o = wss.addOpenListener(null);
+ expect(o.id === 1);
+ expect(o.cb === null);
+ expect(o.error === 'No callback defined');
+ expect(logServiceSpy.error).toHaveBeenCalledWith(
+ 'WSS.addOpenListener(): callback not a function'
+ );
+ });
+
+ it('should not warn if valid listener object, removeOpenListener', () => {
+ expect(wss.removeOpenListener(<Callback>{
+ id: 1,
+ error: 'error',
+ cb: noop
+ })).toBe(undefined);
+ expect(logServiceSpy.warn).not.toHaveBeenCalled();
+ });
+
+ it('should warn if listener is invalid, removeOpenListener', () => {
+ expect(wss.removeOpenListener(<Callback>{})).toBeNull();
+ expect(logServiceSpy.warn).toHaveBeenCalledWith(
+ 'WSS.removeOpenListener(): invalid listener', {}
+ );
+ });
+
+ // Note: handleClose is not currently tested due to all work it does relies
+ // on closure variables that cannot be mocked
+
});
diff --git a/web/gui2/src/main/webapp/tests/app/fw/util/fn.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/util/fn.service.spec.ts
index ff6ceaf..84b5f094 100644
--- a/web/gui2/src/main/webapp/tests/app/fw/util/fn.service.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/fw/util/fn.service.spec.ts
@@ -20,6 +20,7 @@
import { FnService } from '../../../../app/fw/util/fn.service';
import { ActivatedRoute, Params } from '@angular/router';
import { of } from 'rxjs';
+import * as d3 from 'd3';
class MockActivatedRoute extends ActivatedRoute {
constructor(params: Params) {
@@ -32,22 +33,460 @@
* ONOS GUI -- Util -- General Purpose Functions - Unit Tests
*/
describe('FnService', () => {
- let log: LogService;
let ar: ActivatedRoute;
+ let fs: FnService;
+ let mockWindow: Window;
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+
+ const someFunction = () => {};
+ const someArray = [1, 2, 3];
+ const someObject = { foo: 'bar'};
+ const someNumber = 42;
+ const someString = 'xyyzy';
+ const someDate = new Date();
+ const stringArray = ['foo', 'bar'];
beforeEach(() => {
- log = new ConsoleLoggerService();
+ const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn']);
ar = new MockActivatedRoute({'debug': 'TestService'});
+ mockWindow = <any>{
+ innerWidth: 400,
+ innerHeight: 200,
+ navigator: {
+ userAgent: 'defaultUA'
+ }
+ };
+
TestBed.configureTestingModule({
providers: [FnService,
- { provide: LogService, useValue: log },
+ { provide: LogService, useValue: logSpy },
{ provide: ActivatedRoute, useValue: ar },
+ { provide: Window, useFactory: (() => mockWindow ) }
]
});
+
+ fs = TestBed.get(FnService);
+ logServiceSpy = TestBed.get(LogService);
});
- it('should be created', inject([FnService], (service: FnService) => {
- expect(service).toBeTruthy();
- }));
+ it('should be created', () => {
+ expect(fs).toBeTruthy();
+ });
+
+ // === Tests for isF()
+ it('isF(): null for undefined', () => {
+ expect(fs.isF(undefined)).toBeNull();
+ });
+
+ it('isF(): null for null', () => {
+ expect(fs.isF(null)).toBeNull();
+ });
+ it('isF(): the reference for function', () => {
+ expect(fs.isF(someFunction)).toBe(someFunction);
+ });
+ it('isF(): null for string', () => {
+ expect(fs.isF(someString)).toBeNull();
+ });
+ it('isF(): null for number', () => {
+ expect(fs.isF(someNumber)).toBeNull();
+ });
+ it('isF(): null for Date', () => {
+ expect(fs.isF(someDate)).toBeNull();
+ });
+ it('isF(): null for array', () => {
+ expect(fs.isF(someArray)).toBeNull();
+ });
+ it('isF(): null for object', () => {
+ expect(fs.isF(someObject)).toBeNull();
+ });
+
+ // === Tests for isA()
+ it('isA(): null for undefined', () => {
+ expect(fs.isA(undefined)).toBeNull();
+ });
+ it('isA(): null for null', () => {
+ expect(fs.isA(null)).toBeNull();
+ });
+ it('isA(): null for function', () => {
+ expect(fs.isA(someFunction)).toBeNull();
+ });
+ it('isA(): null for string', () => {
+ expect(fs.isA(someString)).toBeNull();
+ });
+ it('isA(): null for number', () => {
+ expect(fs.isA(someNumber)).toBeNull();
+ });
+ it('isA(): null for Date', () => {
+ expect(fs.isA(someDate)).toBeNull();
+ });
+ it('isA(): the reference for array', () => {
+ expect(fs.isA(someArray)).toBe(someArray);
+ });
+ it('isA(): null for object', () => {
+ expect(fs.isA(someObject)).toBeNull();
+ });
+
+ // === Tests for isS()
+ it('isS(): null for undefined', () => {
+ expect(fs.isS(undefined)).toBeNull();
+ });
+ it('isS(): null for null', () => {
+ expect(fs.isS(null)).toBeNull();
+ });
+ it('isS(): null for function', () => {
+ expect(fs.isS(someFunction)).toBeNull();
+ });
+ it('isS(): the reference for string', () => {
+ expect(fs.isS(someString)).toBe(someString);
+ });
+ it('isS(): null for number', () => {
+ expect(fs.isS(someNumber)).toBeNull();
+ });
+ it('isS(): null for Date', () => {
+ expect(fs.isS(someDate)).toBeNull();
+ });
+ it('isS(): null for array', () => {
+ expect(fs.isS(someArray)).toBeNull();
+ });
+ it('isS(): null for object', () => {
+ expect(fs.isS(someObject)).toBeNull();
+ });
+
+ // === Tests for isO()
+ it('isO(): null for undefined', () => {
+ expect(fs.isO(undefined)).toBeNull();
+ });
+ it('isO(): null for null', () => {
+ expect(fs.isO(null)).toBeNull();
+ });
+ it('isO(): null for function', () => {
+ expect(fs.isO(someFunction)).toBeNull();
+ });
+ it('isO(): null for string', () => {
+ expect(fs.isO(someString)).toBeNull();
+ });
+ it('isO(): null for number', () => {
+ expect(fs.isO(someNumber)).toBeNull();
+ });
+ it('isO(): null for Date', () => {
+ expect(fs.isO(someDate)).toBeNull();
+ });
+ it('isO(): null for array', () => {
+ expect(fs.isO(someArray)).toBeNull();
+ });
+ it('isO(): the reference for object', () => {
+ expect(fs.isO(someObject)).toBe(someObject);
+ });
+
+
+ // === Tests for contains()
+ it('contains(): false for non-array', () => {
+ expect(fs.contains(null, 1)).toBeFalsy();
+ });
+ it('contains(): true for contained item', () => {
+ expect(fs.contains(someArray, 1)).toBeTruthy();
+ expect(fs.contains(stringArray, 'bar')).toBeTruthy();
+ });
+ it('contains(): false for non-contained item', () => {
+ expect(fs.contains(someArray, 109)).toBeFalsy();
+ expect(fs.contains(stringArray, 'zonko')).toBeFalsy();
+ });
+
+ // === Tests for areFunctions()
+ it('areFunctions(): true for empty-array', () => {
+ expect(fs.areFunctions({}, [])).toBeTruthy();
+ });
+ it('areFunctions(): true for some api', () => {
+ expect(fs.areFunctions({
+ a: () => {},
+ b: () => {}
+ }, ['b', 'a'])).toBeTruthy();
+ });
+ it('areFunctions(): false for some other api', () => {
+ expect(fs.areFunctions({
+ a: () => {},
+ b: 'not-a-function'
+ }, ['b', 'a'])).toBeFalsy();
+ });
+ it('areFunctions(): extraneous stuff NOT ignored', () => {
+ expect(fs.areFunctions({
+ a: () => {},
+ b: () => {},
+ c: 1,
+ d: 'foo'
+ }, ['a', 'b'])).toBeFalsy();
+ });
+ it('areFunctions(): extraneous stuff ignored (alternate fn)', () => {
+ expect(fs.areFunctionsNonStrict({
+ a: () => {},
+ b: () => {},
+ c: 1,
+ d: 'foo'
+ }, ['a', 'b'])).toBeTruthy();
+ });
+
+ // == use the now-tested areFunctions() on our own api:
+ it('should define api functions', () => {
+ expect(fs.areFunctions(fs, [
+ 'isF', 'isA', 'isS', 'isO', 'contains',
+ 'areFunctions', 'areFunctionsNonStrict', 'windowSize',
+ 'isMobile', 'isChrome', 'isChromeHeadless', 'isSafari',
+ 'isFirefox', 'parseDebugFlags',
+ 'debugOn', 'debug', 'find', 'inArray', 'removeFromArray',
+ 'isEmptyObject', 'cap', 'noPx', 'noPxStyle', 'endsWith',
+ 'inEvilList', 'analyze', 'sanitize'
+// 'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'sameObjProps', 'containsObj', 'cap',
+// 'eecode', 'noPx', 'noPxStyle', 'endsWith', 'addToTrie', 'removeFromTrie', 'trieLookup',
+// 'classNames', 'extend', 'sanitize'
+ ])).toBeTruthy();
+ });
+
+
+ // === Tests for windowSize()
+ it('windowSize(): adjust height', () => {
+ const dim = fs.windowSize(50);
+ expect(dim.width).toEqual(400);
+ expect(dim.height).toEqual(150);
+ });
+
+ it('windowSize(): adjust width', () => {
+ const dim = fs.windowSize(0, 50);
+ expect(dim.width).toEqual(350);
+ expect(dim.height).toEqual(200);
+ });
+
+ it('windowSize(): adjust width and height', () => {
+ const dim = fs.windowSize(101, 201);
+ expect(dim.width).toEqual(199);
+ expect(dim.height).toEqual(99);
+ });
+
+ // === Tests for isMobile()
+ const uaMap = {
+ chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) ' +
+ 'AppleWebKit/537.36 (KHTML, like Gecko) ' +
+ 'Chrome/41.0.2272.89 Safari/537.36',
+
+ iPad: 'Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) ' +
+ 'AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 ' +
+ 'Mobile/11A465 Safari/9537.53',
+
+ iPhone: 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) ' +
+ 'AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 ' +
+ 'Mobile/11A465 Safari/9537.53'
+ };
+
+ function setUa(key) {
+ const str = uaMap[key];
+ expect(str).toBeTruthy();
+ (<any>mockWindow.navigator).userAgent = str;
+ }
+
+ it('isMobile(): should be false for Chrome on Mac OS X', () => {
+ setUa('chrome');
+ expect(fs.isMobile()).toBe(false);
+ });
+ it('isMobile(): should be true for Safari on iPad', () => {
+ setUa('iPad');
+ expect(fs.isMobile()).toBe(true);
+ });
+ it('isMobile(): should be true for Safari on iPhone', () => {
+ setUa('iPhone');
+ expect(fs.isMobile()).toBe(true);
+ });
+
+ // === Tests for find()
+ const dataset = [
+ { id: 'foo', name: 'Furby'},
+ { id: 'bar', name: 'Barbi'},
+ { id: 'baz', name: 'Basil'},
+ { id: 'goo', name: 'Gabby'},
+ { id: 'zoo', name: 'Zevvv'}
+ ];
+
+ it('should not find ooo', () => {
+ expect(fs.find('ooo', dataset)).toEqual(-1);
+ });
+ it('should find foo', () => {
+ expect(fs.find('foo', dataset)).toEqual(0);
+ });
+ it('should find zoo', () => {
+ expect(fs.find('zoo', dataset)).toEqual(4);
+ });
+
+ it('should not find Simon', () => {
+ expect(fs.find('Simon', dataset, 'name')).toEqual(-1);
+ });
+ it('should find Furby', () => {
+ expect(fs.find('Furby', dataset, 'name')).toEqual(0);
+ });
+ it('should find Zevvv', () => {
+ expect(fs.find('Zevvv', dataset, 'name')).toEqual(4);
+ });
+
+
+ // === Tests for inArray()
+ const objRef = { x: 1, y: 2 };
+ const array = [1, 3.14, 'hey', objRef, 'there', true];
+ const array2 = ['b', 'a', 'd', 'a', 's', 's'];
+
+ it('should not find HOO', () => {
+ expect(fs.inArray('HOO', array)).toEqual(-1);
+ });
+ it('should find 1', () => {
+ expect(fs.inArray(1, array)).toEqual(0);
+ });
+ it('should find pi', () => {
+ expect(fs.inArray(3.14, array)).toEqual(1);
+ });
+ it('should find hey', () => {
+ expect(fs.inArray('hey', array)).toEqual(2);
+ });
+ it('should find the object', () => {
+ expect(fs.inArray(objRef, array)).toEqual(3);
+ });
+ it('should find there', () => {
+ expect(fs.inArray('there', array)).toEqual(4);
+ });
+ it('should find true', () => {
+ expect(fs.inArray(true, array)).toEqual(5);
+ });
+
+ it('should find the first occurrence A', () => {
+ expect(fs.inArray('a', array2)).toEqual(1);
+ });
+ it('should find the first occurrence S', () => {
+ expect(fs.inArray('s', array2)).toEqual(4);
+ });
+ it('should not find X', () => {
+ expect(fs.inArray('x', array2)).toEqual(-1);
+ });
+
+ // === Tests for removeFromArray()
+ it('should keep the array the same, for non-match', () => {
+ const array1 = [1, 2, 3];
+ expect(fs.removeFromArray(4, array1)).toBe(false);
+ expect(array1).toEqual([1, 2, 3]);
+ });
+ it('should remove a value', () => {
+ const array1a = [1, 2, 3];
+ expect(fs.removeFromArray(2, array1a)).toBe(true);
+ expect(array1a).toEqual([1, 3]);
+ });
+ it('should remove the first occurrence', () => {
+ const array1b = ['x', 'y', 'z', 'z', 'y'];
+ expect(fs.removeFromArray('y', array1b)).toBe(true);
+ expect(array1b).toEqual(['x', 'z', 'z', 'y']);
+ expect(fs.removeFromArray('x', array1b)).toBe(true);
+ expect(array1b).toEqual(['z', 'z', 'y']);
+ });
+
+ // === Tests for isEmptyObject()
+ it('should return true if an object is empty', () => {
+ expect(fs.isEmptyObject({})).toBe(true);
+ });
+ it('should return false if an object is not empty', () => {
+ expect(fs.isEmptyObject({foo: 'bar'})).toBe(false);
+ });
+
+ // === Tests for cap()
+ it('should ignore non-alpha', () => {
+ expect(fs.cap('123')).toEqual('123');
+ });
+ it('should capitalize first char', () => {
+ expect(fs.cap('Foo')).toEqual('Foo');
+ expect(fs.cap('foo')).toEqual('Foo');
+ expect(fs.cap('foo bar')).toEqual('Foo bar');
+ expect(fs.cap('FOO BAR')).toEqual('Foo bar');
+ expect(fs.cap('foo Bar')).toEqual('Foo bar');
+ });
+
+ // === Tests for noPx()
+ it('should return the value without px suffix', () => {
+ expect(fs.noPx('10px')).toBe(10);
+ expect(fs.noPx('500px')).toBe(500);
+ expect(fs.noPx('-80px')).toBe(-80);
+ });
+
+ // === Tests for noPxStyle()
+ it('should give a style\'s property without px suffix', () => {
+ const d3Elem = d3.select('body')
+ .append('div')
+ .attr('id', 'fooElem')
+ .style('width', '500px')
+ .style('height', '200px')
+ .style('font-size', '12px');
+ expect(fs.noPxStyle(d3Elem, 'width')).toBe(500);
+ expect(fs.noPxStyle(d3Elem, 'height')).toBe(200);
+ expect(fs.noPxStyle(d3Elem, 'font-size')).toBe(12);
+ d3.select('#fooElem').remove();
+ });
+
+ // === Tests for endsWith()
+ it('should return true if string ends with foo', () => {
+ expect(fs.endsWith('barfoo', 'foo')).toBe(true);
+ });
+
+ it('should return false if string doesnt end with foo', () => {
+ expect(fs.endsWith('barfood', 'foo')).toBe(false);
+ });
+
+ // === Tests for sanitize()
+ it('should return foo', () => {
+ expect(fs.sanitize('foo')).toEqual('foo');
+ });
+ it('should retain < b > tags', () => {
+ const str = 'foo <b>bar</b> baz';
+ expect(fs.sanitize(str)).toEqual(str);
+ });
+ it('should retain < i > tags', () => {
+ const str = 'foo <i>bar</i> baz';
+ expect(fs.sanitize(str)).toEqual(str);
+ });
+ it('should retain < p > tags', () => {
+ const str = 'foo <p>bar</p> baz';
+ expect(fs.sanitize(str)).toEqual(str);
+ });
+ it('should retain < em > tags', () => {
+ const str = 'foo <em>bar</em> baz';
+ expect(fs.sanitize(str)).toEqual(str);
+ });
+ it('should retain < strong > tags', () => {
+ const str = 'foo <strong>bar</strong> baz';
+ expect(fs.sanitize(str)).toEqual(str);
+ });
+
+ it('should reject < a > tags', () => {
+ expect(fs.sanitize('test <a href="hah">something</a> this'))
+ .toEqual('test something this');
+ });
+
+ it('should log a warning for < script > tags', () => {
+ expect(fs.sanitize('<script>alert("foo");</script>'))
+ .toEqual('');
+ expect(logServiceSpy.warn).toHaveBeenCalledWith(
+ 'Unsanitary HTML input -- <script> detected!'
+ );
+ });
+ it('should log a warning for < style > tags', () => {
+ expect(fs.sanitize('<style> h1 {color:red;} </style>'))
+ .toEqual('');
+ expect(logServiceSpy.warn).toHaveBeenCalledWith(
+ 'Unsanitary HTML input -- <style> detected!'
+ );
+ });
+
+ it('should log a warning for < iframe > tags', () => {
+ expect(fs.sanitize('Foo<iframe><body><h1>fake</h1></body></iframe>Bar'))
+ .toEqual('FooBar');
+ expect(logServiceSpy.warn).toHaveBeenCalledWith(
+ 'Unsanitary HTML input -- <iframe> detected!'
+ );
+ });
+
+ it('should completely strip < script >, remove < a >, retain < i >', () => {
+ expect(fs.sanitize('Hey <i>this</i> is <script>alert("foo");</script> <a href="meh">cool</a>'))
+ .toEqual('Hey <i>this</i> is cool');
+ });
});
diff --git a/web/gui2/src/main/webapp/tests/app/fw/util/lion.service.spec.ts b/web/gui2/src/main/webapp/tests/app/fw/util/lion.service.spec.ts
index d471005..6535f07 100644
--- a/web/gui2/src/main/webapp/tests/app/fw/util/lion.service.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/fw/util/lion.service.spec.ts
@@ -15,27 +15,64 @@
*
*/
import { TestBed, inject } from '@angular/core/testing';
+import { of } from 'rxjs';
import { LogService } from '../../../../app/log.service';
import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
+import { ActivatedRoute, Params } from '@angular/router';
+import { FnService } from '../../../../app/fw/util/fn.service';
+import { GlyphService } from '../../../../app/fw/svg/glyph.service';
import { LionService } from '../../../../app/fw/util/lion.service';
-import { WebSocketService } from '../../../../app/fw/remote/websocket.service';
+import { UrlFnService } from '../../../../app/fw/remote/urlfn.service';
+import { WSock } from '../../../../app/fw/remote/wsock.service';
+import { WebSocketService, WsOptions } from '../../../../app/fw/remote/websocket.service';
-class MockWebSocketService {}
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+class MockWSock {}
+
+class MockGlyphService {}
+
+class MockUrlFnService {}
/**
* ONOS GUI -- Lion -- Localization Utilities - Unit Tests
*/
describe('LionService', () => {
let log: LogService;
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
beforeEach(() => {
log = new ConsoleLoggerService();
+ ar = new MockActivatedRoute({'debug': 'TestService'});
+ windowMock = <any>{
+ location: <any> {
+ hostname: '',
+ host: '',
+ port: '',
+ protocol: '',
+ search: { debug: 'true'},
+ href: ''
+ }
+ };
+ fs = new FnService(ar, log, windowMock);
TestBed.configureTestingModule({
providers: [LionService,
+ { provide: FnService, useValue: fs },
+ { provide: GlyphService, useClass: MockGlyphService },
{ provide: LogService, useValue: log },
- { provide: WebSocketService, useClass: MockWebSocketService },
+ { provide: UrlFnService, useClass: MockUrlFnService },
+ { provide: WSock, useClass: MockWSock },
+ { provide: WebSocketService, useClass: WebSocketService },
+ { provide: Window, useFactory: (() => windowMock ) },
]
});
});
diff --git a/web/gui2/src/main/webapp/tests/app/onos.component.spec.ts b/web/gui2/src/main/webapp/tests/app/onos.component.spec.ts
index 2831d97..7a15504 100644
--- a/web/gui2/src/main/webapp/tests/app/onos.component.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/onos.component.spec.ts
@@ -14,15 +14,21 @@
* limitations under the License.
*/
import { TestBed, async } from '@angular/core/testing';
-import { RouterModule, RouterOutlet, ChildrenOutletContexts } from '@angular/router';
+import { RouterModule, RouterOutlet, ChildrenOutletContexts, ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+
import { LogService } from '../../app/log.service';
import { ConsoleLoggerService } from '../../app/consolelogger.service';
+
import { IconComponent } from '../../app/fw/svg/icon/icon.component';
import { MastComponent } from '../../app/fw/mast/mast/mast.component';
import { NavComponent } from '../../app/fw/nav/nav/nav.component';
import { OnosComponent } from '../../app/onos.component';
+import { VeilComponent } from '../../app/fw/layer/veil/veil.component';
+
import { DialogService } from '../../app/fw/layer/dialog.service';
import { EeService } from '../../app/fw/util/ee.service';
+import { FnService } from '../../app/fw/util/fn.service';
import { GlyphService } from '../../app/fw/svg/glyph.service';
import { IconService } from '../../app/fw/svg/icon.service';
import { KeyService } from '../../app/fw/util/key.service';
@@ -31,10 +37,17 @@
import { OnosService } from '../../app/onos.service';
import { PanelService } from '../../app/fw/layer/panel.service';
import { QuickHelpService } from '../../app/fw/layer/quickhelp.service';
+import { SvgUtilService } from '../../app/fw/svg/svgutil.service';
import { ThemeService } from '../../app/fw/util/theme.service';
import { SpriteService } from '../../app/fw/svg/sprite.service';
-import { VeilService } from '../../app/fw/layer/veil.service';
-import { WebSocketService } from '../../app/fw/remote/websocket.service';
+import { WebSocketService, WsOptions } from '../../app/fw/remote/websocket.service';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
class MockDialogService {}
@@ -46,8 +59,6 @@
class MockKeyService {}
-class MockLionService {}
-
class MockNavService {}
class MockOnosService {}
@@ -60,18 +71,34 @@
class MockThemeService {}
-class MockVeilService {}
-
-class MockWebSocketService {}
+class MockVeilComponent {}
/**
* ONOS GUI -- Onos Component - Unit Tests
*/
describe('OnosComponent', () => {
let log: LogService;
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
beforeEach(async(() => {
log = new ConsoleLoggerService();
+ ar = new MockActivatedRoute({'debug': 'TestService'});
+
+ windowMock = <any>{
+ location: <any> {
+ hostname: '',
+ host: '',
+ port: '',
+ protocol: '',
+ search: { debug: 'true'},
+ href: ''
+ },
+ innerHeight: 240,
+ innerWidth: 320
+ };
+ fs = new FnService(ar, log, windowMock);
TestBed.configureTestingModule({
declarations: [
@@ -79,16 +106,17 @@
MastComponent,
NavComponent,
OnosComponent,
+ VeilComponent,
RouterOutlet
],
providers: [
{ provide: ChildrenOutletContexts, useClass: ChildrenOutletContexts },
{ provide: DialogService, useClass: MockDialogService },
{ provide: EeService, useClass: MockEeService },
+ { provide: FnService, useValue: fs },
{ provide: GlyphService, useClass: MockGlyphService },
{ provide: IconService, useClass: MockIconService },
{ provide: KeyService, useClass: MockKeyService },
- { provide: LionService, useClass: MockLionService },
{ provide: LogService, useValue: log },
{ provide: NavService, useClass: MockNavService },
{ provide: OnosService, useClass: MockOnosService },
@@ -96,8 +124,7 @@
{ provide: PanelService, useClass: MockPanelService },
{ provide: SpriteService, useClass: MockSpriteService },
{ provide: ThemeService, useClass: MockThemeService },
- { provide: VeilService, useClass: MockVeilService },
- { provide: WebSocketService, useClass: MockWebSocketService },
+ { provide: Window, useFactory: (() => windowMock ) },
]
}).compileComponents();
}));
diff --git a/web/gui2/src/main/webapp/tests/app/view/device/device.component.spec.ts b/web/gui2/src/main/webapp/tests/app/view/device/device.component.spec.ts
index edfaed4..960d241 100644
--- a/web/gui2/src/main/webapp/tests/app/view/device/device.component.spec.ts
+++ b/web/gui2/src/main/webapp/tests/app/view/device/device.component.spec.ts
@@ -18,24 +18,36 @@
import { LogService } from '../../../../app/log.service';
import { ConsoleLoggerService } from '../../../../app/consolelogger.service';
import { DeviceComponent } from '../../../../app/view/device/device.component';
+
import { DetailsPanelService } from '../../../../app/fw/layer/detailspanel.service';
-import { FnService } from '../../../../app/fw/util/fn.service';
+import { FnService, WindowSize } from '../../../../app/fw/util/fn.service';
import { IconService } from '../../../../app/fw/svg/icon.service';
+import { GlyphService } from '../../../../app/fw/svg/glyph.service';
import { KeyService } from '../../../../app/fw/util/key.service';
import { LoadingService } from '../../../../app/fw/layer/loading.service';
import { NavService } from '../../../../app/fw/nav/nav.service';
import { MastService } from '../../../../app/fw/mast/mast.service';
import { PanelService } from '../../../../app/fw/layer/panel.service';
+import { SvgUtilService } from '../../../../app/fw/svg/svgutil.service';
import { TableBuilderService } from '../../../../app/fw/widget/tablebuilder.service';
import { TableDetailService } from '../../../../app/fw/widget/tabledetail.service';
import { WebSocketService } from '../../../../app/fw/remote/websocket.service';
class MockDetailsPanelService {}
-class MockFnService {}
+class MockFnService {
+ windowSize(offH: number = 0, offW: number = 0): WindowSize {
+ return {
+ height: 123,
+ width: 456
+ };
+ }
+}
class MockIconService {}
+class MockGlyphService {}
+
class MockKeyService {}
class MockLoadingService {
@@ -74,6 +86,7 @@
{ provide: DetailsPanelService, useClass: MockDetailsPanelService },
{ provide: FnService, useClass: MockFnService },
{ provide: IconService, useClass: MockIconService },
+ { provide: GlyphService, useClass: MockGlyphService },
{ provide: KeyService, useClass: MockKeyService },
{ provide: LoadingService, useClass: MockLoadingService },
{ provide: MastService, useClass: MockMastService },
@@ -91,7 +104,7 @@
beforeEach(() => {
fixture = TestBed.createComponent(DeviceComponent);
- component = fixture.componentInstance;
+ component = fixture.componentInstance;
fixture.detectChanges();
});