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 {Object} appState App state.
     65  * @param {function(string)} callback Completion callback with the new window's
     66  *     App ID.
     67  */
     68 test.util.async.openMainWindow = function(appState, callback) {
     69   var steps = [
     70     function() {
     71       launchFileManager(appState,
     72                         undefined,  // opt_type
     73                         undefined,  // opt_id
     74                         steps.shift());
     75     },
     76     function(appId) {
     77       test.util.repeatUntilTrue_(function() {
     78         if (!background.appWindows[appId])
     79           return false;
     80         var contentWindow = background.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 background.appWindows) {
    103       if (appId.indexOf(appIdPrefix) == 0 &&
    104           background.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 = JSErrorCount;
    136   for (var appId in background.appWindows) {
    137     var contentWindow = background.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   background.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  * Checkes if the given label and path of the volume are selected.
    200  * @param {Window} contentWindow Window to be tested.
    201  * @param {string} label Correct label the selected volume should have.
    202  * @param {string} path Correct path the selected volume should have.
    203  * @return {boolean} True for success.
    204  */
    205 test.util.sync.checkSelectedVolume = function(contentWindow, label, path) {
    206   var list = contentWindow.document.querySelector('#navigation-list');
    207   var rows = list.querySelectorAll('li');
    208   var selected = [];
    209   for (var i = 0; i < rows.length; ++i) {
    210     if (rows[i].hasAttribute('selected'))
    211       selected.push(rows[i]);
    212   }
    213   // Selected item must be one.
    214   if (selected.length !== 1)
    215     return false;
    216 
    217   if (selected[0].modelItem.path !== path ||
    218       selected[0].querySelector('.root-label').textContent !== label) {
    219     return false;
    220   }
    221 
    222   return true;
    223 };
    224 
    225 /**
    226  * Waits until the window is set to the specified dimensions.
    227  *
    228  * @param {Window} contentWindow Window to be tested.
    229  * @param {number} width Requested width.
    230  * @param {number} height Requested height.
    231  * @param {function(Object)} callback Success callback with the dimensions.
    232  */
    233 test.util.async.waitForWindowGeometry = function(
    234     contentWindow, width, height, callback) {
    235   test.util.repeatUntilTrue_(function() {
    236     if (contentWindow.innerWidth == width &&
    237         contentWindow.innerHeight == height) {
    238       callback({width: width, height: height});
    239       return true;
    240     }
    241     return false;
    242   });
    243 };
    244 
    245 /**
    246  * Waits for an element and returns it as an array of it's attributes.
    247  *
    248  * @param {Window} contentWindow Window to be tested.
    249  * @param {string} targetQuery Query to specify the element.
    250  * @param {?string} iframeQuery Iframe selector or null if no iframe.
    251  * @param {boolean=} opt_inverse True if the function should return if the
    252  *    element disappears, instead of appearing.
    253  * @param {function(Object)} callback Callback with a hash array of attributes
    254  *     and contents as text.
    255  */
    256 test.util.async.waitForElement = function(
    257     contentWindow, targetQuery, iframeQuery, opt_inverse, callback) {
    258   test.util.repeatUntilTrue_(function() {
    259     var doc = test.util.sync.getDocument_(contentWindow, iframeQuery);
    260     if (!doc)
    261       return false;
    262     var element = doc.querySelector(targetQuery);
    263     if (!element)
    264       return !!opt_inverse;
    265     var attributes = {};
    266     for (var i = 0; i < element.attributes.length; i++) {
    267       attributes[element.attributes[i].nodeName] =
    268           element.attributes[i].nodeValue;
    269     }
    270     var text = element.textContent;
    271     callback({attributes: attributes, text: text});
    272     return !opt_inverse;
    273   });
    274 };
    275 
    276 /**
    277  * Calls getFileList until the number of displayed files is different from
    278  * lengthBefore.
    279  *
    280  * @param {Window} contentWindow Window to be tested.
    281  * @param {number} lengthBefore Number of items visible before.
    282  * @param {function(Array.<Array.<string>>)} callback Change callback.
    283  */
    284 test.util.async.waitForFileListChange = function(
    285     contentWindow, lengthBefore, callback) {
    286   test.util.repeatUntilTrue_(function() {
    287     var files = test.util.sync.getFileList(contentWindow);
    288     files.sort();
    289     var notReadyRows = files.filter(function(row) {
    290       return row.filter(function(cell) { return cell == '...'; }).length;
    291     });
    292     if (notReadyRows.length === 0 &&
    293         files.length !== lengthBefore &&
    294         files.length !== 0) {
    295       callback(files);
    296       return true;
    297     } else {
    298       return false;
    299     }
    300   });
    301 };
    302 
    303 /**
    304  * Returns an array of items on the file manager's autocomplete list.
    305  *
    306  * @param {Window} contentWindow Window to be tested.
    307  * @return {Array.<string>} Array of items.
    308  */
    309 test.util.sync.getAutocompleteList = function(contentWindow) {
    310   var list = contentWindow.document.querySelector('#autocomplete-list');
    311   var lines = list.querySelectorAll('li');
    312   var items = [];
    313   for (var j = 0; j < lines.length; ++j) {
    314     var line = lines[j];
    315     items.push(line.innerText);
    316   }
    317   return items;
    318 };
    319 
    320 /**
    321  * Performs autocomplete with the given query and waits until at least
    322  * |numExpectedItems| items are shown, including the first item which
    323  * always looks like "'<query>' - search Drive".
    324  *
    325  * @param {Window} contentWindow Window to be tested.
    326  * @param {string} query Query used for autocomplete.
    327  * @param {number} numExpectedItems number of items to be shown.
    328  * @param {function(Array.<string>)} callback Change callback.
    329  */
    330 test.util.async.performAutocompleteAndWait = function(
    331     contentWindow, query, numExpectedItems, callback) {
    332   // Dispatch a 'focus' event to the search box so that the autocomplete list
    333   // is attached to the search box. Note that calling searchBox.focus() won't
    334   // dispatch a 'focus' event.
    335   var searchBox = contentWindow.document.querySelector('#search-box input');
    336   var focusEvent = contentWindow.document.createEvent('Event');
    337   focusEvent.initEvent('focus', true /* bubbles */, true /* cancelable */);
    338   searchBox.dispatchEvent(focusEvent);
    339 
    340   // Change the value of the search box and dispatch an 'input' event so that
    341   // the autocomplete query is processed.
    342   searchBox.value = query;
    343   var inputEvent = contentWindow.document.createEvent('Event');
    344   inputEvent.initEvent('input', true /* bubbles */, true /* cancelable */);
    345   searchBox.dispatchEvent(inputEvent);
    346 
    347   test.util.repeatUntilTrue_(function() {
    348     var items = test.util.sync.getAutocompleteList(contentWindow);
    349     if (items.length >= numExpectedItems) {
    350       callback(items);
    351       return true;
    352     } else {
    353       return false;
    354     }
    355   });
    356 };
    357 
    358 /**
    359  * Waits until a dialog with an OK button is shown and accepts it.
    360  *
    361  * @param {Window} contentWindow Window to be tested.
    362  * @param {function()} callback Success callback.
    363  */
    364 test.util.async.waitAndAcceptDialog = function(contentWindow, callback) {
    365   test.util.repeatUntilTrue_(function() {
    366     var button = contentWindow.document.querySelector('.cr-dialog-ok');
    367     if (!button)
    368       return false;
    369     button.click();
    370     // Wait until the dialog is removed from the DOM.
    371     test.util.repeatUntilTrue_(function() {
    372       if (contentWindow.document.querySelector('.cr-dialog-container'))
    373         return false;
    374       callback();
    375       return true;
    376     });
    377     return true;
    378   });
    379 };
    380 
    381 /**
    382  * Fakes pressing the down arrow until the given |filename| is selected.
    383  *
    384  * @param {Window} contentWindow Window to be tested.
    385  * @param {string} filename Name of the file to be selected.
    386  * @return {boolean} True if file got selected, false otherwise.
    387  */
    388 test.util.sync.selectFile = function(contentWindow, filename) {
    389   var table = contentWindow.document.querySelector('#detail-table');
    390   var rows = table.querySelectorAll('li');
    391   for (var index = 0; index < rows.length; ++index) {
    392     test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'Down', false);
    393     var selection = test.util.sync.getSelectedFiles(contentWindow);
    394     if (selection.length === 1 && selection[0] === filename)
    395       return true;
    396   }
    397   console.error('Failed to select file "' + filename + '"');
    398   return false;
    399 };
    400 
    401 /**
    402  * Open the file by selectFile and fakeMouseDoubleClick.
    403  *
    404  * @param {Window} contentWindow Window to be tested.
    405  * @param {string} filename Name of the file to be opened.
    406  * @return {boolean} True if file got selected and a double click message is
    407  *     sent, false otherwise.
    408  */
    409 test.util.sync.openFile = function(contentWindow, filename) {
    410   var query = '#file-list li.table-row[selected] .filename-label span';
    411   return test.util.sync.selectFile(contentWindow, filename) &&
    412          test.util.sync.fakeMouseDoubleClick(contentWindow, query);
    413 };
    414 
    415 /**
    416  * Selects a volume specified by its icon name
    417  *
    418  * @param {Window} contentWindow Window to be tested.
    419  * @param {string} iconName Name of the volume icon.
    420  * @param {function(boolean)} callback Callback function to notify the caller
    421  *     whether the target is found and mousedown and click events are sent.
    422  */
    423 test.util.async.selectVolume = function(contentWindow, iconName, callback) {
    424   var query = '[volume-type-icon=' + iconName + ']';
    425   var driveQuery = '[volume-type-icon=drive]';
    426   var isDriveSubVolume = iconName == 'drive_recent' ||
    427                          iconName == 'drive_shared_with_me' ||
    428                          iconName == 'drive_offline';
    429   var preSelection = false;
    430   var steps = {
    431     checkQuery: function() {
    432       if (contentWindow.document.querySelector(query)) {
    433         steps.sendEvents();
    434         return;
    435       }
    436       // If the target volume is sub-volume of drive, we must click 'drive'
    437       // before clicking the sub-item.
    438       if (!preSelection) {
    439         if (!isDriveSubVolume) {
    440           callback(false);
    441           return;
    442         }
    443         if (!(test.util.sync.fakeMouseDown(contentWindow, driveQuery) &&
    444               test.util.sync.fakeMouseClick(contentWindow, driveQuery))) {
    445           callback(false);
    446           return;
    447         }
    448         preSelection = true;
    449       }
    450       setTimeout(steps.checkQuery, 50);
    451     },
    452     sendEvents: function() {
    453       // To change the selected volume, we have to send both events 'mousedown'
    454       // and 'click' to the navigation list.
    455       callback(test.util.sync.fakeMouseDown(contentWindow, query) &&
    456                test.util.sync.fakeMouseClick(contentWindow, query));
    457     }
    458   };
    459   steps.checkQuery();
    460 };
    461 
    462 /**
    463  * Waits the contents of file list becomes to equal to expected contents.
    464  *
    465  * @param {Window} contentWindow Window to be tested.
    466  * @param {Array.<Array.<string>>} expected Expected contents of file list.
    467  * @param {{orderCheck:boolean=, ignoreLastModifiedTime:boolean=}=} opt_options
    468  *     Options of the comparison. If orderCheck is true, it also compares the
    469  *     order of files. If ignoreLastModifiedTime is true, it compares the file
    470  *     without its last modified time.
    471  * @param {function()} callback Callback function to notify the caller that
    472  *     expected files turned up.
    473  */
    474 test.util.async.waitForFiles = function(
    475     contentWindow, expected, opt_options, callback) {
    476   var options = opt_options || {};
    477   test.util.repeatUntilTrue_(function() {
    478     var files = test.util.sync.getFileList(contentWindow);
    479     if (!options.orderCheck) {
    480       files.sort();
    481       expected.sort();
    482     }
    483     if (options.ignoreLastModifiedTime) {
    484       for (var i = 0; i < Math.min(files.length, expected.length); i++) {
    485         files[i][3] = '';
    486         expected[i][3] = '';
    487       }
    488     }
    489     if (chrome.test.checkDeepEq(expected, files)) {
    490       callback(true);
    491       return true;
    492     }
    493     return false;
    494   });
    495 };
    496 
    497 /**
    498  * Executes Javascript code on a webview and returns the result.
    499  *
    500  * @param {Window} contentWindow Window to be tested.
    501  * @param {string} webViewQuery Selector for the web view.
    502  * @param {string} code Javascript code to be executed within the web view.
    503  * @param {function(*)} callback Callback function with results returned by the
    504  *     script.
    505  */
    506 test.util.async.executeScriptInWebView = function(
    507     contentWindow, webViewQuery, code, callback) {
    508   var webView = contentWindow.document.querySelector(webViewQuery);
    509   webView.executeScript({code: code}, callback);
    510 };
    511 
    512 /**
    513  * Sends an event to the element specified by |targetQuery|.
    514  *
    515  * @param {Window} contentWindow Window to be tested.
    516  * @param {string} targetQuery Query to specify the element.
    517  * @param {Event} event Event to be sent.
    518  * @param {string=} opt_iframeQuery Optional iframe selector.
    519  * @return {boolean} True if the event is sent to the target, false otherwise.
    520  */
    521 test.util.sync.sendEvent = function(
    522     contentWindow, targetQuery, event, opt_iframeQuery) {
    523   var doc = test.util.sync.getDocument_(contentWindow, opt_iframeQuery);
    524   if (doc) {
    525     var target = doc.querySelector(targetQuery);
    526     if (target) {
    527       target.dispatchEvent(event);
    528       return true;
    529     }
    530   }
    531   console.error('Target element for ' + targetQuery + ' not found.');
    532   return false;
    533 };
    534 
    535 /**
    536  * Sends an fake event having the specified type to the target query.
    537  *
    538  * @param {Window} contentWindow Window to be tested.
    539  * @param {string} targetQuery Query to specify the element.
    540  * @param {string} event Type of event.
    541  * @return {boolean} True if the event is sent to the target, false otherwise.
    542  */
    543 test.util.sync.fakeEvent = function(contentWindow, targetQuery, event) {
    544   return test.util.sync.sendEvent(
    545       contentWindow, targetQuery, new Event(event));
    546 };
    547 
    548 /**
    549  * Sends a fake key event to the element specified by |targetQuery| with the
    550  * given |keyIdentifier| and optional |ctrl| modifier to the file manager.
    551  *
    552  * @param {Window} contentWindow Window to be tested.
    553  * @param {string} targetQuery Query to specify the element.
    554  * @param {string} keyIdentifier Identifier of the emulated key.
    555  * @param {boolean} ctrl Whether CTRL should be pressed, or not.
    556  * @param {string=} opt_iframeQuery Optional iframe selector.
    557  * @return {boolean} True if the event is sent to the target, false otherwise.
    558  */
    559 test.util.sync.fakeKeyDown = function(
    560     contentWindow, targetQuery, keyIdentifier, ctrl, opt_iframeQuery) {
    561   var event = new KeyboardEvent(
    562       'keydown',
    563       { bubbles: true, keyIdentifier: keyIdentifier, ctrlKey: ctrl });
    564   return test.util.sync.sendEvent(
    565       contentWindow, targetQuery, event, opt_iframeQuery);
    566 };
    567 
    568 /**
    569  * Simulates a fake mouse click (left button, single click) on the element
    570  * specified by |targetQuery|. This sends 'mouseover', 'mousedown', 'mouseup'
    571  * and 'click' events in turns.
    572  *
    573  * @param {Window} contentWindow Window to be tested.
    574  * @param {string} targetQuery Query to specify the element.
    575  * @param {string=} opt_iframeQuery Optional iframe selector.
    576  * @return {boolean} True if the all events are sent to the target, false
    577  *     otherwise.
    578  */
    579 test.util.sync.fakeMouseClick = function(
    580     contentWindow, targetQuery, opt_iframeQuery) {
    581   var mouseOverEvent = new MouseEvent('mouseover', {bubbles: true, detail: 1});
    582   var resultMouseOver = test.util.sync.sendEvent(
    583       contentWindow, targetQuery, mouseOverEvent, opt_iframeQuery);
    584   var mouseDownEvent = new MouseEvent('mousedown', {bubbles: true, detail: 1});
    585   var resultMouseDown = test.util.sync.sendEvent(
    586       contentWindow, targetQuery, mouseDownEvent, opt_iframeQuery);
    587   var mouseUpEvent = new MouseEvent('mouseup', {bubbles: true, detail: 1});
    588   var resultMouseUp = test.util.sync.sendEvent(
    589       contentWindow, targetQuery, mouseUpEvent, opt_iframeQuery);
    590   var clickEvent = new MouseEvent('click', {bubbles: true, detail: 1});
    591   var resultClick = test.util.sync.sendEvent(
    592       contentWindow, targetQuery, clickEvent, opt_iframeQuery);
    593   return resultMouseOver && resultMouseDown && resultMouseUp && resultClick;
    594 };
    595 
    596 /**
    597  * Simulates a fake double click event (left button) to the element specified by
    598  * |targetQuery|.
    599  *
    600  * @param {Window} contentWindow Window to be tested.
    601  * @param {string} targetQuery Query to specify the element.
    602  * @param {string=} opt_iframeQuery Optional iframe selector.
    603  * @return {boolean} True if the event is sent to the target, false otherwise.
    604  */
    605 test.util.sync.fakeMouseDoubleClick = function(
    606     contentWindow, targetQuery, opt_iframeQuery) {
    607   // Double click is always preceded with a single click.
    608   if (!test.util.sync.fakeMouseClick(
    609       contentWindow, targetQuery, opt_iframeQuery)) {
    610     return false;
    611   }
    612 
    613   // Send the second click event, but with detail equal to 2 (number of clicks)
    614   // in a row.
    615   var event = new MouseEvent('click', { bubbles: true, detail: 2 });
    616   if (!test.util.sync.sendEvent(
    617       contentWindow, targetQuery, event, opt_iframeQuery)) {
    618     return false;
    619   }
    620 
    621   // Send the double click event.
    622   var event = new MouseEvent('dblclick', { bubbles: true });
    623   if (!test.util.sync.sendEvent(
    624       contentWindow, targetQuery, event, opt_iframeQuery)) {
    625     return false;
    626   }
    627 
    628   return true;
    629 };
    630 
    631 /**
    632  * Sends a fake mouse down event to the element specified by |targetQuery|.
    633  *
    634  * @param {Window} contentWindow Window to be tested.
    635  * @param {string} targetQuery Query to specify the element.
    636  * @param {string=} opt_iframeQuery Optional iframe selector.
    637  * @return {boolean} True if the event is sent to the target, false otherwise.
    638  */
    639 test.util.sync.fakeMouseDown = function(
    640     contentWindow, targetQuery, opt_iframeQuery) {
    641   var event = new MouseEvent('mousedown', { bubbles: true });
    642   return test.util.sync.sendEvent(
    643       contentWindow, targetQuery, event, opt_iframeQuery);
    644 };
    645 
    646 /**
    647  * Sends a fake mouse up event to the element specified by |targetQuery|.
    648  *
    649  * @param {Window} contentWindow Window to be tested.
    650  * @param {string} targetQuery Query to specify the element.
    651  * @param {string=} opt_iframeQuery Optional iframe selector.
    652  * @return {boolean} True if the event is sent to the target, false otherwise.
    653  */
    654 test.util.sync.fakeMouseUp = function(
    655     contentWindow, targetQuery, opt_iframeQuery) {
    656   var event = new MouseEvent('mouseup', { bubbles: true });
    657   return test.util.sync.sendEvent(
    658       contentWindow, targetQuery, event, opt_iframeQuery);
    659 };
    660 
    661 /**
    662  * Selects |filename| and fakes pressing Ctrl+C, Ctrl+V (copy, paste).
    663  *
    664  * @param {Window} contentWindow Window to be tested.
    665  * @param {string} filename Name of the file to be copied.
    666  * @return {boolean} True if copying got simulated successfully. It does not
    667  *     say if the file got copied, or not.
    668  */
    669 test.util.sync.copyFile = function(contentWindow, filename) {
    670   if (!test.util.sync.selectFile(contentWindow, filename))
    671     return false;
    672   // Ctrl+C and Ctrl+V
    673   test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0043', true);
    674   test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0056', true);
    675   return true;
    676 };
    677 
    678 /**
    679  * Selects |filename| and fakes pressing the Delete key.
    680  *
    681  * @param {Window} contentWindow Window to be tested.
    682  * @param {string} filename Name of the file to be deleted.
    683  * @return {boolean} True if deleting got simulated successfully. It does not
    684  *     say if the file got deleted, or not.
    685  */
    686 test.util.sync.deleteFile = function(contentWindow, filename) {
    687   if (!test.util.sync.selectFile(contentWindow, filename))
    688     return false;
    689   // Delete
    690   test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+007F', false);
    691   return true;
    692 };
    693 
    694 /**
    695  * Wait for the elements' style to be changed as the expected values.  The
    696  * queries argument is a list of object that have the query property and the
    697  * styles property. The query property is a string query to specify the
    698  * element. The styles property is a string map of the style name and its
    699  * expected value.
    700  *
    701  * @param {Window} contentWindow Window to be tested.
    702  * @param {Array.<object>} queries Queries that specifies the elements and
    703  *     expected styles.
    704  * @param {function()} callback Callback function to be notified the change of
    705  *     the styles.
    706  */
    707 test.util.async.waitForStyles = function(contentWindow, queries, callback) {
    708   test.util.repeatUntilTrue_(function() {
    709     for (var i = 0; i < queries.length; i++) {
    710       var element = contentWindow.document.querySelector(queries[i].query);
    711       var styles = queries[i].styles;
    712       for (var name in styles) {
    713         if (contentWindow.getComputedStyle(element)[name] != styles[name])
    714           return false;
    715       }
    716     }
    717     callback();
    718     return true;
    719   });
    720 };
    721 
    722 /**
    723  * Execute a command on the document in the specified window.
    724  *
    725  * @param {Window} contentWindow Window to be tested.
    726  * @param {string} command Command name.
    727  * @return {boolean} True if the command is executed successfully.
    728  */
    729 test.util.sync.execCommand = function(contentWindow, command) {
    730   return contentWindow.document.execCommand(command);
    731 };
    732 
    733 /**
    734  * Override the installWebstoreItem method in private api for test.
    735  *
    736  * @param {Window} contentWindow Window to be tested.
    737  * @param {string} expectedItemId Item ID to be called this method with.
    738  * @param {?string} intendedError Error message to be returned when the item id
    739  *     matches. 'null' represents no error.
    740  * @return {boolean} Always return true.
    741  */
    742 test.util.sync.overrideInstallWebstoreItemApi =
    743     function(contentWindow, expectedItemId, intendedError) {
    744   var setLastError = function(message) {
    745     contentWindow.chrome.runtime.lastError =
    746         message ? {message: message} : null;
    747   };
    748 
    749   var installWebstoreItem = function(itemId, callback) {
    750     setTimeout(function() {
    751       if (itemId !== expectedItemId) {
    752         setLastError('Invalid Chrome Web Store item ID');
    753         callback();
    754         return;
    755       }
    756 
    757       setLastError(intendedError);
    758       callback();
    759     });
    760   };
    761 
    762   test.util.executedTasks_ = [];
    763   contentWindow.chrome.fileBrowserPrivate.installWebstoreItem =
    764       installWebstoreItem;
    765   return true;
    766 };
    767 
    768 /**
    769  * Override the task-related methods in private api for test.
    770  *
    771  * @param {Window} contentWindow Window to be tested.
    772  * @param {Array.<Object>} taskList List of tasks to be returned in
    773  *     fileBrowserPrivate.getFileTasks().
    774  * @return {boolean} Always return true.
    775  */
    776 test.util.sync.overrideTasks = function(contentWindow, taskList) {
    777   var getFileTasks = function(urls, mime, onTasks) {
    778     // Call onTask asynchronously (same with original getFileTasks).
    779     setTimeout(function() {
    780       onTasks(taskList);
    781     });
    782   };
    783 
    784   var executeTask = function(taskId, url) {
    785     test.util.executedTasks_.push(taskId);
    786   };
    787 
    788   test.util.executedTasks_ = [];
    789   contentWindow.chrome.fileBrowserPrivate.getFileTasks = getFileTasks;
    790   contentWindow.chrome.fileBrowserPrivate.executeTask = executeTask;
    791   return true;
    792 };
    793 
    794 /**
    795  * Check if Files.app has ordered to execute the given task or not yet. This
    796  * method must be used with test.util.sync.overrideTasks().
    797  *
    798  * @param {Window} contentWindow Window to be tested.
    799  * @param {string} taskId Taskid of the task which should be executed.
    800  * @param {function()} callback Callback function to be notified the order of
    801  *     the execution.
    802  */
    803 test.util.async.waitUntilTaskExecutes =
    804     function(contentWindow, taskId, callback) {
    805   if (!test.util.executedTasks_) {
    806     console.error('Please call overrideTasks() first.');
    807     return;
    808   }
    809 
    810   test.util.repeatUntilTrue_(function() {
    811     if (test.util.executedTasks_.indexOf(taskId) === -1)
    812       return false;
    813     callback();
    814     return true;
    815   });
    816 };
    817 
    818 /**
    819  * Registers message listener, which runs test utility functions.
    820  */
    821 test.util.registerRemoteTestUtils = function() {
    822   // Register the message listener.
    823   var onMessage = chrome.runtime ? chrome.runtime.onMessageExternal :
    824       chrome.extension.onMessageExternal;
    825   // Return true for asynchronous functions and false for synchronous.
    826   onMessage.addListener(function(request, sender, sendResponse) {
    827     // Check the sender.
    828     if (sender.id != test.util.TESTING_EXTENSION_ID) {
    829       console.error('The testing extension must be white-listed.');
    830       return false;
    831     }
    832     // Set a global flag that we are in tests, so other components are aware
    833     // of it.
    834     window.IN_TEST = true;
    835     // Check the function name.
    836     if (!request.func || request.func[request.func.length - 1] == '_') {
    837       request.func = '';
    838     }
    839     // Prepare arguments.
    840     var args = request.args.slice();  // shallow copy
    841     if (request.appId) {
    842       if (!background.appWindows[request.appId]) {
    843         console.error('Specified window not found.');
    844         return false;
    845       }
    846       args.unshift(background.appWindows[request.appId].contentWindow);
    847     }
    848     // Call the test utility function and respond the result.
    849     if (test.util.async[request.func]) {
    850       args[test.util.async[request.func].length - 1] = function() {
    851         console.debug('Received the result of ' + request.func);
    852         sendResponse.apply(null, arguments);
    853       };
    854       console.debug('Waiting for the result of ' + request.func);
    855       test.util.async[request.func].apply(null, args);
    856       return true;
    857     } else if (test.util.sync[request.func]) {
    858       sendResponse(test.util.sync[request.func].apply(null, args));
    859       return false;
    860     } else {
    861       console.error('Invalid function name.');
    862       return false;
    863     }
    864   });
    865 };
    866 
    867 // Register the test utils.
    868 test.util.registerRemoteTestUtils();
    869