cros: Remove default pinned apps trial.
[chromium-blink-merge.git] / chrome / browser / resources / file_manager / background / js / test_util.js
blobcca9b3539dc298fb8df7c2e676f682ec2adaeb1e
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.
5 /**
6  * Namespace for test related things.
7  */
8 var test = test || {};
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 = {};
22 /**
23  * Namespace for synchronous utility functions.
24  */
25 test.util.sync = {};
27 /**
28  * Namespace for asynchronous utility functions.
29  */
30 test.util.async = {};
32 /**
33  * Extension ID of the testing extension.
34  * @type {string}
35  * @const
36  */
37 test.util.TESTING_EXTENSION_ID = 'oobinhbdbiehknkpbpejbbpdbkdjmoco';
39 /**
40  * Interval of checking a condition in milliseconds.
41  * @type {number}
42  * @const
43  * @private
44  */
45 test.util.WAITTING_INTERVAL_ = 50;
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();
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()();
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   });
114  * Gets a document in the Files.app's window, including iframes.
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   }
127   return contentWindow.document;
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;
145  * Resizes the window to the specified dimensions.
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;
158  * Returns an array with the files currently selected in the file manager.
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;
177  * Returns an array with the files on the file manager's file list.
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;
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;
217   if (selected[0].modelItem.path !== path ||
218       selected[0].querySelector('.root-label').textContent !== label) {
219     return false;
220   }
222   return true;
226  * Waits until the window is set to the specified dimensions.
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   });
246  * Waits for an element and returns it as an array of it's attributes.
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   });
277  * Calls getFileList until the number of displayed files is different from
278  * lengthBefore.
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   });
304  * Returns an array of items on the file manager's autocomplete list.
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;
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".
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);
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);
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   });
359  * Waits until a dialog with an OK button is shown and accepts it.
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   });
382  * Fakes pressing the down arrow until the given |filename| is selected.
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;
402  * Open the file by selectFile and fakeMouseDoubleClick.
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);
416  * Selects a volume specified by its icon name
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();
463  * Waits the contents of file list becomes to equal to expected contents.
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   });
498  * Executes Javascript code on a webview and returns the result.
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);
513  * Sends an event to the element specified by |targetQuery|.
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;
536  * Sends an fake event having the specified type to the target query.
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));
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.
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);
569  * Sends a fake mouse click event (left button, single click) to the element
570  * specified by |targetQuery|.
572  * @param {Window} contentWindow Window to be tested.
573  * @param {string} targetQuery Query to specify the element.
574  * @param {string=} opt_iframeQuery Optional iframe selector.
575  * @return {boolean} True if the event is sent to the target, false otherwise.
576  */
577 test.util.sync.fakeMouseClick = function(
578     contentWindow, targetQuery, opt_iframeQuery) {
579   var event = new MouseEvent('click', { bubbles: true, detail: 1 });
580   return test.util.sync.sendEvent(
581       contentWindow, targetQuery, event, opt_iframeQuery);
585  * Simulates a fake double click event (left button) to the element specified by
586  * |targetQuery|.
588  * @param {Window} contentWindow Window to be tested.
589  * @param {string} targetQuery Query to specify the element.
590  * @param {string=} opt_iframeQuery Optional iframe selector.
591  * @return {boolean} True if the event is sent to the target, false otherwise.
592  */
593 test.util.sync.fakeMouseDoubleClick = function(
594     contentWindow, targetQuery, opt_iframeQuery) {
595   // Double click is always preceded with a single click.
596   if (!test.util.sync.fakeMouseClick(
597       contentWindow, targetQuery, opt_iframeQuery)) {
598     return false;
599   }
601   // Send the second click event, but with detail equal to 2 (number of clicks)
602   // in a row.
603   var event = new MouseEvent('click', { bubbles: true, detail: 2 });
604   if (!test.util.sync.sendEvent(
605       contentWindow, targetQuery, event, opt_iframeQuery)) {
606     return false;
607   }
609   // Send the double click event.
610   var event = new MouseEvent('dblclick', { bubbles: true });
611   if (!test.util.sync.sendEvent(
612       contentWindow, targetQuery, event, opt_iframeQuery)) {
613     return false;
614   }
616   return true;
620  * Sends a fake mouse down event to the element specified by |targetQuery|.
622  * @param {Window} contentWindow Window to be tested.
623  * @param {string} targetQuery Query to specify the element.
624  * @param {string=} opt_iframeQuery Optional iframe selector.
625  * @return {boolean} True if the event is sent to the target, false otherwise.
626  */
627 test.util.sync.fakeMouseDown = function(
628     contentWindow, targetQuery, opt_iframeQuery) {
629   var event = new MouseEvent('mousedown', { bubbles: true });
630   return test.util.sync.sendEvent(
631       contentWindow, targetQuery, event, opt_iframeQuery);
635  * Sends a fake mouse up event to the element specified by |targetQuery|.
637  * @param {Window} contentWindow Window to be tested.
638  * @param {string} targetQuery Query to specify the element.
639  * @param {string=} opt_iframeQuery Optional iframe selector.
640  * @return {boolean} True if the event is sent to the target, false otherwise.
641  */
642 test.util.sync.fakeMouseUp = function(
643     contentWindow, targetQuery, opt_iframeQuery) {
644   var event = new MouseEvent('mouseup', { bubbles: true });
645   return test.util.sync.sendEvent(
646       contentWindow, targetQuery, event, opt_iframeQuery);
650  * Selects |filename| and fakes pressing Ctrl+C, Ctrl+V (copy, paste).
652  * @param {Window} contentWindow Window to be tested.
653  * @param {string} filename Name of the file to be copied.
654  * @return {boolean} True if copying got simulated successfully. It does not
655  *     say if the file got copied, or not.
656  */
657 test.util.sync.copyFile = function(contentWindow, filename) {
658   if (!test.util.sync.selectFile(contentWindow, filename))
659     return false;
660   // Ctrl+C and Ctrl+V
661   test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0043', true);
662   test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0056', true);
663   return true;
667  * Selects |filename| and fakes pressing the Delete key.
669  * @param {Window} contentWindow Window to be tested.
670  * @param {string} filename Name of the file to be deleted.
671  * @return {boolean} True if deleting got simulated successfully. It does not
672  *     say if the file got deleted, or not.
673  */
674 test.util.sync.deleteFile = function(contentWindow, filename) {
675   if (!test.util.sync.selectFile(contentWindow, filename))
676     return false;
677   // Delete
678   test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+007F', false);
679   return true;
683  * Wait for the elements' style to be changed as the expected values.  The
684  * queries argument is a list of object that have the query property and the
685  * styles property. The query property is a string query to specify the
686  * element. The styles property is a string map of the style name and its
687  * expected value.
689  * @param {Window} contentWindow Window to be tested.
690  * @param {Array.<object>} queries Queries that specifies the elements and
691  *     expected styles.
692  * @param {function()} callback Callback function to be notified the change of
693  *     the styles.
694  */
695 test.util.async.waitForStyles = function(contentWindow, queries, callback) {
696   test.util.repeatUntilTrue_(function() {
697     for (var i = 0; i < queries.length; i++) {
698       var element = contentWindow.document.querySelector(queries[i].query);
699       var styles = queries[i].styles;
700       for (var name in styles) {
701         if (contentWindow.getComputedStyle(element)[name] != styles[name])
702           return false;
703       }
704     }
705     callback();
706     return true;
707   });
711  * Execute a command on the document in the specified window.
713  * @param {Window} contentWindow Window to be tested.
714  * @param {string} command Command name.
715  * @return {boolean} True if the command is executed successfully.
716  */
717 test.util.sync.execCommand = function(contentWindow, command) {
718   return contentWindow.document.execCommand(command);
722  * Override the installWebstoreItem method in private api for test.
724  * @param {Window} contentWindow Window to be tested.
725  * @param {string} expectedItemId Item ID to be called this method with.
726  * @param {?string} intendedError Error message to be returned when the item id
727  *     matches. 'null' represents no error.
728  * @return {boolean} Always return true.
729  */
730 test.util.sync.overrideInstallWebstoreItemApi =
731     function(contentWindow, expectedItemId, intendedError) {
732   var setLastError = function(message) {
733     contentWindow.chrome.runtime.lastError =
734         message ? {message: message} : null;
735   };
737   var installWebstoreItem = function(itemId, callback) {
738     setTimeout(function() {
739       if (itemId !== expectedItemId) {
740         setLastError('Invalid Chrome Web Store item ID');
741         callback();
742         return;
743       }
745       setLastError(intendedError);
746       callback();
747     });
748   };
750   test.util.executedTasks_ = [];
751   contentWindow.chrome.fileBrowserPrivate.installWebstoreItem =
752       installWebstoreItem;
753   return true;
757  * Override the task-related methods in private api for test.
759  * @param {Window} contentWindow Window to be tested.
760  * @param {Array.<Object>} taskList List of tasks to be returned in
761  *     fileBrowserPrivate.getFileTasks().
762  * @return {boolean} Always return true.
763  */
764 test.util.sync.overrideTasks = function(contentWindow, taskList) {
765   var getFileTasks = function(urls, mime, onTasks) {
766     // Call onTask asynchronously (same with original getFileTasks).
767     setTimeout(function() {
768       onTasks(taskList);
769     });
770   };
772   var executeTask = function(taskId, url) {
773     test.util.executedTasks_.push(taskId);
774   };
776   test.util.executedTasks_ = [];
777   contentWindow.chrome.fileBrowserPrivate.getFileTasks = getFileTasks;
778   contentWindow.chrome.fileBrowserPrivate.executeTask = executeTask;
779   return true;
783  * Check if Files.app has ordered to execute the given task or not yet. This
784  * method must be used with test.util.sync.overrideTasks().
786  * @param {Window} contentWindow Window to be tested.
787  * @param {string} taskId Taskid of the task which should be executed.
788  * @param {function()} callback Callback function to be notified the order of
789  *     the execution.
790  */
791 test.util.async.waitUntilTaskExecutes =
792     function(contentWindow, taskId, callback) {
793   if (!test.util.executedTasks_) {
794     console.error('Please call overrideTasks() first.');
795     return;
796   }
798   test.util.repeatUntilTrue_(function() {
799     if (test.util.executedTasks_.indexOf(taskId) === -1)
800       return false;
801     callback();
802     return true;
803   });
807  * Registers message listener, which runs test utility functions.
808  */
809 test.util.registerRemoteTestUtils = function() {
810   // Register the message listener.
811   var onMessage = chrome.runtime ? chrome.runtime.onMessageExternal :
812       chrome.extension.onMessageExternal;
813   // Return true for asynchronous functions and false for synchronous.
814   onMessage.addListener(function(request, sender, sendResponse) {
815     // Check the sender.
816     if (sender.id != test.util.TESTING_EXTENSION_ID) {
817       console.error('The testing extension must be white-listed.');
818       return false;
819     }
820     // Set a global flag that we are in tests, so other components are aware
821     // of it.
822     window.IN_TEST = true;
823     // Check the function name.
824     if (!request.func || request.func[request.func.length - 1] == '_') {
825       request.func = '';
826     }
827     // Prepare arguments.
828     var args = request.args.slice();  // shallow copy
829     if (request.appId) {
830       if (!background.appWindows[request.appId]) {
831         console.error('Specified window not found.');
832         return false;
833       }
834       args.unshift(background.appWindows[request.appId].contentWindow);
835     }
836     // Call the test utility function and respond the result.
837     if (test.util.async[request.func]) {
838       args[test.util.async[request.func].length - 1] = function() {
839         console.debug('Received the result of ' + request.func);
840         sendResponse.apply(null, arguments);
841       };
842       console.debug('Waiting for the result of ' + request.func);
843       test.util.async[request.func].apply(null, args);
844       return true;
845     } else if (test.util.sync[request.func]) {
846       sendResponse(test.util.sync[request.func].apply(null, args));
847       return false;
848     } else {
849       console.error('Invalid function name.');
850       return false;
851     }
852   });
855 // Register the test utils.
856 test.util.registerRemoteTestUtils();