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 * Opens the main Files.app's window and waits until it is ready.
41 *
42 * @param {Object} appState App state.
43 * @param {function(string)} callback Completion callback with the new window's
44 *     App ID.
45 */
46test.util.async.openMainWindow = function(appState, callback) {
47  launchFileManager(appState,
48                    undefined,  // opt_type
49                    undefined,  // opt_id
50                    callback);
51};
52
53/**
54 * Obtains window information.
55 *
56 * @return {Object.<string, {innerWidth:number, innerHeight:number}>} Map window
57 *     ID and window information.
58 */
59test.util.sync.getWindows = function() {
60  var windows = {};
61  for (var id in background.appWindows) {
62    var windowWrapper = background.appWindows[id];
63    windows[id] = {
64      outerWidth: windowWrapper.contentWindow.outerWidth,
65      outerHeight: windowWrapper.contentWindow.outerHeight
66    };
67  }
68  for (var id in background.dialogs) {
69    windows[id] = {
70      outerWidth: background.dialogs[id].outerWidth,
71      outerHeight: background.dialogs[id].outerHeight
72    };
73  }
74  return windows;
75};
76
77/**
78 * Closes the specified window.
79 *
80 * @param {string} appId AppId of window to be closed.
81 * @return {boolean} Result: True if success, false otherwise.
82 */
83test.util.sync.closeWindow = function(appId) {
84  if (appId in background.appWindows &&
85      background.appWindows[appId].contentWindow) {
86    background.appWindows[appId].close();
87    return true;
88  }
89  return false;
90};
91
92/**
93 * Gets a document in the Files.app's window, including iframes.
94 *
95 * @param {Window} contentWindow Window to be used.
96 * @param {string=} opt_iframeQuery Query for the iframe.
97 * @return {Document=} Returns the found document or undefined if not found.
98 * @private
99 */
100test.util.sync.getDocument_ = function(contentWindow, opt_iframeQuery) {
101  if (opt_iframeQuery) {
102    var iframe = contentWindow.document.querySelector(opt_iframeQuery);
103    return iframe && iframe.contentWindow && iframe.contentWindow.document;
104  }
105
106  return contentWindow.document;
107};
108
109/**
110 * Gets total Javascript error count from background page and each app window.
111 * @return {number} Error count.
112 */
113test.util.sync.getErrorCount = function() {
114  var totalCount = JSErrorCount;
115  for (var appId in background.appWindows) {
116    var contentWindow = background.appWindows[appId].contentWindow;
117    if (contentWindow.JSErrorCount)
118      totalCount += contentWindow.JSErrorCount;
119  }
120  return totalCount;
121};
122
123/**
124 * Resizes the window to the specified dimensions.
125 *
126 * @param {Window} contentWindow Window to be tested.
127 * @param {number} width Window width.
128 * @param {number} height Window height.
129 * @return {boolean} True for success.
130 */
131test.util.sync.resizeWindow = function(contentWindow, width, height) {
132  background.appWindows[contentWindow.appID].resizeTo(width, height);
133  return true;
134};
135
136/**
137 * Returns an array with the files currently selected in the file manager.
138 * TODO(hirono): Integrate the method into getFileList method.
139 *
140 * @param {Window} contentWindow Window to be tested.
141 * @return {Array.<string>} Array of selected files.
142 */
143test.util.sync.getSelectedFiles = function(contentWindow) {
144  var table = contentWindow.document.querySelector('#detail-table');
145  var rows = table.querySelectorAll('li');
146  var selected = [];
147  for (var i = 0; i < rows.length; ++i) {
148    if (rows[i].hasAttribute('selected')) {
149      selected.push(
150          rows[i].querySelector('.filename-label').textContent);
151    }
152  }
153  return selected;
154};
155
156/**
157 * Returns an array with the files on the file manager's file list.
158 *
159 * @param {Window} contentWindow Window to be tested.
160 * @return {Array.<Array.<string>>} Array of rows.
161 */
162test.util.sync.getFileList = function(contentWindow) {
163  var table = contentWindow.document.querySelector('#detail-table');
164  var rows = table.querySelectorAll('li');
165  var fileList = [];
166  for (var j = 0; j < rows.length; ++j) {
167    var row = rows[j];
168    fileList.push([
169      row.querySelector('.filename-label').textContent,
170      row.querySelector('.size').textContent,
171      row.querySelector('.type').textContent,
172      row.querySelector('.date').textContent
173    ]);
174  }
175  return fileList;
176};
177
178/**
179 * Queries all elements.
180 *
181 * @param {Window} contentWindow Window to be tested.
182 * @param {string} targetQuery Query to specify the element.
183 * @param {?string} iframeQuery Iframe selector or null if no iframe.
184 * @param {Array.<string>=} opt_styleNames List of CSS property name to be
185 *     obtained.
186 * @return {Array.<{attributes:Object.<string, string>, text:string,
187 *                  styles:Object.<string, string>, hidden:boolean}>} Element
188 *     information that contains contentText, attribute names and
189 *     values, hidden attribute, and style names and values.
190 */
191test.util.sync.queryAllElements = function(
192    contentWindow, targetQuery, iframeQuery, opt_styleNames) {
193  var doc = test.util.sync.getDocument_(contentWindow, iframeQuery);
194  if (!doc)
195    return [];
196  // The return value of querySelectorAll is not an array.
197  return Array.prototype.map.call(
198      doc.querySelectorAll(targetQuery),
199      function(element) {
200        var attributes = {};
201        for (var i = 0; i < element.attributes.length; i++) {
202          attributes[element.attributes[i].nodeName] =
203              element.attributes[i].nodeValue;
204        }
205        var styles = {};
206        var styleNames = opt_styleNames || [];
207        var computedStyles = contentWindow.getComputedStyle(element);
208        for (var i = 0; i < styleNames.length; i++) {
209          styles[styleNames[i]] = computedStyles[styleNames[i]];
210        }
211        var text = element.textContent;
212        return {
213          attributes: attributes,
214          text: text,
215          styles: styles,
216          // The hidden attribute is not in the element.attributes even if
217          // element.hasAttribute('hidden') is true.
218          hidden: !!element.hidden
219        };
220      });
221};
222
223/**
224 * Assigns the text to the input element.
225 * @param {Window} contentWindow Window to be tested.
226 * @param {string} query Query for the input element.
227 * @param {string} text Text to be assigned.
228 */
229test.util.sync.inputText = function(contentWindow, query, text) {
230  var input = contentWindow.document.querySelector(query);
231  input.value = text;
232};
233
234/**
235 * Fakes pressing the down arrow until the given |filename| is selected.
236 *
237 * @param {Window} contentWindow Window to be tested.
238 * @param {string} filename Name of the file to be selected.
239 * @return {boolean} True if file got selected, false otherwise.
240 */
241test.util.sync.selectFile = function(contentWindow, filename) {
242  var rows = contentWindow.document.querySelectorAll('#detail-table li');
243  test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'Home', false);
244  for (var index = 0; index < rows.length; ++index) {
245    var selection = test.util.sync.getSelectedFiles(contentWindow);
246    if (selection.length === 1 && selection[0] === filename)
247      return true;
248    test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'Down', false);
249  }
250  console.error('Failed to select file "' + filename + '"');
251  return false;
252};
253
254/**
255 * Open the file by selectFile and fakeMouseDoubleClick.
256 *
257 * @param {Window} contentWindow Window to be tested.
258 * @param {string} filename Name of the file to be opened.
259 * @return {boolean} True if file got selected and a double click message is
260 *     sent, false otherwise.
261 */
262test.util.sync.openFile = function(contentWindow, filename) {
263  var query = '#file-list li.table-row[selected] .filename-label span';
264  return test.util.sync.selectFile(contentWindow, filename) &&
265         test.util.sync.fakeMouseDoubleClick(contentWindow, query);
266};
267
268/**
269 * Selects a volume specified by its icon name
270 *
271 * @param {Window} contentWindow Window to be tested.
272 * @param {string} iconName Name of the volume icon.
273 * @param {function(boolean)} callback Callback function to notify the caller
274 *     whether the target is found and mousedown and click events are sent.
275 */
276test.util.async.selectVolume = function(contentWindow, iconName, callback) {
277  var query = '[volume-type-icon=' + iconName + ']';
278  var driveQuery = '[volume-type-icon=drive]';
279  var isDriveSubVolume = iconName == 'drive_recent' ||
280                         iconName == 'drive_shared_with_me' ||
281                         iconName == 'drive_offline';
282  var preSelection = false;
283  var steps = {
284    checkQuery: function() {
285      if (contentWindow.document.querySelector(query)) {
286        steps.sendEvents();
287        return;
288      }
289      // If the target volume is sub-volume of drive, we must click 'drive'
290      // before clicking the sub-item.
291      if (!preSelection) {
292        if (!isDriveSubVolume) {
293          callback(false);
294          return;
295        }
296        if (!(test.util.sync.fakeMouseDown(contentWindow, driveQuery) &&
297              test.util.sync.fakeMouseClick(contentWindow, driveQuery))) {
298          callback(false);
299          return;
300        }
301        preSelection = true;
302      }
303      setTimeout(steps.checkQuery, 50);
304    },
305    sendEvents: function() {
306      // To change the selected volume, we have to send both events 'mousedown'
307      // and 'click' to the navigation list.
308      callback(test.util.sync.fakeMouseDown(contentWindow, query) &&
309               test.util.sync.fakeMouseClick(contentWindow, query));
310    }
311  };
312  steps.checkQuery();
313};
314
315/**
316 * Executes Javascript code on a webview and returns the result.
317 *
318 * @param {Window} contentWindow Window to be tested.
319 * @param {string} webViewQuery Selector for the web view.
320 * @param {string} code Javascript code to be executed within the web view.
321 * @param {function(*)} callback Callback function with results returned by the
322 *     script.
323 */
324test.util.async.executeScriptInWebView = function(
325    contentWindow, webViewQuery, code, callback) {
326  var webView = contentWindow.document.querySelector(webViewQuery);
327  webView.executeScript({code: code}, callback);
328};
329
330/**
331 * Sends an event to the element specified by |targetQuery|.
332 *
333 * @param {Window} contentWindow Window to be tested.
334 * @param {string} targetQuery Query to specify the element.
335 * @param {Event} event Event to be sent.
336 * @param {string=} opt_iframeQuery Optional iframe selector.
337 * @return {boolean} True if the event is sent to the target, false otherwise.
338 */
339test.util.sync.sendEvent = function(
340    contentWindow, targetQuery, event, opt_iframeQuery) {
341  var doc = test.util.sync.getDocument_(contentWindow, opt_iframeQuery);
342  if (doc) {
343    var target = doc.querySelector(targetQuery);
344    if (target) {
345      target.dispatchEvent(event);
346      return true;
347    }
348  }
349  console.error('Target element for ' + targetQuery + ' not found.');
350  return false;
351};
352
353/**
354 * Sends an fake event having the specified type to the target query.
355 *
356 * @param {Window} contentWindow Window to be tested.
357 * @param {string} targetQuery Query to specify the element.
358 * @param {string} eventType Type of event.
359 * @param {Object=} opt_additionalProperties Object contaning additional
360 *     properties.
361 * @return {boolean} True if the event is sent to the target, false otherwise.
362 */
363test.util.sync.fakeEvent = function(contentWindow,
364                                    targetQuery,
365                                    eventType,
366                                    opt_additionalProperties) {
367  var event = new Event(eventType, opt_additionalProperties || {});
368  if (opt_additionalProperties) {
369    for (var name in opt_additionalProperties) {
370      event[name] = opt_additionalProperties[name];
371    }
372  }
373  return test.util.sync.sendEvent(contentWindow, targetQuery, event);
374};
375
376/**
377 * Sends a fake key event to the element specified by |targetQuery| with the
378 * given |keyIdentifier| and optional |ctrl| modifier to the file manager.
379 *
380 * @param {Window} contentWindow Window to be tested.
381 * @param {string} targetQuery Query to specify the element.
382 * @param {string} keyIdentifier Identifier of the emulated key.
383 * @param {boolean} ctrl Whether CTRL should be pressed, or not.
384 * @param {string=} opt_iframeQuery Optional iframe selector.
385 * @return {boolean} True if the event is sent to the target, false otherwise.
386 */
387test.util.sync.fakeKeyDown = function(
388    contentWindow, targetQuery, keyIdentifier, ctrl, opt_iframeQuery) {
389  var event = new KeyboardEvent(
390      'keydown',
391      { bubbles: true, keyIdentifier: keyIdentifier, ctrlKey: ctrl });
392  return test.util.sync.sendEvent(
393      contentWindow, targetQuery, event, opt_iframeQuery);
394};
395
396/**
397 * Simulates a fake mouse click (left button, single click) on the element
398 * specified by |targetQuery|. If the element has the click method, just calls
399 * it. Otherwise, this sends 'mouseover', 'mousedown', 'mouseup' and 'click'
400 * events in turns.
401 *
402 * @param {Window} contentWindow Window to be tested.
403 * @param {string} targetQuery Query to specify the element.
404 * @param {string=} opt_iframeQuery Optional iframe selector.
405 * @return {boolean} True if the all events are sent to the target, false
406 *     otherwise.
407 */
408test.util.sync.fakeMouseClick = function(
409    contentWindow, targetQuery, opt_iframeQuery) {
410  var mouseOverEvent = new MouseEvent('mouseover', {bubbles: true, detail: 1});
411  var resultMouseOver = test.util.sync.sendEvent(
412      contentWindow, targetQuery, mouseOverEvent, opt_iframeQuery);
413  var mouseDownEvent = new MouseEvent('mousedown', {bubbles: true, detail: 1});
414  var resultMouseDown = test.util.sync.sendEvent(
415      contentWindow, targetQuery, mouseDownEvent, opt_iframeQuery);
416  var mouseUpEvent = new MouseEvent('mouseup', {bubbles: true, detail: 1});
417  var resultMouseUp = test.util.sync.sendEvent(
418      contentWindow, targetQuery, mouseUpEvent, opt_iframeQuery);
419  var clickEvent = new MouseEvent('click', {bubbles: true, detail: 1});
420  var resultClick = test.util.sync.sendEvent(
421      contentWindow, targetQuery, clickEvent, opt_iframeQuery);
422  return resultMouseOver && resultMouseDown && resultMouseUp && resultClick;
423};
424
425/**
426 * Simulates a fake mouse click (right button, single click) on the element
427 * specified by |targetQuery|.
428 *
429 * @param {Window} contentWindow Window to be tested.
430 * @param {string} targetQuery Query to specify the element.
431 * @param {string=} opt_iframeQuery Optional iframe selector.
432 * @return {boolean} True if the event is sent to the target, false
433 *     otherwise.
434 */
435test.util.sync.fakeMouseRightClick = function(
436    contentWindow, targetQuery, opt_iframeQuery) {
437  var contextMenuEvent = new MouseEvent('contextmenu', {bubbles: true});
438  var result = test.util.sync.sendEvent(
439      contentWindow, targetQuery, contextMenuEvent, opt_iframeQuery);
440  return result;
441};
442
443/**
444 * Simulates a fake double click event (left button) to the element specified by
445 * |targetQuery|.
446 *
447 * @param {Window} contentWindow Window to be tested.
448 * @param {string} targetQuery Query to specify the element.
449 * @param {string=} opt_iframeQuery Optional iframe selector.
450 * @return {boolean} True if the event is sent to the target, false otherwise.
451 */
452test.util.sync.fakeMouseDoubleClick = function(
453    contentWindow, targetQuery, opt_iframeQuery) {
454  // Double click is always preceded with a single click.
455  if (!test.util.sync.fakeMouseClick(
456      contentWindow, targetQuery, opt_iframeQuery)) {
457    return false;
458  }
459
460  // Send the second click event, but with detail equal to 2 (number of clicks)
461  // in a row.
462  var event = new MouseEvent('click', { bubbles: true, detail: 2 });
463  if (!test.util.sync.sendEvent(
464      contentWindow, targetQuery, event, opt_iframeQuery)) {
465    return false;
466  }
467
468  // Send the double click event.
469  var event = new MouseEvent('dblclick', { bubbles: true });
470  if (!test.util.sync.sendEvent(
471      contentWindow, targetQuery, event, opt_iframeQuery)) {
472    return false;
473  }
474
475  return true;
476};
477
478/**
479 * Sends a fake mouse down event to the element specified by |targetQuery|.
480 *
481 * @param {Window} contentWindow Window to be tested.
482 * @param {string} targetQuery Query to specify the element.
483 * @param {string=} opt_iframeQuery Optional iframe selector.
484 * @return {boolean} True if the event is sent to the target, false otherwise.
485 */
486test.util.sync.fakeMouseDown = function(
487    contentWindow, targetQuery, opt_iframeQuery) {
488  var event = new MouseEvent('mousedown', { bubbles: true });
489  return test.util.sync.sendEvent(
490      contentWindow, targetQuery, event, opt_iframeQuery);
491};
492
493/**
494 * Sends a fake mouse up event to the element specified by |targetQuery|.
495 *
496 * @param {Window} contentWindow Window to be tested.
497 * @param {string} targetQuery Query to specify the element.
498 * @param {string=} opt_iframeQuery Optional iframe selector.
499 * @return {boolean} True if the event is sent to the target, false otherwise.
500 */
501test.util.sync.fakeMouseUp = function(
502    contentWindow, targetQuery, opt_iframeQuery) {
503  var event = new MouseEvent('mouseup', { bubbles: true });
504  return test.util.sync.sendEvent(
505      contentWindow, targetQuery, event, opt_iframeQuery);
506};
507
508/**
509 * Selects |filename| and fakes pressing Ctrl+C, Ctrl+V (copy, paste).
510 *
511 * @param {Window} contentWindow Window to be tested.
512 * @param {string} filename Name of the file to be copied.
513 * @return {boolean} True if copying got simulated successfully. It does not
514 *     say if the file got copied, or not.
515 */
516test.util.sync.copyFile = function(contentWindow, filename) {
517  if (!test.util.sync.selectFile(contentWindow, filename))
518    return false;
519  // Ctrl+C and Ctrl+V
520  test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0043', true);
521  test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0056', true);
522  return true;
523};
524
525/**
526 * Selects |filename| and fakes pressing the Delete key.
527 *
528 * @param {Window} contentWindow Window to be tested.
529 * @param {string} filename Name of the file to be deleted.
530 * @return {boolean} True if deleting got simulated successfully. It does not
531 *     say if the file got deleted, or not.
532 */
533test.util.sync.deleteFile = function(contentWindow, filename) {
534  if (!test.util.sync.selectFile(contentWindow, filename))
535    return false;
536  // Delete
537  test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+007F', false);
538  return true;
539};
540
541/**
542 * Execute a command on the document in the specified window.
543 *
544 * @param {Window} contentWindow Window to be tested.
545 * @param {string} command Command name.
546 * @return {boolean} True if the command is executed successfully.
547 */
548test.util.sync.execCommand = function(contentWindow, command) {
549  return contentWindow.document.execCommand(command);
550};
551
552/**
553 * Override the installWebstoreItem method in private api for test.
554 *
555 * @param {Window} contentWindow Window to be tested.
556 * @param {string} expectedItemId Item ID to be called this method with.
557 * @param {?string} intendedError Error message to be returned when the item id
558 *     matches. 'null' represents no error.
559 * @return {boolean} Always return true.
560 */
561test.util.sync.overrideInstallWebstoreItemApi =
562    function(contentWindow, expectedItemId, intendedError) {
563  var setLastError = function(message) {
564    contentWindow.chrome.runtime.lastError =
565        message ? {message: message} : null;
566  };
567
568  var installWebstoreItem = function(itemId, silentInstallation, callback) {
569    setTimeout(function() {
570      if (itemId !== expectedItemId) {
571        setLastError('Invalid Chrome Web Store item ID');
572        callback();
573        return;
574      }
575
576      setLastError(intendedError);
577      callback();
578    });
579  };
580
581  test.util.executedTasks_ = [];
582  contentWindow.chrome.fileManagerPrivate.installWebstoreItem =
583      installWebstoreItem;
584  return true;
585};
586
587/**
588 * Override the task-related methods in private api for test.
589 *
590 * @param {Window} contentWindow Window to be tested.
591 * @param {Array.<Object>} taskList List of tasks to be returned in
592 *     fileManagerPrivate.getFileTasks().
593 * @return {boolean} Always return true.
594 */
595test.util.sync.overrideTasks = function(contentWindow, taskList) {
596  var getFileTasks = function(urls, onTasks) {
597    // Call onTask asynchronously (same with original getFileTasks).
598    setTimeout(function() {
599      onTasks(taskList);
600    });
601  };
602
603  var executeTask = function(taskId, url) {
604    test.util.executedTasks_.push(taskId);
605  };
606
607  var setDefaultTask = function(taskId) {
608    for (var i = 0; i < taskList.length; i++) {
609      taskList[i].isDefault = taskList[i].taskId === taskId;
610    }
611  };
612
613  test.util.executedTasks_ = [];
614  contentWindow.chrome.fileManagerPrivate.getFileTasks = getFileTasks;
615  contentWindow.chrome.fileManagerPrivate.executeTask = executeTask;
616  contentWindow.chrome.fileManagerPrivate.setDefaultTask = setDefaultTask;
617  return true;
618};
619
620/**
621 * Obtains the list of executed tasks.
622 * @param {Window} contentWindow Window to be tested.
623 * @return {Array.<string>} List of executed task ID.
624 */
625test.util.sync.getExecutedTasks = function(contentWindow) {
626  if (!test.util.executedTasks_) {
627    console.error('Please call overrideTasks() first.');
628    return null;
629  }
630  return test.util.executedTasks_;
631};
632
633/**
634 * Invoke chrome.fileManagerPrivate.visitDesktop(profileId) to cause window
635 * teleportation.
636 *
637 * @param {Window} contentWindow Window to be tested.
638 * @param {string} profileId Destination profile's ID.
639 * @return {boolean} Always return true.
640 */
641test.util.sync.visitDesktop = function(contentWindow, profileId) {
642  contentWindow.chrome.fileManagerPrivate.visitDesktop(profileId);
643  return true;
644};
645
646/**
647 * Runs the 'Move to profileId' menu.
648 *
649 * @param {Window} contentWindow Window to be tested.
650 * @param {string} profileId Destination profile's ID.
651 * @return {boolean} True if the menu is found and run.
652 */
653test.util.sync.runVisitDesktopMenu = function(contentWindow, profileId) {
654  var list = contentWindow.document.querySelectorAll('.visit-desktop');
655  for (var i = 0; i < list.length; ++i) {
656    if (list[i].label.indexOf(profileId) != -1) {
657      var activateEvent = contentWindow.document.createEvent('Event');
658      activateEvent.initEvent('activate');
659      list[i].dispatchEvent(activateEvent);
660      return true;
661    }
662  }
663  return false;
664};
665
666/**
667 * Registers message listener, which runs test utility functions.
668 */
669test.util.registerRemoteTestUtils = function() {
670  // Register the message listener.
671  var onMessage = chrome.runtime ? chrome.runtime.onMessageExternal :
672      chrome.extension.onMessageExternal;
673  // Return true for asynchronous functions and false for synchronous.
674  onMessage.addListener(function(request, sender, sendResponse) {
675    // Check the sender.
676    if (sender.id != test.util.TESTING_EXTENSION_ID) {
677      console.error('The testing extension must be white-listed.');
678      return false;
679    }
680    // Set a global flag that we are in tests, so other components are aware
681    // of it.
682    window.IN_TEST = true;
683    // Check the function name.
684    if (!request.func || request.func[request.func.length - 1] == '_') {
685      request.func = '';
686    }
687    // Prepare arguments.
688    var args = request.args.slice();  // shallow copy
689    if (request.appId) {
690      if (background.appWindows[request.appId]) {
691        args.unshift(background.appWindows[request.appId].contentWindow);
692      } else if (background.dialogs[request.appId]) {
693        args.unshift(background.dialogs[request.appId]);
694      } else {
695        console.error('Specified window not found: ' + request.appId);
696        return false;
697      }
698    }
699    // Call the test utility function and respond the result.
700    if (test.util.async[request.func]) {
701      args[test.util.async[request.func].length - 1] = function() {
702        console.debug('Received the result of ' + request.func);
703        sendResponse.apply(null, arguments);
704      };
705      console.debug('Waiting for the result of ' + request.func);
706      test.util.async[request.func].apply(null, args);
707      return true;
708    } else if (test.util.sync[request.func]) {
709      sendResponse(test.util.sync[request.func].apply(null, args));
710      return false;
711    } else {
712      console.error('Invalid function name.');
713      return false;
714    }
715  });
716};
717
718// Register the test utils.
719test.util.registerRemoteTestUtils();
720