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 /** @private {string} */
42 this.uncommittedText_ = '';
43 /** @private {?Array<number>} */
44 this.extraCells_ = [];
45 port.postMessage = goog.bind(this.handleMessage_, this);
50 * Sets the content and selection (or cursor) of the edit field.
51 * This fakes what happens when the field is edited by other means than
52 * via the braille keyboard.
53 * @param {string} text Text to replace the current content of the field.
54 * @param {number} selectionStart Start of the selection or cursor position.
55 * @param {number=} opt_selectionEnd End of selection, or ommited if the
56 * selection is a cursor.
58 FakeEditor.prototype.setContent = function(
59 text, selectionStart, opt_selectionEnd) {
61 this.selectionStart_ = selectionStart;
62 this.selectionEnd_ = goog.isDef(opt_selectionEnd) ?
63 opt_selectionEnd : selectionStart;
64 this.callOnDisplayContentChanged_();
69 * Sets the selection in the editor.
70 * @param {number} selectionStart Start of the selection or cursor position.
71 * @param {number=} opt_selectionEnd End of selection, or ommited if the
72 * selection is a cursor.
74 FakeEditor.prototype.select = function(selectionStart, opt_selectionEnd) {
75 this.setContent(this.text_, selectionStart, opt_selectionEnd);
80 * Inserts text into the edit field, optionally selecting the inserted
82 * @param {string} newText Text to insert.
83 * @param {boolean=} opt_select If {@code true}, selects the inserted text,
84 * otherwise leaves the cursor at the end of the new text.
86 FakeEditor.prototype.insert = function(newText, opt_select) {
88 this.text_.substring(0, this.selectionStart_) +
90 this.text_.substring(this.selectionEnd_);
92 this.selectionEnd_ = this.selectionStart_ + newText.length;
94 this.selectionStart_ += newText.length;
95 this.selectionEnd_ = this.selectionStart_;
97 this.callOnDisplayContentChanged_();
102 * Sets whether the editor should cause a test failure if the input handler
103 * tries to delete text before the cursor. By default, thi value is
105 * @param {boolean} allowDeletes The new value.
107 FakeEditor.prototype.setAllowDeletes = function(allowDeletes) {
108 this.allowDeletes_ = allowDeletes;
113 * Signals to the input handler that the Braille IME is active or not active,
114 * depending on the argument.
115 * @param {boolean} value Whether the IME is active or not.
117 FakeEditor.prototype.setActive = function(value) {
118 this.message_({type: 'activeState', active: value});
123 * Fails if the current editor content and selection range don't match
124 * the arguments to this function.
125 * @param {string} text Text that should be in the field.
126 * @param {number} selectionStart Start of selection.
127 * @param {number+} opt_selectionEnd End of selection, default to selection
128 * start to indicate a cursor.
130 FakeEditor.prototype.assertContentIs = function(
131 text, selectionStart, opt_selectionEnd) {
132 var selectionEnd = goog.isDef(opt_selectionEnd) ? opt_selectionEnd :
134 assertEquals(text, this.text_);
135 assertEquals(selectionStart, this.selectionStart_);
136 assertEquals(selectionEnd, this.selectionEnd_);
141 * Asserts that the uncommitted text last sent to the IME is the given text.
142 * @param {string} text
144 FakeEditor.prototype.assertUncommittedTextIs = function(text) {
145 assertEquals(text, this.uncommittedText_);
150 * Asserts that the input handler has added 'extra cells' for uncommitted
151 * text into the braille content.
152 * @param {string} cells Cells as a space-separated list of numbers.
154 FakeEditor.prototype.assertExtraCellsAre = function(cells) {
155 assertEqualsJSON(cellsToArray(cells), this.extraCells_);
160 * Sends a message from the IME to the input handler.
161 * @param {Object} msg The message to send.
164 FakeEditor.prototype.message_ = function(msg) {
165 var listener = this.port_.onMessage.getListener();
166 assertNotEquals(null, listener);
172 * Calls the {@code onDisplayContentChanged} method of the input handler
173 * with the current editor content and selection.
176 FakeEditor.prototype.callOnDisplayContentChanged_ = function() {
177 var content = cvox.BrailleUtil.createValue(
178 this.text_, this.selectionStart_, this.selectionEnd_)
179 var grabExtraCells = function() {
180 var span = content.getSpanInstanceOf(cvox.ExtraCellsSpan);
181 assertNotEquals(null, span);
182 // Convert the ArrayBuffer to a normal array for easier comparision.
183 this.extraCells_ = Array.prototype.map.call(new Uint8Array(span.cells),
184 function(a) {return a;});
186 this.inputHandler_.onDisplayContentChanged(content, grabExtraCells);
192 * Informs the input handler that a new text field is focused. The content
193 * of the field is not cleared and should be updated separately.
194 * @param {string} fieldType The type of the field (see the documentation
195 * for the {@code chrome.input.ime} API).
197 FakeEditor.prototype.focus = function(fieldType) {
199 this.message_({type: 'inputContext',
200 context: {type: fieldType,
201 contextID: this.contextID_}});
206 * Inform the input handler that focus left the input field.
208 FakeEditor.prototype.blur = function() {
209 this.message_({type: 'inputContext', context: null});
215 * Handles a message from the input handler to the IME.
216 * @param {Object} msg The message.
219 FakeEditor.prototype.handleMessage_ = function(msg) {
220 assertEquals(this.contextID_, msg.contextID);
223 var deleteBefore = msg.deleteBefore;
224 var newText = msg.newText;
225 assertTrue(goog.isNumber(deleteBefore));
226 assertTrue(goog.isString(newText));
227 assertTrue(deleteBefore <= this.selectionStart_);
228 if (deleteBefore > 0) {
229 assertTrue(this.allowDeletes_);
231 this.text_.substring(0, this.selectionStart_ - deleteBefore) +
232 this.text_.substring(this.selectionEnd_);
233 this.selectionStart_ -= deleteBefore;
234 this.selectionEnd_ = this.selectionStart_;
235 this.callOnDisplayContentChanged_();
237 this.insert(newText);
239 case 'setUncommitted':
240 assertTrue(goog.isString(msg.text));
241 this.uncommittedText_ = msg.text;
243 case 'commitUncommitted':
244 this.insert(this.uncommittedText_);
245 this.uncommittedText_ = '';
248 throw new Error('Unexpected message to IME: ' + JSON.stringify(msg));
253 * Fakes a {@code Port} used for message passing in the Chrome extension APIs.
256 function FakePort() {
257 /** @type {FakeChromeEvent} */
258 this.onDisconnect = new FakeChromeEvent();
259 /** @type {FakeChromeEvent} */
260 this.onMessage = new FakeChromeEvent();
261 /** @type {string} */
262 this.name = cvox.BrailleInputHandler.IME_PORT_NAME_;
263 /** @type {{id: string}} */
264 this.sender = {id: cvox.BrailleInputHandler.IME_EXTENSION_ID_};
268 * Mapping from braille cells to Unicode characters.
269 * @const Array<Array<string> >
271 var UNCONTRACTED_TABLE = [
273 ['1', 'a'], ['12', 'b'], ['14', 'c'], ['145', 'd'], ['15', 'e'],
274 ['124', 'f'], ['1245', 'g'], ['125', 'h'], ['24', 'i'], ['245', 'j'],
275 ['13', 'k'], ['123', 'l'], ['134', 'm'], ['1345', 'n'], ['135', 'o'],
276 ['1234', 'p'], ['12345', 'q'], ['1235', 'r'], ['234', 's'], ['2345', 't']
281 * Mapping of braille cells to the corresponding word in Grade 2 US English
282 * braille. This table also includes the uncontracted table above.
283 * If a match 'pattern' starts with '^', it must be at the beginning of
284 * the string or be preceded by a blank cell. Similarly, '$' at the end
285 * of a 'pattern' means that the match must be at the end of the string
286 * or be followed by a blank cell. Note that order is significant in the
287 * table. First match wins.
290 var CONTRACTED_TABLE = [
291 ['12 1235 123', 'braille'],
293 ['1456', 'this']].concat(UNCONTRACTED_TABLE);
296 * A fake braille translator that can do back translation according
297 * to one of the tables above.
298 * @param {Array<Array<number>>} table Backtranslation mapping.
299 * @param {boolean=} opt_capitalize Whether the result should be capitalized.
302 function FakeTranslator(table, opt_capitalize) {
304 this.table_ = table.map(function(entry) {
305 var cells = entry[0];
307 if (cells[0] === '^') {
309 cells = cells.substring(1);
311 if (cells[cells.length - 1] === '$') {
313 cells = cells.substring(0, cells.length - 1);
315 result[0] = cellsToArray(cells);
316 result[1] = entry[1];
319 /** @private {boolean} */
320 this.capitalize_ = opt_capitalize || false;
325 * Implements the {@code cvox.LibLouis.BrailleTranslator.backTranslate} method.
326 * @param {!ArrayBuffer} cells Cells to be translated.
327 * @param {function(?string)} callback Callback for result.
329 FakeTranslator.prototype.backTranslate = function(cells, callback) {
330 var cellsArray = new Uint8Array(cells);
333 while (pos < cellsArray.length) {
335 outer: for (var i = 0, entry; entry = this.table_[i]; ++i) {
336 if (pos + entry[0].length > cellsArray.length) {
339 if (entry.start && pos > 0 && cellsArray[pos - 1] !== 0) {
342 for (var j = 0; j < entry[0].length; ++j) {
343 if (entry[0][j] !== cellsArray[pos + j]) {
347 if (entry.end && pos + j < cellsArray.length &&
348 cellsArray[pos + j] !== 0) {
356 'Backtranslating ' + cellsArray[pos] + ' at ' + pos);
358 pos += match[0].length;
360 if (this.capitalize_) {
361 result = result.toUpperCase();
366 /** @extends {cvox.BrailleTranslatorManager} */
367 function FakeTranslatorManager() {
370 FakeTranslatorManager.prototype = {
371 defaultTranslator: null,
372 uncontractedTranslator: null,
373 changeListener: null,
376 getDefaultTranslator: function() {
377 return this.defaultTranslator;
381 getUncontractedTranslator: function() {
382 return this.uncontractedTranslator;
386 addChangeListener: function(listener) {
387 assertEquals(null, this.changeListener);
390 setTranslators: function(defaultTranslator, uncontractedTranslator) {
391 this.defaultTranslator = defaultTranslator;
392 this.uncontractedTranslator = uncontractedTranslator;
393 if (this.changeListener) {
394 this.changeListener();
400 * Converts a list of cells, represented as a string, to an array.
401 * @param {string} cells A string with space separated groups of digits.
402 * Each group corresponds to one braille cell and each digit in a group
403 * corresponds to a particular dot in the cell (1 to 8). As a special
404 * case, the digit 0 by itself represents a blank cell.
405 * @return {Array<number>} An array with each cell encoded as a bit
406 * pattern (dot 1 uses bit 0, etc).
408 function cellsToArray(cells) {
411 return cells.split(/\s+/).map(function(cellString) {
413 assertTrue(cellString.length > 0);
414 if (cellString != '0') {
415 for (var i = 0; i < cellString.length; ++i) {
416 var dot = cellString.charCodeAt(i) - '0'.charCodeAt(0);
417 assertTrue(dot >= 1);
418 assertTrue(dot <= 8);
419 cell |= 1 << (dot - 1);
429 * @extends {ChromeVoxUnitTestBase}
431 function CvoxBrailleInputHandlerUnitTest() {}
433 CvoxBrailleInputHandlerUnitTest.prototype = {
434 __proto__: ChromeVoxUnitTestBase.prototype,
438 'cvox.BrailleInputHandler',
443 * Creates an editor and establishes a connection from the IME.
444 * @return {FakeEditor}
446 createEditor: function() {
447 chrome.runtime.onConnectExternal.getListener()(this.port);
448 return new FakeEditor(this.port, this.inputHandler);
452 * Sends a series of braille cells to the input handler.
453 * @param {string} cells Braille cells, encoded as described in
454 * {@code cellsToArray}.
455 * @return {boolean} {@code true} iff all cells were sent successfully.
457 sendCells: function(cells) {
458 return cellsToArray(cells).reduce(function(prevResult, cell) {
459 var event = {command: cvox.BrailleKeyCommand.DOTS, brailleDots: cell};
460 return prevResult && this.inputHandler.onBrailleKeyEvent(event);
465 * Sends a standard key event (such as backspace) to the braille input
467 * @param {string} keyCode The key code name.
468 * @return {boolean} Whether the event was handled.
470 sendKeyEvent: function(keyCode) {
471 var event = {command: cvox.BrailleKeyCommand.STANDARD_KEY,
472 standardKeyCode: keyCode};
473 return this.inputHandler.onBrailleKeyEvent(event);
477 * Shortcut for asserting that the value expansion mode is {@code NONE}.
479 assertExpandingNone: function() {
480 assertEquals(cvox.ExpandingBrailleTranslator.ExpansionType.NONE,
481 this.inputHandler.getExpansionType());
485 * Shortcut for asserting that the value expansion mode is {@code SELECTION}.
487 assertExpandingSelection: function() {
488 assertEquals(cvox.ExpandingBrailleTranslator.ExpansionType.SELECTION,
489 this.inputHandler.getExpansionType());
493 * Shortcut for asserting that the value expansion mode is {@code ALL}.
495 assertExpandingAll: function() {
496 assertEquals(cvox.ExpandingBrailleTranslator.ExpansionType.ALL,
497 this.inputHandler.getExpansionType());
500 storeKeyEvent: function(event, opt_callback) {
501 var storedCopy = {keyCode: event.keyCode, keyName: event.keyName,
502 charValue: event.charValue};
503 if (event.type == 'keydown') {
504 this.keyEvents.push(storedCopy);
506 assertEquals('keyup', event.type);
507 assertTrue(this.keyEvents.length > 0);
508 assertEqualsJSON(storedCopy, this.keyEvents[this.keyEvents.length - 1]);
510 if (goog.isDef(opt_callback)) {
517 chrome.runtime.onConnectExternal = new FakeChromeEvent();
518 this.port = new FakePort();
519 this.translatorManager = new FakeTranslatorManager();
520 this.inputHandler = new cvox.BrailleInputHandler(this.translatorManager);
521 this.inputHandler.init();
522 this.uncontractedTranslator = new FakeTranslator(UNCONTRACTED_TABLE);
523 this.contractedTranslator = new FakeTranslator(CONTRACTED_TABLE, true);
524 chrome.virtualKeyboardPrivate.sendKeyEvent =
525 this.storeKeyEvent.bind(this);
530 TEST_F('CvoxBrailleInputHandlerUnitTest', 'ConnectFromUnknownExtension',
532 this.port.sender.id = 'your unknown friend';
533 chrome.runtime.onConnectExternal.getListener()(this.port);
534 this.port.onMessage.assertNoListener();
538 TEST_F('CvoxBrailleInputHandlerUnitTest', 'NoTranslator', function() {
539 var editor = this.createEditor();
540 editor.setContent('blah', 0);
541 editor.setActive(true);
542 editor.focus('email');
543 assertFalse(this.sendCells('145 135 125'));
544 editor.setActive(false);
546 editor.assertContentIs('blah', 0);
550 TEST_F('CvoxBrailleInputHandlerUnitTest', 'InputUncontracted', function() {
551 this.translatorManager.setTranslators(this.uncontractedTranslator, null);
552 var editor = this.createEditor();
553 editor.setActive(true);
555 // Focus and type in a text field.
556 editor.focus('text');
557 assertTrue(this.sendCells('125 15 123 123 135')); // hello
558 editor.assertContentIs('hello', 'hello'.length);
559 this.assertExpandingNone();
561 // Move the cursor and type in the middle.
563 assertTrue(this.sendCells('0 2345 125 15 1235 15 0')); // ' there '
564 editor.assertContentIs('he there llo', 'he there '.length);
566 // Field changes by some other means.
567 editor.insert('you!');
568 // Then type on the braille keyboard again.
569 assertTrue(this.sendCells('0 125 15')); // ' he'
570 editor.assertContentIs('he there you! hello', 'he there you! he'.length);
573 editor.setActive(false);
577 TEST_F('CvoxBrailleInputHandlerUnitTest', 'InputContracted', function() {
578 var editor = this.createEditor();
579 this.translatorManager.setTranslators(this.contractedTranslator,
580 this.uncontractedTranslator);
581 editor.setContent('', 0);
582 editor.setActive(true);
583 editor.focus('text');
584 this.assertExpandingSelection();
586 // First, type a 'b'.
587 assertTrue(this.sendCells('12'));
588 editor.assertContentIs('', 0);
589 // Remember that the contracted translator produces uppercase.
590 editor.assertUncommittedTextIs('BUT');
591 editor.assertExtraCellsAre('12');
592 this.assertExpandingNone();
594 // Typing 'rl' changes to a different contraction.
595 assertTrue(this.sendCells('1235 123'));
596 editor.assertUncommittedTextIs('BRAILLE');
597 editor.assertContentIs('', 0);
598 editor.assertExtraCellsAre('12 1235 123');
599 this.assertExpandingNone();
601 // Now, finish the word.
602 assertTrue(this.sendCells('0'));
603 editor.assertContentIs('BRAILLE ', 'BRAILLE '.length);
604 editor.assertUncommittedTextIs('');
605 editor.assertExtraCellsAre('');
606 this.assertExpandingSelection();
608 // Move the cursor to the beginning.
610 this.assertExpandingSelection();
612 // Typing now uses the uncontracted table.
613 assertTrue(this.sendCells('12')); // 'b'
614 editor.assertContentIs('bBRAILLE ', 1);
615 this.assertExpandingSelection();
616 editor.select('bBRAILLE'.length);
617 this.assertExpandingSelection();
618 assertTrue(this.sendCells('12')); // 'b'
619 editor.assertContentIs('bBRAILLEb ', 'bBRAILLEb'.length);
620 // Move to the end, where contracted typing should work.
621 editor.select('bBRAILLEb '.length);
622 assertTrue(this.sendCells('1456 0')); // Symbol for 'this', then space.
623 this.assertExpandingSelection();
624 editor.assertContentIs('bBRAILLEb THIS ', 'bBRAILLEb THIS '.length);
626 // Move to between the two words.
627 editor.select('bBRAILLEb'.length);
628 this.assertExpandingSelection();
629 assertTrue(this.sendCells('0 12')); // Space plus 'b' for 'but'
630 editor.assertUncommittedTextIs('BUT');
631 editor.assertExtraCellsAre('12');
632 editor.assertContentIs('bBRAILLEb THIS ', 'bBRAILLEb '.length);
633 this.assertExpandingNone();
637 TEST_F('CvoxBrailleInputHandlerUnitTest', 'TypingUrlWithContracted',
639 var editor = this.createEditor();
640 this.translatorManager.setTranslators(this.contractedTranslator,
641 this.uncontractedTranslator);
642 editor.setActive(true);
644 this.assertExpandingAll();
645 assertTrue(this.sendCells('1245')); // 'g'
646 editor.insert('oogle.com', true /*select*/);
647 editor.assertContentIs('google.com', 1, 'google.com'.length);
648 this.assertExpandingAll();
649 this.sendCells('135'); // 'o'
650 editor.insert('ogle.com', true /*select*/);
651 editor.assertContentIs('google.com', 2, 'google.com'.length);
652 this.assertExpandingAll();
654 editor.assertContentIs('go ', 'go '.length);
655 // In a URL, even when the cursor is in whitespace, all of the value
656 // is expanded to uncontracted braille.
657 this.assertExpandingAll();
661 TEST_F('CvoxBrailleInputHandlerUnitTest', 'Backspace', function() {
662 var editor = this.createEditor();
663 this.translatorManager.setTranslators(this.contractedTranslator,
664 this.uncontractedTranslator);
665 editor.setActive(true);
666 editor.focus('text');
668 // Add some text that we can delete later.
669 editor.setContent('Text ', 'Text '.length);
671 // Type 'brl' to make sure replacement works when deleting text.
672 assertTrue(this.sendCells('12 1235 123'));
673 editor.assertUncommittedTextIs('BRAILLE');
675 // Delete what we just typed, one cell at a time.
676 this.sendKeyEvent('Backspace');
677 editor.assertUncommittedTextIs('BR');
678 this.sendKeyEvent('Backspace');
679 editor.assertUncommittedTextIs('BUT');
680 this.sendKeyEvent('Backspace');
681 editor.assertUncommittedTextIs('');
683 // Now, backspace should be handled as usual, synthetizing key events.
684 assertEquals(0, this.keyEvents.length);
685 this.sendKeyEvent('Backspace');
686 assertEqualsJSON([{keyCode: 8, keyName: 'Backspace', charValue: 8}],
691 TEST_F('CvoxBrailleInputHandlerUnitTest', 'KeysImeNotActive', function() {
692 var editor = this.createEditor();
693 this.sendKeyEvent('Enter');
694 this.sendKeyEvent('ArrowUp');
695 assertEqualsJSON([{keyCode: 13, keyName: 'Enter', charValue: 0x0A},
696 {keyCode: 38, keyName: 'ArrowUp', charValue: 0x41}],