Home | History | Annotate | Download | only in js
      1 // Copyright (c) 2013 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 /**
      6  * Namespace for test related things.
      7  */
      8 var test = test || {};
      9 
     10 /**
     11  * Namespace for test utility functions.
     12  *
     13  * Public functions in the test.util.sync and the test.util.async namespaces are
     14  * published to test cases and can be called by using callRemoteTestUtil. The
     15  * arguments are serialized as JSON internally. If application ID is passed to
     16  * callRemoteTestUtil, the content window of the application is added as the
     17  * first argument. The functions in the test.util.async namespace are passed the
     18  * callback function as the last argument.
     19  */
     20 test.util = {};
     21 
     22 /**
     23  * Namespace for synchronous utility functions.
     24  */
     25 test.util.sync = {};
     26 
     27 /**
     28  * Namespace for asynchronous utility functions.
     29  */
     30 test.util.async = {};
     31 
     32 /**
     33  * Extension ID of the testing extension.
     34  * @type {string}
     35  * @const
     36  */
     37 test.util.TESTING_EXTENSION_ID = 'oobinhbdbiehknkpbpejbbpdbkdjmoco';
     38 
     39 /**
     40  * Interval of checking a condition in milliseconds.
     41  * @type {number}
     42  * @const
     43  * @private
     44  */
     45 test.util.WAITTING_INTERVAL_ = 50;
     46 
     47 /**
     48  * Repeats the function until it returns true.
     49  * @param {function()} closure Function expected to return true.
     50  * @private
     51  */
     52 test.util.repeatUntilTrue_ = function(closure) {
     53   var step = function() {
     54     if (closure())
     55       return;
     56     setTimeout(step, test.util.WAITTING_INTERVAL_);
     57   };
     58   step();
     59 };
     60 
     61 /**
     62  * Opens the main Files.app's window and waits until it is ready.
     63  *
     64  * @param {string} path Path of the directory to be opened.
     65  * @param {function(string)} callback Completion callback with the new window's
     66  *     App ID.
     67  */
     68 test.util.async.openMainWindow = function(path, callback) {
     69   var steps = [
     70     function() {
     71       launchFileManager({defaultPath: path},
     72                         undefined,  // opt_type
     73                         undefined,  // opt_id
     74                         steps.shift());
     75     },
     76     function(appId) {
     77       test.util.repeatUntilTrue_(function() {
     78         if (!appWindows[appId])
     79           return false;
     80         var contentWindow = appWindows[appId].contentWindow;
     81         var table = contentWindow.document.querySelector('#detail-table');
     82         if (!table)
     83           return false;
     84         callback(appId);
     85         return true;
     86       });
     87     }
     88   ];
     89   steps.shift()();
     90 };
     91 
     92 /**
     93  * Waits for a window with the specified App ID prefix. Eg. `files` will match
     94  * windows such as files#0, files#1, etc.
     95  *
     96  * @param {string} appIdPrefix ID prefix of the requested window.
     97  * @param {function(string)} callback Completion callback with the new window's
     98  *     App ID.
     99  */
    100 test.util.async.waitForWindow = function(appIdPrefix, callback) {
    101   test.util.repeatUntilTrue_(function() {
    102     for (var appId in appWindows) {
    103       if (appId.indexOf(appIdPrefix) == 0 &&
    104           appWindows[appId].contentWindow) {
    105         callback(appId);
    106         return true;
    107       }
    108     }
    109     return false;
    110   });
    111 };
    112 
    113 /**
    114  * Gets a document in the Files.app's window, including iframes.
    115  *
    116  * @param {Window} contentWindow Window to be used.
    117  * @param {string=} opt_iframeQuery Query for the iframe.
    118  * @return {Document=} Returns the found document or undefined if not found.
    119  * @private
    120  */
    121 test.util.sync.getDocument_ = function(contentWindow, opt_iframeQuery) {
    122   if (opt_iframeQuery) {
    123     var iframe = contentWindow.document.querySelector(opt_iframeQuery);
    124     return iframe && iframe.contentWindow && iframe.contentWindow.document;
    125   }
    126 
    127   return contentWindow.document;
    128 };
    129 
    130 /**
    131  * Gets total Javascript error count from each app window.
    132  * @return {number} Error count.
    133  */
    134 test.util.sync.getErrorCount = function() {
    135   var totalCount = 0;
    136   for (var appId in appWindows) {
    137     var contentWindow = appWindows[appId].contentWindow;
    138     if (contentWindow.JSErrorCount)
    139       totalCount += contentWindow.JSErrorCount;
    140   }
    141   return totalCount;
    142 };
    143 
    144 /**
    145  * Resizes the window to the specified dimensions.
    146  *
    147  * @param {Window} contentWindow Window to be tested.
    148  * @param {number} width Window width.
    149  * @param {number} height Window height.
    150  * @return {boolean} True for success.
    151  */
    152 test.util.sync.resizeWindow = function(contentWindow, width, height) {
    153   appWindows[contentWindow.appID].resizeTo(width, height);
    154   return true;
    155 };
    156 
    157 /**
    158  * Returns an array with the files currently selected in the file manager.
    159  *
    160  * @param {Window} contentWindow Window to be tested.
    161  * @return {Array.<string>} Array of selected files.
    162  */
    163 test.util.sync.getSelectedFiles = function(contentWindow) {
    164   var table = contentWindow.document.querySelector('#detail-table');
    165   var rows = table.querySelectorAll('li');
    166   var selected = [];
    167   for (var i = 0; i < rows.length; ++i) {
    168     if (rows[i].hasAttribute('selected')) {
    169       selected.push(
    170           rows[i].querySelector('.filename-label').textContent);
    171     }
    172   }
    173   return selected;
    174 };
    175 
    176 /**
    177  * Returns an array with the files on the file manager's file list.
    178  *
    179  * @param {Window} contentWindow Window to be tested.
    180  * @return {Array.<Array.<string>>} Array of rows.
    181  */
    182 test.util.sync.getFileList = function(contentWindow) {
    183   var table = contentWindow.document.querySelector('#detail-table');
    184   var rows = table.querySelectorAll('li');
    185   var fileList = [];
    186   for (var j = 0; j < rows.length; ++j) {
    187     var row = rows[j];
    188     fileList.push([
    189       row.querySelector('.filename-label').textContent,
    190       row.querySelector('.size').textContent,
    191       row.querySelector('.type').textContent,
    192       row.querySelector('.date').textContent
    193     ]);
    194   }
    195   return fileList;
    196 };
    197 
    198 /**
    199  * Waits until the window is set to the specified dimensions.
    200  *
    201  * @param {Window} contentWindow Window to be tested.
    202  * @param {number} width Requested width.
    203  * @param {number} height Requested height.
    204  * @param {function(Object)} callback Success callback with the dimensions.
    205  */
    206 test.util.async.waitForWindowGeometry = function(
    207     contentWindow, width, height, callback) {
    208   test.util.repeatUntilTrue_(function() {
    209     if (contentWindow.innerWidth == width &&
    210         contentWindow.innerHeight == height) {
    211       callback({width: width, height: height});
    212       return true;
    213     }
    214     return false;
    215   });
    216 };
    217 
    218 /**
    219  * Waits for an element and returns it as an array of it's attributes.
    220  *
    221  * @param {Window} contentWindow Window to be tested.
    222  * @param {string} targetQuery Query to specify the element.
    223  * @param {?string} iframeQuery Iframe selector or null if no iframe.
    224  * @param {boolean=} opt_inverse True if the function should return if the
    225  *    element disappears, instead of appearing.
    226  * @param {function(Object)} callback Callback with a hash array of attributes
    227  *     and contents as text.
    228  */
    229 test.util.async.waitForElement = function(
    230     contentWindow, targetQuery, iframeQuery, opt_inverse, callback) {
    231   test.util.repeatUntilTrue_(function() {
    232     var doc = test.util.sync.getDocument_(contentWindow, iframeQuery);
    233     if (!doc)
    234       return false;
    235     var element = doc.querySelector(targetQuery);
    236     if (!element)
    237       return !!opt_inverse;
    238     var attributes = {};
    239     for (var i = 0; i < element.attributes.length; i++) {
    240       attributes[element.attributes[i].nodeName] =
    241           element.attributes[i].nodeValue;
    242     }
    243     var text = element.textContent;
    244     callback({attributes: attributes, text: text});
    245     return !opt_inverse;
    246   });
    247 };
    248 
    249 /**
    250  * Calls getFileList until the number of displayed files is different from
    251  * lengthBefore.
    252  *
    253  * @param {Window} contentWindow Window to be tested.
    254  * @param {number} lengthBefore Number of items visible before.
    255  * @param {function(Array.<Array.<string>>)} callback Change callback.
    256  */
    257 test.util.async.waitForFileListChange = function(
    258     contentWindow, lengthBefore, callback) {
    259   test.util.repeatUntilTrue_(function() {
    260     var files = test.util.sync.getFileList(contentWindow);
    261     files.sort();
    262     var notReadyRows = files.filter(function(row) {
    263       return row.filter(function(cell) { return cell == '...'; }).length;
    264     });
    265     if (notReadyRows.length === 0 &&
    266         files.length !== lengthBefore &&
    267         files.length !== 0) {
    268       callback(files);
    269       return true;
    270     } else {
    271       return false;
    272     }
    273   });
    274 };
    275 
    276 /**
    277  * Returns an array of items on the file manager's autocomplete list.
    278  *
    279  * @param {Window} contentWindow Window to be tested.
    280  * @return {Array.<string>} Array of items.
    281  */
    282 test.util.sync.getAutocompleteList = function(contentWindow) {
    283   var list = contentWindow.document.querySelector('#autocomplete-list');
    284   var lines = list.querySelectorAll('li');
    285   var items = [];
    286   for (var j = 0; j < lines.length; ++j) {
    287     var line = lines[j];
    288     items.push(line.innerText);
    289   }
    290   return items;
    291 };
    292 
    293 /**
    294  * Performs autocomplete with the given query and waits until at least
    295  * |numExpectedItems| items are shown, including the first item which
    296  * always looks like "'<query>' - search Drive".
    297  *
    298  * @param {Window} contentWindow Window to be tested.
    299  * @param {string} query Query used for autocomplete.
    300  * @param {number} numExpectedItems number of items to be shown.
    301  * @param {function(Array.<string>)} callback Change callback.
    302  */
    303 test.util.async.performAutocompleteAndWait = function(
    304     contentWindow, query, numExpectedItems, callback) {
    305   // Dispatch a 'focus' event to the search box so that the autocomplete list
    306   // is attached to the search box. Note that calling searchBox.focus() won't
    307   // dispatch a 'focus' event.
    308   var searchBox = contentWindow.document.querySelector('#search-box');
    309   var focusEvent = contentWindow.document.createEvent('Event');
    310   focusEvent.initEvent('focus', true /* bubbles */, true /* cancelable */);
    311   searchBox.dispatchEvent(focusEvent);
    312 
    313   // Change the value of the search box and dispatch an 'input' event so that
    314   // the autocomplete query is processed.
    315   searchBox.value = query;
    316   var inputEvent = contentWindow.document.createEvent('Event');
    317   inputEvent.initEvent('input', true /* bubbles */, true /* cancelable */);
    318   searchBox.dispatchEvent(inputEvent);
    319 
    320   test.util.repeatUntilTrue_(function() {
    321     var items = test.util.sync.getAutocompleteList(contentWindow);
    322     if (items.length >= numExpectedItems) {
    323       callback(items);
    324       return true;
    325     } else {
    326       return false;
    327     }
    328   });
    329 };
    330 
    331 /**
    332  * Waits until a dialog with an OK button is shown and accepts it.
    333  *
    334  * @param {Window} contentWindow Window to be tested.
    335  * @param {function()} callback Success callback.
    336  */
    337 test.util.async.waitAndAcceptDialog = function(contentWindow, callback) {
    338   test.util.repeatUntilTrue_(function() {
    339     var button = contentWindow.document.querySelector('.cr-dialog-ok');
    340     if (!button)
    341       return false;
    342     button.click();
    343     // Wait until the dialog is removed from the DOM.
    344     test.util.repeatUntilTrue_(function() {
    345       if (contentWindow.document.querySelector('.cr-dialog-container'))
    346         return false;
    347       callback();
    348       return true;
    349     });
    350     return true;
    351   });
    352 };
    353 
    354 /**
    355  * Fakes pressing the down arrow until the given |filename| is selected.
    356  *
    357  * @param {Window} contentWindow Window to be tested.
    358  * @param {string} filename Name of the file to be selected.
    359  * @return {boolean} True if file got selected, false otherwise.
    360  */
    361 test.util.sync.selectFile = function(contentWindow, filename) {
    362   var table = contentWindow.document.querySelector('#detail-table');
    363   var rows = table.querySelectorAll('li');
    364   for (var index = 0; index < rows.length; ++index) {
    365     test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'Down', false);
    366     var selection = test.util.sync.getSelectedFiles(contentWindow);
    367     if (selection.length === 1 && selection[0] === filename)
    368       return true;
    369   }
    370   console.error('Failed to select file "' + filename + '"');
    371   return false;
    372 };
    373 
    374 /**
    375  * Selects a volume specified by its icon name
    376  *
    377  * @param {Window} contentWindow Window to be tested.
    378  * @param {string} iconName Name of the volume icon.
    379  * @param {function(boolean)} callback Callback function to notify the caller
    380  *     whether the target is found and mousedown and click events are sent.
    381  */
    382 test.util.async.selectVolume = function(contentWindow, iconName, callback) {
    383   var query = '[volume-type-icon=' + iconName + ']';
    384   var driveQuery = '[volume-type-icon=drive]';
    385   var isDriveSubVolume = iconName == 'drive_recent' ||
    386                          iconName == 'drive_shared_with_me' ||
    387                          iconName == 'drive_offline';
    388   var preSelection = false;
    389   var steps = {
    390     checkQuery: function() {
    391       if (contentWindow.document.querySelector(query)) {
    392         steps.sendEvents();
    393         return;
    394       }
    395       // If the target volume is sub-volume of drive, we must click 'drive'
    396       // before clicking the sub-item.
    397       if (!preSelection) {
    398         if (!isDriveSubVolume) {
    399           callback(false);
    400           return;
    401         }
    402         if (!(test.util.sync.fakeMouseDown(contentWindow, driveQuery) &&
    403               test.util.sync.fakeMouseClick(contentWindow, driveQuery))) {
    404           callback(false);
    405           return;
    406         }
    407         preSelection = true;
    408       }
    409       setTimeout(steps.checkQuery, 50);
    410     },
    411     sendEvents: function() {
    412       // To change the selected volume, we have to send both events 'mousedown'
    413       // and 'click' to the navigation list.
    414       callback(test.util.sync.fakeMouseDown(contentWindow, query) &&
    415                test.util.sync.fakeMouseClick(contentWindow, query));
    416     }
    417   };
    418   steps.checkQuery();
    419 };
    420 
    421 /**
    422  * Waits the contents of file list becomes to equal to expected contents.
    423  *
    424  * @param {Window} contentWindow Window to be tested.
    425  * @param {Array.<Array.<string>>} expected Expected contents of file list.
    426  * @param {boolean=} opt_orderCheck If it is true, this function also compares
    427  *     the order of files.
    428  * @param {function()} callback Callback function to notify the caller that
    429  *     expected files turned up.
    430  */
    431 test.util.async.waitForFiles = function(
    432     contentWindow, expected, opt_orderCheck, callback) {
    433   test.util.repeatUntilTrue_(function() {
    434     var files = test.util.sync.getFileList(contentWindow);
    435     if (!opt_orderCheck)
    436       files.sort();
    437     if (chrome.test.checkDeepEq(expected, files)) {
    438       callback(true);
    439       return true;
    440     }
    441     return false;
    442   });
    443 };
    444 
    445 /**
    446  * Executes Javascript code on a webview and returns the result.
    447  *
    448  * @param {Window} contentWindow Window to be tested.
    449  * @param {string} webViewQuery Selector for the web view.
    450  * @param {string} code Javascript code to be executed within the web view.
    451  * @param {function(*)} callback Callback function with results returned by the
    452  *     script.
    453  */
    454 test.util.async.executeScriptInWebView = function(
    455     contentWindow, webViewQuery, code, callback) {
    456   var webView = contentWindow.document.querySelector(webViewQuery);
    457   webView.executeScript({code: code}, callback);
    458 };
    459 
    460 /**
    461  * Sends an event to the element specified by |targetQuery|.
    462  *
    463  * @param {Window} contentWindow Window to be tested.
    464  * @param {string} targetQuery Query to specify the element.
    465  * @param {Event} event Event to be sent.
    466  * @param {string=} opt_iframeQuery Optional iframe selector.
    467  * @return {boolean} True if the event is sent to the target, false otherwise.
    468  */
    469 test.util.sync.sendEvent = function(
    470     contentWindow, targetQuery, event, opt_iframeQuery) {
    471   var doc = test.util.sync.getDocument_(contentWindow, opt_iframeQuery);
    472   if (doc) {
    473     var target = doc.querySelector(targetQuery);
    474     if (target) {
    475       target.dispatchEvent(event);
    476       return true;
    477     }
    478   }
    479   console.error('Target element for ' + targetQuery + ' not found.');
    480   return false;
    481 };
    482 
    483 /**
    484  * Sends an fake event having the specified type to the target query.
    485  *
    486  * @param {Window} contentWindow Window to be tested.
    487  * @param {string} targetQuery Query to specify the element.
    488  * @param {string} event Type of event.
    489  * @return {boolean} True if the event is sent to the target, false otherwise.
    490  */
    491 test.util.sync.fakeEvent = function(contentWindow, targetQuery, event) {
    492   return test.util.sync.sendEvent(
    493       contentWindow, targetQuery, new Event(event));
    494 };
    495 
    496 /**
    497  * Sends a fake key event to the element specified by |targetQuery| with the
    498  * given |keyIdentifier| and optional |ctrl| modifier to the file manager.
    499  *
    500  * @param {Window} contentWindow Window to be tested.
    501  * @param {string} targetQuery Query to specify the element.
    502  * @param {string} keyIdentifier Identifier of the emulated key.
    503  * @param {boolean} ctrl Whether CTRL should be pressed, or not.
    504  * @param {string=} opt_iframeQuery Optional iframe selector.
    505  * @return {boolean} True if the event is sent to the target, false otherwise.
    506  */
    507 test.util.sync.fakeKeyDown = function(
    508     contentWindow, targetQuery, keyIdentifier, ctrl, opt_iframeQuery) {
    509   var event = new KeyboardEvent(
    510       'keydown',
    511       { bubbles: true, keyIdentifier: keyIdentifier, ctrlKey: ctrl });
    512   return test.util.sync.sendEvent(
    513       contentWindow, targetQuery, event, opt_iframeQuery);
    514 };
    515 
    516 /**
    517  * Sends a fake mouse click event (left button, single click) to the element
    518  * specified by |targetQuery|.
    519  *
    520  * @param {Window} contentWindow Window to be tested.
    521  * @param {string} targetQuery Query to specify the element.
    522  * @param {string=} opt_iframeQuery Optional iframe selector.
    523  * @return {boolean} True if the event is sent to the target, false otherwise.
    524  */
    525 test.util.sync.fakeMouseClick = function(
    526     contentWindow, targetQuery, opt_iframeQuery) {
    527   var event = new MouseEvent('click', { bubbles: true, detail: 1 });
    528   return test.util.sync.sendEvent(
    529       contentWindow, targetQuery, event, opt_iframeQuery);
    530 };
    531 
    532 /**
    533  * Simulates a fake double click event (left button) to the element specified by
    534  * |targetQuery|.
    535  *
    536  * @param {Window} contentWindow Window to be tested.
    537  * @param {string} targetQuery Query to specify the element.
    538  * @param {string=} opt_iframeQuery Optional iframe selector.
    539  * @return {boolean} True if the event is sent to the target, false otherwise.
    540  */
    541 test.util.sync.fakeMouseDoubleClick = function(
    542     contentWindow, targetQuery, opt_iframeQuery) {
    543   // Double click is always preceeded with a single click.
    544   if (!test.util.sync.fakeMouseClick(
    545       contentWindow, targetQuery, opt_iframeQuery)) {
    546     return false;
    547   }
    548 
    549   // Send the second click event, but with detail equal to 2 (number of clicks)
    550   // in a row.
    551   var event = new MouseEvent('click', { bubbles: true, detail: 2 });
    552   if (!test.util.sync.sendEvent(
    553       contentWindow, targetQuery, event, opt_iframeQuery)) {
    554     return false;
    555   }
    556 
    557   // Send the double click event.
    558   var event = new MouseEvent('dblclick', { bubbles: true });
    559   if (!test.util.sync.sendEvent(
    560       contentWindow, targetQuery, event, opt_iframeQuery)) {
    561     return false;
    562   }
    563 
    564   return true;
    565 };
    566 
    567 /**
    568  * Sends a fake mouse down event to the element specified by |targetQuery|.
    569  *
    570  * @param {Window} contentWindow Window to be tested.
    571  * @param {string} targetQuery Query to specify the element.
    572  * @param {string=} opt_iframeQuery Optional iframe selector.
    573  * @return {boolean} True if the event is sent to the target, false otherwise.
    574  */
    575 test.util.sync.fakeMouseDown = function(
    576     contentWindow, targetQuery, opt_iframeQuery) {
    577   var event = new MouseEvent('mousedown', { bubbles: true });
    578   return test.util.sync.sendEvent(
    579       contentWindow, targetQuery, event, opt_iframeQuery);
    580 };
    581 
    582 /**
    583  * Sends a fake mouse up event to the element specified by |targetQuery|.
    584  *
    585  * @param {Window} contentWindow Window to be tested.
    586  * @param {string} targetQuery Query to specify the element.
    587  * @param {string=} opt_iframeQuery Optional iframe selector.
    588  * @return {boolean} True if the event is sent to the target, false otherwise.
    589  */
    590 test.util.sync.fakeMouseUp = function(
    591     contentWindow, targetQuery, opt_iframeQuery) {
    592   var event = new MouseEvent('mouseup', { bubbles: true });
    593   return test.util.sync.sendEvent(
    594       contentWindow, targetQuery, event, opt_iframeQuery);
    595 };
    596 
    597 /**
    598  * Selects |filename| and fakes pressing Ctrl+C, Ctrl+V (copy, paste).
    599  *
    600  * @param {Window} contentWindow Window to be tested.
    601  * @param {string} filename Name of the file to be copied.
    602  * @return {boolean} True if copying got simulated successfully. It does not
    603  *     say if the file got copied, or not.
    604  */
    605 test.util.sync.copyFile = function(contentWindow, filename) {
    606   if (!test.util.sync.selectFile(contentWindow, filename))
    607     return false;
    608   // Ctrl+C and Ctrl+V
    609   test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0043', true);
    610   test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0056', true);
    611   return true;
    612 };
    613 
    614 /**
    615  * Selects |filename| and fakes pressing the Delete key.
    616  *
    617  * @param {Window} contentWindow Window to be tested.
    618  * @param {string} filename Name of the file to be deleted.
    619  * @return {boolean} True if deleting got simulated successfully. It does not
    620  *     say if the file got deleted, or not.
    621  */
    622 test.util.sync.deleteFile = function(contentWindow, filename) {
    623   if (!test.util.sync.selectFile(contentWindow, filename))
    624     return false;
    625   // Delete
    626   test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+007F', false);
    627   return true;
    628 };
    629 
    630 /**
    631  * Wait for the elements' style to be changed as the expected values.  The
    632  * queries argument is a list of object that have the query property and the
    633  * styles property. The query property is a string query to specify the
    634  * element. The styles property is a string map of the style name and its
    635  * expected value.
    636  *
    637  * @param {Window} contentWindow Window to be tested.
    638  * @param {Array.<object>} queries Queries that specifies the elements and
    639  *   expected styles.
    640  * @param {function()} callback Callback function to be notified the change of
    641  *     the styles.
    642  */
    643 test.util.async.waitForStyles = function(contentWindow, queries, callback) {
    644   test.util.repeatUntilTrue_(function() {
    645     for (var i = 0; i < queries.length; i++) {
    646       var element = contentWindow.document.querySelector(queries[i].query);
    647       var styles = queries[i].styles;
    648       for (var name in styles) {
    649         if (contentWindow.getComputedStyle(element)[name] != styles[name])
    650           return false;
    651       }
    652     }
    653     callback();
    654     return true;
    655   });
    656 };
    657 
    658 /**
    659  * Execute a command on the document in the specified window.
    660  *
    661  * @param {Window} contentWindow Window to be tested.
    662  * @param {string} command Command name.
    663  * @return {boolean} True if the command is executed successfully.
    664  */
    665 test.util.sync.execCommand = function(contentWindow, command) {
    666   return contentWindow.document.execCommand(command);
    667 };
    668 
    669 /**
    670  * Registers message listener, which runs test utility functions.
    671  */
    672 test.util.registerRemoteTestUtils = function() {
    673   // Register the message listenr.
    674   var onMessage = chrome.runtime ? chrome.runtime.onMessageExternal :
    675       chrome.extension.onMessageExternal;
    676   // Return true for asynchronous functions and false for synchronous.
    677   onMessage.addListener(function(request, sender, sendResponse) {
    678     // Check the sender.
    679     if (sender.id != test.util.TESTING_EXTENSION_ID) {
    680       console.error('The testing extension must be white-listed.');
    681       return false;
    682     }
    683     // Set a global flag that we are in tests, so other components are aware
    684     // of it.
    685     window.IN_TEST = true;
    686     // Check the function name.
    687     if (!request.func || request.func[request.func.length - 1] == '_') {
    688       request.func = '';
    689     }
    690     // Prepare arguments.
    691     var args = request.args.slice();  // shallow copy
    692     if (request.appId) {
    693       if (!appWindows[request.appId]) {
    694         console.error('Specified window not found.');
    695         return false;
    696       }
    697       args.unshift(appWindows[request.appId].contentWindow);
    698     }
    699     // Call the test utility function and respond the result.
    700     if (test.util.async[request.func]) {
    701       args[test.util.async[request.func].length - 1] = function() {
    702         console.debug('Received the result of ' + request.func);
    703         sendResponse.apply(null, arguments);
    704       };
    705       console.debug('Waiting for the result of ' + request.func);
    706       test.util.async[request.func].apply(null, args);
    707       return true;
    708     } else if (test.util.sync[request.func]) {
    709       sendResponse(test.util.sync[request.func].apply(null, args));
    710       return false;
    711     } else {
    712       console.error('Invalid function name.');
    713       return false;
    714     }
    715   });
    716 };
    717 
    718 // Register the test utils.
    719 test.util.registerRemoteTestUtils();
    720