• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
8var 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 */
20test.util = {};
21
22/**
23 * Namespace for synchronous utility functions.
24 */
25test.util.sync = {};
26
27/**
28 * Namespace for asynchronous utility functions.
29 */
30test.util.async = {};
31
32/**
33 * Extension ID of the testing extension.
34 * @type {string}
35 * @const
36 */
37test.util.TESTING_EXTENSION_ID = 'oobinhbdbiehknkpbpejbbpdbkdjmoco';
38
39/**
40 * Interval of checking a condition in milliseconds.
41 * @type {number}
42 * @const
43 * @private
44 */
45test.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 */
52test.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 */
68test.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 */
100test.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 */
121test.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 */
134test.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 */
152test.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 */
163test.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 */
182test.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 */
205test.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 */
233test.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 */
256test.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 */
284test.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 */
309test.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 */
330test.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 */
364test.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 */
388test.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 */
409test.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 */
423test.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 */
474test.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 */
506test.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 */
521test.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 */
543test.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 */
559test.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 */
579test.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 */
605test.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 */
639test.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 */
654test.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 */
669test.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 */
686test.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 */
707test.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 */
729test.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 */
742test.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 */
776test.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 */
803test.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 */
821test.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.
868test.util.registerRemoteTestUtils();
869