braille_input_handler_test.unitjs revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// Include test fixture. 6GEN_INCLUDE(['../../testing/chromevox_unittest_base.js']); 7 8GEN_INCLUDE(['../../testing/fake_objects.js']); 9 10// Fake out the Chrome API namespace we depend on. 11var chrome = {}; 12/** Fake chrome.runtime object. */ 13chrome.runtime = {}; 14/** Fake chrome.virtualKeyboardPrivate object. */ 15chrome.virtualKeyboardPrivate = {}; 16 17 18/** 19 * A fake input field that behaves like the Braille IME and also updates 20 * the input manager's knowledge about the display content when text changes 21 * in the edit field. 22 * @param {FakePort} port A fake port. 23 * @param {cvox.BrailleInputHandler} inputHandler to work with. 24 * @constructor 25 */ 26function FakeEditor(port, inputHandler) { 27 /** @private {FakePort} */ 28 this.port_ = port; 29 /** @private {cvox.BrailleInputHandler} */ 30 this.inputHandler_ = inputHandler; 31 /** @private {string} */ 32 this.text_ = ''; 33 /** @private {number} */ 34 this.selectionStart_ = 0; 35 /** @private {number} */ 36 this.selectionEnd_ = 0; 37 /** @private {number} */ 38 this.contextID_ = 0; 39 /** @private {boolean} */ 40 this.allowDeletes_ = false; 41 port.postMessage = goog.bind(this.handleMessage_, this); 42} 43 44 45/** 46 * Sets the content and selection (or cursor) of the edit field. 47 * This fakes what happens when the field is edited by other means than 48 * via the braille keyboard. 49 * @param {string} text Text to replace the current content of the field. 50 * @param {number} selectionStart Start of the selection or cursor position. 51 * @param {number=} opt_selectionEnd End of selection, or ommited if the 52 * selection is a cursor. 53 */ 54FakeEditor.prototype.setContent = function( 55 text, selectionStart, opt_selectionEnd) { 56 this.text_ = text; 57 this.selectionStart_ = selectionStart; 58 this.selectionEnd_ = goog.isDef(opt_selectionEnd) ? 59 opt_selectionEnd : selectionStart; 60 this.callOnDisplayContentChanged_(); 61}; 62 63 64/** 65 * Sets the selection in the editor. 66 * @param {number} selectionStart Start of the selection or cursor position. 67 * @param {number=} opt_selectionEnd End of selection, or ommited if the 68 * selection is a cursor. 69 */ 70FakeEditor.prototype.select = function(selectionStart, opt_selectionEnd) { 71 this.setContent(this.text_, selectionStart, opt_selectionEnd); 72}; 73 74 75/** 76 * Inserts text into the edit field, optionally selecting the inserted 77 * text. 78 * @param {string} newText Text to insert. 79 * @param {boolean=} opt_select If {@code true}, selects the inserted text, 80 * otherwise leaves the cursor at the end of the new text. 81 */ 82FakeEditor.prototype.insert = function(newText, opt_select) { 83 this.text_ = 84 this.text_.substring(0, this.selectionStart_) + 85 newText + 86 this.text_.substring(this.selectionEnd_); 87 if (opt_select) { 88 this.selectionEnd_ = this.selectionStart_ + newText.length; 89 } else { 90 this.selectionStart_ += newText.length; 91 this.selectionEnd_ = this.selectionStart_; 92 } 93 this.callOnDisplayContentChanged_(); 94}; 95 96 97/** 98 * Sets whether the editor should cause a test failure if the input handler 99 * tries to delete text before the cursor. By default, thi value is 100 * {@code false}. 101 * @param {boolean} allowDeletes The new value. 102 */ 103FakeEditor.prototype.setAllowDeletes = function(allowDeletes) { 104 this.allowDeletes_ = allowDeletes; 105}; 106 107 108/** 109 * Signals to the input handler that the Braille IME is active or not active, 110 * depending on the argument. 111 * @param {boolean} value Whether the IME is active or not. 112 */ 113FakeEditor.prototype.setActive = function(value) { 114 this.message_({type: 'activeState', active: value}); 115}; 116 117 118/** 119 * Fails if the current editor content and selection range don't match 120 * the arguments to this function. 121 * @param {string} text Text that should be in the field. 122 * @param {number} selectionStart Start of selection. 123 * @param {number+} opt_selectionEnd End of selection, default to selection 124 * start to indicate a cursor. 125 */ 126FakeEditor.prototype.assertContentIs = function( 127 text, selectionStart, opt_selectionEnd) { 128 var selectionEnd = goog.isDef(opt_selectionEnd) ? opt_selectionEnd : 129 selectionStart; 130 assertEquals(text, this.text_); 131 assertEquals(selectionStart, this.selectionStart_); 132 assertEquals(selectionEnd, this.selectionEnd_); 133}; 134 135 136/** 137 * Sends a message from the IME to the input handler. 138 * @param {Object} msg The message to send. 139 * @private 140 */ 141FakeEditor.prototype.message_ = function(msg) { 142 var listener = this.port_.onMessage.getListener(); 143 assertNotEquals(null, listener); 144 listener(msg); 145}; 146 147 148/** 149 * Calls the {@code onDisplayContentChanged} method of the input handler 150 * with the current editor content and selection. 151 * @private 152 */ 153FakeEditor.prototype.callOnDisplayContentChanged_ = function() { 154 this.inputHandler_.onDisplayContentChanged( 155 cvox.BrailleUtil.createValue( 156 this.text_, this.selectionStart_, this.selectionEnd_)); 157}; 158 159 160/** 161 * Informs the input handler that a new text field is focused. The content 162 * of the field is not cleared and should be updated separately. 163 * @param {string} fieldType The type of the field (see the documentation 164 * for the {@code chrome.input.ime} API). 165 */ 166FakeEditor.prototype.focus = function(fieldType) { 167 this.contextID_++; 168 this.message_({type: 'inputContext', 169 context: {type: fieldType, 170 contextID: this.contextID_}}); 171}; 172 173 174/** 175 * Inform the input handler that focus left the input field. 176 */ 177FakeEditor.prototype.blur = function() { 178 this.message_({type: 'inputContext', context: null}); 179 this.contextID_ = 0; 180}; 181 182 183/** 184 * Handles a message from the input handler to the IME. 185 * @param {Object} msg The message. 186 * @private 187 */ 188FakeEditor.prototype.handleMessage_ = function(msg) { 189 assertEquals('replaceText', msg.type); 190 assertEquals(this.contextID_, msg.contextID); 191 var deleteBefore = msg.deleteBefore; 192 var newText = msg.newText; 193 assertTrue(goog.isNumber(deleteBefore)); 194 assertTrue(goog.isString(newText)); 195 assertTrue(deleteBefore <= this.selectionStart_); 196 if (deleteBefore > 0) { 197 assertTrue(this.allowDeletes_); 198 this.text_ = 199 this.text_.substring(0, this.selectionStart_ - deleteBefore) + 200 this.text_.substring(this.selectionEnd_); 201 this.selectionStart_ -= deleteBefore; 202 this.selectionEnd_ = this.selectionStart_; 203 this.callOnDisplayContentChanged_(); 204 } 205 this.insert(newText); 206}; 207 208/* 209 * Fakes a {@code Port} used for message passing in the Chrome extension APIs. 210 * @constructor 211 */ 212function FakePort() { 213 /** @type {FakeChromeEvent} */ 214 this.onDisconnect = new FakeChromeEvent(); 215 /** @type {FakeChromeEvent} */ 216 this.onMessage = new FakeChromeEvent(); 217 /** @type {string} */ 218 this.name = cvox.BrailleInputHandler.IME_PORT_NAME_; 219 /** @type {{id: string}} */ 220 this.sender = {id: cvox.BrailleInputHandler.IME_EXTENSION_ID_}; 221} 222 223/** 224 * Mapping from braille cells to Unicode characters. 225 * @const Array.<Array.<string> > 226 */ 227var UNCONTRACTED_TABLE = [ 228 ['0', ' '], 229 ['1', 'a'], ['12', 'b'], ['14', 'c'], ['145', 'd'], ['15', 'e'], 230 ['124', 'f'], ['1245', 'g'], ['125', 'h'], ['24', 'i'], ['245', 'j'], 231 ['13', 'k'], ['123', 'l'], ['134', 'm'], ['1345', 'n'], ['135', 'o'], 232 ['1234', 'p'], ['12345', 'q'], ['1235', 'r'], ['234', 's'], ['2345', 't'] 233]; 234 235 236/** 237 * Mapping of braille cells to the corresponding word in Grade 2 US English 238 * braille. This table also includes the uncontracted table above. 239 * If a match 'pattern' starts with '^', it must be at the beginning of 240 * the string or be preceded by a blank cell. Similarly, '$' at the end 241 * of a 'pattern' means that the match must be at the end of the string 242 * or be followed by a blank cell. Note that order is significant in the 243 * table. First match wins. 244 * @const 245 */ 246var CONTRACTED_TABLE = [ 247 ['12 1235 123', 'braille'], 248 ['^12$', 'but'], 249 ['1456', 'this']].concat(UNCONTRACTED_TABLE); 250 251/** 252 * A fake braille translator that can do back translation according 253 * to one of the tables above. 254 * @param {Array.<Array.<number>>} table Backtranslation mapping. 255 * @param {boolean=} opt_capitalize Whether the result should be capitalized. 256 * @constructor 257 */ 258function FakeTranslator(table, opt_capitalize) { 259 /** @private */ 260 this.table_ = table.map(function(entry) { 261 var cells = entry[0]; 262 var result = []; 263 if (cells[0] === '^') { 264 result.start = true; 265 cells = cells.substring(1); 266 } 267 if (cells[cells.length - 1] === '$') { 268 result.end = true; 269 cells = cells.substring(0, cells.length - 1); 270 } 271 result[0] = cellsToArray(cells); 272 result[1] = entry[1]; 273 return result; 274 }); 275 /** @private {boolean} */ 276 this.capitalize_ = opt_capitalize || false; 277} 278 279 280/** 281 * Implements the {@code cvox.LibLouis.BrailleTranslator.backTranslate} method. 282 * @param {!ArrayBuffer} cells Cells to be translated. 283 * @param {function(?string)} callback Callback for result. 284 */ 285FakeTranslator.prototype.backTranslate = function(cells, callback) { 286 var cellsArray = new Uint8Array(cells); 287 var result = ''; 288 var pos = 0; 289 while (pos < cellsArray.length) { 290 var match = null; 291 outer: for (var i = 0, entry; entry = this.table_[i]; ++i) { 292 if (pos + entry[0].length > cellsArray.length) { 293 continue; 294 } 295 if (entry.start && pos > 0 && cellsArray[pos - 1] !== 0) { 296 continue; 297 } 298 for (var j = 0; j < entry[0].length; ++j) { 299 if (entry[0][j] !== cellsArray[pos + j]) { 300 continue outer; 301 } 302 } 303 if (entry.end && pos + j < cellsArray.length && 304 cellsArray[pos + j] !== 0) { 305 continue; 306 } 307 match = entry; 308 break; 309 } 310 assertNotEquals( 311 null, match, 312 'Backtranslating ' + cellsArray[pos] + ' at ' + pos); 313 result += match[1]; 314 pos += match[0].length; 315 } 316 if (this.capitalize_) { 317 result = result.toUpperCase(); 318 } 319 callback(result); 320}; 321 322/** 323 * Converts a list of cells, represented as a string, to an array. 324 * @param {string} cells A string with space separated groups of digits. 325 * Each group corresponds to one braille cell and each digit in a group 326 * corresponds to a particular dot in the cell (1 to 8). As a special 327 * case, the digit 0 by itself represents a blank cell. 328 * @return {Array.<number>} An array with each cell encoded as a bit 329 * pattern (dot 1 uses bit 0, etc). 330 */ 331function cellsToArray(cells) { 332 return cells.split(/\s+/).map(function(cellString) { 333 var cell = 0; 334 assertTrue(cellString.length > 0); 335 if (cellString != '0') { 336 for (var i = 0; i < cellString.length; ++i) { 337 var dot = cellString.charCodeAt(i) - '0'.charCodeAt(0); 338 assertTrue(dot >= 1); 339 assertTrue(dot <= 8); 340 cell |= 1 << (dot - 1); 341 } 342 } 343 return cell; 344 }); 345} 346 347/** 348 * Test fixture. 349 * @constructor 350 * @extends {ChromeVoxUnitTestBase} 351 */ 352function CvoxBrailleInputHandlerUnitTest() {} 353 354CvoxBrailleInputHandlerUnitTest.prototype = { 355 __proto__: ChromeVoxUnitTestBase.prototype, 356 357 /** @override */ 358 closureModuleDeps: [ 359 'cvox.BrailleInputHandler', 360 'cvox.BrailleUtil', 361 'cvox.Spannable', 362 ], 363 364 /** 365 * Creates an editor and establishes a connection from the IME. 366 * @return {FakeEditor} 367 */ 368 createEditor: function() { 369 chrome.runtime.onConnectExternal.getListener()(this.port); 370 return new FakeEditor(this.port, this.inputHandler); 371 }, 372 373 /** 374 * Sends a series of braille cells to the input handler. 375 * @param {string} cells Braille cells, encoded as described in 376 * {@code cellsToArray}. 377 * @return {boolean} {@code true} iff all cells were sent successfully. 378 */ 379 sendCells: function(cells) { 380 return cellsToArray(cells).reduce(function(prevResult, cell) { 381 var event = {command: cvox.BrailleKeyCommand.DOTS, brailleDots: cell}; 382 return prevResult && this.inputHandler.onBrailleKeyEvent(event); 383 }.bind(this), true); 384 }, 385 386 /** 387 * Sends a standard key event (such as backspace) to the braille input 388 * handler. 389 * @param {string} keyCode The key code name. 390 * @return {boolean} Whether the event was handled. 391 */ 392 sendKeyEvent: function(keyCode) { 393 var event = {command: cvox.BrailleKeyCommand.STANDARD_KEY, 394 standardKeyCode: keyCode}; 395 return this.inputHandler.onBrailleKeyEvent(event); 396 }, 397 398 /** 399 * Shortcut for asserting that the value expansion mode is {@code NONE}. 400 */ 401 assertExpandingNone: function() { 402 assertEquals(cvox.ExpandingBrailleTranslator.ExpansionType.NONE, 403 this.inputHandler.getExpansionType()); 404 }, 405 406 /** 407 * Shortcut for asserting that the value expansion mode is {@code SELECTION}. 408 */ 409 assertExpandingSelection: function() { 410 assertEquals(cvox.ExpandingBrailleTranslator.ExpansionType.SELECTION, 411 this.inputHandler.getExpansionType()); 412 }, 413 414 /** 415 * Shortcut for asserting that the value expansion mode is {@code ALL}. 416 */ 417 assertExpandingAll: function() { 418 assertEquals(cvox.ExpandingBrailleTranslator.ExpansionType.ALL, 419 this.inputHandler.getExpansionType()); 420 }, 421 422 storeKeyEvent: function(event, opt_callback) { 423 var storedCopy = {keyCode: event.keyCode, keyName: event.keyName, 424 charValue: event.charValue}; 425 if (event.type == 'keydown') { 426 this.keyEvents.push(storedCopy); 427 } else { 428 assertEquals('keyup', event.type); 429 assertTrue(this.keyEvents.length > 0); 430 assertEqualsJSON(storedCopy, this.keyEvents[this.keyEvents.length - 1]); 431 } 432 if (goog.isDef(opt_callback)) { 433 callback(); 434 } 435 }, 436 437 /** @override */ 438 setUp: function() { 439 chrome.runtime.onConnectExternal = new FakeChromeEvent(); 440 this.port = new FakePort(); 441 this.inputHandler = new cvox.BrailleInputHandler(); 442 this.inputHandler.init(); 443 this.uncontractedTranslator = new FakeTranslator(UNCONTRACTED_TABLE); 444 this.contractedTranslator = new FakeTranslator(CONTRACTED_TABLE, true); 445 chrome.virtualKeyboardPrivate.sendKeyEvent = 446 this.storeKeyEvent.bind(this); 447 this.keyEvents = []; 448 } 449}; 450 451TEST_F('CvoxBrailleInputHandlerUnitTest', 'ConnectFromUnknownExtension', 452 function() { 453 this.port.sender.id = 'your unknown friend'; 454 chrome.runtime.onConnectExternal.getListener()(this.port); 455 this.port.onMessage.assertNoListener(); 456}); 457 458 459TEST_F('CvoxBrailleInputHandlerUnitTest', 'NoTranslator', function() { 460 var editor = this.createEditor(); 461 editor.setContent('blah', 0); 462 editor.setActive(true); 463 editor.focus('email'); 464 assertFalse(this.sendCells('145 135 125')); 465 editor.setActive(false); 466 editor.blur(); 467 editor.assertContentIs('blah', 0); 468}); 469 470 471TEST_F('CvoxBrailleInputHandlerUnitTest', 'InputUncontracted', function() { 472 this.inputHandler.setTranslator(this.uncontractedTranslator); 473 var editor = this.createEditor(); 474 editor.setActive(true); 475 476 // Focus and type in a text field. 477 editor.focus('text'); 478 assertTrue(this.sendCells('125 15 123 123 135')); // hello 479 editor.assertContentIs('hello', 'hello'.length); 480 this.assertExpandingNone(); 481 482 // Move the cursor and type in the middle. 483 editor.select(2); 484 assertTrue(this.sendCells('0 2345 125 15 1235 15 0')); // ' there ' 485 editor.assertContentIs('he there llo', 'he there '.length); 486 487 // Field changes by some other means. 488 editor.insert('you!'); 489 // Then type on the braille keyboard again. 490 assertTrue(this.sendCells('0 125 15')); // ' he' 491 editor.assertContentIs('he there you! hello', 'he there you! he'.length); 492 493 editor.blur(); 494 editor.setActive(false); 495}); 496 497 498TEST_F('CvoxBrailleInputHandlerUnitTest', 'InputContracted', function() { 499 var editor = this.createEditor(); 500 this.inputHandler.setTranslator(this.contractedTranslator, 501 this.uncontractedTranslator); 502 editor.setActive(true); 503 editor.focus('text'); 504 this.assertExpandingSelection(); 505 506 // First, type a 'b'. 507 assertTrue(this.sendCells('12')); 508 // Remember that the contracted translator produces uppercase. 509 editor.assertContentIs('BUT', 'BUT'.length); 510 this.assertExpandingNone(); 511 512 // From here on, the input handler needs to replace already entered text. 513 editor.setAllowDeletes(true); 514 // Typing 'rl' changes to a different contraction. 515 assertTrue(this.sendCells('1235 123')); 516 editor.assertContentIs('BRAILLE', 'BRAILLE'.length); 517 // Now, finish the word. 518 assertTrue(this.sendCells('0')); 519 editor.assertContentIs('BRAILLE ', 'BRAILLE '.length); 520 this.assertExpandingNone(); 521 522 // Move the cursor to the beginning. 523 editor.select(0); 524 this.assertExpandingSelection(); 525 526 // Typing now uses the uncontracted table. 527 assertTrue(this.sendCells('12')); // 'b' 528 editor.assertContentIs('bBRAILLE ', 1); 529 this.assertExpandingSelection(); 530 editor.select('bBRAILLE'.length); 531 this.assertExpandingSelection(); 532 assertTrue(this.sendCells('12')); // 'b' 533 editor.assertContentIs('bBRAILLEb ', 'bBRAILLEb'.length); 534 // Move to the end, where contracted typing should work. 535 editor.select('bBRAILLEb '.length); 536 assertTrue(this.sendCells('1456 0')); // Symbol for 'this', then space. 537 this.assertExpandingNone(); 538 editor.assertContentIs('bBRAILLEb THIS ', 'bBRAILLEb this '.length); 539 540 // Move between the two words. 541 editor.select('bBRAILLEb'.length); 542 this.assertExpandingSelection(); 543 assertTrue(this.sendCells('0')); 544 assertTrue(this.sendCells('12')); // 'b' for 'but' 545 editor.assertContentIs('bBRAILLEb BUT THIS ', 'bBRAILLEb BUT'.length); 546 this.assertExpandingNone(); 547}); 548 549 550TEST_F('CvoxBrailleInputHandlerUnitTest', 'TypingUrlWithContracted', 551 function() { 552 var editor = this.createEditor(); 553 this.inputHandler.setTranslator(this.contractedTranslator, 554 this.uncontractedTranslator); 555 editor.setActive(true); 556 editor.focus('url'); 557 this.assertExpandingAll(); 558 assertTrue(this.sendCells('1245')); // 'g' 559 editor.insert('oogle.com', true /*select*/); 560 editor.assertContentIs('google.com', 1, 'google.com'.length); 561 this.assertExpandingAll(); 562 this.sendCells('135'); // 'o' 563 editor.insert('ogle.com', true /*select*/); 564 editor.assertContentIs('google.com', 2, 'google.com'.length); 565 this.assertExpandingAll(); 566 this.sendCells('0'); 567 editor.assertContentIs('go ', 'go '.length); 568 // In a URL, even when the cursor is in whitespace, all of the value 569 // is expanded to uncontracted braille. 570 this.assertExpandingAll(); 571}); 572 573 574TEST_F('CvoxBrailleInputHandlerUnitTest', 'Backspace', function() { 575 var editor = this.createEditor(); 576 this.inputHandler.setTranslator(this.contractedTranslator, 577 this.uncontractedTranslator); 578 editor.setActive(true); 579 editor.focus('text'); 580 581 // Add some text that we can delete later. 582 editor.setContent('Text ', 'Text '.length); 583 584 // The IME needs to delete text, even when typing. 585 editor.setAllowDeletes(true); 586 // Type 'brl' to make sure replacement works when deleting text. 587 assertTrue(this.sendCells('12 1235 123')); 588 editor.assertContentIs('Text BRAILLE', 'Text BRAILLE'.length); 589 590 // Delete what we just typed, one cell at a time. 591 this.sendKeyEvent('Backspace'); 592 editor.assertContentIs('Text BR', 'Text BR'.length); 593 this.sendKeyEvent('Backspace'); 594 editor.assertContentIs('Text BUT', 'Text BUT'.length); 595 this.sendKeyEvent('Backspace'); 596 editor.assertContentIs('Text ', 'Text '.length); 597 598 // Now, backspace should be handled as usual, synthetizing key events. 599 assertEquals(0, this.keyEvents.length); 600 this.sendKeyEvent('Backspace'); 601 assertEqualsJSON([{keyCode: 8, keyName: 'Backspace', charValue: 8}], 602 this.keyEvents); 603}); 604 605 606TEST_F('CvoxBrailleInputHandlerUnitTest', 'KeysImeNotActive', function() { 607 var editor = this.createEditor(); 608 this.sendKeyEvent('Enter'); 609 this.sendKeyEvent('ArrowUp'); 610 assertEqualsJSON([{keyCode: 13, keyName: 'Enter', charValue: 0x0A}, 611 {keyCode: 38, keyName: 'ArrowUp', charValue: 0x41}], 612 this.keyEvents); 613}); 614