Remove the old signature of NotificationManager::closePersistent().
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / braille / braille_input_handler_test.unitjs
blob59fa53e7f55c2d5b73fa5aefff858142d4d1b3aa
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.
11 var chrome = {};
12 /** Fake chrome.runtime object. */
13 chrome.runtime = {};
14 /** Fake chrome.virtualKeyboardPrivate object. */
15 chrome.virtualKeyboardPrivate = {};
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  */
26 function 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);
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  */
54 FakeEditor.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_();
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  */
70 FakeEditor.prototype.select = function(selectionStart, opt_selectionEnd) {
71   this.setContent(this.text_, selectionStart, opt_selectionEnd);
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  */
82 FakeEditor.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_();
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  */
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.
112  */
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.
125  */
126 FakeEditor.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_);
137  * Sends a message from the IME to the input handler.
138  * @param {Object} msg The message to send.
139  * @private
140  */
141 FakeEditor.prototype.message_ = function(msg) {
142   var listener = this.port_.onMessage.getListener();
143   assertNotEquals(null, listener);
144   listener(msg);
149  * Calls the {@code onDisplayContentChanged} method of the input handler
150  * with the current editor content and selection.
151  * @private
152  */
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).
165  */
166 FakeEditor.prototype.focus = function(fieldType) {
167   this.contextID_++;
168   this.message_({type: 'inputContext',
169                  context: {type: fieldType,
170                            contextID: this.contextID_}});
175  * Inform the input handler that focus left the input field.
176  */
177 FakeEditor.prototype.blur = function() {
178   this.message_({type: 'inputContext', context: null});
179   this.contextID_ = 0;
184  * Handles a message from the input handler to the IME.
185  * @param {Object} msg The message.
186  * @private
187  */
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_);
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);
209  * Fakes a {@code Port} used for message passing in the Chrome extension APIs.
210  * @constructor
211  */
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> >
226  */
227 var 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']
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  */
246 var CONTRACTED_TABLE = [
247   ['12 1235 123', 'braille'],
248   ['^12$', 'but'],
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.
256  * @constructor
257  */
258 function 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;
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  */
285 FakeTranslator.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);
322 /** @extends {cvox.BrailleTranslatorManager} */
323 function FakeTranslatorManager() {
326 FakeTranslatorManager.prototype = {
327   defaultTranslator: null,
328   uncontractedTranslator: null,
329   changeListener: null,
331   /** @override */
332   getDefaultTranslator: function() {
333     return this.defaultTranslator;
334   },
336   /** @override */
337   getUncontractedTranslator: function() {
338     return this.uncontractedTranslator;
339   },
341   /** @override */
342   addChangeListener: function(listener) {
343     assertEquals(null, this.changeListener);
344   },
346   setTranslators: function(defaultTranslator, uncontractedTranslator) {
347     this.defaultTranslator = defaultTranslator;
348     this.uncontractedTranslator = uncontractedTranslator;
349     if (this.changeListener) {
350       this.changeListener();
351     }
352   }
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).
363  */
364 function cellsToArray(cells) {
365   return cells.split(/\s+/).map(function(cellString) {
366     var cell = 0;
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);
374       }
375     }
376     return cell;
377   });
381  * Test fixture.
382  * @constructor
383  * @extends {ChromeVoxUnitTestBase}
384  */
385 function CvoxBrailleInputHandlerUnitTest() {}
387 CvoxBrailleInputHandlerUnitTest.prototype = {
388   __proto__: ChromeVoxUnitTestBase.prototype,
390   /** @override */
391   closureModuleDeps: [
392     'cvox.BrailleInputHandler',
393     'cvox.BrailleUtil',
394   ],
396   /**
397    * Creates an editor and establishes a connection from the IME.
398    * @return {FakeEditor}
399    */
400   createEditor: function() {
401     chrome.runtime.onConnectExternal.getListener()(this.port);
402     return new FakeEditor(this.port, this.inputHandler);
403   },
405   /**
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.
410    */
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);
415     }.bind(this), true);
416   },
418   /**
419    * Sends a standard key event (such as backspace) to the braille input
420    * handler.
421    * @param {string} keyCode The key code name.
422    * @return {boolean} Whether the event was handled.
423    */
424   sendKeyEvent: function(keyCode) {
425     var event = {command: cvox.BrailleKeyCommand.STANDARD_KEY,
426                  standardKeyCode: keyCode};
427     return this.inputHandler.onBrailleKeyEvent(event);
428   },
430   /**
431    * Shortcut for asserting that the value expansion mode is {@code NONE}.
432    */
433   assertExpandingNone: function() {
434     assertEquals(cvox.ExpandingBrailleTranslator.ExpansionType.NONE,
435                  this.inputHandler.getExpansionType());
436   },
438   /**
439    * Shortcut for asserting that the value expansion mode is {@code SELECTION}.
440    */
441   assertExpandingSelection: function() {
442     assertEquals(cvox.ExpandingBrailleTranslator.ExpansionType.SELECTION,
443                  this.inputHandler.getExpansionType());
444   },
446   /**
447    * Shortcut for asserting that the value expansion mode is {@code ALL}.
448    */
449   assertExpandingAll: function() {
450     assertEquals(cvox.ExpandingBrailleTranslator.ExpansionType.ALL,
451                  this.inputHandler.getExpansionType());
452   },
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);
459     } else {
460       assertEquals('keyup', event.type);
461       assertTrue(this.keyEvents.length > 0);
462       assertEqualsJSON(storedCopy, this.keyEvents[this.keyEvents.length - 1]);
463     }
464     if (goog.isDef(opt_callback)) {
465       callback();
466     }
467   },
469   /** @override */
470   setUp: function() {
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);
480     this.keyEvents = [];
481   }
484 TEST_F('CvoxBrailleInputHandlerUnitTest', 'ConnectFromUnknownExtension',
485   function() {
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);
499   editor.blur();
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.
516   editor.select(2);
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);
526   editor.blur();
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.
556   editor.select(0);
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',
584        function() {
585   var editor = this.createEditor();
586   this.translatorManager.setTranslators(this.contractedTranslator,
587                                         this.uncontractedTranslator);
588   editor.setActive(true);
589   editor.focus('url');
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();
599   this.sendCells('0');
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}],
635                    this.keyEvents);
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}],
645                    this.keyEvents);