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.
5 // Include test fixture.
6 GEN_INCLUDE(['../testing/chromevox_unittest_base.js']);
8 GEN_INCLUDE(['../testing/fake_objects.js']);
10 // Fake out the Chrome API namespace we depend on.
12 /** Fake chrome.runtime object. */
14 /** Fake chrome.virtualKeyboardPrivate object. */
15 chrome.virtualKeyboardPrivate = {};
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
22 * @param {FakePort} port A fake port.
23 * @param {cvox.BrailleInputHandler} inputHandler to work with.
26 function FakeEditor(port, inputHandler) {
27 /** @private {FakePort} */
29 /** @private {cvox.BrailleInputHandler} */
30 this.inputHandler_ = inputHandler;
31 /** @private {string} */
33 /** @private {number} */
34 this.selectionStart_ = 0;
35 /** @private {number} */
36 this.selectionEnd_ = 0;
37 /** @private {number} */
39 /** @private {boolean} */
40 this.allowDeletes_ = false;
41 port.postMessage = goog.bind(this.handleMessage_, this);
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.
54 FakeEditor.prototype.setContent = function(
55 text, selectionStart, opt_selectionEnd) {
57 this.selectionStart_ = selectionStart;
58 this.selectionEnd_ = goog.isDef(opt_selectionEnd) ?
59 opt_selectionEnd : selectionStart;
60 this.callOnDisplayContentChanged_();
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.
70 FakeEditor.prototype.select = function(selectionStart, opt_selectionEnd) {
71 this.setContent(this.text_, selectionStart, opt_selectionEnd);
76 * Inserts text into the edit field, optionally selecting the inserted
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.
82 FakeEditor.prototype.insert = function(newText, opt_select) {
84 this.text_.substring(0, this.selectionStart_) +
86 this.text_.substring(this.selectionEnd_);
88 this.selectionEnd_ = this.selectionStart_ + newText.length;
90 this.selectionStart_ += newText.length;
91 this.selectionEnd_ = this.selectionStart_;
93 this.callOnDisplayContentChanged_();
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
101 * @param {boolean} allowDeletes The new value.
103 FakeEditor.prototype.setAllowDeletes = function(allowDeletes) {
104 this.allowDeletes_ = allowDeletes;
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.
113 FakeEditor.prototype.setActive = function(value) {
114 this.message_({type: 'activeState', active: value});
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.
126 FakeEditor.prototype.assertContentIs = function(
127 text, selectionStart, opt_selectionEnd) {
128 var selectionEnd = goog.isDef(opt_selectionEnd) ? opt_selectionEnd :
130 assertEquals(text, this.text_);
131 assertEquals(selectionStart, this.selectionStart_);
132 assertEquals(selectionEnd, this.selectionEnd_);
137 * Sends a message from the IME to the input handler.
138 * @param {Object} msg The message to send.
141 FakeEditor.prototype.message_ = function(msg) {
142 var listener = this.port_.onMessage.getListener();
143 assertNotEquals(null, listener);
149 * Calls the {@code onDisplayContentChanged} method of the input handler
150 * with the current editor content and selection.
153 FakeEditor.prototype.callOnDisplayContentChanged_ = function() {
154 this.inputHandler_.onDisplayContentChanged(
155 cvox.BrailleUtil.createValue(
156 this.text_, this.selectionStart_, this.selectionEnd_));
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).
166 FakeEditor.prototype.focus = function(fieldType) {
168 this.message_({type: 'inputContext',
169 context: {type: fieldType,
170 contextID: this.contextID_}});
175 * Inform the input handler that focus left the input field.
177 FakeEditor.prototype.blur = function() {
178 this.message_({type: 'inputContext', context: null});
184 * Handles a message from the input handler to the IME.
185 * @param {Object} msg The message.
188 FakeEditor.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_);
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_();
205 this.insert(newText);
209 * Fakes a {@code Port} used for message passing in the Chrome extension APIs.
212 function 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_};
224 * Mapping from braille cells to Unicode characters.
225 * @const Array.<Array.<string> >
227 var UNCONTRACTED_TABLE = [
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']
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.
246 var CONTRACTED_TABLE = [
247 ['12 1235 123', 'braille'],
249 ['1456', 'this']].concat(UNCONTRACTED_TABLE);
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.
258 function FakeTranslator(table, opt_capitalize) {
260 this.table_ = table.map(function(entry) {
261 var cells = entry[0];
263 if (cells[0] === '^') {
265 cells = cells.substring(1);
267 if (cells[cells.length - 1] === '$') {
269 cells = cells.substring(0, cells.length - 1);
271 result[0] = cellsToArray(cells);
272 result[1] = entry[1];
275 /** @private {boolean} */
276 this.capitalize_ = opt_capitalize || false;
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.
285 FakeTranslator.prototype.backTranslate = function(cells, callback) {
286 var cellsArray = new Uint8Array(cells);
289 while (pos < cellsArray.length) {
291 outer: for (var i = 0, entry; entry = this.table_[i]; ++i) {
292 if (pos + entry[0].length > cellsArray.length) {
295 if (entry.start && pos > 0 && cellsArray[pos - 1] !== 0) {
298 for (var j = 0; j < entry[0].length; ++j) {
299 if (entry[0][j] !== cellsArray[pos + j]) {
303 if (entry.end && pos + j < cellsArray.length &&
304 cellsArray[pos + j] !== 0) {
312 'Backtranslating ' + cellsArray[pos] + ' at ' + pos);
314 pos += match[0].length;
316 if (this.capitalize_) {
317 result = result.toUpperCase();
322 /** @extends {cvox.BrailleTranslatorManager} */
323 function FakeTranslatorManager() {
326 FakeTranslatorManager.prototype = {
327 defaultTranslator: null,
328 uncontractedTranslator: null,
329 changeListener: null,
332 getDefaultTranslator: function() {
333 return this.defaultTranslator;
337 getUncontractedTranslator: function() {
338 return this.uncontractedTranslator;
342 addChangeListener: function(listener) {
343 assertEquals(null, this.changeListener);
346 setTranslators: function(defaultTranslator, uncontractedTranslator) {
347 this.defaultTranslator = defaultTranslator;
348 this.uncontractedTranslator = uncontractedTranslator;
349 if (this.changeListener) {
350 this.changeListener();
356 * Converts a list of cells, represented as a string, to an array.
357 * @param {string} cells A string with space separated groups of digits.
358 * Each group corresponds to one braille cell and each digit in a group
359 * corresponds to a particular dot in the cell (1 to 8). As a special
360 * case, the digit 0 by itself represents a blank cell.
361 * @return {Array.<number>} An array with each cell encoded as a bit
362 * pattern (dot 1 uses bit 0, etc).
364 function cellsToArray(cells) {
365 return cells.split(/\s+/).map(function(cellString) {
367 assertTrue(cellString.length > 0);
368 if (cellString != '0') {
369 for (var i = 0; i < cellString.length; ++i) {
370 var dot = cellString.charCodeAt(i) - '0'.charCodeAt(0);
371 assertTrue(dot >= 1);
372 assertTrue(dot <= 8);
373 cell |= 1 << (dot - 1);
383 * @extends {ChromeVoxUnitTestBase}
385 function CvoxBrailleInputHandlerUnitTest() {}
387 CvoxBrailleInputHandlerUnitTest.prototype = {
388 __proto__: ChromeVoxUnitTestBase.prototype,
392 'cvox.BrailleInputHandler',
397 * Creates an editor and establishes a connection from the IME.
398 * @return {FakeEditor}
400 createEditor: function() {
401 chrome.runtime.onConnectExternal.getListener()(this.port);
402 return new FakeEditor(this.port, this.inputHandler);
406 * Sends a series of braille cells to the input handler.
407 * @param {string} cells Braille cells, encoded as described in
408 * {@code cellsToArray}.
409 * @return {boolean} {@code true} iff all cells were sent successfully.
411 sendCells: function(cells) {
412 return cellsToArray(cells).reduce(function(prevResult, cell) {
413 var event = {command: cvox.BrailleKeyCommand.DOTS, brailleDots: cell};
414 return prevResult && this.inputHandler.onBrailleKeyEvent(event);
419 * Sends a standard key event (such as backspace) to the braille input
421 * @param {string} keyCode The key code name.
422 * @return {boolean} Whether the event was handled.
424 sendKeyEvent: function(keyCode) {
425 var event = {command: cvox.BrailleKeyCommand.STANDARD_KEY,
426 standardKeyCode: keyCode};
427 return this.inputHandler.onBrailleKeyEvent(event);
431 * Shortcut for asserting that the value expansion mode is {@code NONE}.
433 assertExpandingNone: function() {
434 assertEquals(cvox.ExpandingBrailleTranslator.ExpansionType.NONE,
435 this.inputHandler.getExpansionType());
439 * Shortcut for asserting that the value expansion mode is {@code SELECTION}.
441 assertExpandingSelection: function() {
442 assertEquals(cvox.ExpandingBrailleTranslator.ExpansionType.SELECTION,
443 this.inputHandler.getExpansionType());
447 * Shortcut for asserting that the value expansion mode is {@code ALL}.
449 assertExpandingAll: function() {
450 assertEquals(cvox.ExpandingBrailleTranslator.ExpansionType.ALL,
451 this.inputHandler.getExpansionType());
454 storeKeyEvent: function(event, opt_callback) {
455 var storedCopy = {keyCode: event.keyCode, keyName: event.keyName,
456 charValue: event.charValue};
457 if (event.type == 'keydown') {
458 this.keyEvents.push(storedCopy);
460 assertEquals('keyup', event.type);
461 assertTrue(this.keyEvents.length > 0);
462 assertEqualsJSON(storedCopy, this.keyEvents[this.keyEvents.length - 1]);
464 if (goog.isDef(opt_callback)) {
471 chrome.runtime.onConnectExternal = new FakeChromeEvent();
472 this.port = new FakePort();
473 this.translatorManager = new FakeTranslatorManager();
474 this.inputHandler = new cvox.BrailleInputHandler(this.translatorManager);
475 this.inputHandler.init();
476 this.uncontractedTranslator = new FakeTranslator(UNCONTRACTED_TABLE);
477 this.contractedTranslator = new FakeTranslator(CONTRACTED_TABLE, true);
478 chrome.virtualKeyboardPrivate.sendKeyEvent =
479 this.storeKeyEvent.bind(this);
484 TEST_F('CvoxBrailleInputHandlerUnitTest', 'ConnectFromUnknownExtension',
486 this.port.sender.id = 'your unknown friend';
487 chrome.runtime.onConnectExternal.getListener()(this.port);
488 this.port.onMessage.assertNoListener();
492 TEST_F('CvoxBrailleInputHandlerUnitTest', 'NoTranslator', function() {
493 var editor = this.createEditor();
494 editor.setContent('blah', 0);
495 editor.setActive(true);
496 editor.focus('email');
497 assertFalse(this.sendCells('145 135 125'));
498 editor.setActive(false);
500 editor.assertContentIs('blah', 0);
504 TEST_F('CvoxBrailleInputHandlerUnitTest', 'InputUncontracted', function() {
505 this.translatorManager.setTranslators(this.uncontractedTranslator, null);
506 var editor = this.createEditor();
507 editor.setActive(true);
509 // Focus and type in a text field.
510 editor.focus('text');
511 assertTrue(this.sendCells('125 15 123 123 135')); // hello
512 editor.assertContentIs('hello', 'hello'.length);
513 this.assertExpandingNone();
515 // Move the cursor and type in the middle.
517 assertTrue(this.sendCells('0 2345 125 15 1235 15 0')); // ' there '
518 editor.assertContentIs('he there llo', 'he there '.length);
520 // Field changes by some other means.
521 editor.insert('you!');
522 // Then type on the braille keyboard again.
523 assertTrue(this.sendCells('0 125 15')); // ' he'
524 editor.assertContentIs('he there you! hello', 'he there you! he'.length);
527 editor.setActive(false);
531 TEST_F('CvoxBrailleInputHandlerUnitTest', 'InputContracted', function() {
532 var editor = this.createEditor();
533 this.translatorManager.setTranslators(this.contractedTranslator,
534 this.uncontractedTranslator);
535 editor.setActive(true);
536 editor.focus('text');
537 this.assertExpandingSelection();
539 // First, type a 'b'.
540 assertTrue(this.sendCells('12'));
541 // Remember that the contracted translator produces uppercase.
542 editor.assertContentIs('BUT', 'BUT'.length);
543 this.assertExpandingNone();
545 // From here on, the input handler needs to replace already entered text.
546 editor.setAllowDeletes(true);
547 // Typing 'rl' changes to a different contraction.
548 assertTrue(this.sendCells('1235 123'));
549 editor.assertContentIs('BRAILLE', 'BRAILLE'.length);
550 // Now, finish the word.
551 assertTrue(this.sendCells('0'));
552 editor.assertContentIs('BRAILLE ', 'BRAILLE '.length);
553 this.assertExpandingNone();
555 // Move the cursor to the beginning.
557 this.assertExpandingSelection();
559 // Typing now uses the uncontracted table.
560 assertTrue(this.sendCells('12')); // 'b'
561 editor.assertContentIs('bBRAILLE ', 1);
562 this.assertExpandingSelection();
563 editor.select('bBRAILLE'.length);
564 this.assertExpandingSelection();
565 assertTrue(this.sendCells('12')); // 'b'
566 editor.assertContentIs('bBRAILLEb ', 'bBRAILLEb'.length);
567 // Move to the end, where contracted typing should work.
568 editor.select('bBRAILLEb '.length);
569 assertTrue(this.sendCells('1456 0')); // Symbol for 'this', then space.
570 this.assertExpandingNone();
571 editor.assertContentIs('bBRAILLEb THIS ', 'bBRAILLEb this '.length);
573 // Move between the two words.
574 editor.select('bBRAILLEb'.length);
575 this.assertExpandingSelection();
576 assertTrue(this.sendCells('0'));
577 assertTrue(this.sendCells('12')); // 'b' for 'but'
578 editor.assertContentIs('bBRAILLEb BUT THIS ', 'bBRAILLEb BUT'.length);
579 this.assertExpandingNone();
583 TEST_F('CvoxBrailleInputHandlerUnitTest', 'TypingUrlWithContracted',
585 var editor = this.createEditor();
586 this.translatorManager.setTranslators(this.contractedTranslator,
587 this.uncontractedTranslator);
588 editor.setActive(true);
590 this.assertExpandingAll();
591 assertTrue(this.sendCells('1245')); // 'g'
592 editor.insert('oogle.com', true /*select*/);
593 editor.assertContentIs('google.com', 1, 'google.com'.length);
594 this.assertExpandingAll();
595 this.sendCells('135'); // 'o'
596 editor.insert('ogle.com', true /*select*/);
597 editor.assertContentIs('google.com', 2, 'google.com'.length);
598 this.assertExpandingAll();
600 editor.assertContentIs('go ', 'go '.length);
601 // In a URL, even when the cursor is in whitespace, all of the value
602 // is expanded to uncontracted braille.
603 this.assertExpandingAll();
607 TEST_F('CvoxBrailleInputHandlerUnitTest', 'Backspace', function() {
608 var editor = this.createEditor();
609 this.translatorManager.setTranslators(this.contractedTranslator,
610 this.uncontractedTranslator);
611 editor.setActive(true);
612 editor.focus('text');
614 // Add some text that we can delete later.
615 editor.setContent('Text ', 'Text '.length);
617 // The IME needs to delete text, even when typing.
618 editor.setAllowDeletes(true);
619 // Type 'brl' to make sure replacement works when deleting text.
620 assertTrue(this.sendCells('12 1235 123'));
621 editor.assertContentIs('Text BRAILLE', 'Text BRAILLE'.length);
623 // Delete what we just typed, one cell at a time.
624 this.sendKeyEvent('Backspace');
625 editor.assertContentIs('Text BR', 'Text BR'.length);
626 this.sendKeyEvent('Backspace');
627 editor.assertContentIs('Text BUT', 'Text BUT'.length);
628 this.sendKeyEvent('Backspace');
629 editor.assertContentIs('Text ', 'Text '.length);
631 // Now, backspace should be handled as usual, synthetizing key events.
632 assertEquals(0, this.keyEvents.length);
633 this.sendKeyEvent('Backspace');
634 assertEqualsJSON([{keyCode: 8, keyName: 'Backspace', charValue: 8}],
639 TEST_F('CvoxBrailleInputHandlerUnitTest', 'KeysImeNotActive', function() {
640 var editor = this.createEditor();
641 this.sendKeyEvent('Enter');
642 this.sendKeyEvent('ArrowUp');
643 assertEqualsJSON([{keyCode: 13, keyName: 'Enter', charValue: 0x0A},
644 {keyCode: 38, keyName: 'ArrowUp', charValue: 0x41}],