Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2018-present Open Networking Foundation |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | import { TestBed, inject } from '@angular/core/testing'; |
| 17 | import {ActivatedRoute, Params} from '@angular/router'; |
| 18 | |
| 19 | import { KeysService, KeysToken } from './keys.service'; |
| 20 | import { FnService } from './fn.service'; |
| 21 | import { LogService } from '../log.service'; |
| 22 | import { NavService } from '../nav/nav.service'; |
| 23 | |
| 24 | import {of} from 'rxjs'; |
| 25 | import * as d3 from 'd3'; |
| 26 | |
| 27 | class MockActivatedRoute extends ActivatedRoute { |
| 28 | constructor(params: Params) { |
| 29 | super(); |
| 30 | this.queryParams = of(params); |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | class MockNavService {} |
| 35 | |
| 36 | /* |
| 37 | ONOS GUI -- Key Handler Service - Unit Tests |
| 38 | */ |
| 39 | describe('KeysService', () => { |
| 40 | let ar: ActivatedRoute; |
| 41 | let fs: FnService; |
| 42 | let ks: KeysService; |
| 43 | let mockWindow: Window; |
| 44 | let logServiceSpy: jasmine.SpyObj<LogService>; |
| 45 | |
| 46 | const qhs: any = {}; |
| 47 | let d3Elem: any; |
| 48 | let elem: any; |
| 49 | let last: any; |
| 50 | |
| 51 | beforeEach(() => { |
| 52 | const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn', 'info']); |
| 53 | ar = new MockActivatedRoute({'debug': 'TestService'}); |
| 54 | mockWindow = <any>{ |
| 55 | innerWidth: 400, |
| 56 | innerHeight: 200, |
| 57 | navigator: { |
| 58 | userAgent: 'defaultUA' |
| 59 | }, |
| 60 | location: <any>{ |
| 61 | hostname: 'foo', |
| 62 | host: 'foo', |
| 63 | port: '80', |
| 64 | protocol: 'http', |
| 65 | search: { debug: 'true' }, |
| 66 | href: 'ws://foo:123/onos/ui/websock/path', |
| 67 | absUrl: 'ws://foo:123/onos/ui/websock/path' |
| 68 | } |
| 69 | }; |
| 70 | fs = new FnService(ar, logSpy, mockWindow); |
| 71 | |
| 72 | d3Elem = d3.select('body').append('p').attr('id', 'ptest'); |
| 73 | elem = d3Elem.node(); |
| 74 | last = { |
| 75 | view: null, |
| 76 | key: null, |
| 77 | code: null, |
| 78 | ev: null |
| 79 | }; |
| 80 | |
| 81 | TestBed.configureTestingModule({ |
| 82 | providers: [KeysService, |
| 83 | { provide: FnService, useValue: fs}, |
| 84 | { provide: LogService, useValue: logSpy }, |
| 85 | { provide: ActivatedRoute, useValue: ar }, |
| 86 | { provide: NavService, useClass: MockNavService}, |
| 87 | { provide: 'Window', useFactory: (() => mockWindow ) } |
| 88 | ] |
| 89 | }); |
| 90 | ks = TestBed.get(KeysService); |
| 91 | ks.installOn(d3Elem); |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 92 | logServiceSpy = TestBed.get(LogService); |
| 93 | }); |
| 94 | |
| 95 | afterEach(() => { |
| 96 | d3.select('#ptest').remove(); |
| 97 | }); |
| 98 | |
| 99 | it('should be created', () => { |
| 100 | expect(ks).toBeTruthy(); |
| 101 | }); |
| 102 | |
| 103 | it('should define api functions', () => { |
| 104 | expect(fs.areFunctions(ks, [ |
Sean Condon | b2c483c | 2019-01-16 20:28:55 +0000 | [diff] [blame] | 105 | 'installOn', 'keyBindings', 'unbindKeys', 'dialogKeys', |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 106 | 'addSeq', 'remSeq', 'gestureNotes', 'enableKeys', 'enableGlobalKeys', |
| 107 | 'checkNotGlobal', 'getKeyBindings', |
| 108 | 'matchSeq', 'whatKey', 'textFieldInput', 'keyIn', 'qhlion', 'qhlionShowHide', |
| 109 | 'qhlionHintEsc', 'qhlionHintT', 'setupGlobalKeys', 'quickHelp', |
| 110 | 'escapeKey', 'toggleTheme', 'filterMaskedKeys', 'unexParam', |
| 111 | 'setKeyBindings', 'bindDialogKeys', 'unbindDialogKeys' |
| 112 | ])).toBeTruthy(); |
| 113 | }); |
| 114 | |
| 115 | function jsKeyDown(element, code: string, keyName: string) { |
| 116 | const ev = new KeyboardEvent('keydown', |
| 117 | { code: code, key: keyName }); |
| 118 | |
| 119 | // Chromium Hack |
| 120 | // if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1) { |
| 121 | // Object.defineProperty(ev, 'keyCode', { |
| 122 | // get: () => { return this.keyCodeVal; } |
| 123 | // }); |
| 124 | // Object.defineProperty(ev, 'which', { |
| 125 | // get: () => { return this.keyCodeVal; } |
| 126 | // }); |
| 127 | // } |
| 128 | |
| 129 | if (ev.code !== code.toString()) { |
| 130 | console.warn('keyCode mismatch ' + ev.code + |
Sean Condon | dfc6dba | 2019-11-09 11:50:23 +0000 | [diff] [blame] | 131 | '(' + ev.toString() + ') -> ' + code); |
Sean Condon | f4f54a1 | 2018-10-10 23:25:46 +0100 | [diff] [blame] | 132 | } |
| 133 | element.dispatchEvent(ev); |
| 134 | } |
| 135 | |
| 136 | // === Key binding related tests |
| 137 | it('should start with default key bindings', () => { |
| 138 | const state = ks.getKeyBindings(); |
| 139 | const gk = state.globalKeys; |
| 140 | const mk = state.maskedKeys; |
| 141 | const vk = state.viewKeys; |
| 142 | const vf = state.viewFunction; |
| 143 | |
| 144 | expect(gk.length).toEqual(4); |
| 145 | ['backSlash', 'slash', 'esc', 'T'].forEach((k) => { |
| 146 | expect(fs.contains(gk, k)).toBeTruthy(); |
| 147 | }); |
| 148 | |
| 149 | expect(mk.length).toEqual(3); |
| 150 | ['backSlash', 'slash', 'T'].forEach((k) => { |
| 151 | expect(fs.contains(mk, k)).toBeTruthy(); |
| 152 | }); |
| 153 | |
| 154 | expect(vk.length).toEqual(0); |
| 155 | expect(vf).toBeFalsy(); |
| 156 | }); |
| 157 | |
| 158 | function bindTestKeys(withDescs?) { |
| 159 | const keys = ['A', '1', 'F5', 'equals']; |
| 160 | const kb = {}; |
| 161 | |
| 162 | function cb(view, key, code, ev) { |
| 163 | last.view = view; |
| 164 | last.key = key; |
| 165 | last.code = code; |
| 166 | last.ev = ev; |
| 167 | } |
| 168 | |
| 169 | function bind(k) { |
| 170 | return withDescs ? |
| 171 | [(view, key, code, ev) => {cb(view, key, code, ev); }, 'desc for key ' + k] : |
| 172 | (view, key, code, ev) => {cb(view, key, code, ev); }; |
| 173 | } |
| 174 | |
| 175 | keys.forEach((k) => { |
| 176 | kb[k] = bind(k); |
| 177 | }); |
| 178 | |
| 179 | ks.keyBindings(kb); |
| 180 | } |
| 181 | |
| 182 | function verifyCall(key, code) { |
| 183 | // TODO: update expectation, when view tokens are implemented |
| 184 | expect(last.view).toEqual(KeysToken.KEYEV); |
| 185 | last.view = null; |
| 186 | |
| 187 | expect(last.key).toEqual(key); |
| 188 | last.key = null; |
| 189 | |
| 190 | expect(last.code).toEqual(code); |
| 191 | last.code = null; |
| 192 | |
| 193 | expect(last.ev).toBeTruthy(); |
| 194 | last.ev = null; |
| 195 | } |
| 196 | |
| 197 | function verifyNoCall() { |
| 198 | expect(last.view).toBeNull(); |
| 199 | expect(last.key).toBeNull(); |
| 200 | expect(last.code).toBeNull(); |
| 201 | expect(last.ev).toBeNull(); |
| 202 | } |
| 203 | |
| 204 | function verifyTestKeys() { |
| 205 | jsKeyDown(elem, '65', 'A'); // 'A' |
| 206 | verifyCall('A', '65'); |
| 207 | jsKeyDown(elem, '66', 'B'); // 'B' |
| 208 | verifyNoCall(); |
| 209 | |
| 210 | jsKeyDown(elem, '49', '1'); // '1' |
| 211 | verifyCall('1', '49'); |
| 212 | jsKeyDown(elem, '50', '2'); // '2' |
| 213 | verifyNoCall(); |
| 214 | |
| 215 | jsKeyDown(elem, '116', 'F5'); // 'F5' |
| 216 | verifyCall('F5', '116'); |
| 217 | jsKeyDown(elem, '117', 'F6'); // 'F6' |
| 218 | verifyNoCall(); |
| 219 | |
| 220 | jsKeyDown(elem, '187', '='); // 'equals' |
| 221 | verifyCall('equals', '187'); |
| 222 | jsKeyDown(elem, '189', '-'); // 'dash' |
| 223 | verifyNoCall(); |
| 224 | |
| 225 | const vk = ks.getKeyBindings().viewKeys; |
| 226 | |
| 227 | expect(vk.length).toEqual(4); |
| 228 | ['A', '1', 'F5', 'equals'].forEach((k) => { |
| 229 | expect(fs.contains(vk, k)).toBeTruthy(); |
| 230 | }); |
| 231 | |
| 232 | expect(ks.getKeyBindings().viewFunction).toBeFalsy(); |
| 233 | } |
| 234 | |
| 235 | it('should allow specific key bindings', () => { |
| 236 | bindTestKeys(); |
| 237 | verifyTestKeys(); |
| 238 | }); |
| 239 | |
| 240 | it('should allow specific key bindings with descriptions', () => { |
| 241 | bindTestKeys(true); |
| 242 | verifyTestKeys(); |
| 243 | }); |
| 244 | |
| 245 | it('should warn about masked keys', () => { |
| 246 | const k = { |
| 247 | 'space': (token, key, code, ev) => cb(token, key, code, ev), |
| 248 | 'T': (token, key, code, ev) => cb(token, key, code, ev) |
| 249 | }; |
| 250 | let count = 0; |
| 251 | |
| 252 | function cb(token, key, code, ev) { |
| 253 | count++; |
| 254 | // console.debug('count = ' + count, token, key, code); |
| 255 | } |
| 256 | |
| 257 | ks.keyBindings(k); |
| 258 | |
| 259 | expect(logServiceSpy.warn).toHaveBeenCalledWith('setKeyBindings()\n: Key "T" is reserved'); |
| 260 | |
| 261 | // the 'T' key should NOT invoke our callback |
| 262 | expect(count).toEqual(0); |
| 263 | jsKeyDown(elem, '84', 'T'); // 'T' |
| 264 | expect(count).toEqual(0); |
| 265 | |
| 266 | // but the 'space' key SHOULD invoke our callback |
| 267 | jsKeyDown(elem, '32', ' '); // 'space' |
| 268 | expect(count).toEqual(1); |
| 269 | }); |
| 270 | |
| 271 | it('should block keys when disabled', () => { |
| 272 | let cbCount = 0; |
| 273 | |
| 274 | function cb() { cbCount++; } |
| 275 | |
| 276 | function pressA() { jsKeyDown(elem, '65', 'A'); } // 65 == 'A' keycode |
| 277 | |
| 278 | ks.keyBindings({ A: () => cb() }); |
| 279 | |
| 280 | expect(cbCount).toBe(0); |
| 281 | |
| 282 | pressA(); |
| 283 | expect(cbCount).toBe(1); |
| 284 | |
| 285 | ks.enableKeys(false); |
| 286 | pressA(); |
| 287 | expect(cbCount).toBe(1); |
| 288 | |
| 289 | ks.enableKeys(true); |
| 290 | pressA(); |
| 291 | expect(cbCount).toBe(2); |
| 292 | }); |
| 293 | |
| 294 | // === Gesture notes related tests |
| 295 | it('should start with no notes', () => { |
| 296 | expect(ks.gestureNotes()).toEqual([]); |
| 297 | }); |
| 298 | |
| 299 | it('should allow us to add nodes', () => { |
| 300 | const notes = [ |
| 301 | ['one', 'something about one'], |
| 302 | ['two', 'description of two'] |
| 303 | ]; |
| 304 | ks.gestureNotes(notes); |
| 305 | |
| 306 | expect(ks.gestureNotes()).toEqual(notes); |
| 307 | }); |
| 308 | |
| 309 | it('should ignore non-arrays', () => { |
| 310 | ks.gestureNotes({foo: 4}); |
| 311 | expect(ks.gestureNotes()).toEqual([]); |
| 312 | }); |
| 313 | |
| 314 | // Consider adding test to ensure array contains 2-tuples of strings |
| 315 | }); |